Skip to content

Commit 0715796

Browse files
authored
fix(sandbox): improve cp command & implement extend command (#35)
1 parent 0584cfd commit 0715796

3 files changed

Lines changed: 91 additions & 48 deletions

File tree

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"imports": {
1818
"@cfa/gitignore-parser": "jsr:@cfa/gitignore-parser@^0.1.4",
1919
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8",
20-
"@deno/sandbox": "jsr:@deno/sandbox@^0.4.3",
20+
"@deno/sandbox": "jsr:@deno/sandbox@^0.4.4",
21+
"@std/async": "jsr:@std/async@^1.0.15",
2122
"@std/cli": "jsr:@std/cli@1.0.22",
2223
"@std/dotenv": "jsr:@std/dotenv@^0.225.5",
2324
"@std/encoding": "jsr:@std/encoding@^1.0.10",

deno.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sandbox.ts

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Command } from "@cliffy/command";
22
import { Sandbox } from "@deno/sandbox";
33
import { green, magenta, red } from "@std/fmt/colors";
4+
import { pooledMap } from "@std/async";
5+
import { expandGlob } from "@std/fs";
6+
import { join } from "@std/path";
47

58
import { getAppFromConfig, readConfig, writeConfig } from "./config.ts";
69
import { error, renderTemporalTimestamp, withApp } from "./util.ts";
@@ -36,8 +39,7 @@ export const sandboxCreateCommand = new Command<SandboxContext>()
3639
debug: options.debug,
3740
token,
3841
org,
39-
// deno-lint-ignore no-explicit-any
40-
lifetime: options.lifetime as any,
42+
lifetime: options.lifetime as `${number}s` | `${number}m` | "session",
4143
});
4244

4345
if (options.copy) {
@@ -47,6 +49,8 @@ export const sandboxCreateCommand = new Command<SandboxContext>()
4749
}
4850

