From 7e1a94c2c2a19c95d97d27f86ffed8097dcfc32e Mon Sep 17 00:00:00 2001 From: Tom Lienard Date: Tue, 24 Feb 2026 13:34:33 +0000 Subject: [PATCH 1/4] fix(cli): copy files to local when not present --- .changeset/every-trains-poke.md | 5 ++ packages/sandbox/src/commands/cp.ts | 85 ++++++++++++----------- packages/sandbox/test/commands/cp.test.ts | 23 ++++++ 3 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 .changeset/every-trains-poke.md create mode 100644 packages/sandbox/test/commands/cp.test.ts diff --git a/.changeset/every-trains-poke.md b/.changeset/every-trains-poke.md new file mode 100644 index 0000000..8688f53 --- /dev/null +++ b/.changeset/every-trains-poke.md @@ -0,0 +1,5 @@ +--- +"sandbox": patch +--- + +Fix copying files to local path when not already present diff --git a/packages/sandbox/src/commands/cp.ts b/packages/sandbox/src/commands/cp.ts index 472eda7..658455d 100644 --- a/packages/sandbox/src/commands/cp.ts +++ b/packages/sandbox/src/commands/cp.ts @@ -1,7 +1,6 @@ import { sandboxClient } from "../client"; import * as cmd from "cmd-ts"; import { sandboxId } from "../args/sandbox-id"; -import * as Fs from "cmd-ts/batteries/fs"; import fs from "node:fs/promises"; import path from "node:path"; import { scope } from "../args/scope"; @@ -11,26 +10,27 @@ import chalk from "chalk"; export const args = {} as const; -const localOrRemote = cmd.extendType(cmd.string, { - async from(input) { - const parts = input.split(":"); - if (parts.length === 2) { - const [id, path] = parts; - if (!id || !path) { - throw new Error( - [ - `Invalid copy path format: "${input}".`, - `${chalk.bold("hint:")} Expected format: SANDBOX_ID:PATH (e.g., sbx_abc123:/home/user/file.txt).`, - "╰▶ Local paths should not contain colons.", - ].join("\n"), - ); - } - return { type: "remote", id: await sandboxId.from(id), path } as const; +export const parseLocalOrRemotePath = async (input: string) => { + const parts = input.split(":"); + if (parts.length === 2) { + const [id, path] = parts; + if (!id || !path) { + throw new Error( + [ + `Invalid copy path format: "${input}".`, + `${chalk.bold("hint:")} Expected format: SANDBOX_ID:PATH (e.g., sbx_abc123:/home/user/file.txt).`, + "╰▶ Local paths should not contain colons.", + ].join("\n"), + ); } + return { type: "remote", id: await sandboxId.from(id), path } as const; + } - const file = await Fs.File.from(input); - return { type: "local", file } as const; - }, + return { type: "local", path: input } as const; +}; + +const localOrRemote = cmd.extendType(cmd.string, { + from: parseLocalOrRemotePath, }); export const cp = cmd.command({ @@ -51,23 +51,28 @@ export const cp = cmd.command({ scope, }, async handler({ scope, source, dest }) { - const spinner = ora({ text: "reading file..." }).start(); - const sourceFile = - source.type === "local" - ? await fs.readFile(source.file) - : await (async (src) => { - const sandbox = await sandboxClient.get({ - sandboxId: src.id, - teamId: scope.team, - token: scope.token, - projectId: scope.project, - }); - const file = await sandbox.readFile({ path: src.path }); - if (!file) { - return null; - } - return consume.buffer(file); - })(source); + const spinner = ora({ text: "Reading source file..." }).start(); + let sourceFile: Buffer | null = null; + + if (source.type === "local") { + sourceFile = await fs.readFile(source.path).catch((err) => { + if (err.code === "ENOENT") { + return null; + } + throw err; + }) + } else { + const sandbox = await sandboxClient.get({ + sandboxId: source.id, + teamId: scope.team, + token: scope.token, + projectId: scope.project, + }); + const file = await sandbox.readFile({ path: source.path }); + if (file) { + sourceFile = await consume.buffer(file); + } + } if (!sourceFile) { if (source.type === "remote") { @@ -79,15 +84,15 @@ export const cp = cmd.command({ ].join("\n"), ); } else { - spinner.fail("file not found"); + spinner.fail(`Source file (${source.path}) not found.`); } return; } - spinner.text = "writing file..."; + spinner.text = "Writing to destination file..."; if (dest.type === "local") { - await fs.writeFile(dest.file, sourceFile); + await fs.writeFile(dest.path, sourceFile); } else { const sandbox = await sandboxClient.get({ sandboxId: dest.id, @@ -98,6 +103,6 @@ export const cp = cmd.command({ await sandbox.writeFiles([{ path: dest.path, content: sourceFile }]); } - spinner.succeed("copied successfully!"); + spinner.succeed("Copied successfully!"); }, }); diff --git a/packages/sandbox/test/commands/cp.test.ts b/packages/sandbox/test/commands/cp.test.ts new file mode 100644 index 0000000..6634e0f --- /dev/null +++ b/packages/sandbox/test/commands/cp.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; +import { parseLocalOrRemotePath } from "../../src/commands/cp"; + +describe("copy path parsing", () => { + test("accepts non-existent local paths", async () => { + await expect(parseLocalOrRemotePath("./does-not-exist.txt")).resolves.toEqual( + { + type: "local", + path: "./does-not-exist.txt", + }, + ); + }); + + test("parses remote paths", async () => { + await expect( + parseLocalOrRemotePath("sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik:/etc/os-release"), + ).resolves.toEqual({ + type: "remote", + id: "sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik", + path: "/etc/os-release", + }); + }); +}); From 1a8c969f9b170186603b48b06eb0164839f172e8 Mon Sep 17 00:00:00 2001 From: Tom Lienard Date: Tue, 24 Feb 2026 13:39:27 +0000 Subject: [PATCH 2/4] Rename field for clarity --- packages/sandbox/src/commands/cp.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sandbox/src/commands/cp.ts b/packages/sandbox/src/commands/cp.ts index 658455d..4330c80 100644 --- a/packages/sandbox/src/commands/cp.ts +++ b/packages/sandbox/src/commands/cp.ts @@ -23,7 +23,7 @@ export const parseLocalOrRemotePath = async (input: string) => { ].join("\n"), ); } - return { type: "remote", id: await sandboxId.from(id), path } as const; + return { type: "remote", sandboxId: await sandboxId.from(id), path } as const; } return { type: "local", path: input } as const; @@ -63,7 +63,7 @@ export const cp = cmd.command({ }) } else { const sandbox = await sandboxClient.get({ - sandboxId: source.id, + sandboxId: source.sandboxId, teamId: scope.team, token: scope.token, projectId: scope.project, @@ -79,8 +79,8 @@ export const cp = cmd.command({ const dir = path.dirname(source.path); spinner.fail( [ - `File not found: ${source.path} in sandbox ${source.id}.`, - `${chalk.bold("hint:")} Verify the file path exists using \`sandbox exec ${source.id} ls ${dir}\`.`, + `File not found: ${source.path} in sandbox ${source.sandboxId}.`, + `${chalk.bold("hint:")} Verify the file path exists using \`sandbox exec ${source.sandboxId} ls ${dir}\`.`, ].join("\n"), ); } else { @@ -95,7 +95,7 @@ export const cp = cmd.command({ await fs.writeFile(dest.path, sourceFile); } else { const sandbox = await sandboxClient.get({ - sandboxId: dest.id, + sandboxId: dest.sandboxId, teamId: scope.team, projectId: scope.project, token: scope.token, From 1d5c2893e5dff9e2e21027bbb21abe801ca0da60 Mon Sep 17 00:00:00 2001 From: Tom Lienard Date: Tue, 24 Feb 2026 13:41:57 +0000 Subject: [PATCH 3/4] Add paths to log messages --- packages/sandbox/src/commands/cp.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sandbox/src/commands/cp.ts b/packages/sandbox/src/commands/cp.ts index 4330c80..4aea324 100644 --- a/packages/sandbox/src/commands/cp.ts +++ b/packages/sandbox/src/commands/cp.ts @@ -51,7 +51,7 @@ export const cp = cmd.command({ scope, }, async handler({ scope, source, dest }) { - const spinner = ora({ text: "Reading source file..." }).start(); + const spinner = ora({ text: `Reading source file (${source.path})...` }).start(); let sourceFile: Buffer | null = null; if (source.type === "local") { @@ -89,7 +89,7 @@ export const cp = cmd.command({ return; } - spinner.text = "Writing to destination file..."; + spinner.text = `Writing to destination file (${dest.path})...`; if (dest.type === "local") { await fs.writeFile(dest.path, sourceFile); @@ -103,6 +103,6 @@ export const cp = cmd.command({ await sandbox.writeFiles([{ path: dest.path, content: sourceFile }]); } - spinner.succeed("Copied successfully!"); + spinner.succeed(`Copied ${source.path} to ${dest.path} successfully!`); }, }); From 0ff312df7d71568f6cf7e2c86d4216e9189e72cc Mon Sep 17 00:00:00 2001 From: Tom Lienard Date: Tue, 24 Feb 2026 13:45:54 +0000 Subject: [PATCH 4/4] Fix test --- packages/sandbox/test/commands/cp.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sandbox/test/commands/cp.test.ts b/packages/sandbox/test/commands/cp.test.ts index 6634e0f..7daba80 100644 --- a/packages/sandbox/test/commands/cp.test.ts +++ b/packages/sandbox/test/commands/cp.test.ts @@ -16,7 +16,7 @@ describe("copy path parsing", () => { parseLocalOrRemotePath("sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik:/etc/os-release"), ).resolves.toEqual({ type: "remote", - id: "sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik", + sandboxId: "sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik", path: "/etc/os-release", }); });