huaweicloud-devkit 0.1.25-dev.0 → 0.1.26-dev.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 +5 -2
- package/integrations/opencode/opencode.json +1 -1
- package/integrations/opencode/skills/huaweicloud-core/SKILL.md +2 -0
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.mcp.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +48 -0
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +178 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +20 -3
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +28 -1
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +0 -1
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -11
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +4 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +14 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +5 -3
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +137 -0
- package/plugins/huaweicloud-core/src/safety-policy.mjs +26 -13
- package/plugins/huaweicloud-core/src/setup-cli.mjs +126 -65
- package/plugins/huaweicloud-core/src/tools.mjs +156 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const defaultRulesPath = join(__dirname, '..', 'safety', 'rules', 'cloud-risk-rules.json');
|
|
7
|
+
|
|
8
|
+
const SEVERITY_RANK = {
|
|
9
|
+
deny: 3,
|
|
10
|
+
warn: 2,
|
|
11
|
+
info: 1,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function loadRiskRules(options = {}) {
|
|
15
|
+
const path = options.path || defaultRulesPath;
|
|
16
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function redactEvidence(text) {
|
|
20
|
+
return String(text)
|
|
21
|
+
.replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
|
|
22
|
+
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeText(value) {
|
|
26
|
+
if (typeof value === 'string') return value;
|
|
27
|
+
return JSON.stringify(value, null, 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function evaluationContext(stage, input) {
|
|
31
|
+
if (stage === 'command') {
|
|
32
|
+
const command = normalizeText(input.command || input.text || '');
|
|
33
|
+
return { text: command, command };
|
|
34
|
+
}
|
|
35
|
+
if (stage === 'artifact') {
|
|
36
|
+
const path = String(input.path || '');
|
|
37
|
+
const content = normalizeText(input.content || '');
|
|
38
|
+
return { text: `${path}\n${content}`, path, content };
|
|
39
|
+
}
|
|
40
|
+
if (stage === 'deploy_plan') {
|
|
41
|
+
const plan = normalizeText(input.plan || input.text || input);
|
|
42
|
+
return { text: plan, plan };
|
|
43
|
+
}
|
|
44
|
+
return { text: normalizeText(input) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function conditionMatches(condition, context) {
|
|
48
|
+
const field = condition.field || 'text';
|
|
49
|
+
const value = Object.hasOwn(context, field) ? context[field] : context.text;
|
|
50
|
+
return new RegExp(condition.regex, 'ims').test(String(value || ''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ruleMatches(rule, context) {
|
|
54
|
+
const match = rule.match || {};
|
|
55
|
+
const all = match.all;
|
|
56
|
+
const any = match.any;
|
|
57
|
+
const none = match.none;
|
|
58
|
+
if (Array.isArray(all) && !all.every((condition) => conditionMatches(condition, context))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(any) && !any.some((condition) => conditionMatches(condition, context))) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(none) && none.some((condition) => conditionMatches(condition, context))) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return Array.isArray(all) || Array.isArray(any);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function excerpt(text) {
|
|
71
|
+
const compact = redactEvidence(String(text).replace(/\s+/g, ' ').trim());
|
|
72
|
+
if (compact.length <= 240) return compact;
|
|
73
|
+
return `${compact.slice(0, 237)}...`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function evaluate(stage, inputs, options = {}) {
|
|
77
|
+
const catalog = options.catalog || loadRiskRules(options);
|
|
78
|
+
const items = Array.isArray(inputs) ? inputs : [inputs];
|
|
79
|
+
const findings = [];
|
|
80
|
+
|
|
81
|
+
for (const input of items) {
|
|
82
|
+
const context = evaluationContext(stage, input || {});
|
|
83
|
+
for (const rule of catalog.rules) {
|
|
84
|
+
if (!rule.stages.includes(stage)) continue;
|
|
85
|
+
if (!ruleMatches(rule, context)) continue;
|
|
86
|
+
findings.push({
|
|
87
|
+
ruleId: rule.id,
|
|
88
|
+
title: rule.title,
|
|
89
|
+
category: rule.category,
|
|
90
|
+
severity: rule.severity,
|
|
91
|
+
message: rule.message,
|
|
92
|
+
remediation: rule.remediation,
|
|
93
|
+
source: input?.path || stage,
|
|
94
|
+
evidence: excerpt(context.text),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
100
|
+
const hasDeny = findings.some((finding) => finding.severity === 'deny');
|
|
101
|
+
const hasWarn = findings.some((finding) => finding.severity === 'warn');
|
|
102
|
+
return {
|
|
103
|
+
decision: hasDeny ? 'deny' : hasWarn ? 'warn' : 'allow',
|
|
104
|
+
findings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function evaluateCommandRisk(command, options = {}) {
|
|
109
|
+
return evaluate('command', { command }, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function evaluateArtifacts(artifacts, options = {}) {
|
|
113
|
+
return evaluate('artifact', Array.isArray(artifacts) ? artifacts : [], options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function evaluateDeployPlan(plan, options = {}) {
|
|
117
|
+
return evaluate('deploy_plan', { plan }, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mergeRiskDecision(base, risk) {
|
|
121
|
+
if (!risk || !risk.findings?.length) return base;
|
|
122
|
+
if (risk.decision === 'deny') {
|
|
123
|
+
const topFinding = risk.findings[0];
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
decision: 'deny',
|
|
127
|
+
risk: topFinding.category,
|
|
128
|
+
reason: topFinding.message,
|
|
129
|
+
blockedByRiskRule: true,
|
|
130
|
+
findings: risk.findings,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...base,
|
|
135
|
+
warnings: [...(base.warnings || []), ...risk.findings],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { evaluateCommandRisk, mergeRiskDecision } from './risk-rule-engine.mjs';
|
|
4
5
|
|
|
5
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const policyPath = join(__dirname, '..', 'safety', 'policy.json');
|
|
@@ -92,6 +93,18 @@ function isLocalMetadataCommand(args) {
|
|
|
92
93
|
return args.some((arg) => /^(--help|-h|help|version|--version)$/i.test(String(arg)));
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function commandRiskText(normalizedArgs, options = {}) {
|
|
97
|
+
return options.rawCommand || ['hcloud', ...normalizedArgs].join(' ');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyCommandRiskRules(base, normalizedArgs, options = {}) {
|
|
101
|
+
if (base.decision === 'deny' || options.skipRiskRules === true) {
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
const risk = evaluateCommandRisk(commandRiskText(normalizedArgs, options));
|
|
105
|
+
return mergeRiskDecision(base, risk);
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
export function classifyHcloudArgs(args, options = {}) {
|
|
96
109
|
const policy = options.policy || DEFAULT_POLICY;
|
|
97
110
|
const { service, operation, args: normalizedArgs } = commandOperation(args);
|
|
@@ -106,14 +119,14 @@ export function classifyHcloudArgs(args, options = {}) {
|
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
if (isLocalMetadataCommand(normalizedArgs)) {
|
|
109
|
-
return {
|
|
122
|
+
return applyCommandRiskRules({
|
|
110
123
|
decision: 'allow',
|
|
111
124
|
risk: 'local_metadata',
|
|
112
125
|
reason: 'KooCLI local help and version commands are read-only and do not call Huawei Cloud resource APIs.',
|
|
113
126
|
service,
|
|
114
127
|
operation,
|
|
115
128
|
args: normalizedArgs,
|
|
116
|
-
};
|
|
129
|
+
}, normalizedArgs, options);
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
if (service.toLowerCase() === 'configure') {
|
|
@@ -180,49 +193,49 @@ export function classifyHcloudArgs(args, options = {}) {
|
|
|
180
193
|
};
|
|
181
194
|
}
|
|
182
195
|
if (isObsRead) {
|
|
183
|
-
return {
|
|
196
|
+
return applyCommandRiskRules({
|
|
184
197
|
decision: 'allow',
|
|
185
198
|
risk: 'read_only',
|
|
186
199
|
reason: 'OBS read-only operation.',
|
|
187
200
|
service,
|
|
188
201
|
operation,
|
|
189
202
|
args: normalizedArgs,
|
|
190
|
-
};
|
|
203
|
+
}, normalizedArgs, options);
|
|
191
204
|
}
|
|
192
205
|
if (isObsWrite && options.allowWrites) {
|
|
193
|
-
return {
|
|
206
|
+
return applyCommandRiskRules({
|
|
194
207
|
decision: 'allow',
|
|
195
208
|
risk: 'write',
|
|
196
209
|
reason: 'OBS write operation approved by user.',
|
|
197
210
|
service,
|
|
198
211
|
operation,
|
|
199
212
|
args: normalizedArgs,
|
|
200
|
-
};
|
|
213
|
+
}, normalizedArgs, options);
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
if (isExecution && options.allowWrites) {
|
|
204
|
-
return {
|
|
217
|
+
return applyCommandRiskRules({
|
|
205
218
|
decision: 'allow',
|
|
206
219
|
risk: 'execution',
|
|
207
220
|
reason: 'Huawei Cloud execution/trigger operation approved by user.',
|
|
208
221
|
service,
|
|
209
222
|
operation,
|
|
210
223
|
args: normalizedArgs,
|
|
211
|
-
};
|
|
224
|
+
}, normalizedArgs, options);
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
if (isWrite && options.allowWrites) {
|
|
215
|
-
return {
|
|
228
|
+
return applyCommandRiskRules({
|
|
216
229
|
decision: 'allow',
|
|
217
230
|
risk: 'write',
|
|
218
231
|
reason: 'Huawei Cloud write operation approved by user.',
|
|
219
232
|
service,
|
|
220
233
|
operation,
|
|
221
234
|
args: normalizedArgs,
|
|
222
|
-
};
|
|
235
|
+
}, normalizedArgs, options);
|
|
223
236
|
}
|
|
224
237
|
|
|
225
|
-
return {
|
|
238
|
+
return applyCommandRiskRules({
|
|
226
239
|
decision: 'allow',
|
|
227
240
|
risk: readOnly ? 'read_only' : 'unknown_read',
|
|
228
241
|
reason: readOnly
|
|
@@ -231,7 +244,7 @@ export function classifyHcloudArgs(args, options = {}) {
|
|
|
231
244
|
service,
|
|
232
245
|
operation,
|
|
233
246
|
args: normalizedArgs,
|
|
234
|
-
};
|
|
247
|
+
}, normalizedArgs, options);
|
|
235
248
|
}
|
|
236
249
|
|
|
237
250
|
function splitSimpleCommand(command) {
|
|
@@ -261,7 +274,7 @@ export function classifyTextCommand(command, options = {}) {
|
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
|
|
264
|
-
return classifyHcloudArgs(splitSimpleCommand(text), options);
|
|
277
|
+
return classifyHcloudArgs(splitSimpleCommand(text), { ...options, rawCommand: text });
|
|
265
278
|
}
|
|
266
279
|
|
|
267
280
|
if (/ShowSecretVersion|GetSecretValue|secret_string|secret_binary/i.test(text)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, dirname, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { homedir, platform } from 'node:os';
|
|
@@ -11,9 +11,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
const PLUGIN_ROOT = resolve(__dirname, '..');
|
|
12
12
|
const PACKAGE_ROOT = resolve(PLUGIN_ROOT, '..', '..');
|
|
13
13
|
|
|
14
|
+
let pkgVersion = '0.0.0';
|
|
15
|
+
try {
|
|
16
|
+
pkgVersion = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
17
|
+
} catch {}
|
|
18
|
+
|
|
14
19
|
const BANNER = `
|
|
15
20
|
╔══════════════════════════════════════════════╗
|
|
16
|
-
║ HuaweiCloud DevKit
|
|
21
|
+
║ HuaweiCloud DevKit v${pkgVersion}${' '.repeat(Math.max(0, 22 - String(pkgVersion).length))}║
|
|
17
22
|
║ https://github.com/huaweicloud-mate ║
|
|
18
23
|
╚══════════════════════════════════════════════╝
|
|
19
24
|
`;
|
|
@@ -80,20 +85,6 @@ function printSandboxWarning(reason) {
|
|
|
80
85
|
console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
// Try to auto-accept the KooCLI privacy agreement by answering 'y' on stdin.
|
|
84
|
-
// Returns true when hcloud runs without re-prompting for the agreement.
|
|
85
|
-
function acceptKooCliPrivacy(hcloudBin) {
|
|
86
|
-
const run = () => spawnSync(hcloudBin, ['version'], {
|
|
87
|
-
encoding: 'utf8', timeout: 10000, windowsHide: true, input: 'y\n',
|
|
88
|
-
});
|
|
89
|
-
const first = run();
|
|
90
|
-
const out = (first.stdout || '') + (first.stderr || '');
|
|
91
|
-
if (!/同意并继续使用|agree/i.test(out)) return first.status === 0;
|
|
92
|
-
const second = run();
|
|
93
|
-
const out2 = (second.stdout || '') + (second.stderr || '');
|
|
94
|
-
return second.status === 0 && !/同意并继续使用/.test(out2);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
88
|
function checkNode() {
|
|
98
89
|
const v = process.versions.node.split('.').map(Number);
|
|
99
90
|
if (v[0] < 20) {
|
|
@@ -133,7 +124,7 @@ function updateOpenCodeConfig(pluginDir) {
|
|
|
133
124
|
}
|
|
134
125
|
const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
135
126
|
config.mcp = config.mcp || {};
|
|
136
|
-
config.mcp
|
|
127
|
+
config.mcp['huaweicloud-devkit'] = {
|
|
137
128
|
type: 'local',
|
|
138
129
|
command: ['node', mcpPath],
|
|
139
130
|
enabled: true,
|
|
@@ -147,8 +138,8 @@ function removeOpenCodeConfig() {
|
|
|
147
138
|
if (!existsSync(configPath)) return;
|
|
148
139
|
let config = {};
|
|
149
140
|
try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return; }
|
|
150
|
-
if (!config.mcp?.huaweicloud) return;
|
|
151
|
-
delete config.mcp
|
|
141
|
+
if (!config.mcp?.['huaweicloud-devkit']) return;
|
|
142
|
+
delete config.mcp['huaweicloud-devkit'];
|
|
152
143
|
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
153
144
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
154
145
|
console.log(` OpenCode MCP config cleaned: ${configPath}`);
|
|
@@ -156,14 +147,29 @@ function removeOpenCodeConfig() {
|
|
|
156
147
|
|
|
157
148
|
function hasCodexCLI() {
|
|
158
149
|
const r = spawnSync('codex --version', [], { shell: true, windowsHide: true, stdio: 'pipe' });
|
|
159
|
-
|
|
150
|
+
if (r.status === 0 && r.stdout && r.stdout.toString().includes('codex')) return true;
|
|
151
|
+
// WindowsApps codex.exe may fail with "Access is denied"
|
|
152
|
+
// Fallback: check if codex exists on PATH via where.exe
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
const w = spawnSync('where.exe', ['codex'], { windowsHide: true, stdio: 'pipe' });
|
|
155
|
+
if (w.status === 0 && w.stdout.toString().trim()) return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
function checkHcloud() {
|
|
163
161
|
const bin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
if (!existsSync(bin)) return false;
|
|
163
|
+
try {
|
|
164
|
+
if (statSync(bin).size < 1024) return false;
|
|
165
|
+
} catch { return false; }
|
|
166
|
+
try {
|
|
167
|
+
const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
|
|
168
|
+
const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
|
|
169
|
+
return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
function getMarketplaceName() {
|
|
@@ -186,12 +192,25 @@ function installCodex() {
|
|
|
186
192
|
});
|
|
187
193
|
console.log(` ${r1.stdout ? r1.stdout.toString().trim() : r1.stderr.toString().trim()}`);
|
|
188
194
|
|
|
195
|
+
if (r1.status !== 0 && /Access is denied/i.test((r1.stderr || '').toString())) {
|
|
196
|
+
console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
|
|
197
|
+
console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
189
201
|
console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
|
|
190
202
|
const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
|
|
191
203
|
shell: true, windowsHide: true, stdio: 'pipe',
|
|
192
204
|
});
|
|
193
205
|
console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
|
|
194
|
-
|
|
206
|
+
|
|
207
|
+
if (r2.status !== 0 && /Access is denied/i.test((r2.stderr || '').toString())) {
|
|
208
|
+
console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
|
|
209
|
+
console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true;
|
|
195
214
|
}
|
|
196
215
|
|
|
197
216
|
function uninstallCodex() {
|
|
@@ -276,6 +295,27 @@ async function installCodexDesktop() {
|
|
|
276
295
|
copyDir(safetyDir, join(codexDesktopPluginsDir(), 'safety'));
|
|
277
296
|
console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
|
|
278
297
|
|
|
298
|
+
// Generate .mcp.json with absolute paths for Codex Desktop MCP server discovery
|
|
299
|
+
const mcpServerAbsPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
300
|
+
const mcpConfig = {
|
|
301
|
+
mcpServers: {
|
|
302
|
+
'huaweicloud-devkit': {
|
|
303
|
+
command: 'node',
|
|
304
|
+
args: [mcpServerAbsPath],
|
|
305
|
+
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
writeFileSync(join(codexDesktopPluginsDir(), '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
310
|
+
console.log(` MCP Config -> ${join(codexDesktopPluginsDir(), '.mcp.json')}`);
|
|
311
|
+
|
|
312
|
+
// Copy .codex-plugin manifest for Codex Desktop plugin registration
|
|
313
|
+
const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
|
|
314
|
+
if (existsSync(codexPluginSrc)) {
|
|
315
|
+
copyDir(codexPluginSrc, join(codexDesktopPluginsDir(), '.codex-plugin'));
|
|
316
|
+
console.log(` Plugin Manifest -> ${join(codexDesktopPluginsDir(), '.codex-plugin')}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
279
319
|
const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
280
320
|
const configPath = codexDesktopConfigFile();
|
|
281
321
|
let config = {};
|
|
@@ -283,7 +323,7 @@ async function installCodexDesktop() {
|
|
|
283
323
|
try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
284
324
|
}
|
|
285
325
|
config.mcp = config.mcp || {};
|
|
286
|
-
config.mcp
|
|
326
|
+
config.mcp['huaweicloud-devkit'] = {
|
|
287
327
|
type: 'local',
|
|
288
328
|
command: ['node', mcpPath],
|
|
289
329
|
enabled: true,
|
|
@@ -324,8 +364,8 @@ function uninstallCodexDesktop() {
|
|
|
324
364
|
if (existsSync(configPath)) {
|
|
325
365
|
let config = {};
|
|
326
366
|
try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
327
|
-
if (config.mcp?.huaweicloud) {
|
|
328
|
-
delete config.mcp
|
|
367
|
+
if (config.mcp?.['huaweicloud-devkit']) {
|
|
368
|
+
delete config.mcp['huaweicloud-devkit'];
|
|
329
369
|
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
330
370
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
331
371
|
console.log(' Config cleaned');
|
|
@@ -343,7 +383,7 @@ function registerCodeartsMcp(configPath) {
|
|
|
343
383
|
const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
|
|
344
384
|
const hcloudBin = findHcloudBin();
|
|
345
385
|
if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
|
|
346
|
-
config.mcpServers
|
|
386
|
+
config.mcpServers['huaweicloud-devkit'] = {
|
|
347
387
|
command: 'node',
|
|
348
388
|
args: [mcpPath],
|
|
349
389
|
env,
|
|
@@ -394,8 +434,8 @@ function uninstallCodeArts() {
|
|
|
394
434
|
if (!existsSync(configPath)) continue;
|
|
395
435
|
let config = {};
|
|
396
436
|
try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
397
|
-
if (config.mcpServers?.huaweicloud) {
|
|
398
|
-
delete config.mcpServers
|
|
437
|
+
if (config.mcpServers?.['huaweicloud-devkit']) {
|
|
438
|
+
delete config.mcpServers['huaweicloud-devkit'];
|
|
399
439
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
400
440
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
401
441
|
console.log(` Config cleaned: ${configPath}`);
|
|
@@ -416,7 +456,7 @@ function codeartsStatus() {
|
|
|
416
456
|
if (existsSync(codeartsMcpSettingsFile())) {
|
|
417
457
|
try {
|
|
418
458
|
const config = JSON.parse(readFileSync(codeartsMcpSettingsFile(), 'utf8'));
|
|
419
|
-
console.log(` MCP config: ${config.mcpServers?.huaweicloud ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
459
|
+
console.log(` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
420
460
|
} catch {
|
|
421
461
|
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
422
462
|
}
|
|
@@ -438,7 +478,7 @@ function opencodeStatus() {
|
|
|
438
478
|
if (existsSync(configPath)) {
|
|
439
479
|
try {
|
|
440
480
|
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
441
|
-
console.log(` MCP config: ${config.mcp?.huaweicloud ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
481
|
+
console.log(` MCP config: ${config.mcp?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
442
482
|
} catch {
|
|
443
483
|
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
444
484
|
}
|
|
@@ -478,18 +518,25 @@ async function cmdInstall() {
|
|
|
478
518
|
console.log('\n[Codex]');
|
|
479
519
|
if (!hasCodexCLI()) {
|
|
480
520
|
if (target === 'codex') {
|
|
481
|
-
console.log(` \x1b[31mCodex CLI not found
|
|
482
|
-
|
|
521
|
+
console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
|
|
522
|
+
if (process.platform === 'win32') {
|
|
523
|
+
console.log(` \x1b[33mTip: Codex Desktop on Windows installs codex.exe under WindowsApps,\x1b[0m`);
|
|
524
|
+
console.log(` \x1b[33m which may fail with "Access is denied". Try instead:\x1b[0m`);
|
|
525
|
+
console.log(` \x1b[33m npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
|
|
526
|
+
}
|
|
527
|
+
console.log(` \x1b[31mOr install Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
|
|
483
528
|
process.exit(1);
|
|
484
529
|
}
|
|
485
530
|
console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
|
|
486
|
-
|
|
531
|
+
if (process.platform === 'win32') {
|
|
532
|
+
console.log(' \x1b[33mTip: try --target codex-desktop for Codex Desktop on Windows\x1b[0m');
|
|
533
|
+
} else {
|
|
534
|
+
console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
|
|
535
|
+
}
|
|
487
536
|
} else {
|
|
488
537
|
installCodex();
|
|
489
538
|
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
|
|
539
|
+
} console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
|
|
493
540
|
const appName = target === 'codearts' ? 'CodeArts'
|
|
494
541
|
: target === 'codex-desktop' ? 'Codex Desktop'
|
|
495
542
|
: target === 'codex' ? 'Codex' : 'OpenCode';
|
|
@@ -589,38 +636,45 @@ async function cmdDoctor() {
|
|
|
589
636
|
// Node.js
|
|
590
637
|
check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
|
|
591
638
|
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
639
|
+
// MCP server — check OpenCode and Codex Desktop paths
|
|
640
|
+
const opencodePluginDir = opencodePluginsDir();
|
|
641
|
+
const codexPluginDir = codexDesktopPluginsDir();
|
|
642
|
+
const mcpOk = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs'))
|
|
643
|
+
|| existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs'));
|
|
644
|
+
const mcpTarget = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ? 'OpenCode'
|
|
645
|
+
: existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ? 'Codex Desktop' : '';
|
|
646
|
+
check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
|
|
596
647
|
|
|
597
648
|
if (mcpOk) {
|
|
598
|
-
|
|
599
|
-
const test = spawnSync('node', [join(pluginDir, 'src', 'mcp-server.mjs')], {
|
|
600
|
-
env: { ...process.env, HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
601
|
-
timeout: 3000, stdio: 'pipe', windowsHide: true,
|
|
602
|
-
});
|
|
603
|
-
// MCP server reads stdin for JSON-RPC, so it will hang briefly then get killed
|
|
604
|
-
// We just check that the process spawned OK
|
|
605
|
-
check('MCP server can start', true, '');
|
|
649
|
+
check(`MCP server can start (${mcpTarget})`, true, '');
|
|
606
650
|
}
|
|
607
651
|
|
|
608
|
-
const safetyOk = existsSync(join(
|
|
609
|
-
|
|
652
|
+
const safetyOk = existsSync(join(opencodePluginDir, 'safety', 'policy.json'))
|
|
653
|
+
|| existsSync(join(codexPluginDir, 'safety', 'policy.json'));
|
|
654
|
+
check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
|
|
610
655
|
|
|
611
|
-
|
|
656
|
+
// MCP config — check OpenCode and Codex Desktop
|
|
612
657
|
let mcpConfigured = false;
|
|
658
|
+
let mcpCfgTarget = '';
|
|
659
|
+
const opencodeCfg = opencodeConfigFile();
|
|
613
660
|
if (existsSync(opencodeCfg)) {
|
|
614
661
|
try {
|
|
615
662
|
const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
|
|
616
|
-
|
|
663
|
+
if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'OpenCode'; }
|
|
664
|
+
} catch {}
|
|
665
|
+
}
|
|
666
|
+
const codexCfg = codexDesktopConfigFile();
|
|
667
|
+
if (!mcpConfigured && existsSync(codexCfg)) {
|
|
668
|
+
try {
|
|
669
|
+
const cfg = JSON.parse(readFileSync(codexCfg, 'utf8'));
|
|
670
|
+
if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'Codex Desktop'; }
|
|
617
671
|
} catch {}
|
|
618
672
|
}
|
|
619
|
-
check('
|
|
673
|
+
check('MCP configured', mcpConfigured, mcpCfgTarget ? `Found in ${mcpCfgTarget} config` : 'Run: npx huaweicloud-devkit install');
|
|
620
674
|
|
|
621
675
|
// hcloud CLI
|
|
622
676
|
const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
|
|
623
|
-
const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000
|
|
677
|
+
const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
|
|
624
678
|
const hcloudOut = (hcloudCheck.stdout || '').toString() + (hcloudCheck.stderr || '').toString();
|
|
625
679
|
const hcloudOk = hcloudCheck.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(hcloudOut);
|
|
626
680
|
check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
|
|
@@ -760,18 +814,25 @@ async function cmdInstallHcloud() {
|
|
|
760
814
|
console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
|
|
761
815
|
console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
|
|
762
816
|
|
|
763
|
-
// Auto-accept the KooCLI privacy agreement so first run does not hang
|
|
764
|
-
console.log('\n Accepting KooCLI privacy agreement...');
|
|
765
817
|
const hcloudBin = join(installDir, 'hcloud.exe');
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
818
|
+
|
|
819
|
+
// Ask user before accepting the privacy agreement — never auto-accept.
|
|
820
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
821
|
+
const agree = await new Promise((resolve) => {
|
|
822
|
+
rl.question('\n KooCLI requires accepting its privacy agreement. Do you accept? (y/N) ', (answer) => {
|
|
823
|
+
rl.close();
|
|
824
|
+
resolve(/^\s*y\s*$/i.test(answer));
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
if (agree) {
|
|
828
|
+
const r = spawnSync(hcloudBin, ['version'], { input: 'y\n', encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
829
|
+
if (r.status === 0) {
|
|
830
|
+
console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
|
|
772
831
|
} else {
|
|
773
|
-
console.log('
|
|
832
|
+
console.log(' \x1b[33m无法写入配置目录。请在码道外终端运行: echo "y" | hcloud version\x1b[0m');
|
|
774
833
|
}
|
|
834
|
+
} else {
|
|
835
|
+
console.log(' \x1b[33m请手动接受隐私协议:在终端运行 hcloud version 并按提示操作\x1b[0m');
|
|
775
836
|
}
|
|
776
837
|
|
|
777
838
|
console.log(' Or restart terminal and: hcloud version');
|