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

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 (25) hide show
  1. package/README.md +25 -2
  2. package/README.zh-CN.md +25 -2
  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 +2 -1
  8. package/plugins/huaweicloud-core/safety/policy.json +16 -1
  9. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
  10. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +67 -0
  11. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
  12. package/plugins/huaweicloud-core/src/auth/credentials.mjs +74 -0
  13. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
  14. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +97 -0
  15. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
  16. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
  17. package/plugins/huaweicloud-core/src/setup-cli.mjs +513 -66
  18. package/plugins/huaweicloud-core/src/tools.mjs +166 -0
  19. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
  20. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
  21. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
  22. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
  23. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
  24. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
  25. 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, 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,114 @@ 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 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
+ {
390
+ name: 'huaweicloud_sandbox_release',
391
+ 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.',
392
+ inputSchema: {
393
+ type: 'object',
394
+ properties: {
395
+ session_id: { type: 'string', description: 'Session ID from huaweicloud_sandbox_connect' },
396
+ dev_stage_id: { type: 'string', description: 'DevStation environment ID (alternative to session_id)' },
397
+ },
398
+ },
399
+ },
288
400
  ];
289
401
 
290
402
  export async function callTool(name, args = {}) {
@@ -328,6 +440,33 @@ export async function callTool(name, args = {}) {
328
440
  return searchMarketplace(args.query || '', args.category || '');
329
441
  case 'huaweicloud_setup_obs_config':
330
442
  return setupObsConfig(args.profile);
443
+ case 'huaweicloud_auth_status':
444
+ return getAuthStatus(args.target || 'all');
445
+ case 'huaweicloud_auth_sync':
446
+ return syncAuth(args.target || 'all');
447
+ case 'huaweicloud_sandbox_exec_with_session': {
448
+ const sandboxWsId2 = args.workspace_id || DEFAULT_WORKSPACE_ID;
449
+ const sandboxUser2 = args.username || 'root';
450
+ const sandboxTimeout2 = args.timeout_ms || 30000;
451
+ const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
452
+ return { stdout: sandboxResult2.stdout, exitCode: sandboxResult2.exitCode };
453
+ }
454
+ case 'huaweicloud_sandbox_close_session': {
455
+ const sandboxWsId3 = args.workspace_id || DEFAULT_WORKSPACE_ID;
456
+ const sandboxUser3 = args.username || 'root';
457
+ const closed = await closeSession(sandboxWsId3, sandboxUser3);
458
+ return closed ? 'ok' : 'not_connected';
459
+ }
460
+ case 'huaweicloud_sandbox_check_user':
461
+ return await hdkitCheckUser();
462
+ case 'huaweicloud_sandbox_sign_agreement':
463
+ return await hdkitSignAgreement();
464
+ case 'huaweicloud_sandbox_connect':
465
+ return await hdkitConnect(args);
466
+ case 'huaweicloud_sandbox_credentials':
467
+ return await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
468
+ case 'huaweicloud_sandbox_release':
469
+ return await hdkitRelease(args.session_id, args.dev_stage_id);
331
470
  default:
332
471
  throw new Error(`Unknown tool: ${name}`);
333
472
  }
@@ -395,6 +534,33 @@ async function showProfileRedacted(profile) {
395
534
  }
396
535
 
397
536
  async function setupObsConfig(profile) {
537
+ const stored = readGlobalCredentials();
538
+ if (stored?.ak && stored?.sk) {
539
+ try {
540
+ const obs = writeObsConfigFile(stored);
541
+ return {
542
+ ok: true,
543
+ existed: false,
544
+ created: true,
545
+ path: obs.path,
546
+ region: stored.region,
547
+ endpoint: obs.endpoint,
548
+ source: 'global-credentials',
549
+ note: 'OBS credentials synced from the global credential vault. OBS commands (hcloud OBS ls, mb, cp, etc.) should now work.',
550
+ };
551
+ } catch (error) {
552
+ return {
553
+ ok: false,
554
+ error: error.message,
555
+ nextStep: 'Run "npx huaweicloud-devkit auth init" to refresh credentials and region.',
556
+ };
557
+ }
558
+ }
559
+
560
+ return setupObsConfigFromHcloud(profile);
561
+ }
562
+
563
+ async function setupObsConfigFromHcloud(profile) {
398
564
  const obsConfigPath = join(homedir(), '.obsutilconfig');
399
565
  if (existsSync(obsConfigPath)) {
400
566
  return { ok: true, existed: true, path: obsConfigPath, note: 'OBS config already exists. Delete ~/.obsutilconfig first if you need to re-sync.' };
@@ -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
+ };