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..4aea324 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", sandboxId: 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,46 +51,51 @@ 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 (${source.path})...` }).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.sandboxId, + 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") { 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 { - spinner.fail("file not found"); + spinner.fail(`Source file (${source.path}) not found.`); } return; } - spinner.text = "writing file..."; + spinner.text = `Writing to destination file (${dest.path})...`; 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, + sandboxId: dest.sandboxId, teamId: scope.team, projectId: scope.project, token: scope.token, @@ -98,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!`); }, }); diff --git a/packages/sandbox/test/commands/cp.test.ts b/packages/sandbox/test/commands/cp.test.ts new file mode 100644 index 0000000..7daba80 --- /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", + sandboxId: "sbx_Z1bhKlvVP1ecxCg2ewRUSU0hg1ik", + path: "/etc/os-release", + }); + }); +});