huaweicloud-devkit 1.0.2-dev.1 → 1.0.2-dev.11

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 (33) hide show
  1. package/README.md +27 -3
  2. package/README.zh-CN.md +27 -3
  3. package/package.json +1 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.mcp.json +2 -1
  8. package/plugins/huaweicloud-core/safety/policy.json +15 -1
  9. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
  10. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
  11. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +1 -1
  12. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +1 -1
  13. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
  14. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -0
  15. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +2 -1
  16. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +3 -1
  17. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +1 -1
  18. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
  19. package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -0
  20. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
  21. package/plugins/huaweicloud-core/src/mcp-server.mjs +11 -0
  22. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -0
  23. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
  24. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
  25. package/plugins/huaweicloud-core/src/setup-cli.mjs +560 -71
  26. package/plugins/huaweicloud-core/src/tools.mjs +159 -3
  27. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
  28. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
  29. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
  30. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
  31. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
  32. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
  33. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -0
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
6
6
  import { homedir, platform } from 'node:os';
7
7
  import { createInterface } from 'node:readline';
8
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';
9
11
 
10
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
13
  const PLUGIN_ROOT = resolve(__dirname, '..');
@@ -127,11 +129,21 @@ function removeIfExists(p) {
127
129
 
128
130
  function updateOpenCodeConfig(pluginDir) {
129
131
  const configPath = opencodeConfigFile();
132
+ const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
130
133
  let config = {};
131
134
  if (existsSync(configPath)) {
132
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
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
+ }
133
146
  }
134
- const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
135
147
  config.mcp = config.mcp || {};
136
148
  config.mcp['huaweicloud-devkit'] = {
137
149
  type: 'local',
@@ -296,6 +308,101 @@ function uninstallOpenCode() {
296
308
  removeOpenCodeConfig();
297
309
  }
298
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
+
299
406
  async function installCodexDesktop() {
300
407
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
301
408
  const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
@@ -313,7 +420,7 @@ async function installCodexDesktop() {
313
420
  console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
314
421
 
315
422
  // Generate .mcp.json with absolute paths for Codex Desktop MCP server discovery
316
- const mcpServerAbsPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
423
+ const mcpServerAbsPath = codexMcpServerPath();
317
424
  const mcpConfig = {
318
425
  mcpServers: {
319
426
  'huaweicloud-devkit': {
@@ -333,29 +440,50 @@ async function installCodexDesktop() {
333
440
  console.log(` Plugin Manifest -> ${join(codexDesktopPluginsDir(), '.codex-plugin')}`);
334
441
  }
335
442
 
336
- const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
337
- const configPath = codexConfigToml();
338
- const section = [
339
- '[mcp_servers.huaweicloud-devkit]',
340
- 'command = "node"',
341
- `args = ["${mcpPath}"]`,
342
- '',
343
- '[mcp_servers.huaweicloud-devkit.env]',
344
- 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
345
- '',
346
- ].join('\n');
347
- let existing = '';
348
- if (existsSync(configPath)) {
349
- try { existing = readFileSync(configPath, 'utf8'); } catch {}
350
- if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
351
- console.log(` Config already configured: ${configPath}`);
352
- return;
353
- }
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')}`);
354
483
  }
355
- mkdirSync(dirname(configPath), { recursive: true });
356
- if (existing && !existing.endsWith('\n')) existing += '\n';
357
- writeFileSync(configPath, existing + section);
358
- console.log(` Config updated: ${configPath}`);
484
+
485
+ ensureCodexConfigSection(mcpServerAbsPath);
486
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
359
487
  }
360
488
 
361
489
  function uninstallCodexDesktop() {
@@ -386,32 +514,28 @@ function uninstallCodexDesktop() {
386
514
  if (removeIfExists(codexDesktopPluginsDir())) {
387
515
  console.log(' Removed MCP server and safety policy');
388
516
  }
389
- const configPath = codexConfigToml();
390
- if (existsSync(configPath)) {
391
- const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
392
- const out = [];
393
- let skip = false;
394
- for (const line of lines) {
395
- if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
396
- if (skip && line.startsWith('[')) skip = false;
397
- if (!skip) out.push(line);
398
- }
399
- while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
400
- writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
401
- console.log(' Config cleaned');
402
- }
517
+ removeCodexConfigSection();
403
518
  }
404
519
 
405
520
  function registerCodeartsMcp(configPath) {
406
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, '/');
407
525
  let config = {};
408
526
  if (existsSync(configPath)) {
409
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
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
+ }
410
537
  }
411
538
  config.mcpServers = config.mcpServers || {};
412
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
413
- const hcloudBin = findHcloudBin();
414
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
415
539
  config.mcpServers['huaweicloud-devkit'] = {
416
540
  command: 'node',
417
541
  args: [mcpPath],
@@ -443,6 +567,28 @@ async function installCodeArts() {
443
567
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
444
568
  }
445
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
+
446
592
  function uninstallCodeArts() {
447
593
  let removed = 0;
448
594
  for (const skillsDir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
@@ -492,6 +638,38 @@ function codeartsStatus() {
492
638
  }
493
639
  }
494
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
+
495
673
  async function installWorkBuddy() {
496
674
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
497
675
  const srcDir = join(PLUGIN_ROOT, 'src');
@@ -506,25 +684,26 @@ async function installWorkBuddy() {
506
684
  copyDir(safetyDir, join(pluginDest, 'safety'));
507
685
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
508
686
 
509
- // Write MCP config to ~/.workbuddy/mcp.json
510
- const mcpPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
511
- const configPath = workbuddyMcpConfigFile();
512
- let config = {};
513
- if (existsSync(configPath)) {
514
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
515
- }
516
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
517
- const hcloudBin = findHcloudBin();
518
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
519
- config.mcpServers = config.mcpServers || {};
520
- config.mcpServers['huaweicloud-devkit'] = {
521
- command: 'node',
522
- args: [mcpPath],
523
- env,
524
- };
525
- mkdirSync(dirname(configPath), { recursive: true });
526
- writeFileSync(configPath, JSON.stringify(config, null, 2));
527
- console.log(` MCP config updated: ${configPath}`);
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());
528
707
  }
529
708
 
530
709
  function uninstallWorkBuddy() {
@@ -678,7 +857,10 @@ async function cmdInstall() {
678
857
  console.log(`\nKooCLI (hcloud) detected.`);
679
858
  }
680
859
 
681
- console.log(`\nAfter restart + hcloud setup, run: npx huaweicloud-devkit doctor`);
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`);
682
864
 
683
865
  // Write install marker for doctor to detect
684
866
  const markerDir = target === 'codearts' ? codeartsPluginsDir()
@@ -729,6 +911,19 @@ async function cmdUninstall() {
729
911
  }
730
912
  }
731
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
+ }
732
927
  console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
733
928
  }
734
929
 
@@ -846,7 +1041,7 @@ async function cmdDoctor() {
846
1041
  // Check auth
847
1042
  const authCheck = spawnSync(`"${hcloudBin}" configure list`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
848
1043
  const hasAuth = authCheck.status === 0 && /access.?key/i.test(authCheck.stdout.toString());
849
- check('hcloud credentials configured', hasAuth, 'Run: hcloud configure init');
1044
+ check('hcloud credentials configured', hasAuth, 'Run: npx huaweicloud-devkit auth init');
850
1045
  }
851
1046
 
852
1047
  // Skills
@@ -903,13 +1098,105 @@ async function cmdUpdate() {
903
1098
  console.log(BANNER);
904
1099
  const target = parseTarget();
905
1100
 
906
- if (target === 'opencode' || target === 'all') {
907
- if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))
908
- && !existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))
909
- && !codexStatus()) {
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) {
910
1194
  console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
911
1195
  return;
912
1196
  }
1197
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1198
+ console.log(`\x1b[33mMCP 工具在重启各 agent 会话后才生效。\x1b[0m`);
1199
+ return;
913
1200
  }
914
1201
 
915
1202
  await cmdUninstall();
@@ -976,9 +1263,24 @@ async function cmdInstallHcloud() {
976
1263
  // Clean up zip
977
1264
  rmSync(zipPath, { force: true });
978
1265
 
979
- // Add to PATH
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).
980
1269
  console.log(' Adding to user PATH...');
981
- spawnSync('setx', ['PATH', `${process.env.PATH};${installDir}`], { stdio: 'inherit', windowsHide: true });
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 });
982
1284
 
983
1285
  console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
984
1286
  console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
@@ -1037,11 +1339,191 @@ async function cmdInstallHcloud() {
1037
1339
 
1038
1340
  console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
1039
1341
  console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
1040
- console.log(' Interactive (safe): hcloud configure init');
1342
+ console.log(' Unified credentials (recommended): npx huaweicloud-devkit auth init');
1343
+ console.log(' KooCLI only (alternative): hcloud configure init');
1041
1344
  console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
1042
1345
  console.log('\nThen run: npx huaweicloud-devkit doctor');
1043
1346
  }
1044
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
+
1045
1527
  async function main() {
1046
1528
  const cmd = process.argv[2] || 'help';
1047
1529
 
@@ -1072,6 +1554,9 @@ async function main() {
1072
1554
  case 'install-hcloud':
1073
1555
  await cmdInstallHcloud();
1074
1556
  break;
1557
+ case 'auth':
1558
+ await cmdAuth();
1559
+ break;
1075
1560
  case 'help':
1076
1561
  case '--help':
1077
1562
  case '-h':
@@ -1086,6 +1571,7 @@ async function main() {
1086
1571
  console.log(' status Show installation status');
1087
1572
  console.log(' doctor Self-check: hcloud, MCP, skills, auth');
1088
1573
  console.log(' install-hcloud Show KooCLI install commands for your OS');
1574
+ console.log(' auth Manage unified auth: init | sync | status');
1089
1575
  console.log(' help Show this help');
1090
1576
  console.log('\nOptions:');
1091
1577
  console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, all');
@@ -1095,6 +1581,9 @@ async function main() {
1095
1581
  console.log(' npx huaweicloud-devkit install --target codearts');
1096
1582
  console.log(' npx huaweicloud-devkit install --target workbuddy');
1097
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');
1098
1587
  break;
1099
1588
  }
1100
1589
  }