huaweicloud-devkit 1.0.2-dev.1 → 1.0.2-dev.2

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,105 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createConnection, getCredentials } from './hwlink-api.mjs';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const WS_EXEC_SRC = join(__dirname, '..', 'ws-exec');
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(join(WS_EXEC_SRC, 'index.js'));
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(join(WS_EXEC_SRC, 'index.js'));
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 };
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
6
6
  import { homedir, platform } from 'node:os';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { spawnSync } from 'node:child_process';
9
+ import { getAuthStatus, syncAuth } from './auth/service.mjs';
10
+ import { globalCredentialsPath, readGlobalCredentials, writeGlobalCredentials, writeObsConfig } from './auth/credentials.mjs';
9
11
 
10
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
13
  const PLUGIN_ROOT = resolve(__dirname, '..');
@@ -1042,6 +1044,175 @@ async function cmdInstallHcloud() {
1042
1044
  console.log('\nThen run: npx huaweicloud-devkit doctor');
1043
1045
  }
1044
1046
 
1047
+ function readLineQuestion(prompt) {
1048
+ return new Promise((resolve) => {
1049
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1050
+ rl.question(prompt, (answer) => {
1051
+ rl.close();
1052
+ resolve(answer.trim());
1053
+ });
1054
+ });
1055
+ }
1056
+
1057
+ async function readSecret(prompt) {
1058
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1059
+ return readLineQuestion(prompt);
1060
+ }
1061
+
1062
+ process.stdout.write(prompt);
1063
+ const wasRaw = process.stdin.isRaw;
1064
+ process.stdin.setRawMode(true);
1065
+ process.stdin.resume();
1066
+
1067
+ return new Promise((resolve) => {
1068
+ let value = '';
1069
+ const onData = (chunk) => {
1070
+ for (const ch of chunk.toString('utf8')) {
1071
+ if (ch === '\r' || ch === '\n') {
1072
+ cleanup();
1073
+ resolve(value.trim());
1074
+ return;
1075
+ }
1076
+ if (ch === '\u0003') {
1077
+ cleanup();
1078
+ process.exit(130);
1079
+ }
1080
+ if (ch === '\b' || ch === '\u007f') {
1081
+ value = value.slice(0, -1);
1082
+ continue;
1083
+ }
1084
+ value += ch;
1085
+ }
1086
+ };
1087
+ const cleanup = () => {
1088
+ process.stdin.setRawMode(wasRaw);
1089
+ process.stdin.pause();
1090
+ process.stdin.off('data', onData);
1091
+ process.stdout.write('\n');
1092
+ };
1093
+ process.stdin.on('data', onData);
1094
+ });
1095
+ }
1096
+
1097
+ function configureHcloud(credentials) {
1098
+ const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1099
+ const args = [
1100
+ 'configure',
1101
+ 'set',
1102
+ `--cli-access-key=${credentials.ak}`,
1103
+ `--cli-secret-key=${credentials.sk}`,
1104
+ `--cli-region=${credentials.region || ''}`,
1105
+ ];
1106
+ const r = spawnSync(hcloudBin, args, {
1107
+ shell: false,
1108
+ windowsHide: true,
1109
+ stdio: 'pipe',
1110
+ timeout: 30000,
1111
+ });
1112
+ return {
1113
+ ok: r.status === 0,
1114
+ code: r.status,
1115
+ error: String(r.stderr || '').trim().slice(0, 240),
1116
+ };
1117
+ }
1118
+
1119
+ function printAuthAgents(agents = {}) {
1120
+ for (const [agent, info] of Object.entries(agents)) {
1121
+ console.log(` ${agent}: ${info.configured ? '[OK]' : '[MISSING]'}`);
1122
+ }
1123
+ }
1124
+
1125
+ function printAuthStatus(status) {
1126
+ console.log(`Credentials vault: ${status.credentialsConfigured ? 'configured' : 'missing'} (${status.credentialsPath})`);
1127
+ console.log(`OBS config: ${status.obsConfigured ? 'configured' : 'missing'} (${status.obsConfigPath})`);
1128
+ console.log(`KooCLI: ${status.kooCliInstalled ? 'installed' : 'missing'}`);
1129
+ console.log('Agent MCP registration:');
1130
+ printAuthAgents(status.agents);
1131
+ }
1132
+
1133
+ async function cmdAuthInit() {
1134
+ console.log(BANNER);
1135
+ console.log('HuaweiCloud DevKit Unified Authentication Setup\n');
1136
+
1137
+ let ak = process.env.HW_ACCESS_KEY || '';
1138
+ let sk = process.env.HW_SECRET_KEY || '';
1139
+ let securityToken = process.env.HW_SECURITY_TOKEN || '';
1140
+ let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
1141
+
1142
+ if (!ak) ak = await readSecret('Access Key ID (AK): ');
1143
+ if (!sk) sk = await readSecret('Secret Access Key (SK): ');
1144
+ if (!securityToken) securityToken = await readLineQuestion('Security Token (optional, press Enter to skip): ');
1145
+ if (!region) region = await readLineQuestion('Region (e.g. cn-north-4): ');
1146
+
1147
+ if (!ak || !sk) {
1148
+ console.error('\nAK and SK are required.');
1149
+ process.exitCode = 1;
1150
+ return;
1151
+ }
1152
+ if (!region) {
1153
+ console.error('\nRegion is required to generate the OBS endpoint.');
1154
+ process.exitCode = 1;
1155
+ return;
1156
+ }
1157
+
1158
+ const vaultPath = writeGlobalCredentials({ ak, sk, securityToken, region });
1159
+ console.log(`\nCredentials stored: ${vaultPath}`);
1160
+
1161
+ try {
1162
+ const obs = writeObsConfig({ ak, sk, securityToken, region });
1163
+ console.log(`OBS config synced: ${obs.path} (${obs.endpoint})`);
1164
+ } catch (error) {
1165
+ console.log(`OBS config sync failed: ${error.message}`);
1166
+ }
1167
+
1168
+ if (findHcloudBin()) {
1169
+ const result = configureHcloud({ ak, sk, region });
1170
+ console.log(result.ok ? 'KooCLI profile updated.' : `KooCLI update failed: ${result.error || result.code}`);
1171
+ } else {
1172
+ console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
1173
+ }
1174
+
1175
+ console.log('\nNext steps:');
1176
+ console.log(' npx huaweicloud-devkit install --target all');
1177
+ console.log(' Restart your agent sessions.');
1178
+ }
1179
+
1180
+ async function cmdAuthSync() {
1181
+ const target = parseTarget();
1182
+ console.log(BANNER);
1183
+ console.log('Synchronizing Huawei Cloud authentication...\n');
1184
+
1185
+ const credentials = readGlobalCredentials();
1186
+ if (!credentials?.ak || !credentials?.sk) {
1187
+ console.error('No global credentials found. Run "npx huaweicloud-devkit auth init" first.');
1188
+ process.exitCode = 1;
1189
+ return;
1190
+ }
1191
+
1192
+ const result = syncAuth(target);
1193
+ if (result.ok) {
1194
+ console.log(`OBS config synced: ${result.obs.path} (${result.obs.endpoint})`);
1195
+ } else {
1196
+ console.error(result.error);
1197
+ }
1198
+ console.log('Agent MCP registration:');
1199
+ printAuthAgents(result.agents);
1200
+ }
1201
+
1202
+ async function cmdAuthStatus() {
1203
+ const target = parseTarget();
1204
+ console.log(BANNER);
1205
+ console.log('HuaweiCloud DevKit Authentication Status\n');
1206
+ printAuthStatus(getAuthStatus(target));
1207
+ }
1208
+
1209
+ async function cmdAuth() {
1210
+ const sub = (process.argv[3] || 'status').toLowerCase();
1211
+ if (sub === 'init' || sub === 'setup') return cmdAuthInit();
1212
+ if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
1213
+ return cmdAuthStatus();
1214
+ }
1215
+
1045
1216
  async function main() {
1046
1217
  const cmd = process.argv[2] || 'help';
1047
1218
 
@@ -1072,6 +1243,9 @@ async function main() {
1072
1243
  case 'install-hcloud':
1073
1244
  await cmdInstallHcloud();
1074
1245
  break;
1246
+ case 'auth':
1247
+ await cmdAuth();
1248
+ break;
1075
1249
  case 'help':
1076
1250
  case '--help':
1077
1251
  case '-h':
@@ -1086,6 +1260,7 @@ async function main() {
1086
1260
  console.log(' status Show installation status');
1087
1261
  console.log(' doctor Self-check: hcloud, MCP, skills, auth');
1088
1262
  console.log(' install-hcloud Show KooCLI install commands for your OS');
1263
+ console.log(' auth Manage unified auth: init | sync | status');
1089
1264
  console.log(' help Show this help');
1090
1265
  console.log('\nOptions:');
1091
1266
  console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, all');
@@ -1095,6 +1270,9 @@ async function main() {
1095
1270
  console.log(' npx huaweicloud-devkit install --target codearts');
1096
1271
  console.log(' npx huaweicloud-devkit install --target workbuddy');
1097
1272
  console.log(' npx huaweicloud-devkit install --target all');
1273
+ console.log(' npx huaweicloud-devkit auth init');
1274
+ console.log(' npx huaweicloud-devkit auth sync --target all');
1275
+ console.log(' npx huaweicloud-devkit auth status --target all');
1098
1276
  break;
1099
1277
  }
1100
1278
  }
@@ -6,6 +6,10 @@ import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
8
8
  import { searchMarketplace } from './search-market.mjs';
9
+ import { execOneShot, execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
10
+ import { hdkitCheckUser, hdkitSignAgreement, hdkitConnect, hdkitCredentials, hdkitRelease } from './sandbox/hdkitservice-api.mjs';
11
+ import { getAuthStatus, syncAuth } from './auth/service.mjs';
12
+ import { readGlobalCredentials, writeObsConfig as writeObsConfigFile } from './auth/credentials.mjs';
9
13
 
10
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
15
  const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
@@ -285,6 +289,128 @@ export const TOOL_DEFINITIONS = [
285
289
  },
286
290
  },
287
291
  },
292
+ {
293
+ name: 'huaweicloud_auth_status',
294
+ description: 'Check unified Huawei Cloud authentication status across the global credential vault, OBS, KooCLI, and all supported agent MCP registrations. Returns only redacted/status information, never credentials.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ target: { type: 'string', description: 'Agent target to check: opencode, codex, codex-desktop, codearts, workbuddy, or all (default).' },
299
+ },
300
+ },
301
+ },
302
+ {
303
+ name: 'huaweicloud_auth_sync',
304
+ description: 'Synchronize credentials from the global Huawei Cloud credential vault to OBS and report agent registration status. Does not write secrets into any agent config.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ target: { type: 'string', description: 'Agent target to report after sync: opencode, codex, codex-desktop, codearts, workbuddy, or all (default).' },
309
+ },
310
+ },
311
+ },
312
+ {
313
+ name: 'huaweicloud_sandbox_exec',
314
+ description: 'Execute a command on a Huawei Cloud workspace terminal via hwlink (one-shot, no session reuse). Each call creates a new connection. Shell state does NOT persist across calls.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ required: ['command'],
318
+ properties: {
319
+ command: { type: 'string', description: 'The shell command to execute on the remote workspace' },
320
+ workspace_id: { type: 'string', description: 'The workspace ID' },
321
+ username: { type: 'string', description: 'Login username for the remote terminal (default: root)' },
322
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
323
+ },
324
+ },
325
+ },
326
+ {
327
+ name: 'huaweicloud_sandbox_exec_with_session',
328
+ description: 'Execute a command on a workspace terminal with session reuse (state persists across calls). Shell state (cd, env vars, aliases) carries over between calls.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ required: ['command'],
332
+ properties: {
333
+ command: { type: 'string', description: 'The shell command to execute on the remote workspace' },
334
+ workspace_id: { type: 'string', description: 'The workspace ID' },
335
+ username: { type: 'string', description: 'Login username for the remote terminal (default: root)' },
336
+ timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
337
+ },
338
+ },
339
+ },
340
+ {
341
+ name: 'huaweicloud_sandbox_close_session',
342
+ description: 'Close the persistent terminal session for a workspace.',
343
+ inputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ workspace_id: { type: 'string', description: 'The workspace ID' },
347
+ username: { type: 'string', description: 'Login username (default: root)' },
348
+ },
349
+ },
350
+ },
351
+ {
352
+ name: 'huaweicloud_sandbox_check_user',
353
+ description: 'Check if the current user has completed real-name verification and signed the required agreements. Returns realname_verified and agreement_signed status.',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {},
357
+ },
358
+ },
359
+ {
360
+ name: 'huaweicloud_sandbox_sign_agreement',
361
+ description: 'Sign all unsigned or outdated agreements for the current user. Required before huaweicloud_sandbox_connect if check-user returns agreement_signed=false.',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: {},
365
+ },
366
+ },
367
+ {
368
+ name: 'huaweicloud_sandbox_connect',
369
+ description: 'Connect to a sandbox via hdkitservice. One user one instance - reuses existing sandbox if available, otherwise creates a new one. Returns session_id, dev_stage_id, connection_id, and connection_address.',
370
+ inputSchema: {
371
+ type: 'object',
372
+ properties: {
373
+ source: { type: 'string', description: 'Source identifier (default: WEB). Options: VSCODE, CLI, WEB, WEBVNC, WEBPTY, WEBIDE, CURSOR, etc.' },
374
+ template_id: { type: 'string', description: 'Template ID; overrides server default (only for new sandbox)' },
375
+ flavor_id: { type: 'string', description: 'Flavor ID; overrides server default (only for new sandbox)' },
376
+ env: { type: 'object', description: 'Environment variables to set in the sandbox (only for new sandbox)' },
377
+ git: {
378
+ type: 'object',
379
+ description: 'Git repo config (only for new sandbox)',
380
+ properties: {
381
+ repo_url: { type: 'string', description: 'Git repository URL' },
382
+ repo_branch: { type: 'string', description: 'Git branch' },
383
+ repo_name: { type: 'string', description: 'Repository name' },
384
+ target_path: { type: 'string', description: 'Clone target path in sandbox' },
385
+ open_type: { type: 'string', description: 'Open type' },
386
+ },
387
+ },
388
+ },
389
+ },
390
+ },
391
+ {
392
+ name: 'huaweicloud_sandbox_credentials',
393
+ description: 'Configure temporary AK/SK for a sandbox via hdkitservice. Injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
394
+ inputSchema: {
395
+ type: 'object',
396
+ properties: {
397
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
398
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
399
+ enable_sts: { type: 'boolean', description: 'Whether to enable STS temporary AK/SK (default: true)' },
400
+ },
401
+ },
402
+ },
403
+ {
404
+ name: 'huaweicloud_sandbox_release',
405
+ description: 'Release a sandbox via hdkitservice. Shuts down and deletes the sandbox, and cleans up the session. Idempotent - releasing a non-existent sandbox returns success.',
406
+ inputSchema: {
407
+ type: 'object',
408
+ properties: {
409
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
410
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
411
+ },
412
+ },
413
+ },
288
414
  ];
