huaweicloud-devkit 1.0.2-dev.9 → 1.0.2-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.agents/plugins/marketplace.json +20 -20
  2. package/LICENSE +201 -201
  3. package/README.md +163 -138
  4. package/README.zh-CN.md +161 -138
  5. package/bin/setup.cjs +8 -8
  6. package/integrations/opencode/commands/huaweicloud-doctor.md +14 -14
  7. package/integrations/opencode/opencode.json +14 -14
  8. package/integrations/opencode/skills/huaweicloud-core/SKILL.md +32 -32
  9. package/package.json +46 -44
  10. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +44 -44
  11. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +43 -43
  12. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +44 -44
  13. package/plugins/huaweicloud-core/.mcp.json +15 -15
  14. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +44 -44
  15. package/plugins/huaweicloud-core/hooks/hooks.json +26 -26
  16. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +136 -136
  17. package/plugins/huaweicloud-core/safety/policy.json +99 -99
  18. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +199 -199
  19. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +164 -164
  20. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -48
  21. package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -50
  22. package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +101 -101
  23. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +90 -90
  24. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +190 -190
  25. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +124 -124
  26. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -55
  27. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -70
  28. package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +56 -56
  29. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +97 -97
  30. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +29 -29
  31. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +23 -23
  32. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +185 -185
  33. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +133 -133
  34. package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -31
  35. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +55 -55
  36. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -52
  37. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +131 -131
  38. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +58 -58
  39. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +71 -71
  40. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -119
  41. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -142
  42. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -56
  43. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +65 -65
  44. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -111
  45. package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +122 -122
  46. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +56 -56
  47. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -65
  48. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +133 -127
  49. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +49 -49
  50. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +26 -26
  51. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +100 -100
  52. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -40
  53. package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -56
  54. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +224 -224
  55. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +135 -134
  56. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -69
  57. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +84 -84
  58. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +76 -76
  59. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +59 -59
  60. package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +50 -50
  61. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -63
  62. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +56 -56
  63. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -4
  64. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +51 -47
  65. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -4
  66. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +128 -128
  67. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -4
  68. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +110 -95
  69. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -4
  70. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +19 -19
  71. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +27 -27
  72. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +60 -60
  73. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -4
  74. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -83
  75. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -4
  76. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +105 -87
  77. package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -95
  78. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -63
  79. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +265 -265
  80. package/plugins/huaweicloud-core/src/mcp-server.mjs +129 -129
  81. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +155 -0
  82. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +87 -0
  83. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +137 -137
  84. package/plugins/huaweicloud-core/src/safety-policy.mjs +302 -302
  85. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +91 -87
  86. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +162 -153
  87. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +112 -105
  88. package/plugins/huaweicloud-core/src/search-market.mjs +174 -169
  89. package/plugins/huaweicloud-core/src/setup-cli.mjs +2017 -1594
  90. package/plugins/huaweicloud-core/src/tools.mjs +1008 -993
  91. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -427
  92. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -132
  93. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -227
  94. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -202
  95. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -158
  96. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -19
  97. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -338
