covebox 0.1.0 → 0.1.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.
- package/dist/interactive.js +9 -9
- package/dist/pty-client.d.ts +4 -4
- package/dist/pty-client.js +176 -97
- package/package.json +4 -3
package/dist/interactive.js
CHANGED
|
@@ -118,6 +118,9 @@ async function handleCommand(client, line, rl) {
|
|
|
118
118
|
if (sandbox.noVncUrl) {
|
|
119
119
|
console.log(` NoVNC: ${chalk.blue(sandbox.noVncUrl)}`);
|
|
120
120
|
}
|
|
121
|
+
if (sandbox.ptyUrl) {
|
|
122
|
+
console.log(` PTY: ${chalk.green(sandbox.ptyUrl)}`);
|
|
123
|
+
}
|
|
121
124
|
if (sandbox.vncUrl) {
|
|
122
125
|
console.log(` VNC WS: ${chalk.gray(sandbox.vncUrl)}`);
|
|
123
126
|
}
|
|
@@ -242,11 +245,6 @@ async function handleCommand(client, line, rl) {
|
|
|
242
245
|
console.log(chalk.red("Usage: ssh <id>"));
|
|
243
246
|
break;
|
|
244
247
|
}
|
|
245
|
-
const daytonaApiKey = process.env["DAYTONA_API_KEY"];
|
|
246
|
-
if (!daytonaApiKey) {
|
|
247
|
-
console.log(chalk.red("DAYTONA_API_KEY environment variable is required for shell access."));
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
248
|
const sandboxId = await resolveId(client, id);
|
|
251
249
|
if (!sandboxId)
|
|
252
250
|
break;
|
|
@@ -257,18 +255,20 @@ async function handleCommand(client, line, rl) {
|
|
|
257
255
|
console.log(chalk.red(`Sandbox is ${sandbox.state}. Start it first with: start ${sandboxId.slice(0, 12)}`));
|
|
258
256
|
break;
|
|
259
257
|
}
|
|
260
|
-
if (sandbox.
|
|
261
|
-
console.log(
|
|
258
|
+
if (!sandbox.ptyUrl) {
|
|
259
|
+
console.log(chalk.red("Shell access not available for this sandbox."));
|
|
260
|
+
break;
|
|
262
261
|
}
|
|
263
262
|
console.log(chalk.gray(`\nConnecting to ${sandboxId.slice(0, 12)}...\n`));
|
|
264
263
|
// Close readline before PTY (removes its stdin listeners)
|
|
265
264
|
if (rl) {
|
|
266
265
|
rl.close();
|
|
267
266
|
}
|
|
268
|
-
// Only remove keypress listener (readline's), not all listeners
|
|
269
267
|
process.stdin.removeAllListeners("keypress");
|
|
268
|
+
// Small delay for bun's event loop to settle after readline close
|
|
269
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
270
270
|
try {
|
|
271
|
-
await runPtySession(
|
|
271
|
+
await runPtySession(sandbox.ptyUrl);
|
|
272
272
|
}
|
|
273
273
|
catch (err) {
|
|
274
274
|
console.error(chalk.red(`Connection failed: ${err}`));
|
package/dist/pty-client.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PTY Client using
|
|
2
|
+
* PTY Client using WebSocket
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Connects to the sandbox's PTY endpoint for interactive terminal access.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Run an interactive PTY session
|
|
7
|
+
* Run an interactive PTY session
|
|
8
8
|
*/
|
|
9
|
-
export declare function runPtySession(
|
|
9
|
+
export declare function runPtySession(ptyUrl: string): Promise<void>;
|
package/dist/pty-client.js
CHANGED
|
@@ -1,110 +1,189 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PTY Client using
|
|
2
|
+
* PTY Client using WebSocket
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Connects to the sandbox's PTY endpoint for interactive terminal access.
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
// Message types (must match cove-proxy/pty.go)
|
|
8
|
+
const MsgStdin = 0x00;
|
|
9
|
+
const MsgStdout = 0x01;
|
|
10
|
+
const MsgResize = 0x10;
|
|
11
|
+
const MsgCreateSession = 0x20;
|
|
12
|
+
const MsgAttachSession = 0x22;
|
|
13
|
+
const MsgCreateWindow = 0x30;
|
|
14
|
+
const MsgCreatePane = 0x40;
|
|
15
|
+
const MsgResponse = 0xf0;
|
|
16
|
+
const MsgError = 0xff;
|
|
7
17
|
/**
|
|
8
|
-
* Run an interactive PTY session
|
|
18
|
+
* Run an interactive PTY session
|
|
9
19
|
*/
|
|
10
|
-
export async function runPtySession(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
stdinHandler = null;
|
|
27
|
-
}
|
|
28
|
-
if (resizeHandler) {
|
|
29
|
-
process.stdout.removeListener("resize", resizeHandler);
|
|
30
|
-
resizeHandler = null;
|
|
31
|
-
}
|
|
32
|
-
// Restore terminal to cooked mode
|
|
33
|
-
if (process.stdin.isTTY) {
|
|
34
|
-
process.stdin.setRawMode(false);
|
|
35
|
-
}
|
|
36
|
-
// Reset encoding to default (buffer) so readline can manage it
|
|
37
|
-
process.stdin.setEncoding("utf8");
|
|
38
|
-
process.stdin.pause();
|
|
39
|
-
// Disconnect PTY
|
|
40
|
-
if (pty) {
|
|
41
|
-
try {
|
|
42
|
-
await pty.disconnect();
|
|
20
|
+
export async function runPtySession(ptyUrl) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const rows = process.stdout.rows || 24;
|
|
23
|
+
const cols = process.stdout.columns || 80;
|
|
24
|
+
let ws = null;
|
|
25
|
+
let paneId = null;
|
|
26
|
+
let cleanedUp = false;
|
|
27
|
+
let stdinHandler = null;
|
|
28
|
+
let resizeHandler = null;
|
|
29
|
+
const cleanup = () => {
|
|
30
|
+
if (cleanedUp)
|
|
31
|
+
return;
|
|
32
|
+
cleanedUp = true;
|
|
33
|
+
if (stdinHandler) {
|
|
34
|
+
process.stdin.removeListener("data", stdinHandler);
|
|
35
|
+
stdinHandler = null;
|
|
43
36
|
}
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
if (resizeHandler) {
|
|
38
|
+
process.stdout.removeListener("resize", resizeHandler);
|
|
39
|
+
resizeHandler = null;
|
|
46
40
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
try {
|
|
51
|
-
const sandbox = await client.get(sandboxId);
|
|
52
|
-
// Create PTY session with unique ID
|
|
53
|
-
const sessionId = `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
-
pty = await sandbox.process.createPty({
|
|
55
|
-
id: sessionId,
|
|
56
|
-
cols,
|
|
57
|
-
rows,
|
|
58
|
-
onData: (data) => {
|
|
59
|
-
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
60
|
-
process.stdout.write(text);
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
// Wait for connection to be established
|
|
64
|
-
await pty.waitForConnection();
|
|
65
|
-
// Set raw mode for stdin (disables local echo)
|
|
66
|
-
if (process.stdin.isTTY) {
|
|
67
|
-
process.stdin.setRawMode(true);
|
|
68
|
-
}
|
|
69
|
-
process.stdin.resume();
|
|
70
|
-
process.stdin.setEncoding("utf8");
|
|
71
|
-
// Handle stdin - use named function so we can remove it later
|
|
72
|
-
stdinHandler = (data) => {
|
|
73
|
-
if (pty && !cleanedUp) {
|
|
74
|
-
pty.sendInput(data.toString()).catch(() => {
|
|
75
|
-
// Connection may have closed
|
|
76
|
-
});
|
|
41
|
+
if (process.stdin.isTTY) {
|
|
42
|
+
process.stdin.setRawMode(false);
|
|
77
43
|
}
|
|
44
|
+
process.stdin.setEncoding("utf8");
|
|
45
|
+
process.stdin.pause();
|
|
46
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
ws.close();
|
|
48
|
+
}
|
|
49
|
+
ws = null;
|
|
78
50
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (pty && !cleanedUp) {
|
|
83
|
-
const newRows = process.stdout.rows || 24;
|
|
84
|
-
const newCols = process.stdout.columns || 80;
|
|
85
|
-
pty.resize(newCols, newRows).catch(() => {
|
|
86
|
-
// Ignore resize errors
|
|
87
|
-
});
|
|
51
|
+
const sendBinary = (data) => {
|
|
52
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
53
|
+
ws.send(data);
|
|
88
54
|
}
|
|
89
55
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
56
|
+
const sendJson = (msgType, payload) => {
|
|
57
|
+
const json = JSON.stringify(payload);
|
|
58
|
+
const buf = Buffer.alloc(1 + json.length);
|
|
59
|
+
buf[0] = msgType;
|
|
60
|
+
buf.write(json, 1);
|
|
61
|
+
sendBinary(buf);
|
|
62
|
+
};
|
|
63
|
+
const sendStdin = (data) => {
|
|
64
|
+
if (paneId === null)
|
|
65
|
+
return;
|
|
66
|
+
const dataBuf = Buffer.from(data);
|
|
67
|
+
const buf = Buffer.alloc(5 + dataBuf.length);
|
|
68
|
+
buf[0] = MsgStdin;
|
|
69
|
+
buf.writeUInt32BE(paneId, 1);
|
|
70
|
+
dataBuf.copy(buf, 5);
|
|
71
|
+
sendBinary(buf);
|
|
72
|
+
};
|
|
73
|
+
const sendResize = (newRows, newCols) => {
|
|
74
|
+
if (paneId === null)
|
|
75
|
+
return;
|
|
76
|
+
const buf = Buffer.alloc(9);
|
|
77
|
+
buf[0] = MsgResize;
|
|
78
|
+
buf.writeUInt32BE(paneId, 1);
|
|
79
|
+
buf.writeUInt16BE(newRows, 5);
|
|
80
|
+
buf.writeUInt16BE(newCols, 7);
|
|
81
|
+
sendBinary(buf);
|
|
95
82
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
83
|
+
try {
|
|
84
|
+
ws = new WebSocket(ptyUrl, { followRedirects: true });
|
|
85
|
+
ws.on("open", () => {
|
|
86
|
+
// Create session
|
|
87
|
+
sendJson(MsgCreateSession, { name: "cli-session" });
|
|
88
|
+
});
|
|
89
|
+
ws.on("message", (data) => {
|
|
90
|
+
if (data.length < 1)
|
|
91
|
+
return;
|
|
92
|
+
const msgType = data[0];
|
|
93
|
+
const payload = data.slice(1);
|
|
94
|
+
if (msgType === MsgStdout && paneId !== null) {
|
|
95
|
+
// Skip pane ID (4 bytes), output the rest
|
|
96
|
+
const output = payload.slice(4);
|
|
97
|
+
process.stdout.write(output);
|
|
98
|
+
}
|
|
99
|
+
else if (msgType === MsgResponse) {
|
|
100
|
+
try {
|
|
101
|
+
const response = JSON.parse(payload.toString());
|
|
102
|
+
if (response.type === "session_created" && response.session) {
|
|
103
|
+
// Attach to session
|
|
104
|
+
const sessionId = response.session.id;
|
|
105
|
+
const buf = Buffer.alloc(1 + 36);
|
|
106
|
+
buf[0] = MsgAttachSession;
|
|
107
|
+
buf.write(sessionId, 1);
|
|
108
|
+
sendBinary(buf);
|
|
109
|
+
}
|
|
110
|
+
else if (response.type === "attached") {
|
|
111
|
+
// Create window
|
|
112
|
+
sendJson(MsgCreateWindow, { name: "main" });
|
|
113
|
+
}
|
|
114
|
+
else if (response.type === "window_created" && response.window) {
|
|
115
|
+
// Create pane
|
|
116
|
+
sendJson(MsgCreatePane, {
|
|
117
|
+
windowId: response.window.id,
|
|
118
|
+
rows,
|
|
119
|
+
cols,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (response.type === "pane_created" && response.pane) {
|
|
123
|
+
paneId = response.pane.id;
|
|
124
|
+
// Now we're connected - set up terminal
|
|
125
|
+
if (process.stdin.isTTY) {
|
|
126
|
+
process.stdin.setRawMode(true);
|
|
127
|
+
}
|
|
128
|
+
process.stdin.resume();
|
|
129
|
+
process.stdin.setEncoding("utf8");
|
|
130
|
+
stdinHandler = (inputData) => {
|
|
131
|
+
if (!cleanedUp) {
|
|
132
|
+
sendStdin(inputData.toString());
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
process.stdin.on("data", stdinHandler);
|
|
136
|
+
resizeHandler = () => {
|
|
137
|
+
if (!cleanedUp) {
|
|
138
|
+
const newRows = process.stdout.rows || 24;
|
|
139
|
+
const newCols = process.stdout.columns || 80;
|
|
140
|
+
sendResize(newRows, newCols);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
process.stdout.on("resize", resizeHandler);
|
|
144
|
+
}
|
|
145
|
+
else if (response.type === "pane_closed") {
|
|
146
|
+
// Shell exited - close connection
|
|
147
|
+
cleanup();
|
|
148
|
+
resolve();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Ignore JSON parse errors
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (msgType === MsgError) {
|
|
157
|
+
try {
|
|
158
|
+
const error = JSON.parse(payload.toString());
|
|
159
|
+
console.error(`\nPTY Error: ${error.error}`);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Ignore
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
ws.on("close", () => {
|
|
167
|
+
cleanup();
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
ws.on("error", (err) => {
|
|
171
|
+
cleanup();
|
|
172
|
+
// Extract meaningful error message from WebSocket error
|
|
173
|
+
const message = err.message || err.code || String(err);
|
|
174
|
+
reject(new Error(message));
|
|
175
|
+
});
|
|
176
|
+
// Handle signals
|
|
177
|
+
const handleSignal = () => {
|
|
178
|
+
cleanup();
|
|
179
|
+
resolve();
|
|
180
|
+
};
|
|
181
|
+
process.once("SIGINT", handleSignal);
|
|
182
|
+
process.once("SIGTERM", handleSignal);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
cleanup();
|
|
186
|
+
reject(error);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
110
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "covebox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for Cove sandboxes - cloud development environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
"test:e2e": "tsx src/test-e2e.ts"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@daytonaio/sdk": "^0.115.2",
|
|
22
21
|
"chalk": "^5.6.2",
|
|
23
22
|
"cli-table3": "^0.6.5",
|
|
24
23
|
"commander": "^12.1.0",
|
|
25
24
|
"conf": "^13.1.0",
|
|
26
|
-
"ora": "^8.2.0"
|
|
25
|
+
"ora": "^8.2.0",
|
|
26
|
+
"ws": "^8.18.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.15.32",
|
|
30
|
+
"@types/ws": "^8.5.13",
|
|
30
31
|
"tsx": "^4.21.0",
|
|
31
32
|
"typescript": "^5.7.3"
|
|
32
33
|
}
|