289
415
 
290
416
  export async function callTool(name, args = {}) {
@@ -328,6 +454,40 @@ export async function callTool(name, args = {}) {
328
454
  return searchMarketplace(args.query || '', args.category || '');
329
455
  case 'huaweicloud_setup_obs_config':
330
456
  return setupObsConfig(args.profile);
457
+ case 'huaweicloud_auth_status':
458
+ return getAuthStatus(args.target || 'all');
459
+ case 'huaweicloud_auth_sync':
460
+ return syncAuth(args.target || 'all');
461
+ case 'huaweicloud_sandbox_exec': {
462
+ const sandboxWsId1 = args.workspace_id || DEFAULT_WORKSPACE_ID;
463
+ const sandboxUser1 = args.username || 'root';
464
+ const sandboxTimeout1 = args.timeout_ms || 30000;
465
+ const sandboxResult1 = await execOneShot(sandboxWsId1, args.command, sandboxUser1, sandboxTimeout1);
466
+ return { stdout: sandboxResult1.stdout, exitCode: sandboxResult1.exitCode };
467
+ }
468
+ case 'huaweicloud_sandbox_exec_with_session': {
469
+ const sandboxWsId2 = args.workspace_id || DEFAULT_WORKSPACE_ID;
470
+ const sandboxUser2 = args.username || 'root';
471
+ const sandboxTimeout2 = args.timeout_ms || 30000;
472
+ const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
473
+ return { stdout: sandboxResult2.stdout, exitCode: sandboxResult2.exitCode };
474
+ }
475
+ case 'huaweicloud_sandbox_close_session': {
476
+ const sandboxWsId3 = args.workspace_id || DEFAULT_WORKSPACE_ID;
477
+ const sandboxUser3 = args.username || 'root';
478
+ const closed = await closeSession(sandboxWsId3, sandboxUser3);
479
+ return closed ? 'ok' : 'not_connected';
480
+ }
481
+ case 'huaweicloud_sandbox_check_user':
482
+ return await hdkitCheckUser();
483
+ case 'huaweicloud_sandbox_sign_agreement':
484
+ return await hdkitSignAgreement();
485
+ case 'huaweicloud_sandbox_connect':
486
+ return await hdkitConnect(args);
487
+ case 'huaweicloud_sandbox_credentials':
488
+ return await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
489
+ case 'huaweicloud_sandbox_release':
490
+ return await hdkitRelease(args.session_id, args.dev_stage_id);
331
491
  default:
332
492
  throw new Error(`Unknown tool: ${name}`);
333
493
  }
@@ -395,6 +555,33 @@ async function showProfileRedacted(profile) {
395
555
  }
396
556
 
397
557
  async function setupObsConfig(profile) {
558
+ const stored = readGlobalCredentials();
559
+ if (stored?.ak && stored?.sk) {
560
+ try {
561
+ const obs = writeObsConfigFile(stored);
562
+ return {
563
+ ok: true,
564
+ existed: false,
565
+ created: true,
566
+ path: obs.path,
567
+ region: stored.region,
568
+ endpoint: obs.endpoint,
569
+ source: 'global-credentials',
570
+ note: 'OBS credentials synced from the global credential vault. OBS commands (hcloud OBS ls, mb, cp, etc.) should now work.',
571
+ };
572
+ } catch (error) {
573
+ return {
574
+ ok: false,
575
+ error: error.message,
576
+ nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
577
+ };
578
+ }
579
+ }
580
+
581
+ return setupObsConfigFromHcloud(profile);
582
+ }
583
+
584
+ async function setupObsConfigFromHcloud(profile) {
398
585
  const obsConfigPath = join(homedir(), '.obsutilconfig');
399
586
  if (existsSync(obsConfigPath)) {
400
587
  return { ok: true, existed: true, path: obsConfigPath, note: 'OBS config already exists. Delete ~/.obsutilconfig first if you need to re-sync.' };