huaweicloud-devkit 0.1.21-dev.0 → 0.1.21

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 (66) hide show
  1. package/.agents/plugins/marketplace.json +20 -20
  2. package/LICENSE +201 -201
  3. package/README.md +169 -148
  4. package/bin/setup.cjs +7 -7
  5. package/integrations/opencode/commands/huaweicloud-doctor.md +14 -14
  6. package/integrations/opencode/opencode.json +14 -14
  7. package/integrations/opencode/skills/huaweicloud-core/SKILL.md +30 -30
  8. package/package.json +43 -43
  9. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  12. package/plugins/huaweicloud-core/.mcp.json +13 -13
  13. package/plugins/huaweicloud-core/hooks/hooks.json +26 -26
  14. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +88 -88
  15. package/plugins/huaweicloud-core/safety/policy.json +85 -85
  16. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +144 -144
  17. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -48
  18. package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -50
  19. package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +101 -101
  20. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +73 -73
  21. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -55
  22. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -70
  23. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +97 -97
  24. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +29 -29
  25. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +23 -23
  26. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +131 -131
  27. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +96 -96
  28. package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -31
  29. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +28 -28
  30. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -52
  31. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +132 -132
  32. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +58 -58
  33. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +71 -71
  34. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -119
  35. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -142
  36. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -56
  37. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +65 -65
  38. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -111
  39. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -65
  40. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +118 -118
  41. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +26 -26
  42. package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -56
  43. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +181 -181
  44. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -69
  45. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +78 -78
  46. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +76 -76
  47. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +59 -59
  48. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -63
  49. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -4
  50. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +47 -47
  51. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -4
  52. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +127 -127
  53. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -4
  54. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +93 -93
  55. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -4
  56. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +19 -19
  57. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +27 -27
  58. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +46 -46
  59. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -4
  60. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -83
  61. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -4
  62. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +265 -265
  63. package/plugins/huaweicloud-core/src/mcp-server.mjs +93 -93
  64. package/plugins/huaweicloud-core/src/safety-policy.mjs +289 -289
  65. package/plugins/huaweicloud-core/src/setup-cli.mjs +869 -672
  66. package/plugins/huaweicloud-core/src/tools.mjs +677 -672
