huaweicloud-devkit 0.1.26-dev.0 → 1.0.1

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 (44) hide show
  1. package/README.md +70 -104
  2. package/README.zh-CN.md +138 -0
  3. package/package.json +3 -2
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +43 -43
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +42 -42
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +43 -43
  7. package/plugins/huaweicloud-core/.mcp.json +3 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +44 -0
  9. package/plugins/huaweicloud-core/safety/policy.json +15 -1
  10. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
  11. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +22 -2
  12. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +17 -0
  13. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +2 -2
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +51 -1
  16. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +23 -3
  17. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +1 -1
  18. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +1 -1
  19. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +1 -1
  20. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +2 -2
  21. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +21 -3
  22. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -0
  23. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +48 -5
  24. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -0
  25. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +5 -2
  26. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -6
  27. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +3 -1
  28. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +1 -1
  29. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
  30. package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -0
  31. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
  32. package/plugins/huaweicloud-core/src/mcp-server.mjs +11 -0
  33. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -0
  34. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
  35. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
  36. package/plugins/huaweicloud-core/src/setup-cli.mjs +733 -74
  37. package/plugins/huaweicloud-core/src/tools.mjs +164 -3
  38. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
  39. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
  40. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
  41. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
  42. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
  43. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
  44. 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');
@@ -17,10 +21,15 @@ function codeartsSkillsDir() {
17
21
  const home = homedir();
18
22
  return join(home, '.codeartsdoer', 'skills');
19
23
  }
24
+ function workbuddySkillsDir() {
25
+ const home = homedir();
26
+ return join(home, '.workbuddy', 'skills');
27
+ }
20
28
  function resolveSkillsRoot() {
21
29
  if (existsSync(SKILLS_ROOT_DEV)) return SKILLS_ROOT_DEV;
22
30
  if (existsSync(codeartsSkillsDir())) return codeartsSkillsDir();
23
31
  if (existsSync(opencodeSkillsDir())) return opencodeSkillsDir();
32
+ if (existsSync(workbuddySkillsDir())) return workbuddySkillsDir();
24
33
  return SKILLS_ROOT_DEV;
25
34
  }
26
35
  const SKILLS_ROOT = resolveSkillsRoot();
@@ -280,6 +289,104 @@ export const TOOL_DEFINITIONS = [
280
289
  },
281
290
  },
282
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 realname_verified and agreement_signed 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 agreement_signed=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
+
283
390
  ];
284
391
 
285
392
  export async function callTool(name, args = {}) {
@@ -323,6 +430,31 @@ export async function callTool(name, args = {}) {
323
430
  return searchMarketplace(args.query || '', args.category || '');
324
431
  case 'huaweicloud_setup_obs_config':
325
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);
326
458
  default:
327
459
  throw new Error(`Unknown tool: ${name}`);
328
460
  }
@@ -390,6 +522,33 @@ async function showProfileRedacted(profile) {
390
522
  }
391
523
 
392
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) {
393
552
  const obsConfigPath = join(homedir(), '.obsutilconfig');
394
553
  if (existsSync(obsConfigPath)) {
395
554
  return { ok: true, existed: true, path: obsConfigPath, note: 'OBS config already exists. Delete ~/.obsutilconfig first if you need to re-sync.' };
@@ -404,7 +563,7 @@ async function setupObsConfig(profile) {
404
563
  ok: false,
405
564
  error: 'Failed to read hcloud profile.',
406
565
  detail: result.error || result.stderr || 'hcloud not installed or not configured',
407
- nextStep: 'Run "hcloud configure init" outside agent chat, then retry.',
566
+ nextStep: 'Run "npx huaweicloud-devkit auth init" outside agent chat, then retry.',
408
567
  };
409
568
  }
410
569
 
@@ -430,7 +589,7 @@ async function setupObsConfig(profile) {
430
589
  return {
431
590
  ok: false,
432
591
  error: 'No credentials found in hcloud profile.',
433
- 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.',
434
593
  };
435
594
  }
436
595
 
@@ -443,7 +602,8 @@ async function setupObsConfig(profile) {
443
602
  }
444
603
 
445
604
  const endpoint = `https://obs.${region}.myhuaweicloud.com`;
446
- 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`;
447
607
 
448
608
  try {
449
609
  writeFileSync(obsConfigPath, configContent, { encoding: 'utf8', mode: 0o600 });
@@ -549,6 +709,7 @@ function serviceCatalog(intent = '') {
549
709
  { keywords: ['cts', 'audit', 'trace', 'tracker'], skills: ['huawei-cts'], services: ['CTS'] },
550
710
  { keywords: ['cbr', 'backup', 'restore', 'vault', 'snapshot'], skills: ['huawei-cbr'], services: ['CBR'] },
551
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'] },
552
713
  { keywords: ['dds', 'dcs', 'mongodb', 'redis', 'memcached', 'cache', 'document db'], skills: ['huawei-dds-dcs'], services: ['DDS', 'DCS'] },
553
714
  ];
554
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
+ };