covebox 0.1.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.
@@ -0,0 +1,68 @@
1
+ export interface Resources {
2
+ cpu: number;
3
+ memory: number;
4
+ disk: number;
5
+ }
6
+ export interface Sandbox {
7
+ id: string;
8
+ name: string;
9
+ state: "creating" | "running" | "stopped" | "archived" | "error";
10
+ resources: Resources;
11
+ createdAt: string;
12
+ vncUrl?: string;
13
+ proxyUrl?: string;
14
+ noVncUrl?: string;
15
+ ptyUrl?: string;
16
+ }
17
+ export interface SshAccessResponse {
18
+ token: string;
19
+ sshCommand: string;
20
+ }
21
+ export interface CreateSandboxRequest {
22
+ name?: string;
23
+ snapshot?: string;
24
+ resources?: Partial<Resources>;
25
+ envVars?: Record<string, string>;
26
+ autoStopMinutes?: number;
27
+ ephemeral?: boolean;
28
+ }
29
+ export interface ApiKey {
30
+ id: string;
31
+ key?: string;
32
+ name: string;
33
+ tier: "free" | "pro" | "enterprise";
34
+ rateLimit: number;
35
+ createdAt: string;
36
+ lastUsedAt?: string;
37
+ }
38
+ export interface ErrorResponse {
39
+ error: string;
40
+ code: string;
41
+ details?: Record<string, unknown>;
42
+ }
43
+ export declare class CoveClient {
44
+ private baseUrl;
45
+ private apiKey;
46
+ constructor(baseUrl: string, apiKey: string);
47
+ private request;
48
+ health(): Promise<{
49
+ status: string;
50
+ }>;
51
+ listSandboxes(): Promise<{
52
+ sandboxes: Sandbox[];
53
+ total: number;
54
+ }>;
55
+ createSandbox(request: CreateSandboxRequest): Promise<Sandbox>;
56
+ getSandbox(sandboxId: string): Promise<Sandbox>;
57
+ deleteSandbox(sandboxId: string): Promise<void>;
58
+ startSandbox(sandboxId: string): Promise<void>;
59
+ stopSandbox(sandboxId: string): Promise<void>;
60
+ createSshAccess(sandboxId: string, expiresInMinutes?: number): Promise<SshAccessResponse>;
61
+ listApiKeys(): Promise<{
62
+ keys: ApiKey[];
63
+ total: number;
64
+ }>;
65
+ createApiKey(name: string, tier?: string): Promise<ApiKey>;
66
+ getApiKey(keyId: string): Promise<ApiKey>;
67
+ deleteApiKey(keyId: string): Promise<void>;
68
+ }
package/dist/client.js ADDED
@@ -0,0 +1,70 @@
1
+ // Cove API Client
2
+ export class CoveClient {
3
+ baseUrl;
4
+ apiKey;
5
+ constructor(baseUrl, apiKey) {
6
+ this.baseUrl = baseUrl.replace(/\/$/, "");
7
+ this.apiKey = apiKey;
8
+ }
9
+ async request(method, path, body) {
10
+ const url = `${this.baseUrl}${path}`;
11
+ const headers = {
12
+ "Content-Type": "application/json",
13
+ Authorization: `Bearer ${this.apiKey}`,
14
+ };
15
+ const response = await fetch(url, {
16
+ method,
17
+ headers,
18
+ body: body ? JSON.stringify(body) : undefined,
19
+ });
20
+ if (!response.ok) {
21
+ const error = (await response.json());
22
+ throw new Error(`${error.code}: ${error.error}`);
23
+ }
24
+ if (response.status === 204) {
25
+ return undefined;
26
+ }
27
+ return response.json();
28
+ }
29
+ // Health
30
+ async health() {
31
+ return this.request("GET", "/health");
32
+ }
33
+ // Sandboxes
34
+ async listSandboxes() {
35
+ return this.request("GET", "/sandboxes");
36
+ }
37
+ async createSandbox(request) {
38
+ return this.request("POST", "/sandboxes", request);
39
+ }
40
+ async getSandbox(sandboxId) {
41
+ return this.request("GET", `/sandboxes/${sandboxId}`);
42
+ }
43
+ async deleteSandbox(sandboxId) {
44
+ return this.request("DELETE", `/sandboxes/${sandboxId}`);
45
+ }
46
+ async startSandbox(sandboxId) {
47
+ return this.request("POST", `/sandboxes/${sandboxId}/start`);
48
+ }
49
+ async stopSandbox(sandboxId) {
50
+ return this.request("POST", `/sandboxes/${sandboxId}/stop`);
51
+ }
52
+ async createSshAccess(sandboxId, expiresInMinutes) {
53
+ return this.request("POST", `/sandboxes/${sandboxId}/ssh`, {
54
+ expiresInMinutes,
55
+ });
56
+ }
57
+ // API Keys
58
+ async listApiKeys() {
59
+ return this.request("GET", "/api-keys");
60
+ }
61
+ async createApiKey(name, tier) {
62
+ return this.request("POST", "/api-keys", { name, tier });
63
+ }
64
+ async getApiKey(keyId) {
65
+ return this.request("GET", `/api-keys/${keyId}`);
66
+ }
67
+ async deleteApiKey(keyId) {
68
+ return this.request("DELETE", `/api-keys/${keyId}`);
69
+ }
70
+ }
@@ -0,0 +1,10 @@
1
+ interface CoveConfig {
2
+ apiKey?: string;
3
+ baseUrl: string;
4
+ }
5
+ export declare function getConfig(): CoveConfig;
6
+ export declare function setApiKey(apiKey: string): void;
7
+ export declare function setBaseUrl(baseUrl: string): void;
8
+ export declare function clearConfig(): void;
9
+ export declare function getConfigPath(): string;
10
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ // CLI Configuration Management
2
+ import Conf from "conf";
3
+ const config = new Conf({
4
+ projectName: "cove-cli",
5
+ defaults: {
6
+ baseUrl: "https://cove.devbox.new/api",
7
+ },
8
+ });
9
+ export function getConfig() {
10
+ return {
11
+ apiKey: config.get("apiKey"),
12
+ baseUrl: config.get("baseUrl"),
13
+ };
14
+ }
15
+ export function setApiKey(apiKey) {
16
+ config.set("apiKey", apiKey);
17
+ }
18
+ export function setBaseUrl(baseUrl) {
19
+ config.set("baseUrl", baseUrl);
20
+ }
21
+ export function clearConfig() {
22
+ config.clear();
23
+ }
24
+ export function getConfigPath() {
25
+ return config.path;
26
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import Table from "cli-table3";
6
+ import { CoveClient } from "./client.js";
7
+ import { getConfig, setApiKey, setBaseUrl, clearConfig, getConfigPath, } from "./config.js";
8
+ import { runInteractive } from "./interactive.js";
9
+ // Check if running in interactive mode (no args or -i flag)
10
+ const args = process.argv.slice(2);
11
+ const isInteractive = args.length === 0 || args[0] === "-i" || args[0] === "--interactive";
12
+ if (isInteractive) {
13
+ runInteractive()
14
+ .then(() => process.exit(0))
15
+ .catch((err) => {
16
+ // Ctrl+C or normal close
17
+ if (err?.code === "ERR_USE_AFTER_CLOSE" || err?.message?.includes("readline was closed")) {
18
+ console.log(chalk.gray("\nGoodbye!\n"));
19
+ process.exit(0);
20
+ }
21
+ console.error(chalk.red(err));
22
+ process.exit(1);
23
+ });
24
+ }
25
+ else {
26
+ runCli();
27
+ }
28
+ function runCli() {
29
+ const program = new Command();
30
+ function getClient() {
31
+ const config = getConfig();
32
+ const apiKey = process.env["COVE_API_KEY"] || config.apiKey;
33
+ const baseUrl = process.env["COVE_API_URL"] || config.baseUrl;
34
+ if (!apiKey) {
35
+ console.error(chalk.red("No API key configured. Run `cove config set-key <key>` or set COVE_API_KEY"));
36
+ process.exit(1);
37
+ }
38
+ return new CoveClient(baseUrl, apiKey);
39
+ }
40
+ // Main program
41
+ program
42
+ .name("cove")
43
+ .description("CLI for the Cove sandbox API")
44
+ .version("0.1.0");
45
+ // Config commands
46
+ const configCmd = program
47
+ .command("config")
48
+ .description("Manage CLI configuration");
49
+ configCmd
50
+ .command("set-key <apiKey>")
51
+ .description("Set the API key")
52
+ .action((apiKey) => {
53
+ setApiKey(apiKey);
54
+ console.log(chalk.green("API key saved"));
55
+ });
56
+ configCmd
57
+ .command("set-url <url>")
58
+ .description("Set the API base URL")
59
+ .action((url) => {
60
+ setBaseUrl(url);
61
+ console.log(chalk.green(`Base URL set to ${url}`));
62
+ });
63
+ configCmd
64
+ .command("show")
65
+ .description("Show current configuration")
66
+ .action(() => {
67
+ const config = getConfig();
68
+ console.log(chalk.bold("Configuration:"));
69
+ console.log(` Base URL: ${chalk.cyan(config.baseUrl)}`);
70
+ console.log(` API Key: ${config.apiKey ? chalk.cyan(config.apiKey.slice(0, 10) + "...") : chalk.gray("(not set)")}`);
71
+ console.log(` Config: ${chalk.gray(getConfigPath())}`);
72
+ });
73
+ configCmd
74
+ .command("clear")
75
+ .description("Clear all configuration")
76
+ .action(() => {
77
+ clearConfig();
78
+ console.log(chalk.yellow("Configuration cleared"));
79
+ });
80
+ // Sandbox commands
81
+ const sandboxCmd = program
82
+ .command("sandbox")
83
+ .alias("sb")
84
+ .description("Manage sandboxes");
85
+ sandboxCmd
86
+ .command("list")
87
+ .alias("ls")
88
+ .description("List sandboxes (running by default)")
89
+ .option("-a, --all", "Show all sandboxes")
90
+ .option("-s, --stopped", "Show only stopped sandboxes")
91
+ .option("-e, --error", "Show only error sandboxes")
92
+ .option("-j, --json", "Output as JSON")
93
+ .action(async (options) => {
94
+ const spinner = ora("Fetching sandboxes...").start();
95
+ try {
96
+ const client = getClient();
97
+ const result = await client.listSandboxes();
98
+ spinner.stop();
99
+ // Filter sandboxes based on flags
100
+ let sandboxes = result.sandboxes;
101
+ if (!options.all) {
102
+ if (options.stopped) {
103
+ sandboxes = sandboxes.filter(sb => sb.state === "stopped");
104
+ }
105
+ else if (options.error) {
106
+ sandboxes = sandboxes.filter(sb => sb.state === "error");
107
+ }
108
+ else {
109
+ // Default: only show running/creating
110
+ sandboxes = sandboxes.filter(sb => sb.state === "running" || sb.state === "creating");
111
+ }
112
+ }
113
+ if (options.json) {
114
+ console.log(JSON.stringify({ sandboxes, total: sandboxes.length }, null, 2));
115
+ return;
116
+ }
117
+ if (sandboxes.length === 0) {
118
+ const stateHint = options.all ? "" : options.stopped ? " stopped" : options.error ? " error" : " running";
119
+ console.log(chalk.gray(`No${stateHint} sandboxes. Use 'sandbox ls -a' to see all.`));
120
+ return;
121
+ }
122
+ const table = new Table({
123
+ head: ["ID", "Name", "State", "CPU", "Mem", "Created"],
124
+ style: { head: ["cyan"] },
125
+ });
126
+ for (const sb of sandboxes) {
127
+ const stateColor = sb.state === "running"
128
+ ? chalk.green
129
+ : sb.state === "error"
130
+ ? chalk.red
131
+ : chalk.yellow;
132
+ table.push([
133
+ sb.id.slice(0, 12),
134
+ sb.name || "-",
135
+ stateColor(sb.state),
136
+ `${sb.resources.cpu}`,
137
+ `${sb.resources.memory}GB`,
138
+ new Date(sb.createdAt).toLocaleDateString(),
139
+ ]);
140
+ }
141
+ console.log(table.toString());
142
+ console.log(chalk.gray(`Total: ${sandboxes.length}`));
143
+ }
144
+ catch (error) {
145
+ spinner.fail("Failed to list sandboxes");
146
+ console.error(chalk.red(String(error)));
147
+ process.exit(1);
148
+ }
149
+ });
150
+ sandboxCmd
151
+ .command("create")
152
+ .description("Create a new sandbox")
153
+ .option("-n, --name <name>", "Sandbox name")
154
+ .option("-s, --snapshot <snapshot>", "Snapshot to use")
155
+ .option("--cpu <cpu>", "CPU cores", parseFloat)
156
+ .option("--memory <memory>", "Memory in GB", parseFloat)
157
+ .option("--disk <disk>", "Disk in GB", parseFloat)
158
+ .option("--ephemeral", "Delete sandbox when stopped")
159
+ .option("--auto-stop <minutes>", "Auto-stop after N minutes", parseInt)
160
+ .option("-j, --json", "Output as JSON")
161
+ .action(async (options) => {
162
+ const spinner = ora("Creating sandbox...").start();
163
+ try {
164
+ const client = getClient();
165
+ const sandbox = await client.createSandbox({
166
+ name: options.name,
167
+ snapshot: options.snapshot,
168
+ resources: options.cpu || options.memory || options.disk
169
+ ? {
170
+ cpu: options.cpu,
171
+ memory: options.memory,
172
+ disk: options.disk,
173
+ }
174
+ : undefined,
175
+ ephemeral: options.ephemeral,
176
+ autoStopMinutes: options.autoStop,
177
+ });
178
+ spinner.succeed("Sandbox created");
179
+ if (options.json) {
180
+ console.log(JSON.stringify(sandbox, null, 2));
181
+ return;
182
+ }
183
+ console.log(chalk.bold("\nSandbox Details:"));
184
+ console.log(` ID: ${chalk.cyan(sandbox.id)}`);
185
+ console.log(` Name: ${sandbox.name || "-"}`);
186
+ console.log(` State: ${chalk.yellow(sandbox.state)}`);
187
+ console.log(` Resources: ${sandbox.resources.cpu} CPU, ${sandbox.resources.memory}GB RAM, ${sandbox.resources.disk}GB disk`);
188
+ if (sandbox.noVncUrl) {
189
+ console.log(` NoVNC: ${chalk.blue(sandbox.noVncUrl)}`);
190
+ }
191
+ if (sandbox.vncUrl) {
192
+ console.log(` VNC WS: ${chalk.gray(sandbox.vncUrl)}`);
193
+ }
194
+ if (sandbox.proxyUrl) {
195
+ console.log(` Proxy: ${chalk.blue(sandbox.proxyUrl)}`);
196
+ }
197
+ }
198
+ catch (error) {
199
+ spinner.fail("Failed to create sandbox");
200
+ console.error(chalk.red(String(error)));
201
+ process.exit(1);
202
+ }
203
+ });
204
+ sandboxCmd
205
+ .command("get <sandboxId>")
206
+ .description("Get sandbox details")
207
+ .option("-j, --json", "Output as JSON")
208
+ .action(async (sandboxId, options) => {
209
+ const spinner = ora("Fetching sandbox...").start();
210
+ try {
211
+ const client = getClient();
212
+ const sandbox = await client.getSandbox(sandboxId);
213
+ spinner.stop();
214
+ if (options.json) {
215
+ console.log(JSON.stringify(sandbox, null, 2));
216
+ return;
217
+ }
218
+ const stateColor = sandbox.state === "running"
219
+ ? chalk.green
220
+ : sandbox.state === "error"
221
+ ? chalk.red
222
+ : chalk.yellow;
223
+ console.log(chalk.bold("Sandbox Details:"));
224
+ console.log(` ID: ${chalk.cyan(sandbox.id)}`);
225
+ console.log(` Name: ${sandbox.name || "-"}`);
226
+ console.log(` State: ${stateColor(sandbox.state)}`);
227
+ console.log(` Resources: ${sandbox.resources.cpu} CPU, ${sandbox.resources.memory}GB RAM, ${sandbox.resources.disk}GB disk`);
228
+ console.log(` Created: ${new Date(sandbox.createdAt).toLocaleString()}`);
229
+ if (sandbox.noVncUrl) {
230
+ console.log(` NoVNC: ${chalk.blue(sandbox.noVncUrl)}`);
231
+ }
232
+ if (sandbox.vncUrl) {
233
+ console.log(` VNC WS: ${chalk.gray(sandbox.vncUrl)}`);
234
+ }
235
+ if (sandbox.proxyUrl) {
236
+ console.log(` Proxy: ${chalk.blue(sandbox.proxyUrl)}`);
237
+ }
238
+ }
239
+ catch (error) {
240
+ spinner.fail("Failed to get sandbox");
241
+ console.error(chalk.red(String(error)));
242
+ process.exit(1);
243
+ }
244
+ });
245
+ sandboxCmd
246
+ .command("start <sandboxId>")
247
+ .description("Start a stopped sandbox")
248
+ .action(async (sandboxId) => {
249
+ const spinner = ora("Starting sandbox...").start();
250
+ try {
251
+ const client = getClient();
252
+ await client.startSandbox(sandboxId);
253
+ spinner.succeed("Sandbox started");
254
+ }
255
+ catch (error) {
256
+ spinner.fail("Failed to start sandbox");
257
+ console.error(chalk.red(String(error)));
258
+ process.exit(1);
259
+ }
260
+ });
261
+ sandboxCmd
262
+ .command("stop <sandboxId>")
263
+ .description("Stop a running sandbox")
264
+ .action(async (sandboxId) => {
265
+ const spinner = ora("Stopping sandbox...").start();
266
+ try {
267
+ const client = getClient();
268
+ await client.stopSandbox(sandboxId);
269
+ spinner.succeed("Sandbox stopped");
270
+ }
271
+ catch (error) {
272
+ spinner.fail("Failed to stop sandbox");
273
+ console.error(chalk.red(String(error)));
274
+ process.exit(1);
275
+ }
276
+ });
277
+ sandboxCmd
278
+ .command("delete <sandboxId>")
279
+ .alias("rm")
280
+ .description("Delete a sandbox")
281
+ .action(async (sandboxId) => {
282
+ const spinner = ora("Deleting sandbox...").start();
283
+ try {
284
+ const client = getClient();
285
+ await client.deleteSandbox(sandboxId);
286
+ spinner.succeed("Sandbox deleted");
287
+ }
288
+ catch (error) {
289
+ spinner.fail("Failed to delete sandbox");
290
+ console.error(chalk.red(String(error)));
291
+ process.exit(1);
292
+ }
293
+ });
294
+ // API Key commands
295
+ const keyCmd = program.command("key").description("Manage API keys");
296
+ keyCmd
297
+ .command("list")
298
+ .alias("ls")
299
+ .description("List all API keys")
300
+ .option("-j, --json", "Output as JSON")
301
+ .action(async (options) => {
302
+ const spinner = ora("Fetching API keys...").start();
303
+ try {
304
+ const client = getClient();
305
+ const result = await client.listApiKeys();
306
+ spinner.stop();
307
+ if (options.json) {
308
+ console.log(JSON.stringify(result, null, 2));
309
+ return;
310
+ }
311
+ if (result.keys.length === 0) {
312
+ console.log(chalk.gray("No API keys found"));
313
+ return;
314
+ }
315
+ const table = new Table({
316
+ head: ["ID", "Name", "Tier", "Rate Limit", "Created", "Last Used"],
317
+ style: { head: ["cyan"] },
318
+ });
319
+ for (const key of result.keys) {
320
+ table.push([
321
+ key.id.slice(0, 12),
322
+ key.name,
323
+ key.tier,
324
+ `${key.rateLimit}/min`,
325
+ new Date(key.createdAt).toLocaleDateString(),
326
+ key.lastUsedAt
327
+ ? new Date(key.lastUsedAt).toLocaleDateString()
328
+ : "-",
329
+ ]);
330
+ }
331
+ console.log(table.toString());
332
+ console.log(chalk.gray(`Total: ${result.total}`));
333
+ }
334
+ catch (error) {
335
+ spinner.fail("Failed to list API keys");
336
+ console.error(chalk.red(String(error)));
337
+ process.exit(1);
338
+ }
339
+ });
340
+ keyCmd
341
+ .command("create <name>")
342
+ .description("Create a new API key")
343
+ .option("-t, --tier <tier>", "API key tier (free, pro, enterprise)", "free")
344
+ .option("-j, --json", "Output as JSON")
345
+ .action(async (name, options) => {
346
+ const spinner = ora("Creating API key...").start();
347
+ try {
348
+ const client = getClient();
349
+ const key = await client.createApiKey(name, options.tier);
350
+ spinner.succeed("API key created");
351
+ if (options.json) {
352
+ console.log(JSON.stringify(key, null, 2));
353
+ return;
354
+ }
355
+ console.log(chalk.bold("\nAPI Key Created:"));
356
+ console.log(` ID: ${chalk.cyan(key.id)}`);
357
+ console.log(` Name: ${key.name}`);
358
+ console.log(` Tier: ${key.tier}`);
359
+ console.log(` Rate: ${key.rateLimit}/min`);
360
+ console.log();
361
+ console.log(chalk.yellow("Save this key - it won't be shown again:"));
362
+ console.log(chalk.green.bold(` ${key.key}`));
363
+ }
364
+ catch (error) {
365
+ spinner.fail("Failed to create API key");
366
+ console.error(chalk.red(String(error)));
367
+ process.exit(1);
368
+ }
369
+ });
370
+ keyCmd
371
+ .command("delete <keyId>")
372
+ .alias("rm")
373
+ .description("Delete an API key")
374
+ .action(async (keyId) => {
375
+ const spinner = ora("Deleting API key...").start();
376
+ try {
377
+ const client = getClient();
378
+ await client.deleteApiKey(keyId);
379
+ spinner.succeed("API key deleted");
380
+ }
381
+ catch (error) {
382
+ spinner.fail("Failed to delete API key");
383
+ console.error(chalk.red(String(error)));
384
+ process.exit(1);
385
+ }
386
+ });
387
+ // Health check
388
+ program
389
+ .command("health")
390
+ .description("Check API health")
391
+ .action(async () => {
392
+ const config = getConfig();
393
+ const baseUrl = process.env["COVE_API_URL"] || config.baseUrl;
394
+ const spinner = ora(`Checking ${baseUrl}...`).start();
395
+ try {
396
+ const response = await fetch(`${baseUrl}/health`);
397
+ const data = await response.json();
398
+ if (data.status === "ok") {
399
+ spinner.succeed(chalk.green("API is healthy"));
400
+ }
401
+ else {
402
+ spinner.warn(chalk.yellow(`API status: ${data.status}`));
403
+ }
404
+ }
405
+ catch {
406
+ spinner.fail(chalk.red("API is unreachable"));
407
+ process.exit(1);
408
+ }
409
+ });
410
+ program.parse();
411
+ }
@@ -0,0 +1 @@
1
+ export declare function runInteractive(): Promise<void>;
@@ -0,0 +1,468 @@
1
+ import * as readline from "readline";
2
+ import { spawnSync } from "child_process";
3
+ import chalk from "chalk";
4
+ import Table from "cli-table3";
5
+ import { CoveClient } from "./client.js";
6
+ import { getConfig, setApiKey, setBaseUrl } from "./config.js";
7
+ import { runPtySession } from "./pty-client.js";
8
+ // Simple inline status for shell mode (ora causes issues with Bun readline)
9
+ function createSpinner(msg) {
10
+ process.stdout.write(chalk.gray(msg + "... "));
11
+ return {
12
+ stop: () => process.stdout.write("\r\x1b[K"),
13
+ succeed: (m) => console.log(`\r\x1b[K${chalk.green("✓")} ${m}`),
14
+ fail: (m) => console.log(`\r\x1b[K${chalk.red("✗")} ${m}`),
15
+ };
16
+ }
17
+ function getClient() {
18
+ const config = getConfig();
19
+ const apiKey = process.env["COVE_API_KEY"] || config.apiKey;
20
+ const baseUrl = process.env["COVE_API_URL"] || config.baseUrl;
21
+ if (!apiKey) {
22
+ return null;
23
+ }
24
+ return new CoveClient(baseUrl, apiKey);
25
+ }
26
+ function formatState(state) {
27
+ switch (state) {
28
+ case "running":
29
+ return chalk.green(state);
30
+ case "error":
31
+ return chalk.red(state);
32
+ case "stopped":
33
+ return chalk.yellow(state);
34
+ case "creating":
35
+ return chalk.blue(state);
36
+ default:
37
+ return chalk.gray(state);
38
+ }
39
+ }
40
+ const COMMANDS = {
41
+ ls: { desc: "List sandboxes (running by default)", usage: "ls [-a|--all] [-s|--stopped] [-e|--error]" },
42
+ new: { desc: "Create a new sandbox", usage: "new [name] [--snapshot=<name>]" },
43
+ get: { desc: "Get sandbox details", usage: "get <id>" },
44
+ start: { desc: "Start sandbox(es)", usage: "start <id> [id2] [id3] ..." },
45
+ stop: { desc: "Stop sandbox(es)", usage: "stop <id> [id2] [id3] ..." },
46
+ rm: { desc: "Delete sandbox(es)", usage: "rm <id> [id2] [id3] ..." },
47
+ ssh: { desc: "Connect to sandbox terminal", usage: "ssh <id>" },
48
+ keys: { desc: "List API keys", usage: "keys" },
49
+ health: { desc: "Check API health", usage: "health" },
50
+ config: { desc: "Show/set config", usage: "config [key] [value]" },
51
+ help: { desc: "Show help", usage: "help [command]" },
52
+ exit: { desc: "Exit the shell", usage: "exit" },
53
+ };
54
+ async function handleCommand(client, line, rl) {
55
+ const parts = line.trim().split(/\s+/);
56
+ const cmd = parts[0]?.toLowerCase();
57
+ const args = parts.slice(1);
58
+ if (!cmd)
59
+ return "continue";
60
+ try {
61
+ switch (cmd) {
62
+ case "ls":
63
+ case "list": {
64
+ // Parse flags
65
+ const showAll = args.includes("-a") || args.includes("--all");
66
+ const showStopped = args.includes("-s") || args.includes("--stopped");
67
+ const showError = args.includes("-e") || args.includes("--error");
68
+ const spin = createSpinner("Fetching sandboxes");
69
+ const result = await client.listSandboxes();
70
+ spin.stop();
71
+ // Filter sandboxes based on flags
72
+ let sandboxes = result.sandboxes;
73
+ if (!showAll) {
74
+ if (showStopped) {
75
+ sandboxes = sandboxes.filter(sb => sb.state === "stopped");
76
+ }
77
+ else if (showError) {
78
+ sandboxes = sandboxes.filter(sb => sb.state === "error");
79
+ }
80
+ else {
81
+ // Default: only show running
82
+ sandboxes = sandboxes.filter(sb => sb.state === "running" || sb.state === "creating");
83
+ }
84
+ }
85
+ if (sandboxes.length === 0) {
86
+ const stateHint = showAll ? "" : showStopped ? " stopped" : showError ? " error" : " running";
87
+ console.log(chalk.gray(`No${stateHint} sandboxes. Use 'ls -a' to see all.`));
88
+ }
89
+ else {
90
+ const table = new Table({
91
+ head: ["ID", "Name", "State", "CPU", "Mem"],
92
+ style: { head: ["cyan"] },
93
+ });
94
+ for (const sb of sandboxes) {
95
+ table.push([
96
+ sb.id.slice(0, 12),
97
+ sb.name || "-",
98
+ formatState(sb.state),
99
+ `${sb.resources.cpu}`,
100
+ `${sb.resources.memory}GB`,
101
+ ]);
102
+ }
103
+ console.log(table.toString());
104
+ }
105
+ break;
106
+ }
107
+ case "new":
108
+ case "create": {
109
+ const name = args.find((a) => !a.startsWith("--"));
110
+ const snapshotArg = args.find((a) => a.startsWith("--snapshot="));
111
+ const snapshot = snapshotArg?.split("=")[1];
112
+ const spin = createSpinner("Creating sandbox");
113
+ const sandbox = await client.createSandbox({
114
+ name: name || undefined,
115
+ snapshot: snapshot || undefined,
116
+ });
117
+ spin.succeed(`Created ${chalk.cyan(sandbox.id.slice(0, 12))}`);
118
+ if (sandbox.noVncUrl) {
119
+ console.log(` NoVNC: ${chalk.blue(sandbox.noVncUrl)}`);
120
+ }
121
+ if (sandbox.vncUrl) {
122
+ console.log(` VNC WS: ${chalk.gray(sandbox.vncUrl)}`);
123
+ }
124
+ break;
125
+ }
126
+ case "get":
127
+ case "info": {
128
+ const id = args[0];
129
+ if (!id) {
130
+ console.log(chalk.red("Usage: get <id>"));
131
+ break;
132
+ }
133
+ const sandboxId = await resolveId(client, id);
134
+ if (!sandboxId)
135
+ break;
136
+ const spin = createSpinner("Fetching");
137
+ const sandbox = await client.getSandbox(sandboxId);
138
+ spin.stop();
139
+ console.log(`${chalk.bold("ID:")} ${sandbox.id}`);
140
+ console.log(`${chalk.bold("Name:")} ${sandbox.name || "-"}`);
141
+ console.log(`${chalk.bold("State:")} ${formatState(sandbox.state)}`);
142
+ console.log(`${chalk.bold("Resources:")} ${sandbox.resources.cpu} CPU, ${sandbox.resources.memory}GB RAM`);
143
+ if (sandbox.noVncUrl) {
144
+ console.log(`${chalk.bold("NoVNC:")} ${chalk.blue(sandbox.noVncUrl)}`);
145
+ }
146
+ if (sandbox.ptyUrl) {
147
+ console.log(`${chalk.bold("PTY:")} ${chalk.green(sandbox.ptyUrl)}`);
148
+ }
149
+ if (sandbox.vncUrl) {
150
+ console.log(`${chalk.bold("VNC WS:")} ${chalk.gray(sandbox.vncUrl)}`);
151
+ }
152
+ if (sandbox.proxyUrl) {
153
+ console.log(`${chalk.bold("Proxy:")} ${chalk.blue(sandbox.proxyUrl)}`);
154
+ }
155
+ break;
156
+ }
157
+ case "start": {
158
+ if (args.length === 0) {
159
+ console.log(chalk.red("Usage: start <id> [id2] [id3] ..."));
160
+ break;
161
+ }
162
+ // Resolve all IDs first
163
+ const sandboxIds = (await Promise.all(args.map(id => resolveId(client, id)))).filter(Boolean);
164
+ if (sandboxIds.length === 0)
165
+ break;
166
+ const spin = createSpinner(`Starting ${sandboxIds.length} sandbox${sandboxIds.length > 1 ? "es" : ""}`);
167
+ const results = await Promise.allSettled(sandboxIds.map(id => client.startSandbox(id)));
168
+ spin.stop();
169
+ const succeeded = results.filter(r => r.status === "fulfilled").length;
170
+ const failed = results.filter(r => r.status === "rejected");
171
+ if (failed.length === 0) {
172
+ console.log(`${chalk.green("✓")} Started ${succeeded} sandbox${succeeded > 1 ? "es" : ""}`);
173
+ }
174
+ else {
175
+ console.log(`${chalk.yellow("!")} Started ${succeeded}/${sandboxIds.length}, ${failed.length} failed`);
176
+ failed.forEach((r, i) => {
177
+ if (r.status === "rejected") {
178
+ console.log(chalk.red(` ${sandboxIds[results.indexOf(r)]?.slice(0, 12)}: ${r.reason}`));
179
+ }
180
+ });
181
+ }
182
+ break;
183
+ }
184
+ case "stop": {
185
+ if (args.length === 0) {
186
+ console.log(chalk.red("Usage: stop <id> [id2] [id3] ..."));
187
+ break;
188
+ }
189
+ // Resolve all IDs first
190
+ const sandboxIds = (await Promise.all(args.map(id => resolveId(client, id)))).filter(Boolean);
191
+ if (sandboxIds.length === 0)
192
+ break;
193
+ const spin = createSpinner(`Stopping ${sandboxIds.length} sandbox${sandboxIds.length > 1 ? "es" : ""}`);
194
+ const results = await Promise.allSettled(sandboxIds.map(id => client.stopSandbox(id)));
195
+ spin.stop();
196
+ const succeeded = results.filter(r => r.status === "fulfilled").length;
197
+ const failed = results.filter(r => r.status === "rejected");
198
+ if (failed.length === 0) {
199
+ console.log(`${chalk.green("✓")} Stopped ${succeeded} sandbox${succeeded > 1 ? "es" : ""}`);
200
+ }
201
+ else {
202
+ console.log(`${chalk.yellow("!")} Stopped ${succeeded}/${sandboxIds.length}, ${failed.length} failed`);
203
+ failed.forEach((r, i) => {
204
+ if (r.status === "rejected") {
205
+ console.log(chalk.red(` ${sandboxIds[results.indexOf(r)]?.slice(0, 12)}: ${r.reason}`));
206
+ }
207
+ });
208
+ }
209
+ break;
210
+ }
211
+ case "rm":
212
+ case "delete": {
213
+ if (args.length === 0) {
214
+ console.log(chalk.red("Usage: rm <id> [id2] [id3] ..."));
215
+ break;
216
+ }
217
+ // Resolve all IDs first
218
+ const sandboxIds = (await Promise.all(args.map(id => resolveId(client, id)))).filter(Boolean);
219
+ if (sandboxIds.length === 0)
220
+ break;
221
+ const spin = createSpinner(`Deleting ${sandboxIds.length} sandbox${sandboxIds.length > 1 ? "es" : ""}`);
222
+ const results = await Promise.allSettled(sandboxIds.map(id => client.deleteSandbox(id)));
223
+ spin.stop();
224
+ const succeeded = results.filter(r => r.status === "fulfilled").length;
225
+ const failed = results.filter(r => r.status === "rejected");
226
+ if (failed.length === 0) {
227
+ console.log(`${chalk.green("✓")} Deleted ${succeeded} sandbox${succeeded > 1 ? "es" : ""}`);
228
+ }
229
+ else {
230
+ console.log(`${chalk.yellow("!")} Deleted ${succeeded}/${sandboxIds.length}, ${failed.length} failed`);
231
+ failed.forEach((r, i) => {
232
+ if (r.status === "rejected") {
233
+ console.log(chalk.red(` ${sandboxIds[results.indexOf(r)]?.slice(0, 12)}: ${r.reason}`));
234
+ }
235
+ });
236
+ }
237
+ break;
238
+ }
239
+ case "ssh": {
240
+ const id = args[0];
241
+ if (!id) {
242
+ console.log(chalk.red("Usage: ssh <id>"));
243
+ break;
244
+ }
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
+ const sandboxId = await resolveId(client, id);
251
+ if (!sandboxId)
252
+ break;
253
+ const spin = createSpinner("Connecting");
254
+ const sandbox = await client.getSandbox(sandboxId);
255
+ spin.stop();
256
+ if (sandbox.state !== "running") {
257
+ console.log(chalk.red(`Sandbox is ${sandbox.state}. Start it first with: start ${sandboxId.slice(0, 12)}`));
258
+ break;
259
+ }
260
+ if (sandbox.noVncUrl) {
261
+ console.log(`NoVNC: ${chalk.blue(sandbox.noVncUrl)}`);
262
+ }
263
+ console.log(chalk.gray(`\nConnecting to ${sandboxId.slice(0, 12)}...\n`));
264
+ // Close readline before PTY (removes its stdin listeners)
265
+ if (rl) {
266
+ rl.close();
267
+ }
268
+ // Only remove keypress listener (readline's), not all listeners
269
+ process.stdin.removeAllListeners("keypress");
270
+ try {
271
+ await runPtySession(sandboxId, daytonaApiKey);
272
+ }
273
+ catch (err) {
274
+ console.error(chalk.red(`Connection failed: ${err}`));
275
+ }
276
+ console.log(chalk.gray("\nSession ended.\n"));
277
+ // Signal main loop to recreate readline
278
+ return "restart-readline";
279
+ }
280
+ case "keys": {
281
+ const spin = createSpinner("Fetching API keys");
282
+ const result = await client.listApiKeys();
283
+ spin.stop();
284
+ if (result.keys.length === 0) {
285
+ console.log(chalk.gray("No API keys"));
286
+ }
287
+ else {
288
+ const table = new Table({
289
+ head: ["ID", "Name", "Tier", "Rate"],
290
+ style: { head: ["cyan"] },
291
+ });
292
+ for (const key of result.keys) {
293
+ table.push([
294
+ key.id.slice(0, 12),
295
+ key.name,
296
+ key.tier,
297
+ `${key.rateLimit}/min`,
298
+ ]);
299
+ }
300
+ console.log(table.toString());
301
+ }
302
+ break;
303
+ }
304
+ case "health": {
305
+ const config = getConfig();
306
+ const baseUrl = process.env["COVE_API_URL"] || config.baseUrl;
307
+ const spin = createSpinner(`Checking ${baseUrl}`);
308
+ try {
309
+ const response = await fetch(`${baseUrl}/health`);
310
+ const data = await response.json();
311
+ if (data.status === "ok") {
312
+ spin.succeed(chalk.green("Healthy"));
313
+ }
314
+ else {
315
+ spin.fail(chalk.yellow(`Status: ${data.status}`));
316
+ }
317
+ }
318
+ catch {
319
+ spin.fail(chalk.red("Unreachable"));
320
+ }
321
+ break;
322
+ }
323
+ case "config": {
324
+ const key = args[0];
325
+ const value = args[1];
326
+ if (!key) {
327
+ const config = getConfig();
328
+ console.log(`${chalk.bold("url:")} ${config.baseUrl}`);
329
+ console.log(`${chalk.bold("apiKey:")} ${config.apiKey ? config.apiKey.slice(0, 10) + "..." : chalk.gray("(not set)")}`);
330
+ }
331
+ else if (key === "url" && value) {
332
+ setBaseUrl(value);
333
+ console.log(chalk.green(`URL set to ${value}`));
334
+ }
335
+ else if (key === "apiKey" && value) {
336
+ setApiKey(value);
337
+ console.log(chalk.green("API key saved"));
338
+ }
339
+ else {
340
+ console.log(chalk.red("Usage: config [url|apiKey] [value]"));
341
+ }
342
+ break;
343
+ }
344
+ case "help":
345
+ case "?": {
346
+ const cmdName = args[0];
347
+ if (cmdName && COMMANDS[cmdName]) {
348
+ const c = COMMANDS[cmdName];
349
+ console.log(`${chalk.bold(cmdName)}: ${c.desc}`);
350
+ console.log(` Usage: ${c.usage}`);
351
+ }
352
+ else {
353
+ console.log(chalk.bold("\nAvailable commands:\n"));
354
+ for (const [name, info] of Object.entries(COMMANDS)) {
355
+ console.log(` ${chalk.cyan(name.padEnd(8))} ${info.desc}`);
356
+ }
357
+ console.log();
358
+ }
359
+ break;
360
+ }
361
+ case "exit":
362
+ case "quit":
363
+ case "q":
364
+ return "exit";
365
+ case "clear":
366
+ case "cls":
367
+ console.clear();
368
+ break;
369
+ default:
370
+ console.log(chalk.red(`Unknown command: ${cmd}. Type 'help' for available commands.`));
371
+ }
372
+ }
373
+ catch (error) {
374
+ console.error(chalk.red(`Error: ${error}`));
375
+ }
376
+ return "continue";
377
+ }
378
+ async function resolveId(client, partialId) {
379
+ // UUID format: 8-4-4-4-12 (36 chars with dashes)
380
+ const isFullUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(partialId);
381
+ if (isFullUuid) {
382
+ return partialId;
383
+ }
384
+ // Search for matching sandbox
385
+ const result = await client.listSandboxes();
386
+ const matches = result.sandboxes.filter((sb) => sb.id.startsWith(partialId) ||
387
+ sb.id.slice(0, 12) === partialId ||
388
+ sb.name === partialId);
389
+ if (matches.length === 0) {
390
+ console.log(chalk.red(`No sandbox found matching: ${partialId}`));
391
+ return null;
392
+ }
393
+ if (matches.length > 1) {
394
+ console.log(chalk.red(`Multiple sandboxes match: ${partialId}`));
395
+ for (const m of matches) {
396
+ console.log(` ${m.id.slice(0, 12)} ${m.name || ""}`);
397
+ }
398
+ return null;
399
+ }
400
+ return matches[0].id;
401
+ }
402
+ function createReadline() {
403
+ return readline.createInterface({
404
+ input: process.stdin,
405
+ output: process.stdout,
406
+ terminal: process.stdin.isTTY ?? false,
407
+ prompt: chalk.cyan("cove> "),
408
+ completer: (line) => {
409
+ const completions = Object.keys(COMMANDS);
410
+ const hits = completions.filter((c) => c.startsWith(line.toLowerCase()));
411
+ return [hits.length ? hits : completions, line];
412
+ },
413
+ });
414
+ }
415
+ export async function runInteractive() {
416
+ console.log(chalk.bold.cyan("\n Cove Shell\n"));
417
+ console.log(chalk.gray(" Type 'help' for commands, 'exit' to quit\n"));
418
+ const client = getClient();
419
+ if (!client) {
420
+ console.log(chalk.yellow("No API key configured."));
421
+ console.log(chalk.gray("Run: cove config set-key <your-api-key>\n"));
422
+ return;
423
+ }
424
+ let rl = createReadline();
425
+ let shouldExit = false;
426
+ while (!shouldExit) {
427
+ // Wait for a line of input
428
+ const line = await new Promise((resolve) => {
429
+ const onLine = (input) => {
430
+ rl.removeListener("close", onClose);
431
+ resolve(input);
432
+ };
433
+ const onClose = () => {
434
+ rl.removeListener("line", onLine);
435
+ resolve(null);
436
+ };
437
+ rl.once("line", onLine);
438
+ rl.once("close", onClose);
439
+ rl.prompt();
440
+ });
441
+ // Handle Ctrl+C / EOF
442
+ if (line === null) {
443
+ break;
444
+ }
445
+ // Process the command
446
+ const result = await handleCommand(client, line, rl);
447
+ if (result === "exit") {
448
+ shouldExit = true;
449
+ }
450
+ else if (result === "restart-readline") {
451
+ // After PTY session, recreate readline with proper terminal reset
452
+ // Ensure not in raw mode
453
+ if (process.stdin.isTTY) {
454
+ process.stdin.setRawMode(false);
455
+ }
456
+ // Use stty to reset terminal
457
+ spawnSync("stty", ["sane"], { stdio: "inherit" });
458
+ // Fully reset stdin state
459
+ process.stdin.resume();
460
+ process.stdin.setEncoding("utf8");
461
+ process.stdin.ref();
462
+ // Create fresh readline
463
+ rl = createReadline();
464
+ }
465
+ // "continue" - just loop back
466
+ }
467
+ console.log(chalk.gray("\nGoodbye!\n"));
468
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * PTY Client using Daytona SDK
3
+ *
4
+ * Provides interactive terminal access to sandboxes using Daytona's native PTY support.
5
+ */
6
+ /**
7
+ * Run an interactive PTY session using Daytona SDK
8
+ */
9
+ export declare function runPtySession(sandboxId: string, apiKey: string): Promise<void>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * PTY Client using Daytona SDK
3
+ *
4
+ * Provides interactive terminal access to sandboxes using Daytona's native PTY support.
5
+ */
6
+ import { Daytona } from "@daytonaio/sdk";
7
+ /**
8
+ * Run an interactive PTY session using Daytona SDK
9
+ */
10
+ export async function runPtySession(sandboxId, apiKey) {
11
+ const client = new Daytona({ apiKey });
12
+ // Get terminal size
13
+ const rows = process.stdout.rows || 24;
14
+ const cols = process.stdout.columns || 80;
15
+ let pty = null;
16
+ let cleanedUp = false;
17
+ let stdinHandler = null;
18
+ let resizeHandler = null;
19
+ const cleanup = async () => {
20
+ if (cleanedUp)
21
+ return;
22
+ cleanedUp = true;
23
+ // Remove event listeners first
24
+ if (stdinHandler) {
25
+ process.stdin.removeListener("data", stdinHandler);
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();
43
+ }
44
+ catch {
45
+ // Ignore disconnect errors
46
+ }
47
+ pty = null;
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
+ });
77
+ }
78
+ };
79
+ process.stdin.on("data", stdinHandler);
80
+ // Handle resize
81
+ resizeHandler = () => {
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
+ });
88
+ }
89
+ };
90
+ process.stdout.on("resize", resizeHandler);
91
+ // Handle process signals
92
+ const handleSignal = async () => {
93
+ await cleanup();
94
+ process.exit(0);
95
+ };
96
+ process.once("SIGINT", handleSignal);
97
+ process.once("SIGTERM", handleSignal);
98
+ // Wait for PTY to exit
99
+ const result = await pty.wait();
100
+ console.log(`\nSession ended (exit code: ${result.exitCode})`);
101
+ }
102
+ catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ console.error(`\nPTY Error: ${message}`);
105
+ throw error;
106
+ }
107
+ finally {
108
+ await cleanup();
109
+ }
110
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "covebox",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Cove sandboxes - cloud development environments",
5
+ "type": "module",
6
+ "bin": {
7
+ "covebox": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/**/*",
11
+ "!dist/test-*"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsx src/index.ts",
16
+ "start": "node dist/index.js",
17
+ "test": "tsx src/test-e2e.ts",
18
+ "test:e2e": "tsx src/test-e2e.ts"
19
+ },
20
+ "dependencies": {
21
+ "@daytonaio/sdk": "^0.115.2",
22
+ "chalk": "^5.6.2",
23
+ "cli-table3": "^0.6.5",
24
+ "commander": "^12.1.0",
25
+ "conf": "^13.1.0",
26
+ "ora": "^8.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.15.32",
30
+ "tsx": "^4.21.0",
31
+ "typescript": "^5.7.3"
32
+ }
33
+ }