@@ -1,672 +1,869 @@
1
- #!/usr/bin/env node
2
-
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, 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
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const PLUGIN_ROOT = resolve(__dirname, '..');
12
- const PACKAGE_ROOT = resolve(PLUGIN_ROOT, '..', '..');
13
-
14
- const BANNER = `
15
- ╔══════════════════════════════════════════════╗
16
- ║ HuaweiCloud DevKit v0.1.0 ║
17
- ║ https://github.com/huaweicloud-mate ║
18
- ╚══════════════════════════════════════════════╝
19
- `;
20
-
21
- function configRoot(target = 'opencode') {
22
- const home = homedir();
23
- return join(home, '.config', target);
24
- }
25
-
26
- function opencodeSkillsDir() { return join(configRoot('opencode'), 'skills'); }
27
- function opencodeCommandsDir() { return join(configRoot('opencode'), 'commands'); }
28
- function opencodePluginsDir() { return join(configRoot('opencode'), 'huaweicloud-plugins'); }
29
- function opencodeConfigFile() {
30
- const jsonc = join(configRoot('opencode'), 'opencode.jsonc');
31
- if (existsSync(jsonc)) return jsonc;
32
- return join(configRoot('opencode'), 'opencode.json');
33
- }
34
-
35
- function codexDesktopSkillsDir() { return join(homedir(), '.agents', 'skills'); }
36
- function codexDesktopCommandsDir() { return join(homedir(), '.agents', 'commands'); }
37
- function codexDesktopPluginsDir() { return join(homedir(), '.agents', 'huaweicloud-plugins'); }
38
- function codexDesktopConfigFile() { return join(homedir(), '.agents', 'opencode.json'); }
39
-
40
- function checkNode() {
41
- const v = process.versions.node.split('.').map(Number);
42
- if (v[0] < 20) {
43
- console.error(`\x1b[31mNode.js >= 20 required (current: ${process.version})\x1b[0m`);
44
- process.exit(1);
45
- }
46
- console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
47
- }
48
-
49
- function copyDir(src, dest) {
50
- if (!existsSync(src)) return;
51
- mkdirSync(dest, { recursive: true });
52
- for (const entry of readdirSync(src, { withFileTypes: true })) {
53
- const s = join(src, entry.name);
54
- const d = join(dest, entry.name);
55
- if (entry.isDirectory()) {
56
- copyDir(s, d);
57
- } else {
58
- copyFileSync(s, d);
59
- }
60
- }
61
- }
62
-
63
- function removeIfExists(p) {
64
- if (existsSync(p)) {
65
- rmSync(p, { recursive: true, force: true });
66
- return true;
67
- }
68
- return false;
69
- }
70
-
71
- function updateOpenCodeConfig(pluginDir) {
72
- const configPath = opencodeConfigFile();
73
- let config = {};
74
- if (existsSync(configPath)) {
75
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
76
- }
77
- const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
78
- config.mcp = config.mcp || {};
79
- config.mcp.huaweicloud = {
80
- type: 'local',
81
- command: ['node', mcpPath],
82
- enabled: true,
83
- };
84
- writeFileSync(configPath, JSON.stringify(config, null, 2));
85
- console.log(` OpenCode MCP config updated: ${configPath}`);
86
- }
87
-
88
- function removeOpenCodeConfig() {
89
- const configPath = opencodeConfigFile();
90
- if (!existsSync(configPath)) return;
91
- let config = {};
92
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return; }
93
- if (!config.mcp?.huaweicloud) return;
94
- delete config.mcp.huaweicloud;
95
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
96
- writeFileSync(configPath, JSON.stringify(config, null, 2));
97
- console.log(` OpenCode MCP config cleaned: ${configPath}`);
98
- }
99
-
100
- function hasCodexCLI() {
101
- const r = spawnSync('codex --version', [], { shell: true, windowsHide: true, stdio: 'pipe' });
102
- return r.status === 0 && r.stdout && r.stdout.toString().includes('codex');
103
- }
104
-
105
- function checkHcloud() {
106
- const r = spawnSync('hcloud version', [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
107
- return r.status === 0 && /KooCLI|Current.*version/i.test(r.stdout ? r.stdout.toString() : '');
108
- }
109
-
110
- function getMarketplaceName() {
111
- const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
112
- try {
113
- const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
114
- if (manifest.name) return manifest.name;
115
- } catch {}
116
- return 'huaweicloud-devkit';
117
- }
118
-
119
- function installCodex() {
120
- const marketplaceRoot = PACKAGE_ROOT;
121
- const pluginName = 'huaweicloud-core';
122
- const marketplaceName = getMarketplaceName();
123
-
124
- console.log(` Registering Codex marketplace: ${marketplaceRoot}`);
125
- const r1 = spawnSync(`codex plugin marketplace add "${marketplaceRoot}"`, [], {
126
- shell: true, windowsHide: true, stdio: 'pipe',
127
- });
128
- console.log(` ${r1.stdout ? r1.stdout.toString().trim() : r1.stderr.toString().trim()}`);
129
-
130
- console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
131
- const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
132
- shell: true, windowsHide: true, stdio: 'pipe',
133
- });
134
- console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
135
- return r2.status === 0;
136
- }
137
-
138
- function uninstallCodex() {
139
- const pluginName = 'huaweicloud-core';
140
- const marketplaceName = getMarketplaceName();
141
- console.log(` Removing Codex plugin: ${pluginName}@${marketplaceName}`);
142
- const r = spawnSync(`codex plugin remove "${pluginName}@${marketplaceName}"`, [], {
143
- shell: true, windowsHide: true, stdio: 'pipe',
144
- });
145
- console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
146
- }
147
-
148
- function codexStatus() {
149
- const r = spawnSync('codex plugin list', [], { shell: true, windowsHide: true, stdio: 'pipe' });
150
- const out = r.stdout ? r.stdout.toString() : '';
151
- return out.includes('huaweicloud-core');
152
- }
153
-
154
- async function installOpenCode() {
155
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
156
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
157
- const srcDir = join(PLUGIN_ROOT, 'src');
158
- const safetyDir = join(PLUGIN_ROOT, 'safety');
159
- const pluginDest = opencodePluginsDir();
160
-
161
- copyDir(skillsSrc, opencodeSkillsDir());
162
- console.log(` Skills -> ${opencodeSkillsDir()}`);
163
- copyDir(commandsSrc, opencodeCommandsDir());
164
- console.log(` Commands -> ${opencodeCommandsDir()}`);
165
- copyDir(srcDir, join(pluginDest, 'src'));
166
- console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
167
- copyDir(safetyDir, join(pluginDest, 'safety'));
168
- console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
169
- updateOpenCodeConfig(pluginDest);
170
- }
171
-
172
- function uninstallOpenCode() {
173
- let removed = 0;
174
-
175
- const skills = opencodeSkillsDir();
176
- if (existsSync(skills)) {
177
- for (const entry of readdirSync(skills, { withFileTypes: true })) {
178
- if (entry.name.startsWith('huawei')) {
179
- removeIfExists(join(skills, entry.name));
180
- removed++;
181
- }
182
- }
183
- console.log(` Removed ${removed} skills`);
184
- }
185
-
186
- const commands = opencodeCommandsDir();
187
- let cmdRemoved = 0;
188
- if (existsSync(commands)) {
189
- for (const entry of readdirSync(commands, { withFileTypes: true })) {
190
- if (entry.name.startsWith('huawei')) {
191
- removeIfExists(join(commands, entry.name));
192
- cmdRemoved++;
193
- }
194
- }
195
- if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
196
- }
197
-
198
- if (removeIfExists(opencodePluginsDir())) {
199
- console.log(' Removed MCP server and safety policy');
200
- }
201
- removeOpenCodeConfig();
202
- }
203
-
204
- async function installCodexDesktop() {
205
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
206
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
207
- const srcDir = join(PLUGIN_ROOT, 'src');
208
- const safetyDir = join(PLUGIN_ROOT, 'safety');
209
-
210
- copyDir(skillsSrc, codexDesktopSkillsDir());
211
- console.log(` Skills -> ${codexDesktopSkillsDir()}`);
212
- copyDir(commandsSrc, codexDesktopCommandsDir());
213
- console.log(` Commands -> ${codexDesktopCommandsDir()}`);
214
- mkdirSync(codexDesktopPluginsDir(), { recursive: true });
215
- copyDir(srcDir, join(codexDesktopPluginsDir(), 'src'));
216
- console.log(` MCP Server -> ${join(codexDesktopPluginsDir(), 'src')}`);
217
- copyDir(safetyDir, join(codexDesktopPluginsDir(), 'safety'));
218
- console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
219
-
220
- const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
221
- const configPath = codexDesktopConfigFile();
222
- let config = {};
223
- if (existsSync(configPath)) {
224
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
225
- }
226
- config.mcp = config.mcp || {};
227
- config.mcp.huaweicloud = {
228
- type: 'local',
229
- command: ['node', mcpPath],
230
- enabled: true,
231
- };
232
- writeFileSync(configPath, JSON.stringify(config, null, 2));
233
- console.log(` Config updated: ${configPath}`);
234
- }
235
-
236
- function uninstallCodexDesktop() {
237
- const skillsDir = codexDesktopSkillsDir();
238
- let removed = 0;
239
- if (existsSync(skillsDir)) {
240
- for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
241
- if (entry.name.startsWith('huawei')) {
242
- removeIfExists(join(skillsDir, entry.name));
243
- removed++;
244
- }
245
- }
246
- console.log(` Removed ${removed} skills`);
247
- }
248
-
249
- const cmdDir = codexDesktopCommandsDir();
250
- let cmdRemoved = 0;
251
- if (existsSync(cmdDir)) {
252
- for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
253
- if (entry.name.startsWith('huawei')) {
254
- removeIfExists(join(cmdDir, entry.name));
255
- cmdRemoved++;
256
- }
257
- }
258
- if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
259
- }
260
-
261
- if (removeIfExists(codexDesktopPluginsDir())) {
262
- console.log(' Removed MCP server and safety policy');
263
- }
264
- const configPath = codexDesktopConfigFile();
265
- if (existsSync(configPath)) {
266
- let config = {};
267
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
268
- if (config.mcp?.huaweicloud) {
269
- delete config.mcp.huaweicloud;
270
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
271
- writeFileSync(configPath, JSON.stringify(config, null, 2));
272
- console.log(' Config cleaned');
273
- }
274
- }
275
- }
276
-
277
- function opencodeStatus() {
278
- const pluginDir = opencodePluginsDir();
279
- const skillsDir = opencodeSkillsDir();
280
- console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
281
- console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
282
- let skillCount = 0;
283
- if (existsSync(skillsDir)) {
284
- skillCount = readdirSync(skillsDir, { withFileTypes: true })
285
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
286
- }
287
- console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
288
- const configPath = opencodeConfigFile();
289
- if (existsSync(configPath)) {
290
- try {
291
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
292
- console.log(` MCP config: ${config.mcp?.huaweicloud ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
293
- } catch {
294
- console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
295
- }
296
- }
297
- }
298
-
299
- function parseTarget() {
300
- const idx = process.argv.indexOf('--target');
301
- if (idx < 0) return 'opencode';
302
- const val = (process.argv[idx + 1] || '').toLowerCase();
303
- if (val === 'codex') return 'codex';
304
- if (val === 'codex-desktop') return 'codex-desktop';
305
- if (val === 'all') return 'all';
306
- return 'opencode';
307
- }
308
-
309
- async function cmdInstall() {
310
- const target = parseTarget();
311
- console.log(BANNER);
312
- console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
313
- checkNode();
314
-
315
- if (target === 'opencode' || target === 'all') {
316
- console.log('[OpenCode]');
317
- await installOpenCode();
318
- }
319
- if (target === 'codex-desktop' || target === 'all') {
320
- console.log('\n[Codex Desktop]');
321
- await installCodexDesktop();
322
- }
323
- if (target === 'codex' || target === 'all') {
324
- console.log('\n[Codex]');
325
- if (!hasCodexCLI()) {
326
- if (target === 'codex') {
327
- console.log(` \x1b[31mCodex CLI not found. Install Codex first: https://github.com/openai/codex-cli\x1b[0m`);
328
- console.log(` Then re-run: npx huaweicloud-devkit install --target codex`);
329
- process.exit(1);
330
- }
331
- console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
332
- console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
333
- } else {
334
- installCodex();
335
- }
336
- }
337
-
338
- console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
339
- console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
340
- console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 OpenCode 会话后才生效 ║`);
341
- console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 重新打开,直接描述华为云任务即可 ║`);
342
- console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
343
- console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
344
-
345
- const hcloudOk = checkHcloud();
346
- if (!hcloudOk) {
347
- console.log(`\n\x1b[33mKooCLI (hcloud) is not installed.`);
348
- console.log(` Run: npx huaweicloud-devkit install-hcloud\x1b[0m`);
349
- } else {
350
- console.log(`\nKooCLI (hcloud) detected.`);
351
- }
352
-
353
- console.log(`\nAfter restart + hcloud setup, run: npx huaweicloud-devkit doctor`);
354
-
355
- // Write install marker for doctor to detect
356
- writeFileSync(join(opencodePluginsDir(), '.installed'), new Date().toISOString());
357
- if (target === 'opencode' || target === 'all') {
358
- console.log('Or describe your Huawei Cloud task in OpenCode');
359
- }
360
- if (target === 'codex' || target === 'all') {
361
- console.log('Or mention @huaweicloud-core in Codex');
362
- }
363
- }
364
-
365
- async function cmdUninstall() {
366
- const target = parseTarget();
367
- console.log(BANNER);
368
- console.log(`Uninstalling HuaweiCloud DevKit${target !== 'opencode' ? ` from ${target}` : ''}...\n`);
369
-
370
- if (target === 'opencode' || target === 'all') {
371
- console.log('[OpenCode]');
372
- await uninstallOpenCode();
373
- }
374
- if (target === 'codex' || target === 'all') {
375
- console.log('\n[Codex]');
376
- uninstallCodexDesktop();
377
- if (!hasCodexCLI()) {
378
- console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
379
- } else {
380
- uninstallCodex();
381
- }
382
- }
383
- console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
384
- }
385
-
386
- async function cmdStatus() {
387
- const target = parseTarget();
388
- console.log(BANNER);
389
- console.log(`HuaweiCloud DevKit Status\n`);
390
-
391
- if (target === 'opencode' || target === 'all') {
392
- console.log('[OpenCode]');
393
- opencodeStatus();
394
- }
395
- if (target === 'codex' || target === 'all') {
396
- console.log('\n[Codex]');
397
- if (!hasCodexCLI()) {
398
- console.log(' \x1b[33mCodex CLI not found.\x1b[0m');
399
- } else {
400
- console.log(` Plugin: ${codexStatus() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
401
- }
402
- }
403
- console.log('\nEnvironment:');
404
- console.log(` Node.js: ${process.version}`);
405
- console.log(` Platform: ${platform()}`);
406
- }
407
-
408
- async function cmdDoctor() {
409
- console.log(BANNER);
410
- console.log('HuaweiCloud DevKit Doctor\n');
411
-
412
- let pass = 0, warn = 0, fail = 0;
413
-
414
- function check(label, ok, msg) {
415
- if (ok) { console.log(` \x1b[32m[PASS]\x1b[0m ${label}`); pass++; }
416
- else { console.log(` \x1b[31m[FAIL]\x1b[0m ${label} — ${msg}`); fail++; }
417
- }
418
-
419
- // Node.js
420
- check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
421
-
422
- // OpenCode installed files
423
- const pluginDir = opencodePluginsDir();
424
- const mcpOk = existsSync(join(pluginDir, 'src', 'mcp-server.mjs'));
425
- check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit-test install');
426
-
427
- if (mcpOk) {
428
- // Try to start MCP server briefly
429
- const test = spawnSync('node', [join(pluginDir, 'src', 'mcp-server.mjs')], {
430
- env: { ...process.env, HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
431
- timeout: 3000, stdio: 'pipe', windowsHide: true,
432
- });
433
- // MCP server reads stdin for JSON-RPC, so it will hang briefly then get killed
434
- // We just check that the process spawned OK
435
- check('MCP server can start', true, '');
436
- }
437
-
438
- const safetyOk = existsSync(join(pluginDir, 'safety', 'policy.json'));
439
- check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit-test install');
440
-
441
- const opencodeCfg = opencodeConfigFile();
442
- let mcpConfigured = false;
443
- if (existsSync(opencodeCfg)) {
444
- try {
445
- const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
446
- mcpConfigured = !!(cfg.mcp && cfg.mcp.huaweicloud);
447
- } catch {}
448
- }
449
- check('OpenCode MCP configured', mcpConfigured, `Add MCP to ${opencodeCfg} — run: npx huaweicloud-devkit-test install`);
450
-
451
- // hcloud CLI
452
- const hcloudBin = process.env.HCLOUD_BIN || 'hcloud';
453
- const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
454
- const hcloudOk = hcloudCheck.status === 0 && hcloudCheck.stdout.toString().includes('KooCLI');
455
- check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
456
-
457
- if (hcloudOk) {
458
- const ver = (hcloudCheck.stdout.toString().match(/(\d+\.\d+\.\d+)/) || [])[1] || 'unknown';
459
- console.log(` Version: ${ver}`);
460
-
461
- // Check auth
462
- const authCheck = spawnSync(`"${hcloudBin}" configure list`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
463
- const hasAuth = authCheck.status === 0 && /access.?key/i.test(authCheck.stdout.toString());
464
- check('hcloud credentials configured', hasAuth, 'Run: hcloud configure init');
465
- }
466
-
467
- // Skills
468
- const skillsOptions = [opencodeSkillsDir(), codexDesktopSkillsDir()];
469
- let skillCount = 0, skillsDir = '';
470
- for (const dir of skillsOptions) {
471
- if (!existsSync(dir)) continue;
472
- const count = readdirSync(dir, { withFileTypes: true })
473
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
474
- if (count > skillCount) { skillCount = count; skillsDir = dir; }
475
- }
476
- const skillsOk = skillCount >= 6;
477
- check(`Skills installed (${skillCount})`, skillsOk, 'Run: npx huaweicloud-devkit-test install');
478
-
479
- console.log(`\nResults: ${pass} pass, ${warn} warn, ${fail} fail`);
480
-
481
- if (mcpConfigured && !hcloudOk) {
482
- console.log('\n\x1b[33mMCP is configured but hcloud is not installed. Install hcloud then restart OpenCode.\x1b[0m');
483
- }
484
- if (fail > 0) {
485
- console.log('\x1b[33mFix failures above, then restart your OpenCode / Codex session.\x1b[0m');
486
- }
487
- if (fail === 0 && mcpConfigured) {
488
- console.log('\n\x1b[32mAll checks passed.\x1b[0m Restart OpenCode, then describe your Huawei Cloud task');
489
- }
490
-
491
- // Detect "installed but not restarted" scenario
492
- const installedMarker = join(opencodePluginsDir(), '.installed');
493
- if (mcpConfigured && existsSync(installedMarker)) {
494
- console.log(`\n\x1b[1m\x1b[31m╔══════════════════════════════════════════╗`);
495
- console.log(`\x1b[1m\x1b[31m请重启 OpenCode!MCP 工具尚未激活 ║`);
496
- console.log(`\x1b[1m\x1b[31m║ 关闭当前会话 → 重新打开即可 ║`);
497
- console.log(`\x1b[1m\x1b[31m╚══════════════════════════════════════════╝\x1b[0m`);
498
- }
499
- }
500
-
501
- async function cmdUpdate() {
502
- console.log(BANNER);
503
- const target = parseTarget();
504
-
505
- if (target === 'opencode' || target === 'all') {
506
- if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs')) && !codexStatus()) {
507
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
508
- return;
509
- }
510
- }
511
-
512
- await cmdUninstall();
513
- console.log('');
514
- await cmdInstall();
515
- }
516
-
517
- async function cmdReinstall() {
518
- console.log(BANNER);
519
- if (!(await confirm('This will remove and reinstall all HuaweiCloud DevKit files. Continue?'))) {
520
- console.log('Cancelled.');
521
- return;
522
- }
523
- confirmed = true;
524
- await cmdUninstall();
525
- console.log('');
526
- await cmdInstall();
527
- }
528
-
529
- let confirmed = false;
530
- async function confirm(msg) {
531
- if (confirmed) return true;
532
- const rl = createInterface({ input: process.stdin, output: process.stdout });
533
- return new Promise((ok) => {
534
- rl.question(`${msg} [y/N] `, (a) => {
535
- rl.close();
536
- ok(a.toLowerCase() === 'y' || a.toLowerCase() === 'yes');
537
- });
538
- });
539
- }
540
-
541
- async function cmdInstallHcloud() {
542
- console.log(BANNER);
543
- console.log('Installing KooCLI (hcloud)...\n');
544
-
545
- const os = platform();
546
- const arch = process.arch;
547
- const baseUrl = 'https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest';
548
- const installDir = os === 'win32'
549
- ? join(homedir(), 'hcloud')
550
- : join(homedir(), '.local', 'bin');
551
-
552
- if (os === 'win32') {
553
- const url = `${baseUrl}/huaweicloud-cli-windows-amd64.zip`;
554
- const zipPath = join(installDir, 'hcloud.zip');
555
-
556
- console.log(`[Windows] Auto-installing to ${installDir}...`);
557
-
558
- try {
559
- mkdirSync(installDir, { recursive: true });
560
-
561
- // Download
562
- console.log(` Downloading ${url}...`);
563
- const psCmd = `[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '${url}' -OutFile '${zipPath}' -UseBasicParsing`;
564
- spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { stdio: 'inherit', windowsHide: true });
565
-
566
- // Extract
567
- console.log(' Extracting...');
568
- spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -Path '${zipPath}' -DestinationPath '${installDir}' -Force`], { stdio: 'inherit', windowsHide: true });
569
-
570
- // Clean up zip
571
- rmSync(zipPath, { force: true });
572
-
573
- // Add to PATH
574
- console.log(' Adding to user PATH...');
575
- spawnSync('setx', ['PATH', `${process.env.PATH};${installDir}`], { stdio: 'inherit', windowsHide: true });
576
-
577
- console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
578
- console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
579
- console.log(' Or restart terminal and: hcloud version');
580
- } catch (e) {
581
- console.log(`\n\x1b[33mAuto-install failed: ${e.message}\x1b[0m`);
582
- console.log(` Manual: download ${url}, unzip to ${installDir}, add to PATH`);
583
- console.log(` Guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_01.html`);
584
- }
585
- } else if (os === 'linux') {
586
- console.log('[Linux] One-liner install:');
587
- 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');
588
- console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64' : 'AMD64'}`);
589
- const pkg = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
590
- console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
591
- console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
592
- console.log(` mv hcloud ~/.local/bin/`);
593
- console.log(` hcloud version`);
594
- console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_02.html`);
595
- } else if (os === 'darwin') {
596
- console.log('[macOS] One-liner install:');
597
- 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');
598
- console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64 (Apple Silicon)' : 'AMD64 (Intel)'}`);
599
- const pkg = arch === 'arm64' ? 'mac-arm64' : 'mac-amd64';
600
- console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
601
- console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
602
- console.log(` mv hcloud /usr/local/bin/`);
603
- console.log(` hcloud version`);
604
- console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_03.html`);
605
- }
606
-
607
- console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
608
- console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
609
- console.log(' Interactive (safe): hcloud configure init');
610
- console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
611
- console.log('\nThen run: npx huaweicloud-devkit doctor');
612
- }
613
-
614
- async function main() {
615
- const cmd = process.argv[2] || 'help';
616
-
617
- switch (cmd) {
618
- case 'install':
619
- case 'i':
620
- await cmdInstall();
621
- break;
622
- case 'uninstall':
623
- case 'remove':
624
- await cmdUninstall();
625
- break;
626
- case 'update':
627
- case 'upgrade':
628
- await cmdUpdate();
629
- break;
630
- case 'reinstall':
631
- await cmdReinstall();
632
- break;
633
- case 'status':
634
- case 'info':
635
- await cmdStatus();
636
- break;
637
- case 'doctor':
638
- case 'check':
639
- await cmdDoctor();
640
- break;
641
- case 'install-hcloud':
642
- await cmdInstallHcloud();
643
- break;
644
- case 'help':
645
- case '--help':
646
- case '-h':
647
- default:
648
- console.log(BANNER);
649
- console.log('Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|all>]\n');
650
- console.log('Commands:');
651
- console.log(' install Install skills, MCP server, safety policy');
652
- console.log(' uninstall Remove installed files');
653
- console.log(' update Update to latest version');
654
- console.log(' reinstall Full clean reinstall');
655
- console.log(' status Show installation status');
656
- console.log(' doctor Self-check: hcloud, MCP, skills, auth');
657
- console.log(' install-hcloud Show KooCLI install commands for your OS');
658
- console.log(' help Show this help');
659
- console.log('\nOptions:');
660
- console.log(' --target Target agent: opencode (default), codex, all');
661
- console.log('\nExamples:');
662
- console.log(' npx huaweicloud-devkit install');
663
- console.log(' npx huaweicloud-devkit install --target codex');
664
- console.log(' npx huaweicloud-devkit install --target all');
665
- break;
666
- }
667
- }
668
-
669
- main().catch((e) => {
670
- console.error(`\x1b[31mError: ${e.message}\x1b[0m`);
671
- process.exit(1);
672
- });
1
+ #!/usr/bin/env node
2
+
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, 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
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const PLUGIN_ROOT = resolve(__dirname, '..');
12
+ const PACKAGE_ROOT = resolve(PLUGIN_ROOT, '..', '..');
13
+
14
+ const BANNER = `
15
+ ╔══════════════════════════════════════════════╗
16
+ ║ HuaweiCloud DevKit v0.1.0 ║
17
+ ║ https://github.com/huaweicloud-mate ║
18
+ ╚══════════════════════════════════════════════╝
19
+ `;
20
+
21
+ function configRoot(target = 'opencode') {
22
+ const home = homedir();
23
+ return join(home, '.config', target);
24
+ }
25
+
26
+ function opencodeSkillsDir() { return join(configRoot('opencode'), 'skills'); }
27
+ function opencodeCommandsDir() { return join(configRoot('opencode'), 'commands'); }
28
+ function opencodePluginsDir() { return join(configRoot('opencode'), 'huaweicloud-plugins'); }
29
+ function opencodeConfigFile() {
30
+ const jsonc = join(configRoot('opencode'), 'opencode.jsonc');
31
+ if (existsSync(jsonc)) return jsonc;
32
+ return join(configRoot('opencode'), 'opencode.json');
33
+ }
34
+
35
+ function codexDesktopSkillsDir() { return join(homedir(), '.agents', 'skills'); }
36
+ function codexDesktopCommandsDir() { return join(homedir(), '.agents', 'commands'); }
37
+ function codexDesktopPluginsDir() { return join(homedir(), '.agents', 'huaweicloud-plugins'); }
38
+ function codexDesktopConfigFile() { return join(homedir(), '.agents', 'opencode.json'); }
39
+
40
+ function codeartsSkillsDir() { return join(homedir(), '.codeartsdoer', 'skills'); }
41
+ function codeartsMcpSettingsDir() { return join(homedir(), '.codeartsdoer', 'mcp'); }
42
+ function codeartsMcpSettingsFile() { return join(codeartsMcpSettingsDir(), 'mcp_settings.json'); }
43
+ function codeartsProjectDir() { return join(process.cwd(), '.codeartsdoer'); }
44
+ function codeartsProjectSkillsDir() { return join(codeartsProjectDir(), 'skills'); }
45
+ function codeartsProjectMcpSettingsFile() { return join(codeartsProjectDir(), 'mcp', 'mcp_settings.json'); }
46
+ function codeartsPluginsDir() { return join(homedir(), '.codeartsdoer', 'huaweicloud-plugins'); }
47
+
48
+ // Detect CodeArts sandbox mode (bash_mode in permission config).
49
+ function detectCodeartsSandbox() {
50
+ try {
51
+ const configPath = join(homedir(), '.codeartsdoer', 'codearts-data', 'storage', 'permission', 'config.json');
52
+ if (!existsSync(configPath)) return null;
53
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
54
+ return typeof config.bash_mode === 'string' ? config.bash_mode : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ // Locate an existing hcloud executable (HCLOUD_BIN, ~/hcloud on Windows, ~/.local/bin elsewhere).
61
+ function findHcloudBin() {
62
+ if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
63
+ const rel = platform() === 'win32' ? join('hcloud', 'hcloud.exe') : join('.local', 'bin', 'hcloud');
64
+ const candidate = join(homedir(), rel);
65
+ return existsSync(candidate) ? candidate : null;
66
+ }
67
+
68
+ function printSandboxWarning(reason) {
69
+ console.log(`\n\x1b[1m\x1b[31m⚠ 检测到码道沙箱模式 (bash_mode: sandbox)\x1b[0m`);
70
+ console.log(`\x1b[31m ${reason}\x1b[0m`);
71
+ console.log(`\x1b[31m 请任选其一继续:\x1b[0m`);
72
+ console.log(`\x1b[31m A. 在码道外的终端安装并使用 KooCLI (推荐):`);
73
+ console.log(`\x1b[31m https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`);
74
+ console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 权限 → Bash 模式)`);
75
+ console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
76
+ }
77
+
78
+ // Try to auto-accept the KooCLI privacy agreement by answering 'y' on stdin.
79
+ // Returns true when hcloud runs without re-prompting for the agreement.
80
+ function acceptKooCliPrivacy(hcloudBin) {
81
+ const run = () => spawnSync(hcloudBin, ['version'], {
82
+ encoding: 'utf8', timeout: 10000, windowsHide: true, input: 'y\n',
83
+ });
84
+ const first = run();
85
+ const out = (first.stdout || '') + (first.stderr || '');
86
+ if (!/同意并继续使用|agree/i.test(out)) return first.status === 0;
87
+ const second = run();
88
+ const out2 = (second.stdout || '') + (second.stderr || '');
89
+ return second.status === 0 && !/同意并继续使用/.test(out2);
90
+ }
91
+
92
+ function checkNode() {
93
+ const v = process.versions.node.split('.').map(Number);
94
+ if (v[0] < 20) {
95
+ console.error(`\x1b[31mNode.js >= 20 required (current: ${process.version})\x1b[0m`);
96
+ process.exit(1);
97
+ }
98
+ console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
99
+ }
100
+
101
+ function copyDir(src, dest) {
102
+ if (!existsSync(src)) return;
103
+ mkdirSync(dest, { recursive: true });
104
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
105
+ const s = join(src, entry.name);
106
+ const d = join(dest, entry.name);
107
+ if (entry.isDirectory()) {
108
+ copyDir(s, d);
109
+ } else {
110
+ copyFileSync(s, d);
111
+ }
112
+ }
113
+ }
114
+
115
+ function removeIfExists(p) {
116
+ if (existsSync(p)) {
117
+ rmSync(p, { recursive: true, force: true });
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function updateOpenCodeConfig(pluginDir) {
124
+ const configPath = opencodeConfigFile();
125
+ let config = {};
126
+ if (existsSync(configPath)) {
127
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
128
+ }
129
+ const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
130
+ config.mcp = config.mcp || {};
131
+ config.mcp.huaweicloud = {
132
+ type: 'local',
133
+ command: ['node', mcpPath],
134
+ enabled: true,
135
+ };
136
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
137
+ console.log(` OpenCode MCP config updated: ${configPath}`);
138
+ }
139
+
140
+ function removeOpenCodeConfig() {
141
+ const configPath = opencodeConfigFile();
142
+ if (!existsSync(configPath)) return;
143
+ let config = {};
144
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return; }
145
+ if (!config.mcp?.huaweicloud) return;
146
+ delete config.mcp.huaweicloud;
147
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
148
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
149
+ console.log(` OpenCode MCP config cleaned: ${configPath}`);
150
+ }
151
+
152
+ function hasCodexCLI() {
153
+ const r = spawnSync('codex --version', [], { shell: true, windowsHide: true, stdio: 'pipe' });
154
+ return r.status === 0 && r.stdout && r.stdout.toString().includes('codex');
155
+ }
156
+
157
+ function checkHcloud() {
158
+ const bin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
159
+ const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000, input: 'y\n' });
160
+ const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
161
+ return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
162
+ }
163
+
164
+ function getMarketplaceName() {
165
+ const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
166
+ try {
167
+ const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
168
+ if (manifest.name) return manifest.name;
169
+ } catch {}
170
+ return 'huaweicloud-devkit';
171
+ }
172
+
173
+ function installCodex() {
174
+ const marketplaceRoot = PACKAGE_ROOT;
175
+ const pluginName = 'huaweicloud-core';
176
+ const marketplaceName = getMarketplaceName();
177
+
178
+ console.log(` Registering Codex marketplace: ${marketplaceRoot}`);
179
+ const r1 = spawnSync(`codex plugin marketplace add "${marketplaceRoot}"`, [], {
180
+ shell: true, windowsHide: true, stdio: 'pipe',
181
+ });
182
+ console.log(` ${r1.stdout ? r1.stdout.toString().trim() : r1.stderr.toString().trim()}`);
183
+
184
+ console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
185
+ const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
186
+ shell: true, windowsHide: true, stdio: 'pipe',
187
+ });
188
+ console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
189
+ return r2.status === 0;
190
+ }
191
+
192
+ function uninstallCodex() {
193
+ const pluginName = 'huaweicloud-core';
194
+ const marketplaceName = getMarketplaceName();
195
+ console.log(` Removing Codex plugin: ${pluginName}@${marketplaceName}`);
196
+ const r = spawnSync(`codex plugin remove "${pluginName}@${marketplaceName}"`, [], {
197
+ shell: true, windowsHide: true, stdio: 'pipe',
198
+ });
199
+ console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
200
+ }
201
+
202
+ function codexStatus() {
203
+ const r = spawnSync('codex plugin list', [], { shell: true, windowsHide: true, stdio: 'pipe' });
204
+ const out = r.stdout ? r.stdout.toString() : '';
205
+ return out.includes('huaweicloud-core');
206
+ }
207
+
208
+ async function installOpenCode() {
209
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
210
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
211
+ const srcDir = join(PLUGIN_ROOT, 'src');
212
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
213
+ const pluginDest = opencodePluginsDir();
214
+
215
+ copyDir(skillsSrc, opencodeSkillsDir());
216
+ console.log(` Skills -> ${opencodeSkillsDir()}`);
217
+ copyDir(commandsSrc, opencodeCommandsDir());
218
+ console.log(` Commands -> ${opencodeCommandsDir()}`);
219
+ copyDir(srcDir, join(pluginDest, 'src'));
220
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
221
+ copyDir(safetyDir, join(pluginDest, 'safety'));
222
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
223
+ updateOpenCodeConfig(pluginDest);
224
+ }
225
+
226
+ function uninstallOpenCode() {
227
+ let removed = 0;
228
+
229
+ const skills = opencodeSkillsDir();
230
+ if (existsSync(skills)) {
231
+ for (const entry of readdirSync(skills, { withFileTypes: true })) {
232
+ if (entry.name.startsWith('huawei')) {
233
+ removeIfExists(join(skills, entry.name));
234
+ removed++;
235
+ }
236
+ }
237
+ console.log(` Removed ${removed} skills`);
238
+ }
239
+
240
+ const commands = opencodeCommandsDir();
241
+ let cmdRemoved = 0;
242
+ if (existsSync(commands)) {
243
+ for (const entry of readdirSync(commands, { withFileTypes: true })) {
244
+ if (entry.name.startsWith('huawei')) {
245
+ removeIfExists(join(commands, entry.name));
246
+ cmdRemoved++;
247
+ }
248
+ }
249
+ if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
250
+ }
251
+
252
+ if (removeIfExists(opencodePluginsDir())) {
253
+ console.log(' Removed MCP server and safety policy');
254
+ }
255
+ removeOpenCodeConfig();
256
+ }
257
+
258
+ async function installCodexDesktop() {
259
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
260
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
261
+ const srcDir = join(PLUGIN_ROOT, 'src');
262
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
263
+
264
+ copyDir(skillsSrc, codexDesktopSkillsDir());
265
+ console.log(` Skills -> ${codexDesktopSkillsDir()}`);
266
+ copyDir(commandsSrc, codexDesktopCommandsDir());
267
+ console.log(` Commands -> ${codexDesktopCommandsDir()}`);
268
+ mkdirSync(codexDesktopPluginsDir(), { recursive: true });
269
+ copyDir(srcDir, join(codexDesktopPluginsDir(), 'src'));
270
+ console.log(` MCP Server -> ${join(codexDesktopPluginsDir(), 'src')}`);
271
+ copyDir(safetyDir, join(codexDesktopPluginsDir(), 'safety'));
272
+ console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
273
+
274
+ const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
275
+ const configPath = codexDesktopConfigFile();
276
+ let config = {};
277
+ if (existsSync(configPath)) {
278
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
279
+ }
280
+ config.mcp = config.mcp || {};
281
+ config.mcp.huaweicloud = {
282
+ type: 'local',
283
+ command: ['node', mcpPath],
284
+ enabled: true,
285
+ };
286
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
287
+ console.log(` Config updated: ${configPath}`);
288
+ }
289
+
290
+ function uninstallCodexDesktop() {
291
+ const skillsDir = codexDesktopSkillsDir();
292
+ let removed = 0;
293
+ if (existsSync(skillsDir)) {
294
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
295
+ if (entry.name.startsWith('huawei')) {
296
+ removeIfExists(join(skillsDir, entry.name));
297
+ removed++;
298
+ }
299
+ }
300
+ console.log(` Removed ${removed} skills`);
301
+ }
302
+
303
+ const cmdDir = codexDesktopCommandsDir();
304
+ let cmdRemoved = 0;
305
+ if (existsSync(cmdDir)) {
306
+ for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
307
+ if (entry.name.startsWith('huawei')) {
308
+ removeIfExists(join(cmdDir, entry.name));
309
+ cmdRemoved++;
310
+ }
311
+ }
312
+ if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
313
+ }
314
+
315
+ if (removeIfExists(codexDesktopPluginsDir())) {
316
+ console.log(' Removed MCP server and safety policy');
317
+ }
318
+ const configPath = codexDesktopConfigFile();
319
+ if (existsSync(configPath)) {
320
+ let config = {};
321
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
322
+ if (config.mcp?.huaweicloud) {
323
+ delete config.mcp.huaweicloud;
324
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
325
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
326
+ console.log(' Config cleaned');
327
+ }
328
+ }
329
+ }
330
+
331
+ function registerCodeartsMcp(configPath) {
332
+ const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
333
+ let config = {};
334
+ if (existsSync(configPath)) {
335
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
336
+ }
337
+ config.mcpServers = config.mcpServers || {};
338
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
339
+ const hcloudBin = findHcloudBin();
340
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
341
+ config.mcpServers.huaweicloud = {
342
+ command: 'node',
343
+ args: [mcpPath],
344
+ env,
345
+ enabled: true,
346
+ };
347
+ mkdirSync(dirname(configPath), { recursive: true });
348
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
349
+ console.log(` MCP config updated: ${configPath}`);
350
+ }
351
+
352
+ async function installCodeArts() {
353
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
354
+ const srcDir = join(PLUGIN_ROOT, 'src');
355
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
356
+
357
+ copyDir(skillsSrc, codeartsSkillsDir());
358
+ console.log(` Skills -> ${codeartsSkillsDir()}`);
359
+ copyDir(skillsSrc, codeartsProjectSkillsDir());
360
+ console.log(` Skills -> ${codeartsProjectSkillsDir()}`);
361
+
362
+ const pluginDest = codeartsPluginsDir();
363
+ copyDir(srcDir, join(pluginDest, 'src'));
364
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
365
+ copyDir(safetyDir, join(pluginDest, 'safety'));
366
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
367
+
368
+ registerCodeartsMcp(codeartsMcpSettingsFile());
369
+ registerCodeartsMcp(codeartsProjectMcpSettingsFile());
370
+ }
371
+
372
+ function uninstallCodeArts() {
373
+ let removed = 0;
374
+ for (const skillsDir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
375
+ if (!existsSync(skillsDir)) continue;
376
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
377
+ if (entry.name.startsWith('huawei')) {
378
+ removeIfExists(join(skillsDir, entry.name));
379
+ removed++;
380
+ }
381
+ }
382
+ }
383
+ if (removed > 0) console.log(` Removed ${removed} skills`);
384
+
385
+ if (removeIfExists(codeartsPluginsDir())) {
386
+ console.log(' Removed MCP server and safety policy');
387
+ }
388
+ for (const configPath of [codeartsMcpSettingsFile(), codeartsProjectMcpSettingsFile()]) {
389
+ if (!existsSync(configPath)) continue;
390
+ let config = {};
391
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
392
+ if (config.mcpServers?.huaweicloud) {
393
+ delete config.mcpServers.huaweicloud;
394
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
395
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
396
+ console.log(` Config cleaned: ${configPath}`);
397
+ }
398
+ }
399
+ }
400
+
401
+ function codeartsStatus() {
402
+ const pluginDir = codeartsPluginsDir();
403
+ console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
404
+ console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
405
+ let skillCount = 0;
406
+ if (existsSync(codeartsSkillsDir())) {
407
+ skillCount = readdirSync(codeartsSkillsDir(), { withFileTypes: true })
408
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
409
+ }
410
+ console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
411
+ if (existsSync(codeartsMcpSettingsFile())) {
412
+ try {
413
+ const config = JSON.parse(readFileSync(codeartsMcpSettingsFile(), 'utf8'));
414
+ console.log(` MCP config: ${config.mcpServers?.huaweicloud ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
415
+ } catch {
416
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
417
+ }
418
+ }
419
+ }
420
+
421
+ function opencodeStatus() {
422
+ const pluginDir = opencodePluginsDir();
423
+ const skillsDir = opencodeSkillsDir();
424
+ console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
425
+ console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
426
+ let skillCount = 0;
427
+ if (existsSync(skillsDir)) {
428
+ skillCount = readdirSync(skillsDir, { withFileTypes: true })
429
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
430
+ }
431
+ console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
432
+ const configPath = opencodeConfigFile();
433
+ if (existsSync(configPath)) {
434
+ try {
435
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
436
+ console.log(` MCP config: ${config.mcp?.huaweicloud ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
437
+ } catch {
438
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
439
+ }
440
+ }
441
+ }
442
+
443
+ function parseTarget() {
444
+ const idx = process.argv.indexOf('--target');
445
+ if (idx < 0) return 'opencode';
446
+ const val = (process.argv[idx + 1] || '').toLowerCase();
447
+ if (val === 'codex') return 'codex';
448
+ if (val === 'codex-desktop') return 'codex-desktop';
449
+ if (val === 'codearts') return 'codearts';
450
+ if (val === 'all') return 'all';
451
+ return 'opencode';
452
+ }
453
+
454
+ async function cmdInstall() {
455
+ const target = parseTarget();
456
+ console.log(BANNER);
457
+ console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
458
+ checkNode();
459
+
460
+ if (target === 'opencode' || target === 'all') {
461
+ console.log('[OpenCode]');
462
+ await installOpenCode();
463
+ }
464
+ if (target === 'codex-desktop' || target === 'all') {
465
+ console.log('\n[Codex Desktop]');
466
+ await installCodexDesktop();
467
+ }
468
+ if (target === 'codearts' || target === 'all') {
469
+ console.log('\n[CodeArts]');
470
+ await installCodeArts();
471
+ }
472
+ if (target === 'codex' || target === 'all') {
473
+ console.log('\n[Codex]');
474
+ if (!hasCodexCLI()) {
475
+ if (target === 'codex') {
476
+ console.log(` \x1b[31mCodex CLI not found. Install Codex first: https://github.com/openai/codex-cli\x1b[0m`);
477
+ console.log(` Then re-run: npx huaweicloud-devkit install --target codex`);
478
+ process.exit(1);
479
+ }
480
+ console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
481
+ console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
482
+ } else {
483
+ installCodex();
484
+ }
485
+ }
486
+
487
+ console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
488
+ const appName = target === 'codearts' ? 'CodeArts'
489
+ : target === 'codex-desktop' ? 'Codex Desktop'
490
+ : target === 'codex' ? 'Codex' : 'OpenCode';
491
+ const pad = ' '.repeat(24 - appName.length);
492
+ console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
493
+ console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
494
+ console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 → 重新打开,直接描述华为云任务即可 ║`);
495
+ console.log(`\x1b[1m\x1b[33m重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
496
+ console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
497
+
498
+ const hcloudOk = checkHcloud();
499
+ if (!hcloudOk) {
500
+ console.log(`\n\x1b[33mKooCLI (hcloud) is not installed.`);
501
+ console.log(` Run: npx huaweicloud-devkit install-hcloud\x1b[0m`);
502
+ } else {
503
+ console.log(`\nKooCLI (hcloud) detected.`);
504
+ }
505
+
506
+ console.log(`\nAfter restart + hcloud setup, run: npx huaweicloud-devkit doctor`);
507
+
508
+ // Write install marker for doctor to detect
509
+ const markerDir = target === 'codearts' ? codeartsPluginsDir() : opencodePluginsDir();
510
+ writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
511
+ if (target === 'opencode' || target === 'all') {
512
+ console.log('Or describe your Huawei Cloud task in OpenCode');
513
+ }
514
+ if (target === 'codearts' || target === 'all') {
515
+ console.log('Or describe your Huawei Cloud task in CodeArts');
516
+ }
517
+ if (target === 'codex' || target === 'all') {
518
+ console.log('Or mention @huaweicloud-core in Codex');
519
+ }
520
+ }
521
+
522
+ async function cmdUninstall() {
523
+ const target = parseTarget();
524
+ console.log(BANNER);
525
+ console.log(`Uninstalling HuaweiCloud DevKit${target !== 'opencode' ? ` from ${target}` : ''}...\n`);
526
+
527
+ if (target === 'opencode' || target === 'all') {
528
+ console.log('[OpenCode]');
529
+ await uninstallOpenCode();
530
+ }
531
+ if (target === 'codearts' || target === 'all') {
532
+ console.log('\n[CodeArts]');
533
+ uninstallCodeArts();
534
+ }
535
+ if (target === 'codex' || target === 'all') {
536
+ console.log('\n[Codex]');
537
+ uninstallCodexDesktop();
538
+ if (!hasCodexCLI()) {
539
+ console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
540
+ } else {
541
+ uninstallCodex();
542
+ }
543
+ }
544
+ console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
545
+ }
546
+
547
+ async function cmdStatus() {
548
+ const target = parseTarget();
549
+ console.log(BANNER);
550
+ console.log(`HuaweiCloud DevKit Status\n`);
551
+
552
+ if (target === 'opencode' || target === 'all') {
553
+ console.log('[OpenCode]');
554
+ opencodeStatus();
555
+ }
556
+ if (target === 'codearts' || target === 'all') {
557
+ console.log('\n[CodeArts]');
558
+ codeartsStatus();
559
+ }
560
+ if (target === 'codex' || target === 'all') {
561
+ console.log('\n[Codex]');
562
+ if (!hasCodexCLI()) {
563
+ console.log(' \x1b[33mCodex CLI not found.\x1b[0m');
564
+ } else {
565
+ console.log(` Plugin: ${codexStatus() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
566
+ }
567
+ }
568
+ console.log('\nEnvironment:');
569
+ console.log(` Node.js: ${process.version}`);
570
+ console.log(` Platform: ${platform()}`);
571
+ }
572
+
573
+ async function cmdDoctor() {
574
+ console.log(BANNER);
575
+ console.log('HuaweiCloud DevKit Doctor\n');
576
+
577
+ let pass = 0, warn = 0, fail = 0;
578
+
579
+ function check(label, ok, msg) {
580
+ if (ok) { console.log(` \x1b[32m[PASS]\x1b[0m ${label}`); pass++; }
581
+ else { console.log(` \x1b[31m[FAIL]\x1b[0m ${label} ${msg}`); fail++; }
582
+ }
583
+
584
+ // Node.js
585
+ check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
586
+
587
+ // OpenCode installed files
588
+ const pluginDir = opencodePluginsDir();
589
+ const mcpOk = existsSync(join(pluginDir, 'src', 'mcp-server.mjs'));
590
+ check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit-test install');
591
+
592
+ if (mcpOk) {
593
+ // Try to start MCP server briefly
594
+ const test = spawnSync('node', [join(pluginDir, 'src', 'mcp-server.mjs')], {
595
+ env: { ...process.env, HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
596
+ timeout: 3000, stdio: 'pipe', windowsHide: true,
597
+ });
598
+ // MCP server reads stdin for JSON-RPC, so it will hang briefly then get killed
599
+ // We just check that the process spawned OK
600
+ check('MCP server can start', true, '');
601
+ }
602
+
603
+ const safetyOk = existsSync(join(pluginDir, 'safety', 'policy.json'));
604
+ check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit-test install');
605
+
606
+ const opencodeCfg = opencodeConfigFile();
607
+ let mcpConfigured = false;
608
+ if (existsSync(opencodeCfg)) {
609
+ try {
610
+ const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
611
+ mcpConfigured = !!(cfg.mcp && cfg.mcp.huaweicloud);
612
+ } catch {}
613
+ }
614
+ check('OpenCode MCP configured', mcpConfigured, `Add MCP to ${opencodeCfg} — run: npx huaweicloud-devkit-test install`);
615
+
616
+ // hcloud CLI
617
+ const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
618
+ const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000, input: 'y\n' });
619
+ const hcloudOut = (hcloudCheck.stdout || '').toString() + (hcloudCheck.stderr || '').toString();
620
+ const hcloudOk = hcloudCheck.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(hcloudOut);
621
+ check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
622
+
623
+ // CodeArts sandbox mode warning
624
+ const sandboxMode = detectCodeartsSandbox();
625
+ if (sandboxMode === 'sandbox') {
626
+ console.log(` \x1b[33m[WARN]\x1b[0m CodeArts sandbox mode active (bash_mode: sandbox)`);
627
+ console.log(` KooCLI may fail to write config in ~/.hcloud/ and hang on the privacy agreement.`);
628
+ console.log(` Fix: disable sandbox (Settings → Permissions) or use a terminal outside CodeArts.`);
629
+ warn++;
630
+ }
631
+
632
+ if (hcloudOk) {
633
+ const ver = (hcloudCheck.stdout.toString().match(/(\d+\.\d+\.\d+)/) || [])[1] || 'unknown';
634
+ console.log(` Version: ${ver}`);
635
+
636
+ // Check auth
637
+ const authCheck = spawnSync(`"${hcloudBin}" configure list`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
638
+ const hasAuth = authCheck.status === 0 && /access.?key/i.test(authCheck.stdout.toString());
639
+ check('hcloud credentials configured', hasAuth, 'Run: hcloud configure init');
640
+ }
641
+
642
+ // Skills
643
+ const skillsOptions = [opencodeSkillsDir(), codexDesktopSkillsDir(), codeartsSkillsDir()];
644
+ let skillCount = 0, skillsDir = '';
645
+ for (const dir of skillsOptions) {
646
+ if (!existsSync(dir)) continue;
647
+ const count = readdirSync(dir, { withFileTypes: true })
648
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
649
+ if (count > skillCount) { skillCount = count; skillsDir = dir; }
650
+ }
651
+ const skillsOk = skillCount >= 6;
652
+ check(`Skills installed (${skillCount})`, skillsOk, 'Run: npx huaweicloud-devkit-test install');
653
+
654
+ console.log(`\nResults: ${pass} pass, ${warn} warn, ${fail} fail`);
655
+
656
+ if (mcpConfigured && !hcloudOk) {
657
+ console.log('\n\x1b[33mMCP is configured but hcloud is not installed. Install hcloud then restart OpenCode.\x1b[0m');
658
+ }
659
+ if (fail > 0) {
660
+ console.log('\x1b[33mFix failures above, then restart your OpenCode / Codex session.\x1b[0m');
661
+ }
662
+ if (fail === 0 && mcpConfigured) {
663
+ console.log('\n\x1b[32mAll checks passed.\x1b[0m Restart OpenCode, then describe your Huawei Cloud task');
664
+ }
665
+
666
+ // Detect "installed but not restarted" scenario
667
+ const installedMarker = join(opencodePluginsDir(), '.installed');
668
+ if (mcpConfigured && existsSync(installedMarker)) {
669
+ console.log(`\n\x1b[1m\x1b[31m╔══════════════════════════════════════════╗`);
670
+ console.log(`\x1b[1m\x1b[31m║ 请重启 OpenCode!MCP 工具尚未激活 ║`);
671
+ console.log(`\x1b[1m\x1b[31m║ 关闭当前会话 → 重新打开即可 ║`);
672
+ console.log(`\x1b[1m\x1b[31m╚══════════════════════════════════════════╝\x1b[0m`);
673
+ }
674
+ }
675
+
676
+ async function cmdUpdate() {
677
+ console.log(BANNER);
678
+ const target = parseTarget();
679
+
680
+ if (target === 'opencode' || target === 'all') {
681
+ if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs')) && !codexStatus()) {
682
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
683
+ return;
684
+ }
685
+ }
686
+
687
+ await cmdUninstall();
688
+ console.log('');
689
+ await cmdInstall();
690
+ }
691
+
692
+ async function cmdReinstall() {
693
+ console.log(BANNER);
694
+ if (!(await confirm('This will remove and reinstall all HuaweiCloud DevKit files. Continue?'))) {
695
+ console.log('Cancelled.');
696
+ return;
697
+ }
698
+ confirmed = true;
699
+ await cmdUninstall();
700
+ console.log('');
701
+ await cmdInstall();
702
+ }
703
+
704
+ let confirmed = false;
705
+ async function confirm(msg) {
706
+ if (confirmed) return true;
707
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
708
+ return new Promise((ok) => {
709
+ rl.question(`${msg} [y/N] `, (a) => {
710
+ rl.close();
711
+ ok(a.toLowerCase() === 'y' || a.toLowerCase() === 'yes');
712
+ });
713
+ });
714
+ }
715
+
716
+ async function cmdInstallHcloud() {
717
+ console.log(BANNER);
718
+ console.log('Installing KooCLI (hcloud)...\n');
719
+
720
+ const os = platform();
721
+ const arch = process.arch;
722
+ const baseUrl = 'https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest';
723
+ const installDir = os === 'win32'
724
+ ? join(homedir(), 'hcloud')
725
+ : join(homedir(), '.local', 'bin');
726
+
727
+ if (os === 'win32') {
728
+ const url = `${baseUrl}/huaweicloud-cli-windows-amd64.zip`;
729
+ const zipPath = join(installDir, 'hcloud.zip');
730
+
731
+ console.log(`[Windows] Auto-installing to ${installDir}...`);
732
+
733
+ try {
734
+ mkdirSync(installDir, { recursive: true });
735
+
736
+ // Download
737
+ console.log(` Downloading ${url}...`);
738
+ const psCmd = `[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '${url}' -OutFile '${zipPath}' -UseBasicParsing`;
739
+ const dl = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { stdio: 'inherit', windowsHide: true, timeout: 180000 });
740
+ if (dl.status !== 0) throw new Error(`下载失败 (PowerShell exit ${dl.status})`);
741
+
742
+ // Extract
743
+ console.log(' Extracting...');
744
+ const ex = spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -Path '${zipPath}' -DestinationPath '${installDir}' -Force`], { stdio: 'inherit', windowsHide: true, timeout: 60000 });
745
+ if (ex.status !== 0) throw new Error(`解压失败 (PowerShell exit ${ex.status})`);
746
+ if (!existsSync(join(installDir, 'hcloud.exe'))) throw new Error('hcloud.exe 未生成,可能已被安全软件拦截');
747
+
748
+ // Clean up zip
749
+ rmSync(zipPath, { force: true });
750
+
751
+ // Add to PATH
752
+ console.log(' Adding to user PATH...');
753
+ spawnSync('setx', ['PATH', `${process.env.PATH};${installDir}`], { stdio: 'inherit', windowsHide: true });
754
+
755
+ console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
756
+ console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
757
+
758
+ // Auto-accept the KooCLI privacy agreement so first run does not hang
759
+ console.log('\n Accepting KooCLI privacy agreement...');
760
+ const hcloudBin = join(installDir, 'hcloud.exe');
761
+ if (acceptKooCliPrivacy(hcloudBin)) {
762
+ console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
763
+ } else {
764
+ console.log(' \x1b[33m无法自动接受隐私协议(可能因沙箱阻止写入配置目录)。\x1b[0m');
765
+ if (detectCodeartsSandbox() === 'sandbox') {
766
+ printSandboxWarning('KooCLI 需要写入配置目录 (如 ~/.hcloud/root) 以保存隐私协议同意状态,但沙箱模式阻止了写入。');
767
+ } else {
768
+ console.log(' 请在码道外终端运行一次: hcloud version 并按提示输入 y');
769
+ }
770
+ }
771
+
772
+ console.log(' Or restart terminal and: hcloud version');
773
+ } catch (e) {
774
+ console.log(`\n\x1b[33mAuto-install failed: ${e.message}\x1b[0m`);
775
+ console.log(` Manual: download ${url}, unzip to ${installDir}, add to PATH`);
776
+ console.log(` Guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_01.html`);
777
+ if (detectCodeartsSandbox() === 'sandbox') {
778
+ printSandboxWarning('沙箱模式拦截了 KooCLI 自动安装(无法创建/写入安装目录)。');
779
+ }
780
+ }
781
+ } else if (os === 'linux') {
782
+ console.log('[Linux] One-liner install:');
783
+ 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');
784
+ console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64' : 'AMD64'}`);
785
+ const pkg = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
786
+ console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
787
+ console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
788
+ console.log(` mv hcloud ~/.local/bin/`);
789
+ console.log(` hcloud version`);
790
+ console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_02.html`);
791
+ } else if (os === 'darwin') {
792
+ console.log('[macOS] One-liner install:');
793
+ 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');
794
+ console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64 (Apple Silicon)' : 'AMD64 (Intel)'}`);
795
+ const pkg = arch === 'arm64' ? 'mac-arm64' : 'mac-amd64';
796
+ console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
797
+ console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
798
+ console.log(` mv hcloud /usr/local/bin/`);
799
+ console.log(` hcloud version`);
800
+ console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_03.html`);
801
+ }
802
+
803
+ console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
804
+ console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
805
+ console.log(' Interactive (safe): hcloud configure init');
806
+ console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
807
+ console.log('\nThen run: npx huaweicloud-devkit doctor');
808
+ }
809
+
810
+ async function main() {
811
+ const cmd = process.argv[2] || 'help';
812
+
813
+ switch (cmd) {
814
+ case 'install':
815
+ case 'i':
816
+ await cmdInstall();
817
+ break;
818
+ case 'uninstall':
819
+ case 'remove':
820
+ await cmdUninstall();
821
+ break;
822
+ case 'update':
823
+ case 'upgrade':
824
+ await cmdUpdate();
825
+ break;
826
+ case 'reinstall':
827
+ await cmdReinstall();
828
+ break;
829
+ case 'status':
830
+ case 'info':
831
+ await cmdStatus();
832
+ break;
833
+ case 'doctor':
834
+ case 'check':
835
+ await cmdDoctor();
836
+ break;
837
+ case 'install-hcloud':
838
+ await cmdInstallHcloud();
839
+ break;
840
+ case 'help':
841
+ case '--help':
842
+ case '-h':
843
+ default:
844
+ console.log(BANNER);
845
+ console.log('Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|all>]\n');
846
+ console.log('Commands:');
847
+ console.log(' install Install skills, MCP server, safety policy');
848
+ console.log(' uninstall Remove installed files');
849
+ console.log(' update Update to latest version');
850
+ console.log(' reinstall Full clean reinstall');
851
+ console.log(' status Show installation status');
852
+ console.log(' doctor Self-check: hcloud, MCP, skills, auth');
853
+ console.log(' install-hcloud Show KooCLI install commands for your OS');
854
+ console.log(' help Show this help');
855
+ console.log('\nOptions:');
856
+ console.log(' --target Target agent: opencode (default), codex, codearts, all');
857
+ console.log('\nExamples:');
858
+ console.log(' npx huaweicloud-devkit install');
859
+ console.log(' npx huaweicloud-devkit install --target codex');
860
+ console.log(' npx huaweicloud-devkit install --target codearts');
861
+ console.log(' npx huaweicloud-devkit install --target all');
862
+ break;
863
+ }
864
+ }
865
+
866
+ main().catch((e) => {
867
+ console.error(`\x1b[31mError: ${e.message}\x1b[0m`);
868
+ process.exit(1);
869
+ });