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.
- package/README.md +27 -3
- package/README.zh-CN.md +27 -3
- package/package.json +1 -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/.mcp.json +2 -1
- package/plugins/huaweicloud-core/safety/policy.json +15 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +2 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +3 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +1 -1
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +11 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -0
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +560 -71
- package/plugins/huaweicloud-core/src/tools.mjs +159 -3
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
- package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
// Verify a credential file ended up with 0600. On Windows-mounted drives inside WSL
|
|
6
|
+
// (drvfs/9p) chmod is silently ignored, so the file can be world-readable (0777).
|
|
7
|
+
// Native Windows has no POSIX modes (statSync always reports 0666), so skip the check there.
|
|
8
|
+
function ensurePrivateMode(path) {
|
|
9
|
+
if (process.platform === 'win32') return;
|
|
10
|
+
try { chmodSync(path, 0o600); } catch {}
|
|
11
|
+
try {
|
|
12
|
+
const mode = statSync(path).mode & 0o777;
|
|
13
|
+
if (mode !== 0o600) {
|
|
14
|
+
console.warn(`\x1b[33m[WARN]\x1b[0m Could not set 0600 on ${path} (current mode ${mode.toString(8)}). Credentials may be readable by other users.`);
|
|
15
|
+
console.warn(`\x1b[33m If running under WSL, move the credential home to the Linux filesystem:\x1b[0m`);
|
|
16
|
+
console.warn(`\x1b[33m export HUAWEICLOUD_HOME=$HOME (then re-run auth init)\x1b[0m`);
|
|
17
|
+
console.warn(`\x1b[33m Or skip file storage entirely with HW_ACCESS_KEY/HW_SECRET_KEY environment variables.\x1b[0m`);
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function baseHome() {
|
|
23
|
+
return process.env.HUAWEICLOUD_HOME || homedir();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function globalCredentialsPath() {
|
|
27
|
+
return join(baseHome(), '.config', 'huaweicloud', 'credentials.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function obsConfigPath() {
|
|
31
|
+
return join(baseHome(), '.obsutilconfig');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readGlobalCredentials() {
|
|
35
|
+
const path = globalCredentialsPath();
|
|
36
|
+
if (!existsSync(path)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeGlobalCredentials(credentials = {}) {
|
|
45
|
+
const path = globalCredentialsPath();
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
47
|
+
const payload = {
|
|
48
|
+
ak: String(credentials.ak || ''),
|
|
49
|
+
sk: String(credentials.sk || ''),
|
|
50
|
+
securityToken: String(credentials.securityToken || ''),
|
|
51
|
+
region: String(credentials.region || ''),
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(path, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
54
|
+
ensurePrivateMode(path);
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeObsConfig(credentials = {}) {
|
|
59
|
+
const region = String(credentials.region || '');
|
|
60
|
+
const ak = String(credentials.ak || '');
|
|
61
|
+
const sk = String(credentials.sk || '');
|
|
62
|
+
const securityToken = String(credentials.securityToken || '');
|
|
63
|
+
if (!region || !ak || !sk) {
|
|
64
|
+
throw new Error('region, ak, and sk are required to write OBS config');
|
|
65
|
+
}
|
|
66
|
+
const path = obsConfigPath();
|
|
67
|
+
const endpoint = credentials.endpoint || `https://obs.${region}.myhuaweicloud.com`;
|
|
68
|
+
// Flat key=value format (no [default] section) as written by KooCLI 7.x `hcloud OBS config`.
|
|
69
|
+
const content = `endpoint=${endpoint}\nak=${ak}\nsk=${sk}${securityToken ? `\ntoken=${securityToken}` : ''}\n`;
|
|
70
|
+
writeFileSync(path, content, { encoding: 'utf8', mode: 0o600 });
|
|
71
|
+
ensurePrivateMode(path);
|
|
72
|
+
return { path, endpoint };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveCredentials(options = {}) {
|
|
76
|
+
let ak = process.env.HW_ACCESS_KEY;
|
|
77
|
+
let sk = process.env.HW_SECRET_KEY;
|
|
78
|
+
let securityToken = process.env.HW_SECURITY_TOKEN;
|
|
79
|
+
let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
|
|
80
|
+
|
|
81
|
+
const stored = readGlobalCredentials();
|
|
82
|
+
if (stored) {
|
|
83
|
+
if (!ak && stored.ak) ak = stored.ak;
|
|
84
|
+
if (!sk && stored.sk) sk = stored.sk;
|
|
85
|
+
if (!securityToken && stored.securityToken) securityToken = stored.securityToken;
|
|
86
|
+
if (!region && stored.region) region = stored.region;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!ak || !sk) {
|
|
90
|
+
if (options.allowMissing) return null;
|
|
91
|
+
throw new Error('Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { ak, sk, securityToken, region };
|
|
95
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
4
|
+
import { globalCredentialsPath, obsConfigPath, readGlobalCredentials, writeObsConfig } from './credentials.mjs';
|
|
5
|
+
|
|
6
|
+
function hcloudInstalled() {
|
|
7
|
+
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
8
|
+
try {
|
|
9
|
+
const r = spawnSync(`"${bin}" version`, [], {
|
|
10
|
+
shell: true,
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
timeout: 5000,
|
|
14
|
+
});
|
|
15
|
+
const out = `${r.stdout || ''}${r.stderr || ''}`;
|
|
16
|
+
return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getAuthStatus(target = 'all') {
|
|
23
|
+
const credentials = readGlobalCredentials();
|
|
24
|
+
return {
|
|
25
|
+
target,
|
|
26
|
+
credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
|
|
27
|
+
credentialsPath: globalCredentialsPath(),
|
|
28
|
+
obsConfigured: existsSync(obsConfigPath()),
|
|
29
|
+
obsConfigPath: obsConfigPath(),
|
|
30
|
+
kooCliInstalled: hcloudInstalled(),
|
|
31
|
+
agents: getAgentRegistrationStatuses(target).agents,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function syncAuth(target = 'all') {
|
|
36
|
+
const credentials = readGlobalCredentials();
|
|
37
|
+
if (!credentials?.ak || !credentials?.sk) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'Global credentials are not configured.',
|
|
41
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" first.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let obs = null;
|
|
46
|
+
try {
|
|
47
|
+
obs = writeObsConfig(credentials);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: error.message,
|
|
52
|
+
nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
59
|
+
credentialsConfigured: true,
|
|
60
|
+
agents: getAgentRegistrationStatuses(target).agents,
|
|
61
|
+
note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
4
7
|
|
|
8
|
+
// The MCP server is now loaded by a live agent session. Clear the install marker
|
|
9
|
+
// in this plugin dir so `doctor` no longer reports "restart needed".
|
|
10
|
+
try {
|
|
11
|
+
const pluginDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const marker = resolve(pluginDir, '.installed');
|
|
13
|
+
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
5
16
|
let buffer = Buffer.alloc(0);
|
|
6
17
|
let useContentLengthFraming = true;
|
|
7
18
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getCredentials } from './hwlink-api.mjs';
|
|
2
|
+
|
|
3
|
+
const HDKIT_BASE_URL =
|
|
4
|
+
process.env.HDKITSERVICE_ENDPOINT ||
|
|
5
|
+
'https://devkit.huaweicloud.com/rest/developer/server/hdkitservice/';
|
|
6
|
+
|
|
7
|
+
async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
8
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
9
|
+
|
|
10
|
+
const headers = {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'X-HW-AK': ak,
|
|
13
|
+
'X-HW-SK': sk,
|
|
14
|
+
};
|
|
15
|
+
if (securitytoken) {
|
|
16
|
+
headers['X-HW-Security-Token'] = securitytoken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const url = `${HDKIT_BASE_URL}${path}`;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
|
|
23
|
+
let resp;
|
|
24
|
+
try {
|
|
25
|
+
resp = await fetch(url, {
|
|
26
|
+
method,
|
|
27
|
+
headers,
|
|
28
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const text = await resp.text();
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`hdkitservice returned non-JSON (status ${resp.status}): ${text.slice(0, 200)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
const err = new Error(
|
|
45
|
+
data.message || `hdkitservice error: ${data.code || resp.status}`
|
|
46
|
+
);
|
|
47
|
+
err.code = data.code;
|
|
48
|
+
err.status = resp.status;
|
|
49
|
+
err.traceId = data.traceId; // 后端实际返回驼峰 traceId
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function hdkitCheckUser() {
|
|
57
|
+
return await hdkitRequest('GET', 'check-user', undefined, 30000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function hdkitSignAgreement() {
|
|
61
|
+
return await hdkitRequest('POST', 'sign-agreement', {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function hdkitConnect(options = {}) {
|
|
65
|
+
const body = {};
|
|
66
|
+
if (options.source) body.source = options.source;
|
|
67
|
+
if (options.env) body.env = options.env;
|
|
68
|
+
if (options.git) body.git = options.git;
|
|
69
|
+
if (options.template_id) body.template_id = options.template_id;
|
|
70
|
+
if (options.flavor_id) body.flavor_id = options.flavor_id;
|
|
71
|
+
|
|
72
|
+
return await hdkitRequest('POST', 'connect', body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function hdkitCredentials(sessionId, devStageId, enableSts = true) {
|
|
76
|
+
const body = { enable_sts: enableSts };
|
|
77
|
+
if (sessionId) body.session_id = sessionId;
|
|
78
|
+
if (devStageId) body.dev_stage_id = devStageId;
|
|
79
|
+
|
|
80
|
+
if (!sessionId && !devStageId) {
|
|
81
|
+
throw new Error('session_id or dev_stage_id is required');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return await hdkitRequest('POST', 'credentials', body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { resolveCredentials } from '../auth/credentials.mjs';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = process.env.HWLINK_ENDPOINT || 'https://devstation.myhuaweicloud.com';
|
|
5
|
+
|
|
6
|
+
function sha256Hex(data) {
|
|
7
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hmacSha256(key, data) {
|
|
11
|
+
return crypto.createHmac('sha256', key).update(data).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function urlEncode(str) {
|
|
15
|
+
const hex = (c) => '%' + (c < 16 ? '0' : '') + c.toString(16).toUpperCase();
|
|
16
|
+
const noEscape = new Set(
|
|
17
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'.split('')
|
|
18
|
+
);
|
|
19
|
+
let out = '';
|
|
20
|
+
for (const ch of str) {
|
|
21
|
+
const c = ch.codePointAt(0);
|
|
22
|
+
out += noEscape.has(ch) && c < 0x80 ? ch : c < 0x80 ? hex(c) : encodeURIComponent(ch);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function timestamp() {
|
|
28
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '') + 'Z';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sortedQs(query) {
|
|
32
|
+
return Object.entries(query)
|
|
33
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
34
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
35
|
+
.join('&');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signRequest(method, path, query, body, ak, sk, securitytoken) {
|
|
39
|
+
const ts = timestamp();
|
|
40
|
+
const host = new URL(BASE_URL).host;
|
|
41
|
+
|
|
42
|
+
const cqs = Object.entries(query)
|
|
43
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
44
|
+
.map(([k, v]) => `${urlEncode(k)}=${urlEncode(v)}`)
|
|
45
|
+
.join('&');
|
|
46
|
+
|
|
47
|
+
const curi =
|
|
48
|
+
'/' +
|
|
49
|
+
path
|
|
50
|
+
.split('/')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map((s) => urlEncode(s))
|
|
53
|
+
.join('/') +
|
|
54
|
+
'/';
|
|
55
|
+
|
|
56
|
+
const signedHeaders = securitytoken
|
|
57
|
+
? 'host;x-sdk-date;x-security-token'
|
|
58
|
+
: 'host;x-sdk-date';
|
|
59
|
+
const canonicalHeaders = securitytoken
|
|
60
|
+
? `host:${host}\nx-sdk-date:${ts}\nx-security-token:${securitytoken}\n`
|
|
61
|
+
: `host:${host}\nx-sdk-date:${ts}\n`;
|
|
62
|
+
|
|
63
|
+
const bodyStr = body ? JSON.stringify(body) : '';
|
|
64
|
+
const payloadHash = sha256Hex(bodyStr);
|
|
65
|
+
|
|
66
|
+
const canonicalRequest = [
|
|
67
|
+
method,
|
|
68
|
+
curi,
|
|
69
|
+
cqs,
|
|
70
|
+
canonicalHeaders,
|
|
71
|
+
signedHeaders,
|
|
72
|
+
payloadHash,
|
|
73
|
+
].join('\n');
|
|
74
|
+
|
|
75
|
+
const stringToSign = `SDK-HMAC-SHA256\n${ts}\n${sha256Hex(canonicalRequest)}`;
|
|
76
|
+
const signature = hmacSha256(sk, stringToSign);
|
|
77
|
+
|
|
78
|
+
const headers = {
|
|
79
|
+
host,
|
|
80
|
+
'x-sdk-date': ts,
|
|
81
|
+
Authorization: `SDK-HMAC-SHA256 Access=${ak}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
82
|
+
};
|
|
83
|
+
if (securitytoken) {
|
|
84
|
+
headers['x-security-token'] = securitytoken;
|
|
85
|
+
}
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getCredentials() {
|
|
90
|
+
const credentials = resolveCredentials();
|
|
91
|
+
return { ak: credentials.ak, sk: credentials.sk, securitytoken: credentials.securityToken };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function apiGet(path, query, ak, sk, securitytoken) {
|
|
95
|
+
query = query || {};
|
|
96
|
+
const qs = sortedQs(query);
|
|
97
|
+
const fullPath = qs ? `${path}?${qs}` : path;
|
|
98
|
+
const headers = signRequest('GET', path, query, undefined, ak, sk, securitytoken);
|
|
99
|
+
const resp = await fetch(`${BASE_URL}${fullPath}`, { headers });
|
|
100
|
+
return { status: resp.status, data: await resp.json() };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function apiPost(path, body, ak, sk, securitytoken) {
|
|
104
|
+
const headers = signRequest('POST', path, {}, body, ak, sk, securitytoken);
|
|
105
|
+
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
return { status: resp.status, data: await resp.json() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createConnection(envId, ak, sk, securitytoken) {
|
|
114
|
+
const { status, data } = await apiPost(
|
|
115
|
+
`/open-api-public/v1/devenvs/${envId}/connections`,
|
|
116
|
+
{ source: 'CLI' },
|
|
117
|
+
ak, sk, securitytoken
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (status !== 200 || data?.error_code !== '0000' || !data?.result?.connection_id) {
|
|
121
|
+
throw new Error(`Failed to create connection: ${JSON.stringify(data)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const connectionId = data.result.connection_id;
|
|
125
|
+
const maxAttempts = 60;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
128
|
+
process.stderr.write(`\rWaiting for connection ${connectionId}... (${i}s)`);
|
|
129
|
+
const { data: getData } = await apiGet(
|
|
130
|
+
`/open-api-public/v1/devenvs/${envId}/connections/${connectionId}`,
|
|
131
|
+
{},
|
|
132
|
+
ak, sk, securitytoken
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
getData?.result?.connection_info?.url &&
|
|
137
|
+
getData.result.connection_info.extensions?.source != null
|
|
138
|
+
) {
|
|
139
|
+
const u = new URL(getData.result.connection_info.url);
|
|
140
|
+
u.searchParams.set('source', String(getData.result.connection_info.extensions.source));
|
|
141
|
+
process.stderr.write(`\rConnection ${connectionId} established (${i}s).\n`);
|
|
142
|
+
return {
|
|
143
|
+
wsUrl: u.toString(),
|
|
144
|
+
source: getData.result.connection_info.extensions.source,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(`Timed out waiting for connection ${connectionId}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { createConnection, getCredentials } from './hwlink-api.mjs';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WORKSPACE_ID = process.env.HW_WORKSPACE_ID || '0107bd9997aa4287bd2b4890b49af07d';
|
|
10
|
+
|
|
11
|
+
function resolveEnv() {
|
|
12
|
+
const env = { ...process.env };
|
|
13
|
+
env.PATH = `${env.HOME || '/root'}/.huawei/bin:${env.PATH || ''}`;
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function runNodeExec(args, timeoutMs = 30000) {
|
|
18
|
+
const env = resolveEnv();
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const proc = spawn('node', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
21
|
+
let stdout = '';
|
|
22
|
+
let stderr = '';
|
|
23
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
24
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
25
|
+
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
proc.kill();
|
|
28
|
+
resolve({ error: 'timed out', exitCode: 124 });
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
|
|
31
|
+
proc.on('close', (code) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
const out = stdout.trim();
|
|
34
|
+
if (out) {
|
|
35
|
+
try {
|
|
36
|
+
resolve({ ...JSON.parse(out), exitCode: code || 0 });
|
|
37
|
+
return;
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
if (code && code !== 0 && !out) {
|
|
41
|
+
resolve({ error: stderr.trim() || `exit code ${code}`, exitCode: code });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
resolve({ data: out, exitCode: code || 0 });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sessions = new Map();
|
|
50
|
+
|
|
51
|
+
async function getSession(workspaceId, username, timeoutMs) {
|
|
52
|
+
const key = `${workspaceId}:${username}`;
|
|
53
|
+
if (sessions.has(key)) return sessions.get(key);
|
|
54
|
+
|
|
55
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
56
|
+
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
57
|
+
|
|
58
|
+
const { connectHwlinkTerminalSession } = await import(WS_EXEC_INDEX_URL);
|
|
59
|
+
const session = await connectHwlinkTerminalSession({
|
|
60
|
+
url: wsUrl,
|
|
61
|
+
source,
|
|
62
|
+
username,
|
|
63
|
+
timeoutMs,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
sessions.set(key, session);
|
|
67
|
+
return session;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function execOneShot(workspaceId, command, username, timeoutMs) {
|
|
71
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
72
|
+
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
73
|
+
|
|
74
|
+
const { executeHwlinkCommand } = await import(WS_EXEC_INDEX_URL);
|
|
75
|
+
return await executeHwlinkCommand({
|
|
76
|
+
url: wsUrl,
|
|
77
|
+
source,
|
|
78
|
+
username,
|
|
79
|
+
command,
|
|
80
|
+
timeoutMs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function execWithSession(workspaceId, command, username, timeoutMs) {
|
|
85
|
+
const session = await getSession(workspaceId, username, timeoutMs);
|
|
86
|
+
return await session.exec(command, { timeoutMs });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function closeSession(workspaceId, username) {
|
|
90
|
+
const key = `${workspaceId}:${username}`;
|
|
91
|
+
const session = sessions.get(key);
|
|
92
|
+
if (!session) return false;
|
|
93
|
+
sessions.delete(key);
|
|
94
|
+
try { session.close(); } catch {}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function closeAllSessions() {
|
|
99
|
+
for (const [key, session] of sessions) {
|
|
100
|
+
sessions.delete(key);
|
|
101
|
+
try { session.close(); } catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { DEFAULT_WORKSPACE_ID, runNodeExec };
|