@computesdk/cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bootstrap.sh +164 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1281 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.3_typescript@5.8.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.3_typescript@5.8.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/pty.ts
|
|
22
|
+
async function startPTY(sandbox, options = {}) {
|
|
23
|
+
const terminal = await sandbox.terminal.create({ pty: true, shell: "/bin/bash" });
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
25
|
+
if (process.stdin.isTTY) {
|
|
26
|
+
process.stdin.setRawMode(true);
|
|
27
|
+
}
|
|
28
|
+
process.stdin.resume();
|
|
29
|
+
let exiting = false;
|
|
30
|
+
let resolvePromise;
|
|
31
|
+
if (options.initialCommand) {
|
|
32
|
+
terminal.write(options.initialCommand + "\n");
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
34
|
+
}
|
|
35
|
+
const ws = terminal._ws;
|
|
36
|
+
if (ws) {
|
|
37
|
+
ws.on("close", () => {
|
|
38
|
+
if (!exiting) {
|
|
39
|
+
doExit();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
terminal.on("output", (data) => {
|
|
44
|
+
if (!exiting) {
|
|
45
|
+
process.stdout.write(data);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
terminal.on("error", (error) => {
|
|
49
|
+
if (!error || error === "undefined") {
|
|
50
|
+
if (!exiting) {
|
|
51
|
+
doExit();
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(`\r
|
|
56
|
+
Terminal error: ${error}\r
|
|
57
|
+
`);
|
|
58
|
+
});
|
|
59
|
+
const onData = (data) => {
|
|
60
|
+
terminal.write(data.toString());
|
|
61
|
+
};
|
|
62
|
+
process.stdin.on("data", onData);
|
|
63
|
+
const onResize = () => {
|
|
64
|
+
if (process.stdout.columns && process.stdout.rows) {
|
|
65
|
+
try {
|
|
66
|
+
terminal.resize(process.stdout.columns, process.stdout.rows);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
process.stdout.on("resize", onResize);
|
|
72
|
+
onResize();
|
|
73
|
+
async function doExit() {
|
|
74
|
+
if (exiting) return;
|
|
75
|
+
exiting = true;
|
|
76
|
+
cleanup();
|
|
77
|
+
try {
|
|
78
|
+
await terminal.destroy();
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
if (resolvePromise) {
|
|
82
|
+
resolvePromise();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function cleanup() {
|
|
86
|
+
process.stdin.removeListener("data", onData);
|
|
87
|
+
process.stdout.removeListener("resize", onResize);
|
|
88
|
+
if (process.stdin.isTTY) {
|
|
89
|
+
process.stdin.setRawMode(false);
|
|
90
|
+
}
|
|
91
|
+
process.stdin.pause();
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
resolvePromise = resolve;
|
|
95
|
+
terminal.on("destroyed", () => {
|
|
96
|
+
if (!exiting) {
|
|
97
|
+
cleanup();
|
|
98
|
+
resolve();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
process.stdin.on("end", () => {
|
|
102
|
+
doExit();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
var init_pty = __esm({
|
|
107
|
+
"src/pty.ts"() {
|
|
108
|
+
"use strict";
|
|
109
|
+
init_esm_shims();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/workspace-cli.ts
|
|
114
|
+
var workspace_cli_exports = {};
|
|
115
|
+
__export(workspace_cli_exports, {
|
|
116
|
+
loadState: () => loadState,
|
|
117
|
+
loadWorkspaceState: () => loadWorkspaceState,
|
|
118
|
+
sandboxList: () => sandboxList,
|
|
119
|
+
saveSandbox: () => saveSandbox,
|
|
120
|
+
saveState: () => saveState,
|
|
121
|
+
workspaceAttach: () => workspaceAttach,
|
|
122
|
+
workspaceCreate: () => workspaceCreate,
|
|
123
|
+
workspaceDestroy: () => workspaceDestroy,
|
|
124
|
+
workspaceList: () => workspaceList
|
|
125
|
+
});
|
|
126
|
+
import { compute as compute2 } from "computesdk";
|
|
127
|
+
import * as fs2 from "fs";
|
|
128
|
+
import * as path3 from "path";
|
|
129
|
+
import * as os2 from "os";
|
|
130
|
+
import * as p3 from "@clack/prompts";
|
|
131
|
+
import pc3 from "picocolors";
|
|
132
|
+
function shellEscape(arg) {
|
|
133
|
+
if (/^[a-zA-Z0-9._\-/:=@]+$/.test(arg)) return arg;
|
|
134
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
135
|
+
}
|
|
136
|
+
function loadState() {
|
|
137
|
+
try {
|
|
138
|
+
if (!fs2.existsSync(WORKSPACE_STATE_FILE)) {
|
|
139
|
+
return { workspaces: {} };
|
|
140
|
+
}
|
|
141
|
+
const raw = fs2.readFileSync(WORKSPACE_STATE_FILE, "utf-8");
|
|
142
|
+
return JSON.parse(raw);
|
|
143
|
+
} catch {
|
|
144
|
+
return { workspaces: {} };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function saveState(state) {
|
|
148
|
+
const dir = path3.dirname(WORKSPACE_STATE_FILE);
|
|
149
|
+
if (!fs2.existsSync(dir)) {
|
|
150
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
151
|
+
}
|
|
152
|
+
fs2.writeFileSync(WORKSPACE_STATE_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
153
|
+
}
|
|
154
|
+
function getWorkspaceId(repo, branch) {
|
|
155
|
+
return `${repo}:${branch}`;
|
|
156
|
+
}
|
|
157
|
+
async function workspaceCreate(options) {
|
|
158
|
+
const { repo, branch, provider } = options;
|
|
159
|
+
const id = getWorkspaceId(repo, branch);
|
|
160
|
+
const state = loadState();
|
|
161
|
+
if (state.workspaces[id]) {
|
|
162
|
+
const existing = state.workspaces[id];
|
|
163
|
+
try {
|
|
164
|
+
const sandbox2 = await compute2.sandbox.getById(existing.sandboxId);
|
|
165
|
+
if (!sandbox2) {
|
|
166
|
+
throw new Error("Sandbox not found");
|
|
167
|
+
}
|
|
168
|
+
existing.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
169
|
+
saveState(state);
|
|
170
|
+
p3.log.info(`Attaching to existing workspace: ${pc3.cyan(id)}`);
|
|
171
|
+
return existing;
|
|
172
|
+
} catch {
|
|
173
|
+
delete state.workspaces[id];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
p3.log.info("Creating sandbox...");
|
|
177
|
+
const spinner4 = p3.spinner();
|
|
178
|
+
spinner4.start("Provisioning via ComputeSDK...");
|
|
179
|
+
let sandbox;
|
|
180
|
+
try {
|
|
181
|
+
sandbox = await compute2.sandbox.create();
|
|
182
|
+
spinner4.stop(`Sandbox ready: ${pc3.cyan(sandbox.sandboxId)}`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
spinner4.stop(pc3.red("Failed to create sandbox"));
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
spinner4.start("Bootstrapping workspace (installing gh CLI, cloning repo)...");
|
|
188
|
+
const bootstrapScript = fs2.readFileSync(
|
|
189
|
+
new URL("./bootstrap.sh", import.meta.url),
|
|
190
|
+
"utf-8"
|
|
191
|
+
);
|
|
192
|
+
try {
|
|
193
|
+
await sandbox.filesystem.writeFile("/tmp/bootstrap.sh", bootstrapScript);
|
|
194
|
+
await sandbox.runCommand("chmod +x /tmp/bootstrap.sh");
|
|
195
|
+
spinner4.stop("Running bootstrap (you may need to authenticate with GitHub)...");
|
|
196
|
+
const result = await sandbox.runCommand(
|
|
197
|
+
`/tmp/bootstrap.sh ${shellEscape(repo)} ${shellEscape(branch)}`,
|
|
198
|
+
{ timeout: 3e5 }
|
|
199
|
+
// 5 min timeout for auth
|
|
200
|
+
);
|
|
201
|
+
const output = result.stdout || "";
|
|
202
|
+
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
203
|
+
let worktreePath = `/workspaces/${path3.basename(repo)}-${branch}`;
|
|
204
|
+
if (jsonMatch) {
|
|
205
|
+
try {
|
|
206
|
+
const bootstrapInfo = JSON.parse(jsonMatch[0]);
|
|
207
|
+
worktreePath = bootstrapInfo.worktree_path || worktreePath;
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const workspace = {
|
|
212
|
+
id,
|
|
213
|
+
repo,
|
|
214
|
+
branch,
|
|
215
|
+
sandboxId: sandbox.sandboxId,
|
|
216
|
+
provider: provider || "e2b",
|
|
217
|
+
// TODO: Get actual provider
|
|
218
|
+
worktreePath,
|
|
219
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
220
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
221
|
+
};
|
|
222
|
+
state.workspaces[id] = workspace;
|
|
223
|
+
saveState(state);
|
|
224
|
+
p3.log.success(`Workspace ready: ${pc3.cyan(id)}`);
|
|
225
|
+
p3.log.info(`Sandbox: ${pc3.dim(sandbox.sandboxId)}`);
|
|
226
|
+
p3.log.info(`Worktree: ${pc3.dim(worktreePath)}`);
|
|
227
|
+
return workspace;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
spinner4.stop(pc3.red("Bootstrap failed"));
|
|
230
|
+
try {
|
|
231
|
+
await sandbox.destroy();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function workspaceAttach(id) {
|
|
238
|
+
const state = loadState();
|
|
239
|
+
const workspace = state.workspaces[id];
|
|
240
|
+
if (!workspace) {
|
|
241
|
+
p3.log.error(`Workspace not found: ${id}`);
|
|
242
|
+
p3.log.info(`Run: compute workspace create --repo owner/repo --branch ${id.split(":")[1] || "main"}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const sandbox = await compute2.sandbox.getById(workspace.sandboxId);
|
|
246
|
+
if (!sandbox) {
|
|
247
|
+
p3.log.error(`Sandbox no longer exists: ${workspace.sandboxId}`);
|
|
248
|
+
delete state.workspaces[id];
|
|
249
|
+
saveState(state);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
workspace.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
253
|
+
saveState(state);
|
|
254
|
+
p3.log.info(`Attaching to workspace: ${pc3.cyan(id)}`);
|
|
255
|
+
p3.log.info(`Changing to: ${pc3.dim(workspace.worktreePath)}`);
|
|
256
|
+
console.log(pc3.gray("Starting shell..."));
|
|
257
|
+
try {
|
|
258
|
+
process.stdout.write("\x1B[2J\x1B[H\x1B[3J");
|
|
259
|
+
await startPTY(sandbox, { initialCommand: `cd ${shellEscape(workspace.worktreePath)}` });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
p3.log.error(`Failed to attach: ${error.message}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function sandboxList() {
|
|
266
|
+
const state = loadState();
|
|
267
|
+
const workspaces = Object.values(state.workspaces);
|
|
268
|
+
if (workspaces.length === 0) {
|
|
269
|
+
p3.log.info("No sandboxes found.");
|
|
270
|
+
p3.log.info("Create one with: compute owner/repo --branch main");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
console.log(pc3.bold("Sandboxes"));
|
|
275
|
+
console.log();
|
|
276
|
+
for (const ws of workspaces) {
|
|
277
|
+
const age = Math.round((Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24));
|
|
278
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
279
|
+
const status = age > 7 ? pc3.red("stale") : age > 1 ? pc3.yellow("idle") : pc3.green("active");
|
|
280
|
+
console.log(` ${status} ${pc3.cyan(ws.sandboxId)}`);
|
|
281
|
+
console.log(` Workspace: ${pc3.dim(ws.id)}`);
|
|
282
|
+
console.log(` Provider: ${ws.provider}`);
|
|
283
|
+
console.log(` Last active: ${pc3.gray(ageStr)}`);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
console.log(pc3.gray('Tip: Use "compute workspaces" to see full workspace details'));
|
|
287
|
+
}
|
|
288
|
+
async function workspaceList() {
|
|
289
|
+
const state = loadState();
|
|
290
|
+
const workspaces = Object.values(state.workspaces);
|
|
291
|
+
if (workspaces.length === 0) {
|
|
292
|
+
p3.log.info("No workspaces found.");
|
|
293
|
+
p3.log.info("Create one with: compute owner/repo --branch main");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.log();
|
|
297
|
+
console.log(pc3.bold("Workspaces"));
|
|
298
|
+
console.log();
|
|
299
|
+
for (const ws of workspaces) {
|
|
300
|
+
const age = Math.round((Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24));
|
|
301
|
+
const ageStr = age === 0 ? "today" : `${age}d ago`;
|
|
302
|
+
console.log(` ${pc3.cyan(ws.id)}`);
|
|
303
|
+
console.log(` Sandbox: ${pc3.dim(ws.sandboxId)} (${ws.provider})`);
|
|
304
|
+
console.log(` Path: ${pc3.dim(ws.worktreePath)}`);
|
|
305
|
+
console.log(` Last used: ${pc3.gray(ageStr)}`);
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function saveSandbox(sandbox) {
|
|
310
|
+
const state = loadState();
|
|
311
|
+
state.workspaces[sandbox.id] = {
|
|
312
|
+
id: sandbox.id,
|
|
313
|
+
repo: "(no repo)",
|
|
314
|
+
branch: "(raw sandbox)",
|
|
315
|
+
sandboxId: sandbox.sandboxId,
|
|
316
|
+
provider: sandbox.provider,
|
|
317
|
+
worktreePath: "/",
|
|
318
|
+
createdAt: sandbox.createdAt,
|
|
319
|
+
lastUsed: sandbox.lastUsed
|
|
320
|
+
};
|
|
321
|
+
saveState(state);
|
|
322
|
+
}
|
|
323
|
+
async function workspaceDestroy(id) {
|
|
324
|
+
const state = loadState();
|
|
325
|
+
if (id) {
|
|
326
|
+
const workspace = state.workspaces[id];
|
|
327
|
+
if (!workspace) {
|
|
328
|
+
p3.log.error(`Workspace not found: ${id}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
p3.log.info(`Destroying workspace: ${pc3.cyan(id)}`);
|
|
332
|
+
try {
|
|
333
|
+
await compute2.sandbox.destroy(workspace.sandboxId);
|
|
334
|
+
delete state.workspaces[id];
|
|
335
|
+
saveState(state);
|
|
336
|
+
p3.log.success("Workspace destroyed");
|
|
337
|
+
} catch (error) {
|
|
338
|
+
p3.log.error(`Failed to destroy: ${error.message}`);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
const choices = Object.values(state.workspaces).map((ws) => ({
|
|
342
|
+
value: ws.id,
|
|
343
|
+
label: `${ws.id} (${ws.provider}, last used ${Math.round((Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24))}d ago)`
|
|
344
|
+
}));
|
|
345
|
+
if (choices.length === 0) {
|
|
346
|
+
p3.log.info("No workspaces to destroy");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const selected = await p3.select({
|
|
350
|
+
message: "Select workspace to destroy",
|
|
351
|
+
options: choices
|
|
352
|
+
});
|
|
353
|
+
if (p3.isCancel(selected)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
await workspaceDestroy(selected);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
var WORKSPACE_STATE_FILE, loadWorkspaceState;
|
|
360
|
+
var init_workspace_cli = __esm({
|
|
361
|
+
"src/workspace-cli.ts"() {
|
|
362
|
+
"use strict";
|
|
363
|
+
init_esm_shims();
|
|
364
|
+
init_pty();
|
|
365
|
+
WORKSPACE_STATE_FILE = path3.join(os2.homedir(), ".computesdk", "workspaces.json");
|
|
366
|
+
loadWorkspaceState = loadState;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// src/index.ts
|
|
371
|
+
init_esm_shims();
|
|
372
|
+
import "dotenv/config";
|
|
373
|
+
import { Command } from "commander";
|
|
374
|
+
import * as p4 from "@clack/prompts";
|
|
375
|
+
import pc4 from "picocolors";
|
|
376
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
377
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
378
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
379
|
+
import { compute as compute3 } from "computesdk";
|
|
380
|
+
|
|
381
|
+
// src/providers.ts
|
|
382
|
+
init_esm_shims();
|
|
383
|
+
import {
|
|
384
|
+
PROVIDER_NAMES,
|
|
385
|
+
isProviderAuthComplete,
|
|
386
|
+
getMissingEnvVars,
|
|
387
|
+
getProviderConfigFromEnv
|
|
388
|
+
} from "computesdk";
|
|
389
|
+
var PROVIDERS_NEEDING_CREDS = [
|
|
390
|
+
"e2b",
|
|
391
|
+
"railway",
|
|
392
|
+
"modal",
|
|
393
|
+
"vercel",
|
|
394
|
+
"daytona",
|
|
395
|
+
"render",
|
|
396
|
+
"namespace",
|
|
397
|
+
"blaxel",
|
|
398
|
+
"codesandbox"
|
|
399
|
+
];
|
|
400
|
+
function detectAvailableProviders() {
|
|
401
|
+
const available = [];
|
|
402
|
+
if (process.env.COMPUTESDK_API_KEY) {
|
|
403
|
+
available.push("computesdk");
|
|
404
|
+
}
|
|
405
|
+
for (const provider of PROVIDER_NAMES) {
|
|
406
|
+
if (PROVIDERS_NEEDING_CREDS.includes(provider) && isProviderAuthComplete(provider)) {
|
|
407
|
+
available.push(provider);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return available;
|
|
411
|
+
}
|
|
412
|
+
function getProviderStatus() {
|
|
413
|
+
const statuses = [];
|
|
414
|
+
const hasGatewayKey = !!process.env.COMPUTESDK_API_KEY;
|
|
415
|
+
statuses.push({
|
|
416
|
+
name: "computesdk",
|
|
417
|
+
ready: hasGatewayKey,
|
|
418
|
+
missing: hasGatewayKey ? [] : ["COMPUTESDK_API_KEY"]
|
|
419
|
+
});
|
|
420
|
+
for (const provider of PROVIDER_NAMES) {
|
|
421
|
+
if (PROVIDERS_NEEDING_CREDS.includes(provider)) {
|
|
422
|
+
statuses.push({
|
|
423
|
+
name: provider,
|
|
424
|
+
ready: isProviderAuthComplete(provider),
|
|
425
|
+
missing: getMissingEnvVars(provider)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return statuses;
|
|
430
|
+
}
|
|
431
|
+
function buildProviderConfig(provider) {
|
|
432
|
+
const config = {
|
|
433
|
+
provider,
|
|
434
|
+
apiKey: process.env.COMPUTESDK_API_KEY
|
|
435
|
+
// Gateway API key (top-level)
|
|
436
|
+
};
|
|
437
|
+
if (provider === "computesdk") {
|
|
438
|
+
config.computesdk = {
|
|
439
|
+
computesdk_api_key: process.env.COMPUTESDK_API_KEY
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (provider !== "computesdk") {
|
|
443
|
+
const providerConfig = getProviderConfigFromEnv(provider);
|
|
444
|
+
if (Object.keys(providerConfig).length > 0) {
|
|
445
|
+
config[provider] = providerConfig;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return config;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/index.ts
|
|
452
|
+
init_pty();
|
|
453
|
+
|
|
454
|
+
// src/auth.ts
|
|
455
|
+
init_esm_shims();
|
|
456
|
+
import * as http from "http";
|
|
457
|
+
import * as crypto from "crypto";
|
|
458
|
+
import { exec } from "child_process";
|
|
459
|
+
import * as fs from "fs";
|
|
460
|
+
import * as path2 from "path";
|
|
461
|
+
import * as os from "os";
|
|
462
|
+
import * as p from "@clack/prompts";
|
|
463
|
+
import pc from "picocolors";
|
|
464
|
+
function getCredentialsDir() {
|
|
465
|
+
return path2.join(os.homedir(), ".computesdk");
|
|
466
|
+
}
|
|
467
|
+
function getCredentialsFile() {
|
|
468
|
+
return path2.join(getCredentialsDir(), "credentials.json");
|
|
469
|
+
}
|
|
470
|
+
var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
471
|
+
var CONSOLE_AUTH_URL = "https://console.computesdk.com/cli/auth";
|
|
472
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
473
|
+
<html>
|
|
474
|
+
<head><title>ComputeSDK CLI</title></head>
|
|
475
|
+
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
476
|
+
<div style="text-align: center;">
|
|
477
|
+
<h1>Authenticated</h1>
|
|
478
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
479
|
+
</div>
|
|
480
|
+
</body>
|
|
481
|
+
</html>`;
|
|
482
|
+
function loadStoredCredentials() {
|
|
483
|
+
try {
|
|
484
|
+
if (!fs.existsSync(getCredentialsFile())) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
const raw = fs.readFileSync(getCredentialsFile(), "utf-8");
|
|
488
|
+
const data = JSON.parse(raw);
|
|
489
|
+
if (typeof data.apiKey === "string" && data.apiKey.length > 0) {
|
|
490
|
+
return data.apiKey;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function storeCredentials(apiKey) {
|
|
498
|
+
if (!fs.existsSync(getCredentialsDir())) {
|
|
499
|
+
fs.mkdirSync(getCredentialsDir(), { recursive: true, mode: 448 });
|
|
500
|
+
}
|
|
501
|
+
fs.writeFileSync(
|
|
502
|
+
getCredentialsFile(),
|
|
503
|
+
JSON.stringify({ apiKey }, null, 2) + "\n",
|
|
504
|
+
{ mode: 384 }
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
function clearStoredCredentials() {
|
|
508
|
+
try {
|
|
509
|
+
if (fs.existsSync(getCredentialsFile())) {
|
|
510
|
+
fs.unlinkSync(getCredentialsFile());
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function openBrowser(url) {
|
|
516
|
+
const command = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
517
|
+
exec(command, (err) => {
|
|
518
|
+
if (err) {
|
|
519
|
+
p.log.warn("Could not open browser automatically.");
|
|
520
|
+
p.log.info(`Open this URL manually:
|
|
521
|
+
${pc.cyan(url)}`);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function runBrowserAuthFlow(options) {
|
|
526
|
+
const timeoutMs = options?.timeoutMs ?? AUTH_TIMEOUT_MS;
|
|
527
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
529
|
+
const server = http.createServer();
|
|
530
|
+
const timeout = setTimeout(() => {
|
|
531
|
+
server.close();
|
|
532
|
+
reject(
|
|
533
|
+
new Error("Authentication timed out after 5 minutes. Please try again.")
|
|
534
|
+
);
|
|
535
|
+
}, timeoutMs);
|
|
536
|
+
server.on("request", (req, res) => {
|
|
537
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
538
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
539
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
540
|
+
res.end("Not Found");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const receivedState = url.searchParams.get("state");
|
|
544
|
+
const token = url.searchParams.get("token");
|
|
545
|
+
if (receivedState !== state) {
|
|
546
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
547
|
+
res.end("Error: State mismatch. Please try authenticating again.");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!token) {
|
|
551
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
552
|
+
res.end("Error: No token received.");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
556
|
+
res.end(SUCCESS_HTML);
|
|
557
|
+
clearTimeout(timeout);
|
|
558
|
+
server.close();
|
|
559
|
+
resolve(token);
|
|
560
|
+
});
|
|
561
|
+
server.listen(0, "127.0.0.1", () => {
|
|
562
|
+
const addr = server.address();
|
|
563
|
+
if (!addr || typeof addr === "string") {
|
|
564
|
+
clearTimeout(timeout);
|
|
565
|
+
server.close();
|
|
566
|
+
reject(new Error("Failed to start auth server"));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const port = addr.port;
|
|
570
|
+
const authUrl = `${CONSOLE_AUTH_URL}?port=${port}&state=${state}`;
|
|
571
|
+
if (options?.onServerReady) {
|
|
572
|
+
options.onServerReady(port, state);
|
|
573
|
+
}
|
|
574
|
+
if (!options?.skipBrowserOpen) {
|
|
575
|
+
openBrowser(authUrl);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
server.on("error", (err) => {
|
|
579
|
+
clearTimeout(timeout);
|
|
580
|
+
reject(new Error(`Auth server error: ${err.message}`));
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async function resolveApiKey(options) {
|
|
585
|
+
if (process.env.COMPUTESDK_API_KEY) {
|
|
586
|
+
return process.env.COMPUTESDK_API_KEY;
|
|
587
|
+
}
|
|
588
|
+
if (!options?.forceLogin) {
|
|
589
|
+
const stored = loadStoredCredentials();
|
|
590
|
+
if (stored) {
|
|
591
|
+
p.log.info(
|
|
592
|
+
`Using stored credentials from ${pc.dim("~/.computesdk/credentials.json")}`
|
|
593
|
+
);
|
|
594
|
+
return stored;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
p.log.info("No API key found. Starting browser authentication...");
|
|
598
|
+
const spinner4 = p.spinner();
|
|
599
|
+
spinner4.start("Waiting for browser authentication...");
|
|
600
|
+
try {
|
|
601
|
+
const apiKey = await runBrowserAuthFlow();
|
|
602
|
+
spinner4.stop("Authentication successful");
|
|
603
|
+
try {
|
|
604
|
+
storeCredentials(apiKey);
|
|
605
|
+
p.log.success(
|
|
606
|
+
`Credentials saved to ${pc.dim("~/.computesdk/credentials.json")}`
|
|
607
|
+
);
|
|
608
|
+
} catch {
|
|
609
|
+
p.log.warn("Could not save credentials to disk. You may need to authenticate again next time.");
|
|
610
|
+
}
|
|
611
|
+
return apiKey;
|
|
612
|
+
} catch (error) {
|
|
613
|
+
spinner4.stop(pc.red("Authentication failed"));
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/setup.ts
|
|
619
|
+
init_esm_shims();
|
|
620
|
+
import * as p2 from "@clack/prompts";
|
|
621
|
+
import pc2 from "picocolors";
|
|
622
|
+
import { compute } from "computesdk";
|
|
623
|
+
async function ensureAuth(options) {
|
|
624
|
+
try {
|
|
625
|
+
const apiKey = await resolveApiKey({ forceLogin: options?.forceLogin });
|
|
626
|
+
process.env.COMPUTESDK_API_KEY = apiKey;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
p2.log.error(error.message);
|
|
629
|
+
p2.log.info(`Get your API key at ${pc2.cyan("https://console.computesdk.com")}`);
|
|
630
|
+
p2.outro(pc2.red("Setup incomplete"));
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function resolveProvider(providerFlag, options) {
|
|
635
|
+
const interactive = options?.interactive ?? false;
|
|
636
|
+
const available = detectAvailableProviders();
|
|
637
|
+
const providerStatus = getProviderStatus();
|
|
638
|
+
if (providerFlag) {
|
|
639
|
+
const match = providerStatus.find(
|
|
640
|
+
(s) => s.name.toLowerCase() === providerFlag.toLowerCase()
|
|
641
|
+
);
|
|
642
|
+
if (!match) {
|
|
643
|
+
p2.log.error(`Unknown provider: ${providerFlag}`);
|
|
644
|
+
p2.log.info(`Available providers: ${providerStatus.map((s) => s.name).join(", ")}`);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
if (!match.ready) {
|
|
648
|
+
p2.log.error(`Provider ${providerFlag} is not configured`);
|
|
649
|
+
if (match.missing.length > 0) {
|
|
650
|
+
p2.log.info(`Missing env vars: ${match.missing.join(", ")}`);
|
|
651
|
+
}
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
return match.name;
|
|
655
|
+
}
|
|
656
|
+
if (available.length === 0) {
|
|
657
|
+
p2.log.error("No provider credentials detected");
|
|
658
|
+
p2.log.info("Set one of: E2B_API_KEY, MODAL_TOKEN_ID, RAILWAY_API_KEY, etc.");
|
|
659
|
+
p2.outro(pc2.red("Setup incomplete"));
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
if (available.length === 1) {
|
|
663
|
+
return available[0];
|
|
664
|
+
}
|
|
665
|
+
if (!interactive) {
|
|
666
|
+
p2.log.error("Multiple providers configured. Use --provider to specify one.");
|
|
667
|
+
p2.log.info(`Available: ${available.join(", ")}`);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
const providerOptions = providerStatus.filter((status) => status.ready).map((status) => ({
|
|
671
|
+
value: status.name,
|
|
672
|
+
label: status.name
|
|
673
|
+
}));
|
|
674
|
+
const provider = await p2.select({
|
|
675
|
+
message: "Select provider",
|
|
676
|
+
options: providerOptions
|
|
677
|
+
});
|
|
678
|
+
if (p2.isCancel(provider)) {
|
|
679
|
+
p2.cancel("Cancelled");
|
|
680
|
+
process.exit(0);
|
|
681
|
+
}
|
|
682
|
+
return provider;
|
|
683
|
+
}
|
|
684
|
+
function configureCompute(provider) {
|
|
685
|
+
const config = buildProviderConfig(provider);
|
|
686
|
+
compute.setConfig(config);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/index.ts
|
|
690
|
+
init_workspace_cli();
|
|
691
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
692
|
+
var __dirname2 = dirname2(__filename2);
|
|
693
|
+
var packageJson = JSON.parse(
|
|
694
|
+
readFileSync3(join3(__dirname2, "..", "package.json"), "utf-8")
|
|
695
|
+
);
|
|
696
|
+
var isDevBuild = import.meta.url.includes("worktrees") || import.meta.url.includes("/packages/cli/");
|
|
697
|
+
var VERSION = isDevBuild ? `${packageJson.version}-dev` : packageJson.version;
|
|
698
|
+
var program = new Command();
|
|
699
|
+
program.name("compute").description("Cloud-native workspace management").version(VERSION).enablePositionalOptions().passThroughOptions();
|
|
700
|
+
program.option("--login", "force re-authentication").option("--logout", "clear stored credentials").action(async (opts) => {
|
|
701
|
+
if (opts.logout) {
|
|
702
|
+
console.log();
|
|
703
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
704
|
+
clearStoredCredentials();
|
|
705
|
+
p4.log.success("Logged out. Stored credentials removed.");
|
|
706
|
+
p4.outro(pc4.green("Done!"));
|
|
707
|
+
process.exit(0);
|
|
708
|
+
}
|
|
709
|
+
if (opts.login) {
|
|
710
|
+
console.log();
|
|
711
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
712
|
+
await ensureAuth({ forceLogin: true });
|
|
713
|
+
p4.outro(pc4.green("Authenticated!"));
|
|
714
|
+
process.exit(0);
|
|
715
|
+
}
|
|
716
|
+
program.help();
|
|
717
|
+
});
|
|
718
|
+
program.command("create").description("Create a new workspace (same as create-compute)").argument("[repo]", "Repository in owner/repo format").option("-b, --branch <branch>", "Branch name", "main").option("-p, --provider <provider>", "provider to use").option("--no-attach", "Create workspace but do not attach").action(async (repo, options) => {
|
|
719
|
+
console.log();
|
|
720
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
721
|
+
await ensureAuth();
|
|
722
|
+
if (!repo) {
|
|
723
|
+
const state = loadWorkspaceState();
|
|
724
|
+
const workspaceIds = Object.keys(state.workspaces);
|
|
725
|
+
if (workspaceIds.length > 0) {
|
|
726
|
+
const choices = workspaceIds.map((id) => {
|
|
727
|
+
const ws = state.workspaces[id];
|
|
728
|
+
const age = Math.round(
|
|
729
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
730
|
+
);
|
|
731
|
+
const ageStr = age === 0 ? "today" : `${age}d ago`;
|
|
732
|
+
return {
|
|
733
|
+
value: id,
|
|
734
|
+
label: `${id} (${ws.provider}, ${ageStr})`
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
const selected = await p4.select({
|
|
738
|
+
message: "Select workspace to attach (or create new)",
|
|
739
|
+
options: [
|
|
740
|
+
...choices,
|
|
741
|
+
{ value: "__new__", label: pc4.gray("+ Create new workspace") }
|
|
742
|
+
]
|
|
743
|
+
});
|
|
744
|
+
if (p4.isCancel(selected)) {
|
|
745
|
+
p4.cancel("Cancelled");
|
|
746
|
+
process.exit(0);
|
|
747
|
+
}
|
|
748
|
+
if (selected !== "__new__") {
|
|
749
|
+
await workspaceAttach(selected);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const repoInput = await p4.text({
|
|
754
|
+
message: "Repository to work on?",
|
|
755
|
+
placeholder: "owner/repo",
|
|
756
|
+
validate: (value) => {
|
|
757
|
+
if (!value || !value.includes("/")) {
|
|
758
|
+
return "Please enter a repository in owner/repo format";
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
if (p4.isCancel(repoInput)) {
|
|
763
|
+
p4.cancel("Cancelled");
|
|
764
|
+
process.exit(0);
|
|
765
|
+
}
|
|
766
|
+
repo = repoInput;
|
|
767
|
+
}
|
|
768
|
+
if (!repo.includes("/")) {
|
|
769
|
+
p4.log.error("Invalid repository format. Use: owner/repo");
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
const provider = await resolveProvider(options.provider, { interactive: true });
|
|
773
|
+
configureCompute(provider);
|
|
774
|
+
const spinner4 = p4.spinner();
|
|
775
|
+
spinner4.start("Creating workspace...");
|
|
776
|
+
try {
|
|
777
|
+
const workspace = await workspaceCreate({
|
|
778
|
+
repo,
|
|
779
|
+
branch: options.branch,
|
|
780
|
+
provider
|
|
781
|
+
});
|
|
782
|
+
spinner4.stop(`Workspace ready: ${pc4.cyan(workspace.id)}`);
|
|
783
|
+
if (options.attach !== false) {
|
|
784
|
+
console.log();
|
|
785
|
+
await workspaceAttach(workspace.id);
|
|
786
|
+
} else {
|
|
787
|
+
p4.outro(pc4.green("Workspace created!"));
|
|
788
|
+
}
|
|
789
|
+
} catch (error) {
|
|
790
|
+
spinner4.stop(pc4.red("Failed to create workspace"));
|
|
791
|
+
p4.log.error(error.message);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
var sandboxCmd = program.command("sandbox").alias("sb").description("Manage raw sandboxes (VMs without git/worktree)");
|
|
796
|
+
sandboxCmd.command("create").description("Create a new sandbox").option("-p, --provider <provider>", "provider to use").option("--timeout <duration>", "sandbox timeout (e.g. 5m, 1h)").option("--name <name>", "sandbox name for findOrCreate").option("--connect", "start interactive shell after creation").option("--silent", "only output the sandbox ID").action(async (opts) => {
|
|
797
|
+
if (!opts.silent) {
|
|
798
|
+
console.log();
|
|
799
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
800
|
+
}
|
|
801
|
+
await ensureAuth();
|
|
802
|
+
const provider = await resolveProvider(opts.provider);
|
|
803
|
+
configureCompute(provider);
|
|
804
|
+
if (!opts.silent) {
|
|
805
|
+
const spinner4 = p4.spinner();
|
|
806
|
+
spinner4.start("Creating sandbox...");
|
|
807
|
+
try {
|
|
808
|
+
const sandbox = await compute3.sandbox.create(
|
|
809
|
+
buildCreateOptions(opts)
|
|
810
|
+
);
|
|
811
|
+
spinner4.stop(`Sandbox ready: ${pc4.cyan(sandbox.sandboxId)}`);
|
|
812
|
+
const { saveSandbox: saveSandbox2 } = await Promise.resolve().then(() => (init_workspace_cli(), workspace_cli_exports));
|
|
813
|
+
await saveSandbox2({
|
|
814
|
+
id: sandbox.sandboxId,
|
|
815
|
+
sandboxId: sandbox.sandboxId,
|
|
816
|
+
provider,
|
|
817
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
818
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
819
|
+
});
|
|
820
|
+
if (opts.connect) {
|
|
821
|
+
console.log(pc4.gray(" Entering shell... (.exit or Ctrl+D to return)"));
|
|
822
|
+
console.log();
|
|
823
|
+
process.stdout.write("\x1B[2J\x1B[H\x1B[3J");
|
|
824
|
+
await startPTY(sandbox);
|
|
825
|
+
}
|
|
826
|
+
} catch (error) {
|
|
827
|
+
spinner4.stop(pc4.red("Failed to create sandbox"));
|
|
828
|
+
p4.log.error(error.message);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
try {
|
|
833
|
+
const sandbox = await compute3.sandbox.create(
|
|
834
|
+
buildCreateOptions(opts)
|
|
835
|
+
);
|
|
836
|
+
process.stdout.write(sandbox.sandboxId);
|
|
837
|
+
const { saveSandbox: saveSandbox2 } = await Promise.resolve().then(() => (init_workspace_cli(), workspace_cli_exports));
|
|
838
|
+
await saveSandbox2({
|
|
839
|
+
id: sandbox.sandboxId,
|
|
840
|
+
sandboxId: sandbox.sandboxId,
|
|
841
|
+
provider,
|
|
842
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
843
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
+
});
|
|
845
|
+
if (opts.connect) {
|
|
846
|
+
console.log();
|
|
847
|
+
process.stdout.write("\x1B[2J\x1B[H\x1B[3J");
|
|
848
|
+
await startPTY(sandbox);
|
|
849
|
+
}
|
|
850
|
+
} catch (error) {
|
|
851
|
+
process.stderr.write(error.message + "\n");
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
sandboxCmd.command("destroy").description("Destroy one or more sandboxes").aliases(["rm", "stop"]).argument("[sandbox_id...]", "sandbox IDs to destroy (optional - will show picker if not provided)").option("-p, --provider <provider>", "provider to use").action(async (sandboxIds, opts) => {
|
|
857
|
+
console.log();
|
|
858
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
859
|
+
const { loadState: loadState2, saveState: saveState2 } = await Promise.resolve().then(() => (init_workspace_cli(), workspace_cli_exports));
|
|
860
|
+
const state = loadState2();
|
|
861
|
+
if (!sandboxIds || sandboxIds.length === 0) {
|
|
862
|
+
const workspaces = Object.values(state.workspaces);
|
|
863
|
+
if (workspaces.length === 0) {
|
|
864
|
+
p4.log.info("No sandboxes to destroy.");
|
|
865
|
+
p4.outro(pc4.green("Done!"));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const choices = workspaces.map((ws) => {
|
|
869
|
+
const age = Math.round(
|
|
870
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
871
|
+
);
|
|
872
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
873
|
+
const status = age > 7 ? pc4.red("stale") : age > 1 ? pc4.yellow("idle") : pc4.green("active");
|
|
874
|
+
return {
|
|
875
|
+
value: ws.sandboxId,
|
|
876
|
+
label: `${status} ${ws.sandboxId} (${ws.provider}, last active ${ageStr})`
|
|
877
|
+
};
|
|
878
|
+
});
|
|
879
|
+
const selected = await p4.multiselect({
|
|
880
|
+
message: "Select sandboxes to destroy (space to select, enter to confirm)",
|
|
881
|
+
options: choices
|
|
882
|
+
});
|
|
883
|
+
if (p4.isCancel(selected) || selected.length === 0) {
|
|
884
|
+
p4.cancel("Cancelled");
|
|
885
|
+
process.exit(0);
|
|
886
|
+
}
|
|
887
|
+
sandboxIds = selected;
|
|
888
|
+
}
|
|
889
|
+
await ensureAuth();
|
|
890
|
+
const provider = await resolveProvider(opts.provider);
|
|
891
|
+
configureCompute(provider);
|
|
892
|
+
const results = await Promise.allSettled(
|
|
893
|
+
sandboxIds.map(async (id) => {
|
|
894
|
+
await compute3.sandbox.destroy(id);
|
|
895
|
+
return id;
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
for (const result of results) {
|
|
899
|
+
if (result.status === "fulfilled") {
|
|
900
|
+
const sandboxId = result.value;
|
|
901
|
+
console.log(pc4.green(`Destroyed ${sandboxId}`));
|
|
902
|
+
const workspaceId = Object.keys(state.workspaces).find(
|
|
903
|
+
(key) => state.workspaces[key].sandboxId === sandboxId
|
|
904
|
+
);
|
|
905
|
+
if (workspaceId) {
|
|
906
|
+
delete state.workspaces[workspaceId];
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
console.error(pc4.red(`Failed to destroy: ${result.reason?.message || result.reason}`));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
saveState2(state);
|
|
913
|
+
});
|
|
914
|
+
sandboxCmd.command("list").alias("ls").description("List all sandboxes").action(async () => {
|
|
915
|
+
console.log();
|
|
916
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
917
|
+
await sandboxList();
|
|
918
|
+
});
|
|
919
|
+
sandboxCmd.command("connect").description("Connect to a sandbox via PTY (same as compute connect)").aliases(["ssh", "shell"]).argument("[sandbox_id]", "sandbox ID (optional - will show picker if not provided)").option("-p, --provider <provider>", "provider to use").action(async (sandboxId, opts) => {
|
|
920
|
+
console.log();
|
|
921
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
922
|
+
await ensureAuth();
|
|
923
|
+
const provider = await resolveProvider(opts.provider);
|
|
924
|
+
configureCompute(provider);
|
|
925
|
+
if (!sandboxId) {
|
|
926
|
+
const { loadState: loadState2 } = await Promise.resolve().then(() => (init_workspace_cli(), workspace_cli_exports));
|
|
927
|
+
const state = loadState2();
|
|
928
|
+
const workspaces = Object.values(state.workspaces);
|
|
929
|
+
if (workspaces.length === 0) {
|
|
930
|
+
p4.log.info("No sandboxes found.");
|
|
931
|
+
p4.log.info("Create one with: compute sandbox create");
|
|
932
|
+
p4.outro(pc4.green("Done!"));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const choices = workspaces.map((ws) => {
|
|
936
|
+
const age = Math.round(
|
|
937
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
938
|
+
);
|
|
939
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
940
|
+
const status = age > 7 ? pc4.red("stale") : age > 1 ? pc4.yellow("idle") : pc4.green("active");
|
|
941
|
+
return {
|
|
942
|
+
value: ws.sandboxId,
|
|
943
|
+
label: `${status} ${ws.sandboxId} (${ws.provider}, last active ${ageStr})`
|
|
944
|
+
};
|
|
945
|
+
});
|
|
946
|
+
const selected = await p4.select({
|
|
947
|
+
message: "Select sandbox to connect",
|
|
948
|
+
options: choices
|
|
949
|
+
});
|
|
950
|
+
if (p4.isCancel(selected)) {
|
|
951
|
+
p4.cancel("Cancelled");
|
|
952
|
+
process.exit(0);
|
|
953
|
+
}
|
|
954
|
+
sandboxId = selected;
|
|
955
|
+
}
|
|
956
|
+
const spinner4 = p4.spinner();
|
|
957
|
+
spinner4.start("Connecting...");
|
|
958
|
+
const sandbox = await compute3.sandbox.getById(sandboxId);
|
|
959
|
+
if (!sandbox) {
|
|
960
|
+
spinner4.stop(pc4.red(`Sandbox not found: ${sandboxId}`));
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
spinner4.stop("");
|
|
964
|
+
process.stdout.write("\x1B[2J\x1B[H\x1B[3J");
|
|
965
|
+
await startPTY(sandbox);
|
|
966
|
+
});
|
|
967
|
+
sandboxCmd.command("exec").description("Execute a command in a sandbox").argument("<sandbox_id>", "sandbox ID").argument("<command...>", "command to run").option("-p, --provider <provider>", "provider to use").option("-e, --env <key=value...>", "environment variables").option("-w, --workdir <directory>", "working directory").passThroughOptions().action(async (sandboxId, command, opts) => {
|
|
968
|
+
await ensureAuth();
|
|
969
|
+
const provider = await resolveProvider(opts.provider);
|
|
970
|
+
configureCompute(provider);
|
|
971
|
+
const sandbox = await compute3.sandbox.getById(sandboxId);
|
|
972
|
+
if (!sandbox) {
|
|
973
|
+
console.error(pc4.red(`Sandbox not found: ${sandboxId}`));
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
const cmd = buildExecCommand(command, opts);
|
|
977
|
+
const result = await sandbox.runCommand(cmd);
|
|
978
|
+
if (result.stdout) {
|
|
979
|
+
process.stdout.write(result.stdout);
|
|
980
|
+
}
|
|
981
|
+
if (result.stderr) {
|
|
982
|
+
process.stderr.write(result.stderr);
|
|
983
|
+
}
|
|
984
|
+
process.exit(result.exitCode);
|
|
985
|
+
});
|
|
986
|
+
program.command("connect").description("Connect to a sandbox via PTY").aliases(["ssh", "shell"]).argument("[sandbox_id]", "sandbox ID (optional - will show picker if not provided)").option("-p, --provider <provider>", "provider to use").action(async (sandboxId, opts) => {
|
|
987
|
+
console.log();
|
|
988
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
989
|
+
await ensureAuth();
|
|
990
|
+
const provider = await resolveProvider(opts.provider);
|
|
991
|
+
configureCompute(provider);
|
|
992
|
+
if (!sandboxId) {
|
|
993
|
+
const { loadState: loadState2 } = await Promise.resolve().then(() => (init_workspace_cli(), workspace_cli_exports));
|
|
994
|
+
const state = loadState2();
|
|
995
|
+
const workspaces = Object.values(state.workspaces);
|
|
996
|
+
if (workspaces.length === 0) {
|
|
997
|
+
p4.log.info("No sandboxes found.");
|
|
998
|
+
p4.log.info("Create one with: compute sandbox create");
|
|
999
|
+
p4.outro(pc4.green("Done!"));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const choices = workspaces.map((ws) => {
|
|
1003
|
+
const age = Math.round(
|
|
1004
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
1005
|
+
);
|
|
1006
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
1007
|
+
const status = age > 7 ? pc4.red("stale") : age > 1 ? pc4.yellow("idle") : pc4.green("active");
|
|
1008
|
+
return {
|
|
1009
|
+
value: ws.sandboxId,
|
|
1010
|
+
label: `${status} ${ws.sandboxId} (${ws.provider}, last active ${ageStr})`
|
|
1011
|
+
};
|
|
1012
|
+
});
|
|
1013
|
+
const selected = await p4.select({
|
|
1014
|
+
message: "Select sandbox to connect",
|
|
1015
|
+
options: choices
|
|
1016
|
+
});
|
|
1017
|
+
if (p4.isCancel(selected)) {
|
|
1018
|
+
p4.cancel("Cancelled");
|
|
1019
|
+
process.exit(0);
|
|
1020
|
+
}
|
|
1021
|
+
sandboxId = selected;
|
|
1022
|
+
}
|
|
1023
|
+
const spinner4 = p4.spinner();
|
|
1024
|
+
spinner4.start("Connecting...");
|
|
1025
|
+
const sandbox = await compute3.sandbox.getById(sandboxId);
|
|
1026
|
+
if (!sandbox) {
|
|
1027
|
+
spinner4.stop(pc4.red(`Sandbox not found: ${sandboxId}`));
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
spinner4.stop("");
|
|
1031
|
+
process.stdout.write("\x1B[2J\x1B[H\x1B[3J");
|
|
1032
|
+
await startPTY(sandbox);
|
|
1033
|
+
});
|
|
1034
|
+
var workspaceCmd = program.command("workspace").alias("ws").description("Manage workspaces (sandbox + git + worktree)");
|
|
1035
|
+
workspaceCmd.command("create").description("Create a new workspace").argument("[repo]", "Repository in owner/repo format").option("-b, --branch <branch>", "Branch name", "main").option("-p, --provider <provider>", "provider to use").option("--no-attach", "Create workspace but do not attach").action(async (repo, options) => {
|
|
1036
|
+
console.log();
|
|
1037
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1038
|
+
await ensureAuth();
|
|
1039
|
+
if (!repo) {
|
|
1040
|
+
p4.log.info("Usage: compute workspace create owner/repo --branch main");
|
|
1041
|
+
p4.outro(pc4.green("Done!"));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const provider = await resolveProvider(options.provider, { interactive: true });
|
|
1045
|
+
configureCompute(provider);
|
|
1046
|
+
const spinner4 = p4.spinner();
|
|
1047
|
+
spinner4.start("Creating workspace...");
|
|
1048
|
+
try {
|
|
1049
|
+
const workspace = await workspaceCreate({
|
|
1050
|
+
repo,
|
|
1051
|
+
branch: options.branch,
|
|
1052
|
+
provider
|
|
1053
|
+
});
|
|
1054
|
+
spinner4.stop(`Workspace ready: ${pc4.cyan(workspace.id)}`);
|
|
1055
|
+
if (options.attach !== false) {
|
|
1056
|
+
console.log();
|
|
1057
|
+
await workspaceAttach(workspace.id);
|
|
1058
|
+
} else {
|
|
1059
|
+
p4.outro(pc4.green("Workspace created!"));
|
|
1060
|
+
}
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
spinner4.stop(pc4.red("Failed to create workspace"));
|
|
1063
|
+
p4.log.error(error.message);
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
workspaceCmd.command("list").alias("ls").description("List all workspaces").action(async () => {
|
|
1068
|
+
console.log();
|
|
1069
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1070
|
+
await workspaceList();
|
|
1071
|
+
});
|
|
1072
|
+
workspaceCmd.command("destroy").alias("rm").description("Destroy a workspace").argument("[id]", "Workspace ID (repo:branch format, optional - will show picker if not provided)").action(async (id) => {
|
|
1073
|
+
console.log();
|
|
1074
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1075
|
+
if (!id) {
|
|
1076
|
+
const state = loadWorkspaceState();
|
|
1077
|
+
const workspaceIds = Object.keys(state.workspaces);
|
|
1078
|
+
if (workspaceIds.length === 0) {
|
|
1079
|
+
p4.log.info("No workspaces to destroy.");
|
|
1080
|
+
p4.outro(pc4.green("Done!"));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const choices = workspaceIds.map((key) => {
|
|
1084
|
+
const ws = state.workspaces[key];
|
|
1085
|
+
const age = Math.round(
|
|
1086
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
1087
|
+
);
|
|
1088
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
1089
|
+
return {
|
|
1090
|
+
value: key,
|
|
1091
|
+
label: `${pc4.cyan(key)} (${ws.provider}, last used ${ageStr})`
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
const selected = await p4.multiselect({
|
|
1095
|
+
message: "Select workspaces to destroy (space to select, enter to confirm)",
|
|
1096
|
+
options: choices
|
|
1097
|
+
});
|
|
1098
|
+
if (p4.isCancel(selected) || selected.length === 0) {
|
|
1099
|
+
p4.cancel("Cancelled");
|
|
1100
|
+
process.exit(0);
|
|
1101
|
+
}
|
|
1102
|
+
for (const workspaceId of selected) {
|
|
1103
|
+
await workspaceDestroy(workspaceId);
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
await workspaceDestroy(id);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
workspaceCmd.command("connect").alias("attach").description("Connect to an existing workspace (with git context)").argument("[id]", "Workspace ID (optional - will show picker if not provided)").action(async (id) => {
|
|
1110
|
+
console.log();
|
|
1111
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1112
|
+
if (!id) {
|
|
1113
|
+
const state = loadWorkspaceState();
|
|
1114
|
+
const workspaceIds = Object.keys(state.workspaces);
|
|
1115
|
+
if (workspaceIds.length === 0) {
|
|
1116
|
+
p4.log.info("No workspaces found.");
|
|
1117
|
+
p4.log.info("Create one with: compute create owner/repo");
|
|
1118
|
+
p4.outro(pc4.green("Done!"));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const choices = workspaceIds.map((key) => {
|
|
1122
|
+
const ws = state.workspaces[key];
|
|
1123
|
+
const age = Math.round(
|
|
1124
|
+
(Date.now() - new Date(ws.lastUsed).getTime()) / (1e3 * 60 * 60 * 24)
|
|
1125
|
+
);
|
|
1126
|
+
const ageStr = age === 0 ? "today" : age === 1 ? "1 day ago" : `${age} days ago`;
|
|
1127
|
+
return {
|
|
1128
|
+
value: key,
|
|
1129
|
+
label: `${pc4.cyan(key)} (${ws.provider}, last used ${ageStr})`
|
|
1130
|
+
};
|
|
1131
|
+
});
|
|
1132
|
+
const selected = await p4.select({
|
|
1133
|
+
message: "Select workspace to connect",
|
|
1134
|
+
options: choices
|
|
1135
|
+
});
|
|
1136
|
+
if (p4.isCancel(selected)) {
|
|
1137
|
+
p4.cancel("Cancelled");
|
|
1138
|
+
process.exit(0);
|
|
1139
|
+
}
|
|
1140
|
+
id = selected;
|
|
1141
|
+
}
|
|
1142
|
+
await workspaceAttach(id);
|
|
1143
|
+
});
|
|
1144
|
+
program.command("run").description("Create a sandbox, run a command, then optionally destroy it").argument("<command...>", "command to run").option("-p, --provider <provider>", "provider to use").option("-e, --env <key=value...>", "environment variables").option("-w, --workdir <directory>", "working directory").passThroughOptions().option("--rm", "destroy sandbox after command finishes").option("--timeout <duration>", "sandbox timeout (e.g. 5m, 1h)").option("--name <name>", "sandbox name").action(async (command, opts) => {
|
|
1145
|
+
await ensureAuth();
|
|
1146
|
+
const provider = await resolveProvider(opts.provider);
|
|
1147
|
+
configureCompute(provider);
|
|
1148
|
+
let sandbox;
|
|
1149
|
+
try {
|
|
1150
|
+
sandbox = await compute3.sandbox.create(
|
|
1151
|
+
buildCreateOptions(opts)
|
|
1152
|
+
);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
console.error(pc4.red(`Failed to create sandbox: ${error.message}`));
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const cmd = buildExecCommand(command, opts);
|
|
1159
|
+
const result = await sandbox.runCommand(cmd);
|
|
1160
|
+
if (result.stdout) {
|
|
1161
|
+
process.stdout.write(result.stdout);
|
|
1162
|
+
}
|
|
1163
|
+
if (result.stderr) {
|
|
1164
|
+
process.stderr.write(result.stderr);
|
|
1165
|
+
}
|
|
1166
|
+
if (opts.rm) {
|
|
1167
|
+
await sandbox.destroy();
|
|
1168
|
+
}
|
|
1169
|
+
process.exit(result.exitCode);
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
if (opts.rm) {
|
|
1172
|
+
try {
|
|
1173
|
+
await sandbox.destroy();
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
console.error(pc4.red(error.message));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
program.command("providers").description("List all providers and their configuration status").action(async () => {
|
|
1182
|
+
console.log();
|
|
1183
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1184
|
+
const statuses = getProviderStatus();
|
|
1185
|
+
console.log();
|
|
1186
|
+
for (const status of statuses) {
|
|
1187
|
+
const icon = status.ready ? pc4.green("\u25CF") : pc4.gray("\u25CB");
|
|
1188
|
+
const name = status.ready ? pc4.white(status.name) : pc4.gray(status.name);
|
|
1189
|
+
const detail = status.ready ? pc4.green("ready") : pc4.gray(`missing: ${status.missing.join(", ")}`);
|
|
1190
|
+
console.log(` ${icon} ${name} ${detail}`);
|
|
1191
|
+
}
|
|
1192
|
+
console.log();
|
|
1193
|
+
});
|
|
1194
|
+
program.command("login").description("Authenticate with ComputeSDK").action(async () => {
|
|
1195
|
+
console.log();
|
|
1196
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1197
|
+
await ensureAuth({ forceLogin: true });
|
|
1198
|
+
p4.outro(pc4.green("Authenticated!"));
|
|
1199
|
+
});
|
|
1200
|
+
program.command("logout").description("Clear stored credentials").action(() => {
|
|
1201
|
+
console.log();
|
|
1202
|
+
p4.intro(pc4.cyan(`@computesdk/cli v${VERSION}`));
|
|
1203
|
+
clearStoredCredentials();
|
|
1204
|
+
p4.log.success("Logged out. Stored credentials removed.");
|
|
1205
|
+
p4.outro(pc4.green("Done!"));
|
|
1206
|
+
});
|
|
1207
|
+
function buildCreateOptions(opts) {
|
|
1208
|
+
const options = {};
|
|
1209
|
+
if (opts.timeout) {
|
|
1210
|
+
options.timeout = parseDuration(opts.timeout);
|
|
1211
|
+
}
|
|
1212
|
+
if (opts.name) {
|
|
1213
|
+
options.name = opts.name;
|
|
1214
|
+
}
|
|
1215
|
+
return options;
|
|
1216
|
+
}
|
|
1217
|
+
function shellEscape2(arg) {
|
|
1218
|
+
if (/^[a-zA-Z0-9._\-/:=@]+$/.test(arg)) return arg;
|
|
1219
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
1220
|
+
}
|
|
1221
|
+
function buildExecCommand(command, opts) {
|
|
1222
|
+
let cmd = command.map(shellEscape2).join(" ");
|
|
1223
|
+
if (opts.workdir) {
|
|
1224
|
+
cmd = `cd ${shellEscape2(opts.workdir)} && ${cmd}`;
|
|
1225
|
+
}
|
|
1226
|
+
if (opts.env && opts.env.length > 0) {
|
|
1227
|
+
const envPrefix = opts.env.map((entry) => {
|
|
1228
|
+
const eqIndex = entry.indexOf("=");
|
|
1229
|
+
if (eqIndex === -1) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`Invalid environment entry "${entry}". Expected format KEY=VALUE.`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const key = entry.slice(0, eqIndex);
|
|
1235
|
+
const val = entry.slice(eqIndex + 1);
|
|
1236
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Invalid environment variable name "${key}" in entry "${entry}".`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
return `${key}=${shellEscape2(val)}`;
|
|
1242
|
+
}).join(" ");
|
|
1243
|
+
cmd = `${envPrefix} ${cmd}`;
|
|
1244
|
+
}
|
|
1245
|
+
return cmd;
|
|
1246
|
+
}
|
|
1247
|
+
function parseDuration(input) {
|
|
1248
|
+
let total = 0;
|
|
1249
|
+
const pattern = /(\d+)\s*(s|m|h)/gi;
|
|
1250
|
+
let match;
|
|
1251
|
+
while ((match = pattern.exec(input)) !== null) {
|
|
1252
|
+
const value = parseInt(match[1], 10);
|
|
1253
|
+
const unit = match[2].toLowerCase();
|
|
1254
|
+
switch (unit) {
|
|
1255
|
+
case "s":
|
|
1256
|
+
total += value * 1e3;
|
|
1257
|
+
break;
|
|
1258
|
+
case "m":
|
|
1259
|
+
total += value * 60 * 1e3;
|
|
1260
|
+
break;
|
|
1261
|
+
case "h":
|
|
1262
|
+
total += value * 60 * 60 * 1e3;
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (total === 0) {
|
|
1267
|
+
const num = parseInt(input, 10);
|
|
1268
|
+
if (!isNaN(num)) return num * 1e3;
|
|
1269
|
+
throw new Error(`Invalid duration: ${input}. Use formats like 30s, 5m, 1h`);
|
|
1270
|
+
}
|
|
1271
|
+
return total;
|
|
1272
|
+
}
|
|
1273
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1274
|
+
console.error(pc4.red(`
|
|
1275
|
+
Error: ${error.message}`));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
});
|
|
1278
|
+
export {
|
|
1279
|
+
shellEscape2 as shellEscape
|
|
1280
|
+
};
|
|
1281
|
+
//# sourceMappingURL=index.js.map
|