Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [2.4.0](https://github.com/codesandbox/codesandbox-sdk/compare/v2.3.0...v2.4.0) (2025-10-16)


### Features

* batch writes in template build ([#205](https://github.com/codesandbox/codesandbox-sdk/issues/205)) ([9b2f3f6](https://github.com/codesandbox/codesandbox-sdk/commit/9b2f3f66c4a0b3fa7c9b2f29c456875bea807ead))
* command error with exit code ([#203](https://github.com/codesandbox/codesandbox-sdk/issues/203)) ([652c2ef](https://github.com/codesandbox/codesandbox-sdk/commit/652c2efb900ce36081f0a1dca6919b288508116b))


### Bug Fixes

* batch session initialization commands ([#207](https://github.com/codesandbox/codesandbox-sdk/issues/207)) ([363faca](https://github.com/codesandbox/codesandbox-sdk/commit/363faca904324d3daecbde8990555dfa3a5eb577))
* handle spacing in env variables of commands ([#202](https://github.com/codesandbox/codesandbox-sdk/issues/202)) ([da5a772](https://github.com/codesandbox/codesandbox-sdk/commit/da5a7724d0d264c8088747c137a45f3cb6c534d6))

## [2.3.0](https://github.com/codesandbox/codesandbox-sdk/compare/v2.2.1...v2.3.0) (2025-09-29)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codesandbox/sdk",
"version": "2.3.0",
"version": "2.4.0",
"description": "The CodeSandbox SDK",
"author": "CodeSandbox",
"license": "MIT",
Expand Down
94 changes: 58 additions & 36 deletions pint-openapi-bundled.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

{
"openapi": "3.0.3",
"info": {
Expand Down Expand Up @@ -715,12 +716,6 @@
"schema": {
"$ref": "#/components/schemas/ExecListResponse"
}
},
"text/event-stream": {
"schema": {
"type": "string",
"description": "Server-Sent Events stream of exec updates"
}
}
}
},
Expand Down Expand Up @@ -1638,14 +1633,62 @@
}
}
},
"/api/v1/ports/stream": {
"/api/v1/stream/execs": {
"get": {
"summary": "List all execs",
"tags": [
"execs"
],
"description": "Returns a list of all active execs using SSE.",
"operationId": "StreamExecsList",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Execs retrieved successfully",
"content": {
"text/event-stream": {
"schema": {
"type": "string",
"description": "Server-Sent Events stream of exec updates"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"default": {
"description": "Unexpected Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/stream/ports": {
"get": {
"summary": "List open ports using Server-Sent Events (SSE)",
"tags": [
"ports"
],
"description": "Lists all open TCP ports on the system AND LISTEN TO THE CHANGES, excluding ignored ports configured in the server.",
"operationId": "ListPortsSSE",
"operationId": "StreamPortsList",
"security": [
{
"bearerAuth": []
Expand All @@ -1655,32 +1698,6 @@
"200": {
"description": "Open ports retrieved successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortsListResponse"
},
"examples": {
"default": {
"summary": "Example ports response",
"value": {
"ports": [
{
"port": 8080,
"address": "0.0.0.0"
},
{
"port": 3000,
"address": "127.0.0.1"
},
{
"port": 5432,
"address": "::1"
}
]
}
}
}
},
"text/event-stream": {
"schema": {
"type": "string",
Expand Down Expand Up @@ -1915,14 +1932,19 @@
"pid": {
"type": "integer",
"description": "Process ID of the exec"
},
"interactive": {
"type": "boolean",
"description": "Whether the exec is interactive"
}
},
"required": [
"id",
"command",
"args",
"status",
"pid"
"pid",
"interactive"
]
},
"ExecListResponse": {
Expand Down Expand Up @@ -2327,4 +2349,4 @@
}
}
}
}
}
42 changes: 26 additions & 16 deletions src/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export class Sandbox {
this.tracer
);

const commands: string[] = [];

// Ensure .private directory exists if env or git is configured
if (customSession.env || customSession.git) {
commands.push(`mkdir -p "$HOME/.private"`);
}

if (customSession.env) {
const envStrings = Object.entries(customSession.env)
.map(([key, value]) => {
Expand All @@ -144,31 +151,34 @@ export class Sandbox {
return `export ${key}='${safe}'`;
})
.join("\n");
const cmd = [
`cat << 'EOF' > "$HOME/.private/.env"`,
envStrings,
`EOF`,
].join("\n");
await client.commands.run(cmd);
commands.push(
[
`cat << 'EOF' > "$HOME/.private/.env"`,
envStrings,
`EOF`,
].join("\n")
);
}

if (customSession.git) {
await Promise.all([
client.commands.run(
`echo "https://${customSession.git.username || "x-access-token"}:${
customSession.git.accessToken
}@${customSession.git.provider}" > $HOME/.private/.gitcredentials`
),
client.commands.run(
`echo "[user]
commands.push(
`echo "https://${customSession.git.username || "x-access-token"}:${
customSession.git.accessToken
}@${customSession.git.provider}" > $HOME/.private/.gitcredentials`
);
commands.push(
`echo "[user]
name = ${customSession.git.name || customSession.id}
email = ${customSession.git.email}
[init]
defaultBranch = main
[credential]
helper = store --file ~/.private/.gitcredentials" > $HOME/.gitconfig`
),
]);
);
}

if (commands.length > 0) {
await client.commands.run(commands.join("\n"));
}

return client;
Expand Down
44 changes: 42 additions & 2 deletions src/SandboxClient/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ export type CommandStatus =
| "KILLED"
| "RESTARTING";

/**
* Error thrown when a command fails with a non-zero exit code.
*/
export class CommandError extends Error {
/**
* The exit code returned by the command.
*/
exitCode: number;

/**
* The output produced by the command.
*/
output: string;

constructor(message: string, exitCode: number, output: string) {
super(message);
this.name = "CommandError";
this.exitCode = exitCode;
this.output = output;

// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CommandError);
}
}
}

const DEFAULT_SHELL_SIZE = { cols: 128, rows: 24 };

// This can not be called Commands due to React Native
Expand Down Expand Up @@ -106,7 +133,10 @@ export class SandboxCommands {
? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries(
passedEnv
)
.map(([key, value]) => `${key}=${value}`)
.map(([key, value]) => {
const escapedValue = String(value).replace(/'/g, "'\\''");
return `${key}='${escapedValue}'`;
})
.join(" ")} bash -c '${escapedCommand}'`
: `source $HOME/.private/.env 2>/dev/null || true && bash -c '${escapedCommand}'`;

Expand Down Expand Up @@ -242,6 +272,11 @@ export class Command {
*/
#status: CommandStatus = "RUNNING";

/**
* The exit code of the command, available after it completes.
*/
private exitCode?: number;

get status(): CommandStatus {
return this.#status;
}
Expand Down Expand Up @@ -276,6 +311,7 @@ export class Command {
this.disposable.addDisposable(
agentClient.shells.onShellExited(({ shellId, exitCode }) => {
if (shellId === this.shell.shellId) {
this.exitCode = exitCode;
this.status = exitCode === 0 ? "FINISHED" : "ERROR";
this.barrier.open();
}
Expand Down Expand Up @@ -390,7 +426,11 @@ export class Command {
return cleaned;
}

throw new Error(`Command ERROR: ${cleaned}`);
throw new CommandError(
`Command failed with exit code ${this.exitCode ?? "unknown"}`,
this.exitCode ?? 1,
cleaned
);
}
);
}
Expand Down
30 changes: 15 additions & 15 deletions src/bin/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,23 +306,23 @@ export const buildCommand: yargs.CommandModule<
updateSpinnerMessage(index, "Writing files to sandbox...")
);

await Promise.all(
filePaths.map((filePath) =>
retryWithDelay(
async () => {
// Use batch write for more efficient file uploads
await retryWithDelay(
async () => {
const files = await Promise.all(
filePaths.map(async (filePath) => {
const fullPath = path.join(argv.directory, filePath);
const content = await fs.readFile(fullPath);
const dirname = path.dirname(filePath);
await session.fs.mkdir(dirname, true);
await session.fs.writeFile(filePath, content, {
create: true,
overwrite: true,
});
},
3,
200
)
)
return {
path: filePath,
content,
};
})
);
await session.fs.batchWrite(files);
},
3,
200
).catch((error) => {
throw new Error(`Failed to write files to sandbox: ${error}`);
});
Expand Down