huaweicloud-devkit 1.0.2-dev.0 → 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.
Files changed (35) hide show
  1. package/README.md +27 -3
  2. package/README.zh-CN.md +27 -3
  3. package/package.json +1 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.mcp.json +3 -1
  8. package/plugins/huaweicloud-core/safety/policy.json +15 -1
  9. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
  10. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
  11. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +1 -1
  12. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +1 -1
  13. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
  14. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +8 -0
  15. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -0
  16. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -0
  17. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +2 -1
  18. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +3 -1
  19. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +1 -1
  20. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
  21. package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -0
  22. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
  23. package/plugins/huaweicloud-core/src/mcp-server.mjs +11 -0
  24. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -0
  25. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
  26. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
  27. package/plugins/huaweicloud-core/src/setup-cli.mjs +582 -68
  28. package/plugins/huaweicloud-core/src/tools.mjs +159 -3
  29. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
  30. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
  31. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
  32. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
  33. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
  34. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
  35. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -0
@@ -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 { execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
10
+ import { hdkitCheckUser, hdkitSignAgreement, hdkitConnect, hdkitCredentials } 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,104 @@ 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_with_session',
314
+ 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.',
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_close_session',
328
+ description: 'Close the persistent terminal session for a workspace.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ workspace_id: { type: 'string', description: 'The workspace ID' },
333
+ username: { type: 'string', description: 'Login username (default: root)' },
334
+ },
335
+ },
336
+ },
337
+ {
338
+ name: 'huaweicloud_sandbox_check_user',
339
+ description: 'Check if the current user has completed real-name verification and signed the required agreements. Returns realnameVerified and agreementSigned status.',
340
+ inputSchema: {
341
+ type: 'object',
342
+ properties: {},
343
+ },
344
+ },
345
+ {
346
+ name: 'huaweicloud_sandbox_sign_agreement',
347
+ description: 'Sign all unsigned or outdated agreements for the current user. Required before huaweicloud_sandbox_connect if check-user returns agreementSigned=false.',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {},
351
+ },
352
+ },
353
+ {
354
+ name: 'huaweicloud_sandbox_connect',
355
+ 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.',
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ source: { type: 'string', description: 'Source identifier (default: WEB). Options: VSCODE, CLI, WEB, WEBVNC, WEBPTY, WEBIDE, CURSOR, etc.' },
360
+ template_id: { type: 'string', description: 'Template ID; overrides server default (only for new sandbox)' },
361
+ flavor_id: { type: 'string', description: 'Flavor ID; overrides server default (only for new sandbox)' },
362
+ env: { type: 'object', description: 'Environment variables to set in the sandbox (only for new sandbox)' },
363
+ git: {
364
+ type: 'object',
365
+ description: 'Git repo config (only for new sandbox)',
366
+ properties: {
367
+ repo_url: { type: 'string', description: 'Git repository URL' },
368
+ repo_branch: { type: 'string', description: 'Git branch' },
369
+ repo_name: { type: 'string', description: 'Repository name' },
370
+ target_path: { type: 'string', description: 'Clone target path in sandbox' },
371
+ open_type: { type: 'string', description: 'Open type' },
372
+ },
373
+ },
374
+ },
375
+ },
376
+ },
377
+ {
378
+ name: 'huaweicloud_sandbox_credentials',
379
+ description: 'Configure temporary AK/SK for a sandbox via hdkitservice. Injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
380
+ inputSchema: {
381
+ type: 'object',
382
+ properties: {
383
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
384
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
385
+ enable_sts: { type: 'boolean', description: 'Whether to enable STS temporary AK/SK (default: true)' },
386
+ },
387
+ },
388
+ },
389
+
288
390
  ];
289
391
 
