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.
@@ -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 v0.1.0
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.huaweicloud = {
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.huaweicloud;
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
- return r.status === 0 && r.stdout && r.stdout.toString().includes('codex');
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
- const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000, input: 'y\n' });
165
- const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
166
- return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
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
- return r2.status === 0;
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.huaweicloud = {
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.huaweicloud;
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.huaweicloud = {
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.huaweicloud;
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. Install Codex first: https://github.com/openai/codex-cli\x1b[0m`);
482
- console.log(` Then re-run: npx huaweicloud-devkit install --target codex`);
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
- console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
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
- // OpenCode installed files
593
- const pluginDir = opencodePluginsDir();
594
- const mcpOk = existsSync(join(pluginDir, 'src', 'mcp-server.mjs'));
595
- check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit-test install');
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
- // Try to start MCP server briefly
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(pluginDir, 'safety', 'policy.json'));
609
- check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit-test install');
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
- const opencodeCfg = opencodeConfigFile();
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
- mcpConfigured = !!(cfg.mcp && cfg.mcp.huaweicloud);
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('OpenCode MCP configured', mcpConfigured, `Add MCP to ${opencodeCfg} run: npx huaweicloud-devkit-test install`);
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, input: 'y\n' });
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
- if (acceptKooCliPrivacy(hcloudBin)) {
767
- console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
768
- } else {
769
- console.log(' \x1b[33m无法自动接受隐私协议(可能因沙箱阻止写入配置目录)。\x1b[0m');
770
- if (detectCodeartsSandbox() === 'sandbox') {
771
- printSandboxWarning('KooCLI 需要写入配置目录 (如 ~/.hcloud/root) 以保存隐私协议同意状态,但沙箱模式阻止了写入。');
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(' 请在码道外终端运行一次: hcloud version 并按提示输入 y');
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');