@@ -1,338 +1,338 @@
1
- import { Buffer } from 'node:buffer';
2
- import { randomBytes } from 'node:crypto';
3
-
4
- const DEFAULT_URL = 'ws://127.0.0.1:8080';
5
- const DEFAULT_TIMEOUT_MS = 30000;
6
-
7
- class WebSocketExecError extends Error {
8
- constructor(message, exitCode, details = {}) {
9
- super(message);
10
- this.name = 'WebSocketExecError';
11
- this.exitCode = exitCode;
12
- Object.assign(this, details);
13
- }
14
- }
15
-
16
- async function eventDataToString(data) {
17
- if (typeof data === 'string') return data;
18
- if (Buffer.isBuffer(data)) return data.toString('utf8');
19
- if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8');
20
- if (ArrayBuffer.isView(data)) {
21
- return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
22
- }
23
- if (data && typeof data.arrayBuffer === 'function') {
24
- return Buffer.from(await data.arrayBuffer()).toString('utf8');
25
- }
26
- return String(data);
27
- }
28
-
29
- function sendLine(ws, line) {
30
- ws.send(`${line}\n`);
31
- }
32
-
33
- function closeQuietly(ws) {
34
- try {
35
- ws.close();
36
- } catch {
37
- // The caller is already settling the operation; close errors are not useful.
38
- }
39
- }
40
-
41
- function escapeRegExp(text) {
42
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43
- }
44
-
45
- function stripEchoedLine(text, line, options = {}) {
46
- const escaped = escapeRegExp(line);
47
- const stripped = text.replace(new RegExp(`(^|\\r?\\n)${escaped}\\r?\\n?`), '$1');
48
- if (!options.allowAttached || stripped !== text) return stripped;
49
- return text.replace(new RegExp(`${escaped}(?:\\r?\\n){0,2}$`), '');
50
- }
51
-
52
- function buildMarkers(nonce = randomBytes(8).toString('hex')) {
53
- const readyPrefix = '__WS_EXEC_READY_';
54
- const donePrefix = '__WS_EXEC_DONE_';
55
- const readySuffix = `${nonce}__`;
56
- const doneSuffix = `${nonce}__:`;
57
- const readyMarker = `${readyPrefix}${readySuffix}`;
58
- const doneMarker = `${donePrefix}${doneSuffix}`;
59
-
60
- return {
61
- nonce,
62
- readyPrefix,
63
- donePrefix,
64
- readySuffix,
65
- doneSuffix,
66
- readyMarker,
67
- doneMarker,
68
- donePattern: new RegExp(`${escapeRegExp(doneMarker)}(\\d+)`),
69
- };
70
- }
71
-
72
- function buildShellCommands(markers) {
73
- return {
74
- readyCommand: `stty -echo 2>/dev/null; export PS1= PS2= PROMPT_COMMAND=; printf '\\n%s%s\\n' '${markers.readyPrefix}' '${markers.readySuffix}'`,
75
- doneCommand: `__ws_exec_rc=$?; printf '\\n%s%s%d\\n' '${markers.donePrefix}' '${markers.doneSuffix}' "$__ws_exec_rc"`,
76
- };
77
- }
78
-
79
- function cleanCommandOutput(output, { inputEchoed, command, doneCommand }) {
80
- let cleaned = output;
81
- if (inputEchoed) cleaned = stripEchoedLine(cleaned, command);
82
- cleaned = stripEchoedLine(cleaned, doneCommand, { allowAttached: true });
83
- return cleaned.replace(/^\r?\n/, '').replace(/\r?\n\r?\n$/, '\n');
84
- }
85
-
86
- function normalizeCommand(command) {
87
- if (!command || !String(command).trim()) {
88
- throw new WebSocketExecError('missing command', 2);
89
- }
90
- return String(command).trim();
91
- }
92
-
93
- function normalizeTimeout(timeoutMs) {
94
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
95
- throw new WebSocketExecError('timeoutMs must be a positive number of milliseconds', 2);
96
- }
97
- return timeoutMs;
98
- }
99
-
100
- class WebSocketShellSession {
101
- constructor(options = {}) {
102
- const {
103
- url = DEFAULT_URL,
104
- timeoutMs = DEFAULT_TIMEOUT_MS,
105
- WebSocketImpl = globalThis.WebSocket,
106
- onFrame,
107
- } = options;
108
-
109
- if (typeof WebSocketImpl !== 'function') {
110
- throw new WebSocketExecError('global WebSocket is unavailable; use Node.js 22+ or pass WebSocketImpl', 2);
111
- }
112
-
113
- this.url = url;
114
- this.timeoutMs = normalizeTimeout(timeoutMs);
115
- this.WebSocketImpl = WebSocketImpl;
116
- this.onFrame = onFrame;
117
- this.state = 'opening';
118
- this.readyBuffer = '';
119
- this.inputEchoed = false;
120
- this.pending = null;
121
- this.queue = Promise.resolve();
122
- this.ws = new WebSocketImpl(url);
123
-
124
- this.readyMarkers = buildMarkers();
125
- const { readyCommand } = buildShellCommands(this.readyMarkers);
126
- this.readyCommand = readyCommand;
127
-
128
- this.readyPromise = new Promise((resolve, reject) => {
129
- this.resolveReady = resolve;
130
- this.rejectReady = reject;
131
- });
132
-
133
- this.readyTimeout = setTimeout(() => {
134
- this.fail(
135
- new WebSocketExecError(`exec ready timeout after ${this.timeoutMs}ms`, 124, {
136
- partialOutput: this.readyBuffer,
137
- phase: 'opening',
138
- }),
139
- );
140
- }, this.timeoutMs);
141
-
142
- this.ws.addEventListener('open', () => {
143
- sendLine(this.ws, this.readyCommand);
144
- });
145
-
146
- this.ws.addEventListener('message', (event) => {
147
- this.handleMessage(event).catch((error) => {
148
- this.fail(
149
- new WebSocketExecError(`exec message handling error: ${error.message}`, 1, {
150
- cause: error,
151
- phase: this.state,
152
- }),
153
- );
154
- });
155
- });
156
-
157
- this.ws.addEventListener('close', () => {
158
- if (this.state !== 'closed') {
159
- this.fail(new WebSocketExecError('exec websocket closed before completion marker', 1, { phase: this.state }));
160
- }
161
- });
162
-
163
- this.ws.addEventListener('error', () => {
164
- if (this.state !== 'closed') {
165
- this.fail(new WebSocketExecError('exec websocket error', 1, { phase: this.state }));
166
- }
167
- });
168
- }
169
-
170
- ready() {
171
- return this.readyPromise;
172
- }
173
-
174
- exec(command, options = {}) {
175
- const run = () => this.runExec(command, options);
176
- const result = this.queue.then(run, run);
177
- this.queue = result.catch(() => {});
178
- return result;
179
- }
180
-
181
- close() {
182
- if (this.state === 'closed') return;
183
- const wasOpening = this.state === 'opening';
184
- const pending = this.pending;
185
- this.state = 'closed';
186
- clearTimeout(this.readyTimeout);
187
- this.pending = null;
188
-
189
- if (wasOpening) {
190
- this.rejectReady(new WebSocketExecError('exec session closed before ready', 1, { phase: 'opening' }));
191
- }
192
-
193
- if (pending) {
194
- clearTimeout(pending.timeout);
195
- pending.reject(new WebSocketExecError('exec session closed before completion marker', 1, { phase: 'running' }));
196
- }
197
-
198
- closeQuietly(this.ws);
199
- }
200
-
201
- fail(error) {
202
- if (this.state === 'closed') return;
203
-
204
- const wasOpening = this.state === 'opening';
205
- const pending = this.pending;
206
- this.state = 'closed';
207
- this.pending = null;
208
- clearTimeout(this.readyTimeout);
209
- closeQuietly(this.ws);
210
-
211
- if (wasOpening) {
212
- this.rejectReady(error);
213
- }
214
-
215
- if (pending) {
216
- clearTimeout(pending.timeout);
217
- pending.reject(error);
218
- }
219
- }
220
-
221
- async handleMessage(event) {
222
- if (this.state === 'closed') return;
223
-
224
- const chunk = await eventDataToString(event.data);
225
- if (this.onFrame) this.onFrame(chunk);
226
-
227
- if (this.state === 'opening') {
228
- this.readyBuffer += chunk;
229
- this.tryCompleteReady();
230
- return;
231
- }
232
-
233
- if (this.pending) {
234
- this.pending.buffer += chunk;
235
- this.tryCompletePending();
236
- }
237
- }
238
-
239
- tryCompleteReady() {
240
- const readyIndex = this.readyBuffer.indexOf(this.readyMarkers.readyMarker);
241
- if (readyIndex === -1) return;
242
-
243
- this.inputEchoed = this.readyBuffer.slice(0, readyIndex).includes(this.readyCommand);
244
- this.readyBuffer = '';
245
- this.state = 'ready';
246
- clearTimeout(this.readyTimeout);
247
- this.resolveReady(this);
248
- }
249
-
250
- runExec(command, options = {}) {
251
- if (this.state !== 'ready') {
252
- return Promise.reject(new WebSocketExecError('exec session is not ready', 1, { phase: this.state }));
253
- }
254
-
255
- const shellCommand = normalizeCommand(command);
256
- const timeoutMs = normalizeTimeout(options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs);
257
- const markers = buildMarkers();
258
- const { doneCommand } = buildShellCommands(markers);
259
-
260
- return new Promise((resolve, reject) => {
261
- this.pending = {
262
- buffer: '',
263
- command: shellCommand,
264
- doneCommand,
265
- markers,
266
- reject,
267
- resolve,
268
- timeout: setTimeout(() => {
269
- this.fail(
270
- new WebSocketExecError(`exec timeout after ${timeoutMs}ms`, 124, {
271
- partialOutput: this.pending ? this.pending.buffer : '',
272
- phase: 'running',
273
- }),
274
- );
275
- }, timeoutMs),
276
- };
277
-
278
- sendLine(this.ws, shellCommand);
279
- sendLine(this.ws, doneCommand);
280
- });
281
- }
282
-
283
- tryCompletePending() {
284
- const pending = this.pending;
285
- if (!pending) return;
286
-
287
- const doneMatch = pending.buffer.match(pending.markers.donePattern);
288
- if (!doneMatch || doneMatch.index === undefined) return;
289
-
290
- const rawOutput = pending.buffer.slice(0, doneMatch.index);
291
- const stdout = cleanCommandOutput(rawOutput, {
292
- inputEchoed: this.inputEchoed,
293
- command: pending.command,
294
- doneCommand: pending.doneCommand,
295
- });
296
-
297
- clearTimeout(pending.timeout);
298
- this.pending = null;
299
- pending.resolve({
300
- stdout,
301
- exitCode: Number(doneMatch[1]),
302
- url: this.url,
303
- command: pending.command,
304
- });
305
- }
306
- }
307
-
308
- async function connectShellSession(options = {}) {
309
- const session = new WebSocketShellSession(options);
310
- await session.ready();
311
- return session;
312
- }
313
-
314
- async function executeCommand(options = {}) {
315
- const { command, timeoutMs = DEFAULT_TIMEOUT_MS, ...sessionOptions } = options;
316
- const shellCommand = normalizeCommand(command);
317
- const normalizedTimeoutMs = normalizeTimeout(timeoutMs);
318
- const session = await connectShellSession({ ...sessionOptions, timeoutMs: normalizedTimeoutMs });
319
- try {
320
- return await session.exec(shellCommand, { timeoutMs: normalizedTimeoutMs });
321
- } finally {
322
- session.close();
323
- }
324
- }
325
-
326
- export {
327
- DEFAULT_TIMEOUT_MS,
328
- DEFAULT_URL,
329
- WebSocketExecError,
330
- WebSocketShellSession,
331
- buildMarkers,
332
- buildShellCommands,
333
- cleanCommandOutput,
334
- connectShellSession,
335
- eventDataToString,
336
- executeCommand,
337
- stripEchoedLine,
338
- };
1
+ import { Buffer } from 'node:buffer';
2
+ import { randomBytes } from 'node:crypto';
3
+
4
+ const DEFAULT_URL = 'ws://127.0.0.1:8080';
5
+ const DEFAULT_TIMEOUT_MS = 30000;
6
+
7
+ class WebSocketExecError extends Error {
8
+ constructor(message, exitCode, details = {}) {
9
+ super(message);
10
+ this.name = 'WebSocketExecError';
11
+ this.exitCode = exitCode;
12
+ Object.assign(this, details);
13
+ }
14
+ }
15
+
16
+ async function eventDataToString(data) {
17
+ if (typeof data === 'string') return data;
18
+ if (Buffer.isBuffer(data)) return data.toString('utf8');
19
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8');
20
+ if (ArrayBuffer.isView(data)) {
21
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
22
+ }
23
+ if (data && typeof data.arrayBuffer === 'function') {
24
+ return Buffer.from(await data.arrayBuffer()).toString('utf8');
25
+ }
26
+ return String(data);
27
+ }
28
+
29
+ function sendLine(ws, line) {
30
+ ws.send(`${line}\n`);
31
+ }
32
+
33
+ function closeQuietly(ws) {
34
+ try {
35
+ ws.close();
36
+ } catch {
37
+ // The caller is already settling the operation; close errors are not useful.
38
+ }
39
+ }
40
+
41
+ function escapeRegExp(text) {
42
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43
+ }
44
+
45
+ function stripEchoedLine(text, line, options = {}) {
46
+ const escaped = escapeRegExp(line);
47
+ const stripped = text.replace(new RegExp(`(^|\\r?\\n)${escaped}\\r?\\n?`), '$1');
48
+ if (!options.allowAttached || stripped !== text) return stripped;
49
+ return text.replace(new RegExp(`${escaped}(?:\\r?\\n){0,2}$`), '');
50
+ }
51
+
52
+ function buildMarkers(nonce = randomBytes(8).toString('hex')) {
53
+ const readyPrefix = '__WS_EXEC_READY_';
54
+ const donePrefix = '__WS_EXEC_DONE_';
55
+ const readySuffix = `${nonce}__`;
56
+ const doneSuffix = `${nonce}__:`;
57
+ const readyMarker = `${readyPrefix}${readySuffix}`;
58
+ const doneMarker = `${donePrefix}${doneSuffix}`;
59
+
60
+ return {
61
+ nonce,
62
+ readyPrefix,
63
+ donePrefix,
64
+ readySuffix,
65
+ doneSuffix,
66
+ readyMarker,
67
+ doneMarker,
68
+ donePattern: new RegExp(`${escapeRegExp(doneMarker)}(\\d+)`),
69
+ };
70
+ }
71
+
72
+ function buildShellCommands(markers) {
73
+ return {
74
+ readyCommand: `stty -echo 2>/dev/null; export PS1= PS2= PROMPT_COMMAND=; printf '\\n%s%s\\n' '${markers.readyPrefix}' '${markers.readySuffix}'`,
75
+ doneCommand: `__ws_exec_rc=$?; printf '\\n%s%s%d\\n' '${markers.donePrefix}' '${markers.doneSuffix}' "$__ws_exec_rc"`,
76
+ };
77
+ }
78
+
79
+ function cleanCommandOutput(output, { inputEchoed, command, doneCommand }) {
80
+ let cleaned = output;
81
+ if (inputEchoed) cleaned = stripEchoedLine(cleaned, command);
82
+ cleaned = stripEchoedLine(cleaned, doneCommand, { allowAttached: true });
83
+ return cleaned.replace(/^\r?\n/, '').replace(/\r?\n\r?\n$/, '\n');
84
+ }
85
+
86
+ function normalizeCommand(command) {
87
+ if (!command || !String(command).trim()) {
88
+ throw new WebSocketExecError('missing command', 2);
89
+ }
90
+ return String(command).trim();
91
+ }
92
+
93
+ function normalizeTimeout(timeoutMs) {
94
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
95
+ throw new WebSocketExecError('timeoutMs must be a positive number of milliseconds', 2);
96
+ }
97
+ return timeoutMs;
98
+ }
99
+
100
+ class WebSocketShellSession {
101
+ constructor(options = {}) {
102
+ const {
103
+ url = DEFAULT_URL,
104
+ timeoutMs = DEFAULT_TIMEOUT_MS,
105
+ WebSocketImpl = globalThis.WebSocket,
106
+ onFrame,
107
+ } = options;
108
+
109
+ if (typeof WebSocketImpl !== 'function') {
110
+ throw new WebSocketExecError('global WebSocket is unavailable; use Node.js 22+ or pass WebSocketImpl', 2);
111
+ }
112
+
113
+ this.url = url;
114
+ this.timeoutMs = normalizeTimeout(timeoutMs);
115
+ this.WebSocketImpl = WebSocketImpl;
116
+ this.onFrame = onFrame;
117
+ this.state = 'opening';
118
+ this.readyBuffer = '';
119
+ this.inputEchoed = false;
120
+ this.pending = null;
121
+ this.queue = Promise.resolve();
122
+ this.ws = new WebSocketImpl(url);
123
+
124
+ this.readyMarkers = buildMarkers();
125
+ const { readyCommand } = buildShellCommands(this.readyMarkers);
126
+ this.readyCommand = readyCommand;
127
+
128
+ this.readyPromise = new Promise((resolve, reject) => {
129
+ this.resolveReady = resolve;
130
+ this.rejectReady = reject;
131
+ });
132
+
133
+ this.readyTimeout = setTimeout(() => {
134
+ this.fail(
135
+ new WebSocketExecError(`exec ready timeout after ${this.timeoutMs}ms`, 124, {
136
+ partialOutput: this.readyBuffer,
137
+ phase: 'opening',
138
+ }),
139
+ );
140
+ }, this.timeoutMs);
141
+
142
+ this.ws.addEventListener('open', () => {
143
+ sendLine(this.ws, this.readyCommand);
144
+ });
145
+
146
+ this.ws.addEventListener('message', (event) => {
147
+ this.handleMessage(event).catch((error) => {
148
+ this.fail(
149
+ new WebSocketExecError(`exec message handling error: ${error.message}`, 1, {
150
+ cause: error,
151
+ phase: this.state,
152
+ }),
153
+ );
154
+ });
155
+ });
156
+
157
+ this.ws.addEventListener('close', () => {
158
+ if (this.state !== 'closed') {
159
+ this.fail(new WebSocketExecError('exec websocket closed before completion marker', 1, { phase: this.state }));
160
+ }
161
+ });
162
+
163
+ this.ws.addEventListener('error', () => {
164
+ if (this.state !== 'closed') {
165
+ this.fail(new WebSocketExecError('exec websocket error', 1, { phase: this.state }));
166
+ }
167
+ });
168
+ }
169
+
170
+ ready() {
171
+ return this.readyPromise;
172
+ }
173
+
174
+ exec(command, options = {}) {
175
+ const run = () => this.runExec(command, options);
176
+ const result = this.queue.then(run, run);
177
+ this.queue = result.catch(() => {});
178
+ return result;
179
+ }
180
+
181
+ close() {
182
+ if (this.state === 'closed') return;
183
+ const wasOpening = this.state === 'opening';
184
+ const pending = this.pending;
185
+ this.state = 'closed';
186
+ clearTimeout(this.readyTimeout);
187
+ this.pending = null;
188
+
189
+ if (wasOpening) {
190
+ this.rejectReady(new WebSocketExecError('exec session closed before ready', 1, { phase: 'opening' }));
191
+ }
192
+
193
+ if (pending) {
194
+ clearTimeout(pending.timeout);
195
+ pending.reject(new WebSocketExecError('exec session closed before completion marker', 1, { phase: 'running' }));
196
+ }
197
+
198
+ closeQuietly(this.ws);
199
+ }
200
+
201
+ fail(error) {
202
+ if (this.state === 'closed') return;
203
+
204
+ const wasOpening = this.state === 'opening';
205
+ const pending = this.pending;
206
+ this.state = 'closed';
207
+ this.pending = null;
208
+ clearTimeout(this.readyTimeout);
209
+ closeQuietly(this.ws);
210
+
211
+ if (wasOpening) {
212
+ this.rejectReady(error);
213
+ }
214
+
215
+ if (pending) {
216
+ clearTimeout(pending.timeout);
217
+ pending.reject(error);
218
+ }
219
+ }
220
+
221
+ async handleMessage(event) {
222
+ if (this.state === 'closed') return;
223
+
224
+ const chunk = await eventDataToString(event.data);
225
+ if (this.onFrame) this.onFrame(chunk);
226
+
227
+ if (this.state === 'opening') {
228
+ this.readyBuffer += chunk;
229
+ this.tryCompleteReady();
230
+ return;
231
+ }
232
+
233
+ if (this.pending) {
234
+ this.pending.buffer += chunk;
235
+ this.tryCompletePending();
236
+ }
237
+ }
238
+
239
+ tryCompleteReady() {
240
+ const readyIndex = this.readyBuffer.indexOf(this.readyMarkers.readyMarker);
241
+ if (readyIndex === -1) return;
242
+
243
+ this.inputEchoed = this.readyBuffer.slice(0, readyIndex).includes(this.readyCommand);
244
+ this.readyBuffer = '';
245
+ this.state = 'ready';
246
+ clearTimeout(this.readyTimeout);
247
+ this.resolveReady(this);
248
+ }
249
+
250
+ runExec(command, options = {}) {
251
+ if (this.state !== 'ready') {
252
+ return Promise.reject(new WebSocketExecError('exec session is not ready', 1, { phase: this.state }));
253
+ }
254
+
255
+ const shellCommand = normalizeCommand(command);
256
+ const timeoutMs = normalizeTimeout(options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs);
257
+ const markers = buildMarkers();
258
+ const { doneCommand } = buildShellCommands(markers);
259
+
260
+ return new Promise((resolve, reject) => {
261
+ this.pending = {
262
+ buffer: '',
263
+ command: shellCommand,
264
+ doneCommand,
265
+ markers,
266
+ reject,
267
+ resolve,
268
+ timeout: setTimeout(() => {
269
+ this.fail(
270
+ new WebSocketExecError(`exec timeout after ${timeoutMs}ms`, 124, {
271
+ partialOutput: this.pending ? this.pending.buffer : '',
272
+ phase: 'running',
273
+ }),
274
+ );
275
+ }, timeoutMs),
276
+ };
277
+
278
+ sendLine(this.ws, shellCommand);
279
+ sendLine(this.ws, doneCommand);
280
+ });
281
+ }
282
+
283
+ tryCompletePending() {
284
+ const pending = this.pending;
285
+ if (!pending) return;
286
+
287
+ const doneMatch = pending.buffer.match(pending.markers.donePattern);
288
+ if (!doneMatch || doneMatch.index === undefined) return;
289
+
290
+ const rawOutput = pending.buffer.slice(0, doneMatch.index);
291
+ const stdout = cleanCommandOutput(rawOutput, {
292
+ inputEchoed: this.inputEchoed,
293
+ command: pending.command,
294
+ doneCommand: pending.doneCommand,
295
+ });
296
+
297
+ clearTimeout(pending.timeout);
298
+ this.pending = null;
299
+ pending.resolve({
300
+ stdout,
301
+ exitCode: Number(doneMatch[1]),
302
+ url: this.url,
303
+ command: pending.command,
304
+ });
305
+ }
306
+ }
307
+
308
+ async function connectShellSession(options = {}) {
309
+ const session = new WebSocketShellSession(options);
310
+ await session.ready();
311
+ return session;
312
+ }
313
+
314
+ async function executeCommand(options = {}) {
315
+ const { command, timeoutMs = DEFAULT_TIMEOUT_MS, ...sessionOptions } = options;
316
+ const shellCommand = normalizeCommand(command);
317
+ const normalizedTimeoutMs = normalizeTimeout(timeoutMs);
318
+ const session = await connectShellSession({ ...sessionOptions, timeoutMs: normalizedTimeoutMs });
319
+ try {
320
+ return await session.exec(shellCommand, { timeoutMs: normalizedTimeoutMs });
321
+ } finally {
322
+ session.close();
323
+ }
324
+ }
325
+
326
+ export {
327
+ DEFAULT_TIMEOUT_MS,
328
+ DEFAULT_URL,
329
+ WebSocketExecError,
330
+ WebSocketShellSession,
331
+ buildMarkers,
332
+ buildShellCommands,
333
+ cleanCommandOutput,
334
+ connectShellSession,
335
+ eventDataToString,
336
+ executeCommand,
337
+ stripEchoedLine,
338
+ };