4951
if (options.lifetime === "session") {
52+
console.log(`Created sandbox with id '${sandbox.id}'`);
53+
5054
const success = await sshIntoSandbox(sandbox);
5155
const stopMessage = "Stopping the sandbox...";
5256
if (success) {
@@ -174,18 +178,18 @@ export const sandboxSshCommand = new Command<SandboxContext>()
174178

175179
export const sandboxCopyCommand = new Command<SandboxContext>()
176180
.description("Copy files from or to a running sandbox")
177-
/*.example(
181+
.example(
178182
"Copy a file from a sandbox to the local machine",
179183
"copy someSandboxId:/app/remote-file.txt ./local-file.txt",
180-
)*/
184+
)
181185
.example(
182186
"Copy a file from the local machine to a sandbox",
183187
"copy ./local-file.txt someSandboxId:/app/remote-file.txt",
184188
)
185-
/*.example(
189+
.example(
186190
"Copy multiple files from a sandbox to the local machine",
187191
"copy someSandboxId:/app/remote-file.txt someSandboxId:/app/another-remote-file.txt ./",
188-
)*/
192+
)
189193
.example(
190194
"Copy multiple files from the local machine to a sandbox",
191195
"copy ./local-file.txt ./another-local-file.txt someSandboxId:/app/",
@@ -194,10 +198,14 @@ export const sandboxCopyCommand = new Command<SandboxContext>()
194198
"Copy a directory from the local machine to a sandbox",
195199
"copy ./ ./another-local-file.txt someSandboxId:/app/",
196200
)
197-
/*.example(
201+
.example(
198202
"Copy files from a sandbox to another sandbox",
199203
"copy someSandboxId:/app/remote-file.txt anotherSandboxId:/app/remote-file.txt",
200-
)*/
204+
)
205+
.example(
206+
"Copy all files from a directory in a sandbox to the local machine",
207+
"copy someSandboxId:/app/* ./",
208+
)
201209
.arguments("<paths...:string>")
202210
.action(async (options, ...paths) => {
203211
if (paths.length < 2) {
@@ -207,56 +215,75 @@ export const sandboxCopyCommand = new Command<SandboxContext>()
207215
const target = paths.pop()!;
208216

209217
if (target.includes(":")) {
210-
const [sandboxId, sandboxPath] = target.split(":");
211-
await using sandbox = await connectToSandbox(options, sandboxId);
218+
const separatorIndex = target.indexOf(":");
219+
const sandboxId = target.slice(0, separatorIndex);
220+
const targetSandboxPath = target.slice(separatorIndex + 1);
221+
222+
await using targetSandbox = await connectToSandbox(options, sandboxId);
212223

213-
//const sourceSandboxPaths = [];
224+
const sourceSandboxPaths = [];
214225
const localPaths = [];
215226

216227
for (const path of paths) {
217228
if (path.includes(":")) {
218-
error(
219-
options.debug,
220-
"Copying between sandboxes is currently not supported",
221-
);
222-
223-
//sourceSandboxPaths.push(path);
229+
sourceSandboxPaths.push(path);
224230
} else {
225231
localPaths.push(path);
226232
}
227233
}
228234

229-
/*const sourceSandboxGroups = groupPathsBySandbox(sourceSandboxPaths);
235+
const sourceSandboxGroups = groupPathsBySandbox(sourceSandboxPaths);
230236
const sourceSandboxes: Record<string, Sandbox> = {};
231237

232238
for (const sandboxId of Object.keys(sourceSandboxGroups)) {
233239
sourceSandboxes[sandboxId] = await connectToSandbox(options, sandboxId);
234-
}*/
240+
}
235241

236242
await Promise.all([
237243
...localPaths.map((path) => {
238-
return sandbox.upload(path, sandboxPath);
244+
return targetSandbox.upload(path, targetSandboxPath);
239245
}),
240-
/*...Object.entries(sourceSandboxGroups).map(
246+
...Object.entries(sourceSandboxGroups).map(
241247
async ([sandboxId, sourceSandboxPaths]) => {
242248
const sourceSandbox = sourceSandboxes[sandboxId];
243249

244250
await Promise.all(
245251
sourceSandboxPaths.map(async (sourceSandboxPath) => {
246252
const tempDir = await Deno.makeTempDir();
247-
await sourceSandbox.download(sourceSandboxPath, tempDir);
248-
await sandbox.upload(tempDir, target);
253+
254+
await Array.fromAsync(pooledMap(
255+
Infinity,
256+
sourceSandbox.expandGlob(sourceSandboxPath),
257+
async (sandboxEntry) => {
258+
const tempPath = join(tempDir, sandboxEntry.path);
259+
await Deno.mkdir(tempPath, { recursive: true });
260+
await sourceSandbox.download(sandboxEntry.path, tempPath);
261+
262+
await Array.fromAsync(pooledMap(
263+
Infinity,
264+
expandGlob(`${tempPath}/*`),
265+
(localEntry) =>
266+
targetSandbox.upload(
267+
localEntry.path,
268+
join(
269+
targetSandboxPath,
270+
localEntry.isDirectory
271+
? "./"
272+
: `./${localEntry.name}`,
273+
),
274+
),
275+
));
276+
},
277+
));
249278
}),
250279
);
251280

252-
await sandbox.close();
281+
await sourceSandbox.close();
253282
},
254-
),*/
283+
),
255284
]);
256285
} else {
257-
error(options.debug, "Copying from sandboxes is currently not supported");
258-
259-
/*for (const path of paths) {
286+
for (const path of paths) {
260287
if (!path.includes(":")) {
261288
error(
262289
options.debug,
@@ -276,13 +303,17 @@ export const sandboxCopyCommand = new Command<SandboxContext>()
276303
Object.entries(groups).map(async ([sandboxId, sandboxPaths]) => {
277304
const sandbox = sandboxes[sandboxId];
278305

279-
await Promise.all(sandboxPaths.map((sandboxPath) => {
280-
return sandbox.download(sandboxPath, target);
306+
await Promise.all(sandboxPaths.map(async (sandboxPath) => {
307+
await Array.fromAsync(pooledMap(
308+
Infinity,
309+
sandbox.expandGlob(sandboxPath),
310+
(entry) => sandbox.download(entry.path, target),
311+
));
281312
}));
282313

283314
await sandbox.close();
284315
}),
285-
);*/
316+
);
286317
}
287318
});
288319