290
392
  export async function callTool(name, args = {}) {
@@ -328,6 +430,31 @@ export async function callTool(name, args = {}) {
328
430
  return searchMarketplace(args.query || '', args.category || '');
329
431
  case 'huaweicloud_setup_obs_config':
330
432
  return setupObsConfig(args.profile);
433
+ case 'huaweicloud_auth_status':
434
+ return getAuthStatus(args.target || 'all');
435
+ case 'huaweicloud_auth_sync':
436
+ return syncAuth(args.target || 'all');
437
+ case 'huaweicloud_sandbox_exec_with_session': {
438
+ const sandboxWsId2 = args.workspace_id || DEFAULT_WORKSPACE_ID;
439
+ const sandboxUser2 = args.username || 'root';
440
+ const sandboxTimeout2 = args.timeout_ms || 30000;
441
+ const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
442
+ return { stdout: sandboxResult2.stdout, exitCode: sandboxResult2.exitCode };
443
+ }
444
+ case 'huaweicloud_sandbox_close_session': {
445
+ const sandboxWsId3 = args.workspace_id || DEFAULT_WORKSPACE_ID;
446
+ const sandboxUser3 = args.username || 'root';
447
+ const closed = await closeSession(sandboxWsId3, sandboxUser3);
448
+ return closed ? 'ok' : 'not_connected';
449
+ }
450
+ case 'huaweicloud_sandbox_check_user':
451
+ return await hdkitCheckUser();
452
+ case 'huaweicloud_sandbox_sign_agreement':
453
+ return await hdkitSignAgreement();
454
+ case 'huaweicloud_sandbox_connect':
455
+ return await hdkitConnect(args);
456
+ case 'huaweicloud_sandbox_credentials':
457
+ return await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
331
458
  default:
332
459
  throw new Error(`Unknown tool: ${name}`);
333
460
  }
@@ -395,6 +522,33 @@ async function showProfileRedacted(profile) {
395
522
  }
396
523
 
397
524
  async function setupObsConfig(profile) {
525
+ const stored = readGlobalCredentials();
526
+ if (stored?.ak && stored?.sk) {
527
+ try {
528
+ const obs = writeObsConfigFile(stored);
529
+ return {
530
+ ok: true,
531
+ existed: false,
532
+ created: true,
533
+ path: obs.path,
534
+ region: stored.region,
535
+ endpoint: obs.endpoint,
536
+ source: 'global-credentials',
537
+ note: 'OBS credentials synced from the global credential vault. OBS commands (hcloud OBS ls, mb, cp, etc.) should now work.',
538
+ };
539
+ } catch (error) {
540
+ return {
541
+ ok: false,
542
+ error: error.message,
543
+ nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
544
+ };
545
+ }
546
+ }
547
+
548
+ return setupObsConfigFromHcloud(profile);
549
+ }
550
+
551
+ async function setupObsConfigFromHcloud(profile) {
398
552
  const obsConfigPath = join(homedir(), '.obsutilconfig');
399
553
  if (existsSync(obsConfigPath)) {
400
554
  return { ok: true, existed: true, path: obsConfigPath, note: 'OBS config already exists. Delete ~/.obsutilconfig first if you need to re-sync.' };
@@ -409,7 +563,7 @@ async function setupObsConfig(profile) {
409
563
  ok: false,
410
564
  error: 'Failed to read hcloud profile.',
411
565
  detail: result.error || result.stderr || 'hcloud not installed or not configured',
412
- nextStep: 'Run "hcloud configure init" outside agent chat, then retry.',
566
+ nextStep: 'Run "npx huaweicloud-devkit auth init" outside agent chat, then retry.',
413
567
  };
414
568
  }
415
569
 
@@ -435,7 +589,7 @@ async function setupObsConfig(profile) {
435
589
  return {
436
590
  ok: false,
437
591
  error: 'No credentials found in hcloud profile.',
438
- nextStep: 'Run "hcloud configure init" outside agent chat to set up credentials first.',
592
+ nextStep: 'Run "npx huaweicloud-devkit auth init" outside agent chat to set up credentials first.',
439
593
  };
440
594
  }
441
595
 
@@ -448,7 +602,8 @@ async function setupObsConfig(profile) {
448
602
  }
449
603
 
450
604
  const endpoint = `https://obs.${region}.myhuaweicloud.com`;
451
- const configContent = `[default]\r\nendpoint=${endpoint}\r\nak=${accessKeyId}\r\nsk=${secretAccessKey}\r\n`;
605
+ // Flat key=value format (no [default] section) as written by KooCLI 7.x `hcloud OBS config`.
606
+ const configContent = `endpoint=${endpoint}\nak=${accessKeyId}\nsk=${secretAccessKey}\n`;
452
607
 
453
608
  try {
454
609
  writeFileSync(obsConfigPath, configContent, { encoding: 'utf8', mode: 0o600 });
@@ -554,6 +709,7 @@ function serviceCatalog(intent = '') {
554
709
  { keywords: ['cts', 'audit', 'trace', 'tracker'], skills: ['huawei-cts'], services: ['CTS'] },
555
710
  { keywords: ['cbr', 'backup', 'restore', 'vault', 'snapshot'], skills: ['huawei-cbr'], services: ['CBR'] },
556
711
  { keywords: ['deployment', 'deploy', 'ci/cd', 'pipeline', 'release'], skills: ['huawei-deployment'], services: ['CloudDeploy'] },
712
+ { keywords: ['sandbox', 'devstation', 'workspace', 'terminal', 'preview', 'hwlink'], skills: ['huawei-sandbox'], services: ['Sandbox', 'DevStation'] },
557
713
  { keywords: ['dds', 'dcs', 'mongodb', 'redis', 'memcached', 'cache', 'document db'], skills: ['huawei-dds-dcs'], services: ['DDS', 'DCS'] },
558
714
  ];
559
715
  const matched = [];
@@ -0,0 +1,427 @@
1
+ import {
2
+ DEFAULT_TIMEOUT_MS,
3
+ WebSocketExecError,
4
+ buildMarkers,
5
+ buildShellCommands,
6
+ cleanCommandOutput,
7
+ } from './ws-exec-client.js';
8
+ import { HwlinkWebSocketMultiplexer } from './hwlink-multiplexer.js';
9
+ import { HwlinkTerminalChannel } from './hwlink-terminal-channel.js';
10
+
11
+ const decoder = new TextDecoder();
12
+ const encoder = new TextEncoder();
13
+
14
+ function normalizeCommand(command) {
15
+ if (!command || !String(command).trim()) {
16
+ throw new WebSocketExecError('missing command', 2);
17
+ }
18
+ return String(command).trim();
19
+ }
20
+
21
+ function normalizeTimeout(timeoutMs) {
22
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
23
+ throw new WebSocketExecError('timeoutMs must be a positive number of milliseconds', 2);
24
+ }
25
+ return timeoutMs;
26
+ }
27
+
28
+ function normalizeSource(source) {
29
+ const numericSource = Number(source);
30
+ if (!Number.isInteger(numericSource) || numericSource < -0x80000000 || numericSource > 0xffffffff) {
31
+ throw new WebSocketExecError('hwlink source must be an int32 or uint32 number', 2);
32
+ }
33
+ return numericSource;
34
+ }
35
+
36
+ function normalizeUrl(url) {
37
+ if (!url || !String(url).trim()) {
38
+ throw new WebSocketExecError('hwlink url is required', 2);
39
+ }
40
+ return String(url);
41
+ }
42
+
43
+ function createHwlinkTerminal(options = {}) {
44
+ const {
45
+ url,
46
+ source,
47
+ username = 'root',
48
+ WebSocketImpl = globalThis.WebSocket,
49
+ protocol = 'devenv',
50
+ cols,
51
+ rows,
52
+ onFrame,
53
+ onData,
54
+ onError,
55
+ onClose,
56
+ trace = false,
57
+ } = options;
58
+
59
+ const normalizedUrl = normalizeUrl(url);
60
+ const normalizedSource = normalizeSource(source);
61
+ const mux = new HwlinkWebSocketMultiplexer(normalizedUrl, normalizedSource, {
62
+ WebSocketImpl,
63
+ protocol,
64
+ onFrame,
65
+ trace,
66
+ });
67
+ const term = new HwlinkTerminalChannel(username);
68
+ let closed = false;
69
+ let readySettled = false;
70
+ let readyResolve;
71
+ let readyReject;
72
+
73
+ const ready = new Promise((resolve, reject) => {
74
+ readyResolve = resolve;
75
+ readyReject = reject;
76
+ });
77
+
78
+ function settleReady(error) {
79
+ if (readySettled) return;
80
+ readySettled = true;
81
+ if (error) readyReject(error);
82
+ else readyResolve(handle);
83
+ }
84
+
85
+ function handleError(error, prefix) {
86
+ const wrapped = new WebSocketExecError(`${prefix}: ${error.message}`, 1, {
87
+ cause: error,
88
+ phase: readySettled ? 'open' : 'opening',
89
+ });
90
+ settleReady(wrapped);
91
+ if (onError) onError(wrapped);
92
+ close();
93
+ }
94
+
95
+ function handleClose() {
96
+ settleReady(new WebSocketExecError('hwlink terminal closed before ready', 1, {
97
+ phase: 'opening',
98
+ }));
99
+ if (closed) return;
100
+ closed = true;
101
+ if (onClose) onClose();
102
+ }
103
+
104
+ term.onReady(() => {
105
+ if (Number.isFinite(cols) && Number.isFinite(rows)) {
106
+ term.resize(cols, rows);
107
+ }
108
+ settleReady();
109
+ });
110
+ term.onData((data) => {
111
+ if (onData) onData(data);
112
+ });
113
+ term.onError((error) => handleError(error, 'hwlink terminal error'));
114
+ term.onClose(handleClose);
115
+ mux.onError = (error) => handleError(error, 'hwlink websocket error');
116
+ mux.onClose = handleClose;
117
+
118
+ function close() {
119
+ if (closed) return;
120
+ closed = true;
121
+ term.close();
122
+ mux.close();
123
+ }
124
+
125
+ const handle = {
126
+ url: normalizedUrl,
127
+ source: normalizedSource,
128
+ username,
129
+ mux,
130
+ term,
131
+ ready,
132
+ close,
133
+ resize: (nextCols, nextRows) => term.resize(nextCols, nextRows),
134
+ sendInput: (data) => term.sendInput(data),
135
+ sendText: (text) => term.sendText(text),
136
+ };
137
+
138
+ term.attach(mux);
139
+ return handle;
140
+ }
141
+
142
+ class HwlinkTerminalExecSession {
143
+ constructor(options = {}) {
144
+ const {
145
+ url,
146
+ source,
147
+ username = 'root',
148
+ timeoutMs = DEFAULT_TIMEOUT_MS,
149
+ WebSocketImpl = globalThis.WebSocket,
150
+ protocol = 'devenv',
151
+ cols,
152
+ rows,
153
+ onFrame,
154
+ onData,
155
+ trace = false,
156
+ } = options;
157
+
158
+ this.url = normalizeUrl(url);
159
+ this.source = normalizeSource(source);
160
+ this.username = username;
161
+ this.timeoutMs = normalizeTimeout(timeoutMs);
162
+ this.onData = onData;
163
+ this.initialCols = cols;
164
+ this.initialRows = rows;
165
+ this.state = 'opening';
166
+ this.readyBuffer = '';
167
+ this.inputEchoed = false;
168
+ this.pending = null;
169
+ this.queue = Promise.resolve();
170
+ this.readyMarkers = buildMarkers();
171
+ const { readyCommand } = buildShellCommands(this.readyMarkers);
172
+ this.readyCommand = readyCommand;
173
+
174
+ this.readyPromise = new Promise((resolve, reject) => {
175
+ this.resolveReady = resolve;
176
+ this.rejectReady = reject;
177
+ });
178
+
179
+ this.readyTimeout = setTimeout(() => {
180
+ this.fail(new WebSocketExecError(`hwlink terminal ready timeout after ${this.timeoutMs}ms`, 124, {
181
+ partialOutput: this.readyBuffer,
182
+ phase: 'opening',
183
+ }));
184
+ }, this.timeoutMs);
185
+
186
+ try {
187
+ this.mux = new HwlinkWebSocketMultiplexer(this.url, this.source, {
188
+ WebSocketImpl,
189
+ protocol,
190
+ onFrame,
191
+ trace,
192
+ });
193
+ } catch (error) {
194
+ clearTimeout(this.readyTimeout);
195
+ throw new WebSocketExecError(error.message, 2, { cause: error, phase: 'opening' });
196
+ }
197
+
198
+ this.term = new HwlinkTerminalChannel(username);
199
+ this.term.onData((data) => this.handleTerminalData(data));
200
+ this.term.onError((error) => {
201
+ this.fail(new WebSocketExecError(`hwlink terminal error: ${error.message}`, 1, {
202
+ cause: error,
203
+ phase: this.state,
204
+ }));
205
+ });
206
+ this.term.onReady(() => {
207
+ if (Number.isFinite(this.initialCols) && Number.isFinite(this.initialRows)) {
208
+ this.term.resize(this.initialCols, this.initialRows);
209
+ }
210
+ this.sendLine(this.readyCommand);
211
+ });
212
+ this.term.onClose(() => {
213
+ if (this.state !== 'closed') {
214
+ this.fail(new WebSocketExecError('hwlink terminal closed before completion marker', 1, {
215
+ phase: this.state,
216
+ }));
217
+ }
218
+ });
219
+ this.mux.onClose = () => {
220
+ if (this.state !== 'closed') {
221
+ this.fail(new WebSocketExecError('hwlink websocket closed before completion marker', 1, {
222
+ phase: this.state,
223
+ }));
224
+ }
225
+ };
226
+ this.mux.onError = (error) => {
227
+ if (this.state !== 'closed') {
228
+ this.fail(new WebSocketExecError(`hwlink websocket error: ${error.message}`, 1, {
229
+ cause: error,
230
+ phase: this.state,
231
+ }));
232
+ }
233
+ };
234
+
235
+ this.term.attach(this.mux);
236
+ }
237
+
238
+ ready() {
239
+ return this.readyPromise;
240
+ }
241
+
242
+ exec(command, options = {}) {
243
+ const run = () => this.runExec(command, options);
244
+ const result = this.queue.then(run, run);
245
+ this.queue = result.catch(() => {});
246
+ return result;
247
+ }
248
+
249
+ resize(cols, rows) {
250
+ this.term.resize(cols, rows);
251
+ }
252
+
253
+ close() {
254
+ if (this.state === 'closed') return;
255
+ const wasOpening = this.state === 'opening';
256
+ const pending = this.pending;
257
+ this.state = 'closed';
258
+ clearTimeout(this.readyTimeout);
259
+ this.pending = null;
260
+
261
+ if (wasOpening) {
262
+ this.rejectReady(new WebSocketExecError('hwlink terminal session closed before ready', 1, {
263
+ phase: 'opening',
264
+ }));
265
+ }
266
+
267
+ if (pending) {
268
+ clearTimeout(pending.timeout);
269
+ pending.reject(new WebSocketExecError('hwlink terminal session closed before completion marker', 1, {
270
+ phase: 'running',
271
+ }));
272
+ }
273
+
274
+ this.term.close();
275
+ this.mux.close();
276
+ }
277
+
278
+ fail(error) {
279
+ if (this.state === 'closed') return;
280
+
281
+ const wasOpening = this.state === 'opening';
282
+ const pending = this.pending;
283
+ this.state = 'closed';
284
+ this.pending = null;
285
+ clearTimeout(this.readyTimeout);
286
+
287
+ if (wasOpening) {
288
+ this.rejectReady(error);
289
+ }
290
+
291
+ if (pending) {
292
+ clearTimeout(pending.timeout);
293
+ pending.reject(error);
294
+ }
295
+
296
+ this.term.close();
297
+ this.mux.close();
298
+ }
299
+
300
+ handleTerminalData(data) {
301
+ if (this.state === 'closed') return;
302
+
303
+ const chunk = decoder.decode(data, { stream: true });
304
+ if (this.onData) this.onData(data, chunk);
305
+
306
+ if (this.state === 'opening') {
307
+ this.readyBuffer += chunk;
308
+ this.tryCompleteReady();
309
+ return;
310
+ }
311
+
312
+ if (this.pending) {
313
+ this.pending.buffer += chunk;
314
+ this.tryCompletePending();
315
+ }
316
+ }
317
+
318
+ tryCompleteReady() {
319
+ const readyIndex = this.readyBuffer.indexOf(this.readyMarkers.readyMarker);
320
+ if (readyIndex === -1) return;
321
+
322
+ this.inputEchoed = this.readyBuffer.slice(0, readyIndex).includes(this.readyCommand);
323
+ this.readyBuffer = '';
324
+ this.state = 'ready';
325
+ clearTimeout(this.readyTimeout);
326
+ this.resolveReady(this);
327
+ }
328
+
329
+ runExec(command, options = {}) {
330
+ if (this.state !== 'ready') {
331
+ return Promise.reject(new WebSocketExecError('hwlink terminal exec session is not ready', 1, {
332
+ phase: this.state,
333
+ }));
334
+ }
335
+
336
+ const shellCommand = normalizeCommand(command);
337
+ const timeoutMs = normalizeTimeout(options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs);
338
+ const markers = buildMarkers();
339
+ const { doneCommand } = buildShellCommands(markers);
340
+
341
+ return new Promise((resolve, reject) => {
342
+ this.pending = {
343
+ buffer: '',
344
+ command: shellCommand,
345
+ doneCommand,
346
+ markers,
347
+ reject,
348
+ resolve,
349
+ timeout: setTimeout(() => {
350
+ this.fail(new WebSocketExecError(`hwlink terminal exec timeout after ${timeoutMs}ms`, 124, {
351
+ partialOutput: this.pending ? this.pending.buffer : '',
352
+ phase: 'running',
353
+ }));
354
+ }, timeoutMs),
355
+ };
356
+
357
+ this.sendLine(shellCommand);
358
+ this.sendLine(doneCommand);
359
+ });
360
+ }
361
+
362
+ tryCompletePending() {
363
+ const pending = this.pending;
364
+ if (!pending) return;
365
+
366
+ const doneMatch = pending.buffer.match(pending.markers.donePattern);
367
+ if (!doneMatch || doneMatch.index === undefined) return;
368
+
369
+ const rawOutput = pending.buffer.slice(0, doneMatch.index);
370
+ const stdout = cleanCommandOutput(rawOutput, {
371
+ inputEchoed: this.inputEchoed,
372
+ command: pending.command,
373
+ doneCommand: pending.doneCommand,
374
+ });
375
+
376
+ clearTimeout(pending.timeout);
377
+ this.pending = null;
378
+ pending.resolve({
379
+ stdout,
380
+ exitCode: Number(doneMatch[1]),
381
+ url: this.url,
382
+ source: this.source,
383
+ username: this.username,
384
+ command: pending.command,
385
+ });
386
+ }
387
+
388
+ sendLine(line) {
389
+ this.term.sendInput(encoder.encode(`${line}\n`));
390
+ }
391
+ }
392
+
393
+ async function connectHwlinkTerminalSession(options = {}) {
394
+ const session = new HwlinkTerminalExecSession(options);
395
+ await session.ready();
396
+ return session;
397
+ }
398
+
399
+ async function connectHwlinkInteractiveTerminal(options = {}) {
400
+ const terminal = createHwlinkTerminal(options);
401
+ await terminal.ready;
402
+ return terminal;
403
+ }
404
+
405
+ async function executeHwlinkCommand(options = {}) {
406
+ const { command, timeoutMs = DEFAULT_TIMEOUT_MS, ...sessionOptions } = options;
407
+ const shellCommand = normalizeCommand(command);
408
+ const normalizedTimeoutMs = normalizeTimeout(timeoutMs);
409
+ const session = await connectHwlinkTerminalSession({
410
+ ...sessionOptions,
411
+ timeoutMs: normalizedTimeoutMs,
412
+ });
413
+ try {
414
+ return await session.exec(shellCommand, { timeoutMs: normalizedTimeoutMs });
415
+ } finally {
416
+ session.close();
417
+ }
418
+ }
419
+
420
+ export {
421
+ HwlinkTerminalExecSession,
422
+ connectHwlinkInteractiveTerminal,
423
+ connectHwlinkTerminalSession,
424
+ createHwlinkTerminal,
425
+ executeHwlinkCommand,
426
+ normalizeSource,
427
+ };