huaweicloud-devkit 1.1.0-next.9 → 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.
- package/README.md +27 -1
- package/README.zh-CN.md +26 -1
- package/package.json +3 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -12
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +71 -7
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
- package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
- package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
- package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +118 -23
- package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
- package/plugins/huaweicloud-core/src/tools.mjs +37 -15
|
@@ -157,10 +157,13 @@ function readPkg(projectPath) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function detectPackageManager(projectPath) {
|
|
160
|
+
function detectPackageManager(projectPath, pkg) {
|
|
161
161
|
if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
162
162
|
if (existsSync(join(projectPath, 'yarn.lock'))) return 'yarn';
|
|
163
163
|
if (existsSync(join(projectPath, 'package-lock.json'))) return 'npm';
|
|
164
|
+
if (existsSync(join(projectPath, 'bun.lockb'))) return 'bun';
|
|
165
|
+
const declared = (pkg?.packageManager || '').split('@')[0];
|
|
166
|
+
if (declared && ['pnpm', 'yarn', 'npm', 'bun'].includes(declared)) return declared;
|
|
164
167
|
return 'npm';
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -234,10 +237,24 @@ function frameworkResult(fw, pm, projectPath) {
|
|
|
234
237
|
return patchCommands({ ...fw, packageManager: pm, rootDir: projectPath }, pm);
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
function readVitepressOutDir(projectPath) {
|
|
241
|
+
const configFiles = ['config.mts', 'config.ts', 'config.mjs', 'config.js'];
|
|
242
|
+
for (const cf of configFiles) {
|
|
243
|
+
const configPath = join(projectPath, '.vitepress', cf);
|
|
244
|
+
if (!existsSync(configPath)) continue;
|
|
245
|
+
try {
|
|
246
|
+
const content = readFileSync(configPath, 'utf8');
|
|
247
|
+
const match = content.match(/outDir\s*:\s*['"]([^'"]+)['"]/);
|
|
248
|
+
if (match) return match[1];
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
237
254
|
export function detectFramework(projectPath) {
|
|
238
255
|
const pkg = readPkg(projectPath);
|
|
239
256
|
const deps = collectDeps(pkg);
|
|
240
|
-
const pm = detectPackageManager(projectPath);
|
|
257
|
+
const pm = detectPackageManager(projectPath, pkg);
|
|
241
258
|
|
|
242
259
|
if (
|
|
243
260
|
existsSync(join(projectPath, 'pnpm-workspace.yaml')) ||
|
|
@@ -287,7 +304,12 @@ export function detectFramework(projectPath) {
|
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
if (existsSync(join(projectPath, '.vitepress'))) {
|
|
290
|
-
|
|
307
|
+
const outDir = readVitepressOutDir(projectPath);
|
|
308
|
+
return frameworkResult(
|
|
309
|
+
outDir ? { ...FRAMEWORKS.vitepress, outputDir: outDir } : FRAMEWORKS.vitepress,
|
|
310
|
+
pm,
|
|
311
|
+
projectPath,
|
|
312
|
+
);
|
|
291
313
|
}
|
|
292
314
|
|
|
293
315
|
if (
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
2
6
|
|
|
3
7
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
4
8
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
@@ -6,6 +10,46 @@ import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
|
6
10
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
11
|
const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
|
|
8
12
|
const DEFAULT_MAX_RETRIES = 1;
|
|
13
|
+
const APPROVAL_TTL_MS = 5 * 60_000;
|
|
14
|
+
const LARGE_OUTPUT_THRESHOLD = 50_000;
|
|
15
|
+
const OUTPUT_DIR = join('/tmp', 'huaweicloud-devkit');
|
|
16
|
+
|
|
17
|
+
const approvalStore = new Map();
|
|
18
|
+
|
|
19
|
+
export function createApprovalToken(rawArgs) {
|
|
20
|
+
const token = randomUUID();
|
|
21
|
+
approvalStore.set(token, { rawArgs, createdAt: Date.now() });
|
|
22
|
+
if (approvalStore.size % 20 === 0) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for (const [k, v] of approvalStore) {
|
|
25
|
+
if (now - v.createdAt > APPROVAL_TTL_MS) approvalStore.delete(k);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function consumeApprovalToken(token) {
|
|
32
|
+
const entry = approvalStore.get(token);
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) {
|
|
35
|
+
approvalStore.delete(token);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
approvalStore.delete(token);
|
|
39
|
+
return entry.rawArgs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveLargeOutput(rawStdout) {
|
|
43
|
+
if (rawStdout.length <= LARGE_OUTPUT_THRESHOLD) return null;
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
46
|
+
const filePath = join(OUTPUT_DIR, `output-${Date.now()}.json`);
|
|
47
|
+
writeFileSync(filePath, rawStdout, { encoding: 'utf8' });
|
|
48
|
+
return filePath;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
9
53
|
|
|
10
54
|
export function planHcloudCommand(args, options = {}) {
|
|
11
55
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
|
|
@@ -26,6 +70,7 @@ export function planHcloudCommand(args, options = {}) {
|
|
|
26
70
|
executableBlock: redactOutput(command),
|
|
27
71
|
warnings,
|
|
28
72
|
classification,
|
|
73
|
+
approvalToken: createApprovalToken(normalizedArgs),
|
|
29
74
|
safeToRun: classification.decision === 'allow',
|
|
30
75
|
};
|
|
31
76
|
}
|
|
@@ -53,12 +98,22 @@ export async function runHcloud(args, options = {}) {
|
|
|
53
98
|
throw new Error('Unreachable retry state.');
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
function discoverHcloudPath() {
|
|
102
|
+
if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
|
|
103
|
+
const candidates =
|
|
104
|
+
process.platform === 'win32'
|
|
105
|
+
? [join(homedir(), 'hcloud', 'hcloud.exe')]
|
|
106
|
+
: [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
|
|
107
|
+
return candidates.find((c) => existsSync(c)) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
function runHcloudOnce(plan, options) {
|
|
57
111
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
58
112
|
const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
|
|
59
|
-
const executable = options.executable || options.env?.HCLOUD_BIN ||
|
|
113
|
+
const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
|
|
60
114
|
const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
|
|
61
115
|
const cwd = options.cwd || undefined;
|
|
116
|
+
const stdin = options.stdin ?? 'y\n';
|
|
62
117
|
|
|
63
118
|
return new Promise((resolve) => {
|
|
64
119
|
const proxySettings = getProxySettings();
|
|
@@ -78,11 +133,11 @@ function runHcloudOnce(plan, options) {
|
|
|
78
133
|
...options.env,
|
|
79
134
|
},
|
|
80
135
|
});
|
|
81
|
-
if (
|
|
82
|
-
if (typeof
|
|
83
|
-
|
|
136
|
+
if (stdin) {
|
|
137
|
+
if (typeof stdin === 'function') {
|
|
138
|
+
stdin(child.stdin);
|
|
84
139
|
} else {
|
|
85
|
-
child.stdin.write(String(
|
|
140
|
+
child.stdin.write(String(stdin));
|
|
86
141
|
child.stdin.end();
|
|
87
142
|
}
|
|
88
143
|
}
|
|
@@ -99,6 +154,13 @@ function runHcloudOnce(plan, options) {
|
|
|
99
154
|
clearTimeout(timer);
|
|
100
155
|
clearTimeout(forceTimer);
|
|
101
156
|
clearTimeout(settleTimer);
|
|
157
|
+
if (result.stdout && String(result.stdout).length > LARGE_OUTPUT_THRESHOLD) {
|
|
158
|
+
const outputFile = saveLargeOutput(stdout);
|
|
159
|
+
if (outputFile) {
|
|
160
|
+
result.outputFile = outputFile;
|
|
161
|
+
result.stdout = String(result.stdout).slice(0, 2000) + `\n...(truncated, full output saved to ${outputFile})`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
102
164
|
resolve(result);
|
|
103
165
|
}
|
|
104
166
|
|
|
@@ -49,6 +49,29 @@ for (const base of [pluginRoot, packageRoot]) {
|
|
|
49
49
|
let buffer = Buffer.alloc(0);
|
|
50
50
|
let useContentLengthFraming = true;
|
|
51
51
|
|
|
52
|
+
// Keep the event loop alive after stdin is closed (Windows Hermes workaround).
|
|
53
|
+
// Node.js exits when no active handles remain; the stdin 'data' listener is
|
|
54
|
+
// the only handle. On Windows, Hermes may close the stdin pipe after the
|
|
55
|
+
// initial handshake, causing the process to exit silently (exit 0).
|
|
56
|
+
//
|
|
57
|
+
// When stdin closes, start a keepalive timer. When stdout also closes (normal
|
|
58
|
+
// shutdown signal from OfficeAce or other agents), clear the timer and exit.
|
|
59
|
+
let keepAlive = null;
|
|
60
|
+
function onStdinClose() {
|
|
61
|
+
if (keepAlive) return;
|
|
62
|
+
keepAlive = setInterval(() => {}, 60000);
|
|
63
|
+
}
|
|
64
|
+
function onStdoutClose() {
|
|
65
|
+
if (keepAlive) {
|
|
66
|
+
clearInterval(keepAlive);
|
|
67
|
+
keepAlive = null;
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 0;
|
|
70
|
+
}
|
|
71
|
+
stdin.on('close', onStdinClose);
|
|
72
|
+
stdin.on('end', onStdinClose);
|
|
73
|
+
stdout.on('close', onStdoutClose);
|
|
74
|
+
|
|
52
75
|
stdin.on('data', (chunk) => {
|
|
53
76
|
buffer = Buffer.concat([buffer, chunk]);
|
|
54
77
|
readFrames();
|
|
@@ -7,6 +7,12 @@ const HDKIT_BASE_URL =
|
|
|
7
7
|
async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
8
8
|
const { ak, sk, securitytoken } = getCredentials();
|
|
9
9
|
|
|
10
|
+
if (!ak || !sk) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Huawei Cloud credentials are not configured. ' +
|
|
13
|
+
'Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
|
|
14
|
+
);
|
|
15
|
+
}
|
|
10
16
|
const headers = {
|
|
11
17
|
'Content-Type': 'application/json',
|
|
12
18
|
'X-HW-AK': ak,
|
|
@@ -49,10 +55,12 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (!resp.ok) {
|
|
52
|
-
const
|
|
53
|
-
|
|
58
|
+
const code = data.code || `HTTP_${resp.status}`;
|
|
59
|
+
const trace = data.traceId ? ` [trace: ${data.traceId}]` : '';
|
|
60
|
+
const err = new Error(`${code}: ${data.message || 'hdkitservice error'}${trace}`);
|
|
61
|
+
err.code = code;
|
|
54
62
|
err.status = resp.status;
|
|
55
|
-
err.traceId = data.traceId;
|
|
63
|
+
err.traceId = data.traceId;
|
|
56
64
|
throw err;
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -694,7 +694,7 @@ export async function deployNginx(
|
|
|
694
694
|
workspaceId,
|
|
695
695
|
{ nginxType, port, project, outputDir, nodePort, publicPort, configName },
|
|
696
696
|
username = 'root',
|
|
697
|
-
timeoutMs =
|
|
697
|
+
timeoutMs = 60000,
|
|
698
698
|
) {
|
|
699
699
|
if (!workspaceId) {
|
|
700
700
|
throw new Error('sandbox deploy nginx: workspace_id is required.');
|
|
@@ -703,6 +703,53 @@ export async function deployNginx(
|
|
|
703
703
|
throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
|
|
704
704
|
}
|
|
705
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
|
+
|
|
706
753
|
const projectPath = `/workspace/${project}`;
|
|
707
754
|
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
708
755
|
|
|
@@ -715,7 +762,7 @@ fi`;
|
|
|
715
762
|
|
|
716
763
|
const templates = {
|
|
717
764
|
spa: `server {
|
|
718
|
-
listen ${
|
|
765
|
+
listen ${targetPort};
|
|
719
766
|
root ${outputPath};
|
|
720
767
|
index index.html;
|
|
721
768
|
|
|
@@ -729,11 +776,12 @@ fi`;
|
|
|
729
776
|
}
|
|
730
777
|
}`,
|
|
731
778
|
proxy: `server {
|
|
732
|
-
listen ${
|
|
779
|
+
listen ${listenPort};
|
|
733
780
|
server_name _;
|
|
781
|
+
large_client_header_buffers 4 32k;
|
|
734
782
|
|
|
735
783
|
location / {
|
|
736
|
-
proxy_pass http://127.0.0.1:${
|
|
784
|
+
proxy_pass http://127.0.0.1:${effectiveNodePort};
|
|
737
785
|
proxy_http_version 1.1;
|
|
738
786
|
proxy_set_header Upgrade $http_upgrade;
|
|
739
787
|
proxy_set_header Connection 'upgrade';
|
|
@@ -743,10 +791,13 @@ fi`;
|
|
|
743
791
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
744
792
|
proxy_cache_bypass $http_upgrade;
|
|
745
793
|
proxy_read_timeout 60s;
|
|
794
|
+
proxy_buffer_size 128k;
|
|
795
|
+
proxy_buffers 4 256k;
|
|
796
|
+
proxy_busy_buffers_size 256k;
|
|
746
797
|
}
|
|
747
798
|
}`,
|
|
748
799
|
static: `server {
|
|
749
|
-
listen ${
|
|
800
|
+
listen ${targetPort};
|
|
750
801
|
root ${outputPath};
|
|
751
802
|
index index.html;
|
|
752
803
|
|
|
@@ -769,14 +820,14 @@ fi`;
|
|
|
769
820
|
const cmd = [
|
|
770
821
|
resolveScript,
|
|
771
822
|
`sudo mkdir -p /etc/nginx/conf.d`,
|
|
772
|
-
`sudo tee /etc/nginx/conf.d/${configName ||
|
|
823
|
+
`sudo tee /etc/nginx/conf.d/${configName || project}.conf > /dev/null << 'NGINX_EOF'`,
|
|
773
824
|
config,
|
|
774
825
|
`NGINX_EOF`,
|
|
775
826
|
`# Resolve symlinks for chmod (chmod does not follow symlinks on Linux)`,
|
|
776
827
|
`chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
|
|
777
828
|
`find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
778
829
|
`find "$REAL_PROJECT" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
|
|
779
|
-
`if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null; else sudo nginx; fi`,
|
|
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`,
|
|
780
831
|
].join('\n');
|
|
781
832
|
|
|
782
833
|
const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
|
|
@@ -785,7 +836,7 @@ fi`;
|
|
|
785
836
|
try {
|
|
786
837
|
const tunnelCheck = await execOneShot(
|
|
787
838
|
workspaceId,
|
|
788
|
-
'devbridge
|
|
839
|
+
'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
|
|
789
840
|
username,
|
|
790
841
|
10000,
|
|
791
842
|
);
|
|
@@ -795,15 +846,17 @@ fi`;
|
|
|
795
846
|
return {
|
|
796
847
|
ok: result.exitCode === 0,
|
|
797
848
|
nginxType,
|
|
798
|
-
port:
|
|
849
|
+
port: targetPort,
|
|
850
|
+
nodePort: effectiveNodePort || undefined,
|
|
799
851
|
outputPath,
|
|
800
852
|
projectPath,
|
|
801
853
|
exitCode: result.exitCode,
|
|
802
854
|
stdout: result.stdout,
|
|
803
855
|
nextStep: 'expose_via_devbridge',
|
|
804
|
-
warning:
|
|
805
|
-
|
|
806
|
-
|
|
856
|
+
warning:
|
|
857
|
+
(!tunnelActive
|
|
858
|
+
? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
|
|
859
|
+
: portWarning) || undefined,
|
|
807
860
|
};
|
|
808
861
|
}
|
|
809
862
|
|
|
@@ -843,7 +896,23 @@ export async function deployCheck(
|
|
|
843
896
|
`fi`,
|
|
844
897
|
``,
|
|
845
898
|
`TOTAL=$((TOTAL+1))`,
|
|
846
|
-
`
|
|
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`,
|
|
847
916
|
` echo "devbridge_tunnel:PASS"`,
|
|
848
917
|
` PASS=$((PASS+1))`,
|
|
849
918
|
`else`,
|
|
@@ -851,8 +920,9 @@ export async function deployCheck(
|
|
|
851
920
|
`fi`,
|
|
852
921
|
``,
|
|
853
922
|
`TOTAL=$((TOTAL+1))`,
|
|
854
|
-
`
|
|
855
|
-
`
|
|
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`,
|
|
856
926
|
` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
857
927
|
` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
|
|
858
928
|
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
|
|
@@ -864,20 +934,43 @@ export async function deployCheck(
|
|
|
864
934
|
` echo "tunnel_url_accessible:FAIL (no tunnel URL)"`,
|
|
865
935
|
`fi`,
|
|
866
936
|
``,
|
|
867
|
-
`${
|
|
868
|
-
isCrossPlatform
|
|
869
|
-
? `
|
|
937
|
+
`${`
|
|
870
938
|
TOTAL=$((TOTAL+1))
|
|
871
939
|
if [ -f "${outputPath}/qr.png" ]; then
|
|
872
940
|
echo "qr_code:PASS"
|
|
873
941
|
PASS=$((PASS+1))
|
|
874
942
|
else
|
|
875
|
-
|
|
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
|
|
876
970
|
fi
|
|
877
|
-
`
|
|
878
|
-
: ''
|
|
879
|
-
}`,
|
|
971
|
+
`}`,
|
|
880
972
|
`echo "SCORE:\${PASS}/\${TOTAL}"`,
|
|
973
|
+
`echo "TUNNEL_URL:\${TUNNEL_URL:-}"`,
|
|
881
974
|
`[ "\${PASS}" = "\${TOTAL}" ] && echo "VERDICT:COMPLETE" || echo "VERDICT:INCOMPLETE"`,
|
|
882
975
|
]
|
|
883
976
|
.filter(Boolean)
|
|
@@ -888,10 +981,11 @@ fi
|
|
|
888
981
|
const checks = {};
|
|
889
982
|
const lines = stdout.split('\n');
|
|
890
983
|
for (const line of lines) {
|
|
891
|
-
const m = line.match(/^(\w+):(
|
|
984
|
+
const m = line.match(/^(\w+):(\w+)\b(.*)/);
|
|
892
985
|
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
893
986
|
}
|
|
894
987
|
const scoreMatch = stdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
988
|
+
const tunnelMatch = stdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
|
|
895
989
|
const complete = /VERDICT:COMPLETE/.test(stdout);
|
|
896
990
|
|
|
897
991
|
const missing = [];
|
|
@@ -907,6 +1001,7 @@ fi
|
|
|
907
1001
|
checkType: isCrossPlatform ? 'cross-platform' : 'standard',
|
|
908
1002
|
checks,
|
|
909
1003
|
score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
|
|
1004
|
+
publicUrl: tunnelMatch ? tunnelMatch[1] : undefined,
|
|
910
1005
|
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
911
1006
|
nextStep: !complete
|
|
912
1007
|
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
@@ -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);
|