@@ -305,11 +336,14 @@ export const sandboxExecCommand = new Command<SandboxContext>()
305336
const child = await sandbox.spawn("bash", {
306337
cwd: options.cwd,
307338
args: ["-c", command.join(" ")],
339+
stdin: "piped",
308340
stdout: options.quiet ? "null" : "inherit",
309341
stderr: options.quiet ? "null" : "inherit",
310342
});
311343

312-
const status = await child.status;
344+
const write = Deno.stdin.readable.pipeTo(child.stdin!);
345+
346+
const [status] = await Promise.all([child.status, write]);
313347
Deno.exit(status.code);
314348
});
315349

@@ -341,8 +375,7 @@ export const sandboxRunCommand = new Command<SandboxContext>()
341375
debug: options.debug,
342376
token,
343377
org,
344-
// deno-lint-ignore no-explicit-any
345-
lifetime: options.lifetime as any,
378+
lifetime: options.lifetime as `${number}s` | `${number}m` | "session",
346379
});
347380

348381
console.log(`Created sandbox with id '${sandbox.id}'`);
@@ -371,39 +404,45 @@ export const sandboxRunCommand = new Command<SandboxContext>()
371404
const child = await sandbox.spawn("bash", {
372405
cwd: options.cwd,
373406
args: ["-c", command.join(" ")],
407+
stdin: "piped",
374408
stdout: options.quiet ? "null" : "inherit",
375409
stderr: options.quiet ? "null" : "inherit",
376410
});
377411

378-
const status = await child.status;
412+
const write = Deno.stdin.readable.pipeTo(child.stdin!);
413+
414+
const [status] = await Promise.all([child.status, write]);
379415
Deno.exit(status.code);
380416
});
381417

382-
/*
383418
export const sandboxExtendCommand = new Command<SandboxContext>()
384419
.description("Extend the lifetime of a running sandbox")
385420
.arguments("<sandbox-id:string> <lifetime:string>")
386421
.action(async (options, sandboxId, lifetime) => {
387422
await using sandbox = await connectToSandbox(options, sandboxId);
388423

389-
console.log(await sandbox.extendLifetime(lifetime));
424+
console.log(
425+
await sandbox.extendLifetime(lifetime as `${number}s` | `${number}m`),
426+
);
390427
});
391-
*/
392428

393-
/*
394429
function groupPathsBySandbox(paths: string[]): Record<string, string[]> {
395-
const groups = {};
430+
const groups: Record<string, string[]> = {};
396431

397432
for (const path of paths) {
398-
const [sandboxId, sandboxPath] = path.split(":");
433+
const separatorIndex = path.indexOf(":");
434+
const sandboxId = path.slice(0, separatorIndex);
435+
const sandboxPath = path.slice(separatorIndex + 1);
436+
399437
if (!groups[sandboxId]) {
400438
groups[sandboxId] = [];
401439
}
440+
402441
groups[sandboxId].push(sandboxPath);
403442
}
404443

405444
return groups;
406-
}*/
445+
}
407446

408447
async function ensureOrg(options: SandboxContext, quiet: boolean = true) {
409448
const config = await readConfig(Deno.cwd(), options.config);
@@ -440,6 +479,7 @@ async function connectToSandbox(
440479

441480
return await Sandbox.connect({
442481
id: sandboxId,
482+
apiEndpoint: options.endpoint,
443483
region: cluster.region,
444484
debug: options.debug,
445485
token,
@@ -597,5 +637,5 @@ export const sandboxCommand = new Command<GlobalOptions>()
597637
.alias("cp")
598638
.command("exec", sandboxExecCommand)
599639
.command("run", sandboxRunCommand)
600-
//.command("extend", sandboxExtendCommand)
640+
.command("extend", sandboxExtendCommand)
601641
.command("ssh", sandboxSshCommand);

0 commit comments

Comments
 (0)