huaweicloud-devkit 1.1.0-next.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +46 -2
  2. package/README.zh-CN.md +45 -2
  3. package/package.json +3 -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/.hermes-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -12
  10. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  11. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
  12. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
  13. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
  16. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
  17. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
  18. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +291 -53
  19. package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
  20. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
  21. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
  22. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
  23. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
  24. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
  25. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
  26. package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
  27. package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
  28. package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
  29. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
  30. package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
  31. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
  32. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +365 -11
  33. package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
  34. package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
  35. package/plugins/huaweicloud-core/src/tools.mjs +184 -14
@@ -299,21 +299,34 @@ async function createTarGz(localDir, exclude = []) {
299
299
  mkdirSync(archiveDir, { recursive: true });
300
300
  const archivePath = join(archiveDir, archiveName);
301
301
 
302
- const args = [];
303
- for (const pattern of exclude) {
304
- if (pattern.startsWith('**/')) {
305
- const base = pattern.slice(3);
306
- for (let depth = 0; depth <= 4; depth++) {
307
- const prefix = depth === 0 ? '' : '*/'.repeat(depth);
308
- args.push('--exclude', `${prefix}${base}`);
302
+ const hasGit = existsSync(join(localDir, '.git'));
303
+ if (hasGit) {
304
+ await execFileAsync('git', [
305
+ '-C',
306
+ localDir,
307
+ 'archive',
308
+ '--format=tar.gz',
309
+ `--prefix=${basename(localDir)}/`,
310
+ `--output=${archivePath}`,
311
+ 'HEAD',
312
+ ]);
313
+ } else {
314
+ const args = [];
315
+ for (const pattern of exclude) {
316
+ if (pattern.startsWith('**/')) {
317
+ const base = pattern.slice(3);
318
+ for (let depth = 0; depth <= 4; depth++) {
319
+ const prefix = depth === 0 ? '' : '*/'.repeat(depth);
320
+ args.push('--exclude', `${prefix}${base}`);
321
+ }
322
+ } else {
323
+ args.push('--exclude', pattern);
309
324
  }
310
- } else {
311
- args.push('--exclude', pattern);
312
325
  }
326
+ args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
327
+ await execFileAsync('tar', args);
313
328
  }
314
- args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
315
329
 
316
- await execFileAsync('tar', args);
317
330
  return archivePath;
318
331
  }
319
332
 
@@ -643,6 +656,22 @@ export async function uploadProjectWithSession(
643
656
  username,
644
657
  timeoutMs,
645
658
  );
659
+ try {
660
+ await execWithSession(
661
+ workspaceId,
662
+ [
663
+ `REAL_PATH=$(readlink -f "${targetParentDir}/${projectName}" 2>/dev/null || echo "${targetParentDir}/${projectName}")`,
664
+ `if [ ! -d "$REAL_PATH" ] && [ -d "${targetParentDir}" ]; then`,
665
+ ` REAL_PATH="${targetParentDir}/${projectName}"`,
666
+ `fi`,
667
+ `chmod -R o+rX "$REAL_PATH" 2>/dev/null || true`,
668
+ `find "$REAL_PATH" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
669
+ `find "$REAL_PATH" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
670
+ ].join('\n'),
671
+ username,
672
+ 15000,
673
+ );
674
+ } catch {}
646
675
  }
647
676
 
648
677
  try {
@@ -661,6 +690,331 @@ export async function uploadProjectWithSession(
661
690
  };
662
691
  }
663
692
 
693
+ export async function deployNginx(
694
+ workspaceId,
695
+ { nginxType, port, project, outputDir, nodePort, publicPort, configName },
696
+ username = 'root',
697
+ timeoutMs = 60000,
698
+ ) {
699
+ if (!workspaceId) {
700
+ throw new Error('sandbox deploy nginx: workspace_id is required.');
701
+ }
702
+ if (!nginxType || !port || !project || !outputDir) {
703
+ throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
704
+ }
705
+
706
+ const nginxCheck = await execOneShot(
707
+ workspaceId,
708
+ 'command -v nginx >/dev/null 2>&1 && echo "INSTALLED" || echo "MISSING"',
709
+ username,
710
+ 10000,
711
+ );
712
+ if (!String(nginxCheck.stdout || '').includes('INSTALLED')) {
713
+ throw new Error(
714
+ 'sandbox deploy nginx: nginx is not installed. Install it first:\n' +
715
+ ' Detect OS: source /etc/os-release && echo $ID\n' +
716
+ ' apt: sudo apt-get update -qq && sudo apt-get install -y -qq nginx\n' +
717
+ ' yum: sudo yum install -y nginx\n' +
718
+ ' dnf: sudo dnf install -y nginx\n' +
719
+ 'Alternatively, skip nginx and use Python HTTP server (see nginx-templates.md).',
720
+ );
721
+ }
722
+
723
+ const listenPort = publicPort || port;
724
+ const basePort = nginxType === 'proxy' ? listenPort : port;
725
+
726
+ let targetPort = basePort;
727
+ let portWarning;
728
+ const maxPortAttempts = 10;
729
+ for (let offset = 0; offset < maxPortAttempts; offset += 1) {
730
+ targetPort = basePort + offset;
731
+ try {
732
+ const portCheck = await execOneShot(
733
+ workspaceId,
734
+ `ss -tlnp 2>/dev/null | grep -q ":${targetPort} " && echo "IN_USE" || echo "FREE"`,
735
+ username,
736
+ 10000,
737
+ );
738
+ if (!String(portCheck.stdout || '').includes('IN_USE')) break;
739
+ if (offset === 0) {
740
+ portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
741
+ }
742
+ } catch {}
743
+ if (offset === maxPortAttempts - 1) {
744
+ throw new Error(
745
+ `sandbox deploy nginx: all ports ${basePort}-${basePort + maxPortAttempts - 1} are in use. Free a port and try again.`,
746
+ );
747
+ }
748
+ }
749
+
750
+ const effectiveNodePort =
751
+ nginxType === 'proxy' ? (nodePort && nodePort !== listenPort ? nodePort : listenPort + 1) : undefined;
752
+
753
+ const projectPath = `/workspace/${project}`;
754
+ const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
755
+
756
+ const resolveScript = `REAL_PROJECT=$(readlink -f "${projectPath}" 2>/dev/null || echo "${projectPath}")
757
+ REAL_OUTPUT="${outputPath}"
758
+ if [ "\${REAL_PROJECT}" != "${projectPath}" ]; then
759
+ REL_OUTPUT=$(echo "${outputDir}" | sed "s|${projectPath}/||")
760
+ REAL_OUTPUT="\${REAL_PROJECT}/\${REL_OUTPUT}"
761
+ fi`;
762
+
763
+ const templates = {
764
+ spa: `server {
765
+ listen ${targetPort};
766
+ root ${outputPath};
767
+ index index.html;
768
+
769
+ location / {
770
+ try_files $uri /index.html;
771
+ }
772
+
773
+ location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
774
+ expires 1h;
775
+ add_header Cache-Control "public, immutable";
776
+ }
777
+ }`,
778
+ proxy: `server {
779
+ listen ${listenPort};
780
+ server_name _;
781
+ large_client_header_buffers 4 32k;
782
+
783
+ location / {
784
+ proxy_pass http://127.0.0.1:${effectiveNodePort};
785
+ proxy_http_version 1.1;
786
+ proxy_set_header Upgrade $http_upgrade;
787
+ proxy_set_header Connection 'upgrade';
788
+ proxy_set_header Host $host;
789
+ proxy_set_header X-Real-IP $remote_addr;
790
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
791
+ proxy_set_header X-Forwarded-Proto $scheme;
792
+ proxy_cache_bypass $http_upgrade;
793
+ proxy_read_timeout 60s;
794
+ proxy_buffer_size 128k;
795
+ proxy_buffers 4 256k;
796
+ proxy_busy_buffers_size 256k;
797
+ }
798
+ }`,
799
+ static: `server {
800
+ listen ${targetPort};
801
+ root ${outputPath};
802
+ index index.html;
803
+
804
+ location / {
805
+ autoindex off;
806
+ }
807
+
808
+ location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
809
+ expires 1h;
810
+ add_header Cache-Control "public, immutable";
811
+ }
812
+ }`,
813
+ };
814
+
815
+ const config = templates[nginxType];
816
+ if (!config) {
817
+ throw new Error(`sandbox deploy nginx: unknown nginxType "${nginxType}". Must be one of: spa, proxy, static`);
818
+ }
819
+
820
+ const cmd = [
821
+ resolveScript,
822
+ `sudo mkdir -p /etc/nginx/conf.d`,
823
+ `sudo tee /etc/nginx/conf.d/${configName || project}.conf > /dev/null << 'NGINX_EOF'`,
824
+ config,
825
+ `NGINX_EOF`,
826
+ `# Resolve symlinks for chmod (chmod does not follow symlinks on Linux)`,
827
+ `chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
828
+ `find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
829
+ `find "$REAL_PROJECT" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
830
+ `if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null || { sudo killall -9 nginx 2>/dev/null; sleep 1; sudo nginx; }; else sudo nginx; fi`,
831
+ ].join('\n');
832
+
833
+ const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
834
+
835
+ let tunnelActive = false;
836
+ try {
837
+ const tunnelCheck = await execOneShot(
838
+ workspaceId,
839
+ 'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
840
+ username,
841
+ 10000,
842
+ );
843
+ tunnelActive = String(tunnelCheck.stdout || '').includes('ACTIVE');
844
+ } catch {}
845
+
846
+ return {
847
+ ok: result.exitCode === 0,
848
+ nginxType,
849
+ port: targetPort,
850
+ nodePort: effectiveNodePort || undefined,
851
+ outputPath,
852
+ projectPath,
853
+ exitCode: result.exitCode,
854
+ stdout: result.stdout,
855
+ nextStep: 'expose_via_devbridge',
856
+ warning:
857
+ (!tunnelActive
858
+ ? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
859
+ : portWarning) || undefined,
860
+ };
861
+ }
862
+
863
+ export async function deployCheck(
864
+ workspaceId,
865
+ { port, project, outputDir, frameworkType },
866
+ username = 'root',
867
+ timeoutMs = 30000,
868
+ ) {
869
+ if (!workspaceId) {
870
+ throw new Error('sandbox deploy check: workspace_id is required.');
871
+ }
872
+
873
+ const projectPath = `/workspace/${project}`;
874
+ const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
875
+ const isCrossPlatform = frameworkType === 'cross-platform';
876
+
877
+ const checkScript = [
878
+ `echo "=== DEPLOY CHECK ==="`,
879
+ `PASS=0`,
880
+ `TOTAL=0`,
881
+ ``,
882
+ `TOTAL=$((TOTAL+1))`,
883
+ `if curl -s -o /dev/null -w "%{http_code}" http://localhost:${port} 2>/dev/null | grep -qE "^(2|3)"; then`,
884
+ ` echo "nginx_serving:PASS (port ${port})"`,
885
+ ` PASS=$((PASS+1))`,
886
+ `else`,
887
+ ` echo "nginx_serving:FAIL"`,
888
+ `fi`,
889
+ ``,
890
+ `TOTAL=$((TOTAL+1))`,
891
+ `if [ -d "${outputPath}" ] && ls -A "${outputPath}" 2>/dev/null | grep -q .; then`,
892
+ ` echo "output_dir:PASS (${outputPath})"`,
893
+ ` PASS=$((PASS+1))`,
894
+ `else`,
895
+ ` echo "output_dir:FAIL (${outputPath} empty or missing)"`,
896
+ `fi`,
897
+ ``,
898
+ `TOTAL=$((TOTAL+1))`,
899
+ `FINGERPRINT_FILE="${outputPath}/.deploy_fingerprint"`,
900
+ `FINGERPRINT_EXPECTED=$(cat "$FINGERPRINT_FILE" 2>/dev/null)`,
901
+ `if [ -n "$FINGERPRINT_EXPECTED" ]; then`,
902
+ ` FINGERPRINT_ACTUAL=$(curl -s http://localhost:${port}/.deploy_fingerprint 2>/dev/null)`,
903
+ ` if [ "$FINGERPRINT_EXPECTED" = "$FINGERPRINT_ACTUAL" ]; then`,
904
+ ` echo "content_verified:PASS"`,
905
+ ` PASS=$((PASS+1))`,
906
+ ` else`,
907
+ ` echo "content_verified:FAIL (fingerprint mismatch — nginx may be serving stale content from a previous deployment)"`,
908
+ ` fi`,
909
+ `else`,
910
+ ` echo "content_verified:SKIP (no fingerprint file)"`,
911
+ ` TOTAL=$((TOTAL-1))`,
912
+ `fi`,
913
+ ``,
914
+ `TOTAL=$((TOTAL+1))`,
915
+ `if devbridge list -j 2>/dev/null | grep -q '"tunnelId"'; then`,
916
+ ` echo "devbridge_tunnel:PASS"`,
917
+ ` PASS=$((PASS+1))`,
918
+ `else`,
919
+ ` echo "devbridge_tunnel:FAIL"`,
920
+ `fi`,
921
+ ``,
922
+ `TOTAL=$((TOTAL+1))`,
923
+ `TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
924
+ `TUNNEL_URL="https://\${TUNNEL_ID}-${port}.cn-north-4-bridge.myhuaweicloud.com"`,
925
+ `if [ -n "$TUNNEL_ID" ] && [ -n "$TUNNEL_URL" ]; then`,
926
+ ` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
927
+ ` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
928
+ ` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
929
+ ` PASS=$((PASS+1))`,
930
+ ` else`,
931
+ ` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> HTTP $HTTP_CODE)"`,
932
+ ` fi`,
933
+ `else`,
934
+ ` echo "tunnel_url_accessible:FAIL (no tunnel URL)"`,
935
+ `fi`,
936
+ ``,
937
+ `${`
938
+ TOTAL=$((TOTAL+1))
939
+ if [ -f "${outputPath}/qr.png" ]; then
940
+ echo "qr_code:PASS"
941
+ PASS=$((PASS+1))
942
+ else
943
+ # Auto-detect cross-platform from project files (more reliable than frameworkType param)
944
+ PROJ_DIR="/workspace/${project}"
945
+ IS_CROSS=0
946
+ if [ -f "$PROJ_DIR/manifest.json" ] || grep -qE '"@tarojs/taro"|"@dcloudio/uni-app"' "$PROJ_DIR/package.json" 2>/dev/null; then
947
+ IS_CROSS=1
948
+ fi
949
+ if [ $IS_CROSS -eq 0 ] && ( [ -f "$PROJ_DIR/app.config.ts" ] || [ -f "$PROJ_DIR/app.config.js" ] ) && grep -qE "pages|tabBar" "$PROJ_DIR/app.config."* 2>/dev/null; then
950
+ IS_CROSS=1
951
+ fi
952
+
953
+ if [ $IS_CROSS -eq 1 ]; then
954
+ if [ -n "$TUNNEL_URL" ]; then
955
+ curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$TUNNEL_URL" 2>/dev/null)" -o "${outputPath}/qr.png" 2>/dev/null
956
+ chmod o+r "${outputPath}/qr.png" 2>/dev/null || true
957
+ if [ -f "${outputPath}/qr.png" ] && [ -s "${outputPath}/qr.png" ]; then
958
+ echo "qr_code:PASS (auto-generated)"
959
+ PASS=$((PASS+1))
960
+ else
961
+ echo "qr_code:FAIL (QR generation failed)"
962
+ fi
963
+ else
964
+ echo "qr_code:FAIL (no tunnel URL for QR generation)"
965
+ fi
966
+ else
967
+ echo "qr_code:SKIP (not a cross-platform project)"
968
+ TOTAL=$((TOTAL-1))
969
+ fi
970
+ fi
971
+ `}`,
972
+ `echo "SCORE:\${PASS}/\${TOTAL}"`,
973
+ `echo "TUNNEL_URL:\${TUNNEL_URL:-}"`,
974
+ `[ "\${PASS}" = "\${TOTAL}" ] && echo "VERDICT:COMPLETE" || echo "VERDICT:INCOMPLETE"`,
975
+ ]
976
+ .filter(Boolean)
977
+ .join('\n');
978
+
979
+ const result = await execOneShot(workspaceId, checkScript, username, timeoutMs);
980
+ const stdout = String(result.stdout || '');
981
+ const checks = {};
982
+ const lines = stdout.split('\n');
983
+ for (const line of lines) {
984
+ const m = line.match(/^(\w+):(\w+)\b(.*)/);
985
+ if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
986
+ }
987
+ const scoreMatch = stdout.match(/SCORE:(\d+)\/(\d+)/);
988
+ const tunnelMatch = stdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
989
+ const complete = /VERDICT:COMPLETE/.test(stdout);
990
+
991
+ const missing = [];
992
+ if (!complete) {
993
+ for (const [key, val] of Object.entries(checks)) {
994
+ if (val.status === 'FAIL') missing.push(key);
995
+ }
996
+ }
997
+
998
+ return {
999
+ ok: true,
1000
+ complete,
1001
+ checkType: isCrossPlatform ? 'cross-platform' : 'standard',
1002
+ checks,
1003
+ score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
1004
+ publicUrl: tunnelMatch ? tunnelMatch[1] : undefined,
1005
+ missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
1006
+ nextStep: !complete
1007
+ ? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
1008
+ ? 'expose_via_devbridge'
1009
+ : missing.includes('nginx_serving')
1010
+ ? 'configure_nginx'
1011
+ : missing.includes('qr_code')
1012
+ ? 'generate_qr_code'
1013
+ : 'review_checks'
1014
+ : 'complete',
1015
+ };
1016
+ }
1017
+
664
1018
  export async function closeSession(workspaceId, username) {
665
1019
  const key = `${workspaceId}:${username}`;
666
1020
  const session = sessions.get(key);
@@ -171,6 +171,7 @@ export async function searchMarketplace(query = '', category = '') {
171
171
  description: truncate(skill.description),
172
172
  triggers: (skill.triggers || []).slice(0, 5),
173
173
  matched,
174
+ installCommand: `npx skills add huaweicloud/huaweicloud-skills --skill ${skill.name}`,
174
175
  });
175
176
  }
176
177
  results.sort((a, b) => b.score - a.score);