mirror of
https://github.com/textcortex/claude-code-sandbox.git
synced 2025-08-04 10:59:28 +00:00
Checkpoint
This commit is contained in:
parent
9909168f72
commit
8db92c8b01
7 changed files with 166 additions and 361 deletions
84
src/cli.ts
84
src/cli.ts
|
@ -47,15 +47,17 @@ program
|
|||
.description("Run Claude Code in isolated Docker containers")
|
||||
.version("0.1.0");
|
||||
|
||||
// Default command (start with web UI)
|
||||
// Default command (always web UI)
|
||||
program
|
||||
.action(async () => {
|
||||
.option("--shell <shell>", "Start with 'claude' or 'bash' shell", /^(claude|bash)$/i)
|
||||
.action(async (options) => {
|
||||
console.log(chalk.blue("🚀 Starting Claude Sandbox..."));
|
||||
|
||||
const config = await loadConfig("./claude-sandbox.config.json");
|
||||
config.webUI = true; // Always use web UI by default
|
||||
config.detached = true; // Web UI requires detached mode
|
||||
config.includeUntracked = false; // Don't include untracked files by default
|
||||
config.includeUntracked = false;
|
||||
if (options.shell) {
|
||||
config.defaultShell = options.shell.toLowerCase();
|
||||
}
|
||||
|
||||
const sandbox = new ClaudeSandbox(config);
|
||||
await sandbox.run();
|
||||
|
@ -67,22 +69,23 @@ program
|
|||
.description("Start a new Claude Sandbox container")
|
||||
.option("-c, --config <path>", "Configuration file", "./claude-sandbox.config.json")
|
||||
.option("-n, --name <name>", "Container name prefix")
|
||||
.option("--no-web", "Disable web UI (use terminal attach)")
|
||||
.option("--no-push", "Disable automatic branch pushing")
|
||||
.option("--no-pr", "Disable automatic PR creation")
|
||||
.option("--include-untracked", "Include untracked files when copying to container")
|
||||
.option("-b, --branch <branch>", "Switch to specific branch on container start (creates if doesn't exist)")
|
||||
.option("--shell <shell>", "Start with 'claude' or 'bash' shell", /^(claude|bash)$/i)
|
||||
.action(async (options) => {
|
||||
console.log(chalk.blue("🚀 Starting new Claude Sandbox container..."));
|
||||
|
||||
const config = await loadConfig(options.config);
|
||||
config.webUI = options.web !== false;
|
||||
config.detached = config.webUI; // Web UI requires detached
|
||||
config.containerPrefix = options.name || config.containerPrefix;
|
||||
config.autoPush = options.push !== false;
|
||||
config.autoCreatePR = options.pr !== false;
|
||||
config.includeUntracked = options.includeUntracked || false;
|
||||
config.targetBranch = options.branch;
|
||||
if (options.shell) {
|
||||
config.defaultShell = options.shell.toLowerCase();
|
||||
}
|
||||
|
||||
const sandbox = new ClaudeSandbox(config);
|
||||
await sandbox.run();
|
||||
|
@ -92,8 +95,7 @@ program
|
|||
program
|
||||
.command("attach [container-id]")
|
||||
.description("Attach to an existing Claude Sandbox container")
|
||||
.option("--no-web", "Use terminal attach instead of web UI")
|
||||
.action(async (containerId, options) => {
|
||||
.action(async (containerId) => {
|
||||
const spinner = ora("Looking for containers...").start();
|
||||
|
||||
try {
|
||||
|
@ -111,56 +113,20 @@ program
|
|||
}
|
||||
}
|
||||
|
||||
spinner.text = "Attaching to container...";
|
||||
spinner.text = "Launching web UI...";
|
||||
|
||||
if (options.web !== false) {
|
||||
// Launch web UI for existing container
|
||||
const webServer = new WebUIServer(docker);
|
||||
const url = await webServer.start();
|
||||
const fullUrl = `${url}?container=${targetContainerId}`;
|
||||
|
||||
spinner.succeed(chalk.green(`Web UI available at: ${fullUrl}`));
|
||||
await webServer.openInBrowser(fullUrl);
|
||||
|
||||
console.log(chalk.yellow("Keep this terminal open to maintain the session"));
|
||||
|
||||
// Keep process running
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
// Direct terminal attach
|
||||
spinner.stop();
|
||||
console.log(chalk.blue(`Attaching to container ${targetContainerId.substring(0, 12)}...`));
|
||||
|
||||
const container = docker.getContainer(targetContainerId);
|
||||
const stream = await container.attach({
|
||||
stream: true,
|
||||
stdin: true,
|
||||
stdout: true,
|
||||
stderr: true
|
||||
});
|
||||
|
||||
// Set up TTY
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
process.stdin.resume();
|
||||
|
||||
// Connect streams
|
||||
stream.pipe(process.stdout);
|
||||
process.stdin.pipe(stream);
|
||||
|
||||
// Handle cleanup
|
||||
const cleanup = () => {
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
process.stdin.pause();
|
||||
stream.end();
|
||||
};
|
||||
|
||||
process.on('SIGINT', cleanup);
|
||||
stream.on('end', cleanup);
|
||||
}
|
||||
// Always launch web UI
|
||||
const webServer = new WebUIServer(docker);
|
||||
const url = await webServer.start();
|
||||
const fullUrl = `${url}?container=${targetContainerId}`;
|
||||
|
||||
spinner.succeed(chalk.green(`Web UI available at: ${fullUrl}`));
|
||||
await webServer.openInBrowser(fullUrl);
|
||||
|
||||
console.log(chalk.yellow("Keep this terminal open to maintain the session"));
|
||||
|
||||
// Keep process running
|
||||
await new Promise(() => {});
|
||||
} catch (error: any) {
|
||||
spinner.fail(chalk.red(`Failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
|
|
|
@ -5,10 +5,10 @@ import { SandboxConfig } from "./types";
|
|||
|
||||
const DEFAULT_CONFIG: SandboxConfig = {
|
||||
dockerImage: "claude-code-sandbox:latest",
|
||||
detached: false,
|
||||
autoPush: true,
|
||||
autoCreatePR: true,
|
||||
autoStartClaude: true,
|
||||
defaultShell: "claude", // Default to Claude mode for backward compatibility
|
||||
claudeConfigPath: path.join(os.homedir(), ".claude.json"),
|
||||
setupCommands: [], // Example: ["npm install", "pip install -r requirements.txt"]
|
||||
allowedTools: ["*"], // All tools allowed in sandbox
|
||||
|
|
318
src/container.ts
318
src/container.ts
|
@ -49,6 +49,9 @@ export class ContainerManager {
|
|||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log(chalk.green("✓ Container ready"));
|
||||
|
||||
// Set up git branch and startup script
|
||||
await this.setupGitAndStartupScript(container, containerConfig.branchName);
|
||||
|
||||
// Run setup commands
|
||||
await this.runSetupCommands(container);
|
||||
|
||||
|
@ -487,231 +490,6 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\
|
|||
return volumes;
|
||||
}
|
||||
|
||||
async attach(containerId: string, branchName?: string): Promise<void> {
|
||||
const container = this.containers.get(containerId);
|
||||
if (!container) {
|
||||
throw new Error("Container not found");
|
||||
}
|
||||
|
||||
console.log(chalk.blue("• Connecting to container..."));
|
||||
|
||||
// Use provided branch name, config target branch, or generate one
|
||||
const targetBranch =
|
||||
branchName ||
|
||||
this.config.targetBranch ||
|
||||
`claude/${
|
||||
new Date().toISOString().replace(/[:.]/g, "-").split("T")[0]
|
||||
}-${Date.now()}`;
|
||||
|
||||
// First, set up the git branch and create startup script
|
||||
try {
|
||||
console.log(chalk.blue("• Setting up git branch and startup script..."));
|
||||
|
||||
// Create different startup scripts based on autoStartClaude setting
|
||||
const startupScript = this.config.autoStartClaude
|
||||
? `#!/bin/bash
|
||||
echo "🚀 Starting Claude Code automatically..."
|
||||
echo "Press Ctrl+C to interrupt and access shell"
|
||||
echo ""
|
||||
claude --dangerously-skip-permissions
|
||||
echo ""
|
||||
echo "Claude exited. You now have access to the shell."
|
||||
echo "Type \"claude --dangerously-skip-permissions\" to restart Claude"
|
||||
echo "Type \"exit\" to end the session"
|
||||
exec /bin/bash`
|
||||
: `#!/bin/bash
|
||||
echo "Welcome to Claude Code Sandbox!"
|
||||
echo "Type \"claude --dangerously-skip-permissions\" to start Claude Code"
|
||||
echo "Type \"exit\" to end the session"
|
||||
exec /bin/bash`;
|
||||
|
||||
const setupExec = await container.exec({
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
`
|
||||
cd /workspace &&
|
||||
sudo chown -R claude:claude /workspace &&
|
||||
git config --global --add safe.directory /workspace &&
|
||||
# Clean up macOS resource fork files in git pack directory
|
||||
find .git/objects/pack -name "._pack-*.idx" -type f -delete 2>/dev/null || true &&
|
||||
# Configure git to use GitHub token if available
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
git config --global url."https://\${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global url."https://\${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"
|
||||
echo "✓ Configured git to use GitHub token"
|
||||
fi &&
|
||||
# Try to checkout existing branch first, then create new if it doesn't exist
|
||||
if git show-ref --verify --quiet refs/heads/"${targetBranch}"; then
|
||||
git checkout "${targetBranch}" &&
|
||||
echo "✓ Switched to existing branch: ${targetBranch}"
|
||||
else
|
||||
git checkout -b "${targetBranch}" &&
|
||||
echo "✓ Created new branch: ${targetBranch}"
|
||||
fi &&
|
||||
echo '${startupScript}' > /home/claude/start-session.sh &&
|
||||
chmod +x /home/claude/start-session.sh &&
|
||||
echo "✓ Startup script created"
|
||||
`,
|
||||
],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
const setupStream = await setupExec.start({});
|
||||
|
||||
// Wait for setup to complete
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let output = "";
|
||||
setupStream.on("data", (chunk) => {
|
||||
output += chunk.toString();
|
||||
process.stdout.write(chunk);
|
||||
});
|
||||
setupStream.on("end", () => {
|
||||
if (
|
||||
output.includes("✓ Created branch") &&
|
||||
output.includes("✓ Startup script created")
|
||||
) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Setup failed"));
|
||||
}
|
||||
});
|
||||
setupStream.on("error", reject);
|
||||
});
|
||||
|
||||
console.log(chalk.green("✓ Container setup completed"));
|
||||
|
||||
// Execute setup commands
|
||||
await this.runSetupCommands(container);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("✗ Setup failed:"), error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Now create an interactive session that runs our startup script
|
||||
console.log(chalk.blue("• Starting interactive session..."));
|
||||
if (this.config.autoStartClaude) {
|
||||
console.log(chalk.yellow("• Claude Code will start automatically"));
|
||||
console.log(
|
||||
chalk.yellow("• Press Ctrl+C to interrupt Claude and access the shell"),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'• Type "claude --dangerously-skip-permissions" to start Claude Code',
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
chalk.yellow('• Press Ctrl+D or type "exit" to end the session'),
|
||||
);
|
||||
|
||||
const exec = await container.exec({
|
||||
Cmd: ["/home/claude/start-session.sh"],
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
WorkingDir: "/workspace",
|
||||
});
|
||||
|
||||
// Start the exec with hijack mode for proper TTY
|
||||
const stream = await exec.start({
|
||||
hijack: true,
|
||||
stdin: true,
|
||||
});
|
||||
|
||||
// Set up TTY properly
|
||||
const originalRawMode = process.stdin.isRaw;
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
process.stdin.resume();
|
||||
|
||||
// Resize handler
|
||||
const resize = async () => {
|
||||
try {
|
||||
await exec.resize({
|
||||
w: process.stdout.columns || 80,
|
||||
h: process.stdout.rows || 24,
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore resize errors
|
||||
}
|
||||
};
|
||||
|
||||
// Initial resize
|
||||
await resize();
|
||||
process.stdout.on("resize", resize);
|
||||
|
||||
// Connect streams bidirectionally
|
||||
stream.pipe(process.stdout);
|
||||
process.stdin.pipe(stream);
|
||||
|
||||
// Set up proper cleanup
|
||||
const cleanup = () => {
|
||||
console.log(chalk.yellow("\nCleaning up session..."));
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(originalRawMode);
|
||||
}
|
||||
process.stdin.pause();
|
||||
process.stdout.removeListener("resize", resize);
|
||||
if (stream && typeof stream.end === "function") {
|
||||
stream.end();
|
||||
}
|
||||
};
|
||||
|
||||
// Return a promise that resolves when the session ends
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let sessionEnded = false;
|
||||
|
||||
const handleEnd = () => {
|
||||
if (!sessionEnded) {
|
||||
sessionEnded = true;
|
||||
console.log(chalk.yellow("\nContainer session ended"));
|
||||
cleanup();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err: Error) => {
|
||||
if (!sessionEnded) {
|
||||
sessionEnded = true;
|
||||
console.error(chalk.red("Stream error:"), err);
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
stream.on("end", handleEnd);
|
||||
stream.on("close", handleEnd);
|
||||
stream.on("error", handleError);
|
||||
|
||||
// Also monitor the exec process
|
||||
const checkExec = async () => {
|
||||
try {
|
||||
const info = await exec.inspect();
|
||||
if (info.ExitCode !== null && !sessionEnded) {
|
||||
handleEnd();
|
||||
}
|
||||
} catch (e) {
|
||||
// Exec might be gone
|
||||
if (!sessionEnded) {
|
||||
handleEnd();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check exec status periodically
|
||||
const statusInterval = setInterval(checkExec, 1000);
|
||||
|
||||
// Clean up interval when session ends
|
||||
stream.on("end", () => clearInterval(statusInterval));
|
||||
stream.on("close", () => clearInterval(statusInterval));
|
||||
});
|
||||
}
|
||||
|
||||
private async _copyWorkingDirectory(
|
||||
container: Docker.Container,
|
||||
workDir: string,
|
||||
|
@ -1087,6 +865,96 @@ exec /bin/bash`;
|
|||
}
|
||||
}
|
||||
|
||||
private async setupGitAndStartupScript(container: any, branchName: string): Promise<void> {
|
||||
console.log(chalk.blue("• Setting up git branch and startup script..."));
|
||||
|
||||
// Determine what to show in the web UI
|
||||
const defaultShell = this.config.defaultShell || "claude";
|
||||
|
||||
// Startup script that keeps session alive
|
||||
const startupScript = defaultShell === "claude"
|
||||
? `#!/bin/bash
|
||||
echo "🚀 Starting Claude Code..."
|
||||
echo "Press Ctrl+C to drop to bash shell"
|
||||
echo ""
|
||||
|
||||
# Run Claude but don't replace the shell process
|
||||
claude --dangerously-skip-permissions
|
||||
|
||||
# After Claude exits, drop to bash
|
||||
echo ""
|
||||
echo "Claude exited. You're now in bash shell."
|
||||
echo "Type 'claude --dangerously-skip-permissions' to restart Claude"
|
||||
echo "Type 'exit' to end the session"
|
||||
echo ""
|
||||
exec /bin/bash`
|
||||
: `#!/bin/bash
|
||||
echo "Welcome to Claude Code Sandbox!"
|
||||
echo "Type 'claude --dangerously-skip-permissions' to start Claude Code"
|
||||
echo "Type 'exit' to end the session"
|
||||
echo ""
|
||||
exec /bin/bash`;
|
||||
|
||||
const setupExec = await container.exec({
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
`
|
||||
cd /workspace &&
|
||||
sudo chown -R claude:claude /workspace &&
|
||||
git config --global --add safe.directory /workspace &&
|
||||
# Clean up macOS resource fork files in git pack directory
|
||||
find .git/objects/pack -name "._pack-*.idx" -type f -delete 2>/dev/null || true &&
|
||||
# Configure git to use GitHub token if available
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
git config --global url."https://\${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global url."https://\${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"
|
||||
echo "✓ Configured git to use GitHub token"
|
||||
fi &&
|
||||
# Try to checkout existing branch first, then create new if it doesn't exist
|
||||
if git show-ref --verify --quiet refs/heads/"${branchName}"; then
|
||||
git checkout "${branchName}" &&
|
||||
echo "✓ Switched to existing branch: ${branchName}"
|
||||
else
|
||||
git checkout -b "${branchName}" &&
|
||||
echo "✓ Created new branch: ${branchName}"
|
||||
fi &&
|
||||
cat > /home/claude/start-session.sh << 'EOF'
|
||||
${startupScript}
|
||||
EOF
|
||||
chmod +x /home/claude/start-session.sh &&
|
||||
echo "✓ Startup script created"
|
||||
`,
|
||||
],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
const setupStream = await setupExec.start({});
|
||||
|
||||
// Wait for setup to complete
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let output = "";
|
||||
setupStream.on("data", (chunk: any) => {
|
||||
output += chunk.toString();
|
||||
process.stdout.write(chunk);
|
||||
});
|
||||
setupStream.on("end", () => {
|
||||
if (
|
||||
(output.includes("✓ Created new branch") || output.includes("✓ Switched to existing branch")) &&
|
||||
output.includes("✓ Startup script created")
|
||||
) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Setup failed"));
|
||||
}
|
||||
});
|
||||
setupStream.on("error", reject);
|
||||
});
|
||||
|
||||
console.log(chalk.green("✓ Git and startup script setup completed"));
|
||||
}
|
||||
|
||||
private async runSetupCommands(container: any): Promise<void> {
|
||||
// Execute custom setup commands if provided
|
||||
if (this.config.setupCommands && this.config.setupCommands.length > 0) {
|
||||
|
|
|
@ -476,11 +476,36 @@ export class ShadowRepository {
|
|||
|
||||
async cleanup(): Promise<void> {
|
||||
if (await fs.pathExists(this.shadowPath)) {
|
||||
await fs.remove(this.shadowPath);
|
||||
console.log(chalk.gray('🧹 Shadow repository cleaned up'));
|
||||
try {
|
||||
// Try to force remove with rm -rf first
|
||||
await execAsync(`rm -rf "${this.shadowPath}"`);
|
||||
console.log(chalk.gray('🧹 Shadow repository cleaned up'));
|
||||
} catch (error) {
|
||||
// Fallback to fs.remove with retry logic
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
await fs.remove(this.shadowPath);
|
||||
console.log(chalk.gray('🧹 Shadow repository cleaned up'));
|
||||
break;
|
||||
} catch (err) {
|
||||
retries--;
|
||||
if (retries === 0) {
|
||||
console.error(chalk.yellow('⚠ Failed to cleanup shadow repository:'), err);
|
||||
} else {
|
||||
// Wait a bit before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (await fs.pathExists(this.rsyncExcludeFile)) {
|
||||
await fs.remove(this.rsyncExcludeFile);
|
||||
try {
|
||||
await fs.remove(this.rsyncExcludeFile);
|
||||
} catch (error) {
|
||||
// Ignore exclude file cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
87
src/index.ts
87
src/index.ts
|
@ -71,76 +71,23 @@ export class ClaudeSandbox {
|
|||
await this.gitMonitor.start(branchName);
|
||||
console.log(chalk.blue("✓ Git monitoring started"));
|
||||
|
||||
// Launch web UI if requested
|
||||
if (this.config.webUI) {
|
||||
this.webServer = new WebUIServer(this.docker);
|
||||
|
||||
// Pass repo info to web server
|
||||
this.webServer.setRepoInfo(process.cwd(), branchName);
|
||||
|
||||
const webUrl = await this.webServer.start();
|
||||
|
||||
// Open browser to the web UI with container ID
|
||||
const fullUrl = `${webUrl}?container=${containerId}`;
|
||||
await this.webServer.openInBrowser(fullUrl);
|
||||
|
||||
console.log(chalk.green(`\n✓ Web UI available at: ${fullUrl}`));
|
||||
console.log(chalk.yellow("Keep this terminal open to maintain the session"));
|
||||
|
||||
// Keep the process running
|
||||
await new Promise(() => {}); // This will keep the process alive
|
||||
}
|
||||
// Attach to container or run detached
|
||||
else if (!this.config.detached) {
|
||||
console.log(chalk.blue("Preparing to attach to container..."));
|
||||
|
||||
// Set up cleanup handler
|
||||
const cleanup = async () => {
|
||||
console.log(chalk.blue("\nShutting down..."));
|
||||
await this.cleanup();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
// Handle process signals
|
||||
process.on("SIGINT", cleanup);
|
||||
process.on("SIGTERM", cleanup);
|
||||
|
||||
try {
|
||||
console.log(chalk.gray("About to call attach method..."));
|
||||
await this.containerManager.attach(containerId, branchName);
|
||||
console.log(chalk.gray("Attach method completed"));
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Failed to attach to container:"), error);
|
||||
await this.cleanup();
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.blue(
|
||||
"Running in detached mode. Container is running in the background.",
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(`Container ID: ${containerId}`));
|
||||
console.log(chalk.yellow("\nTo connect to the container, run:"));
|
||||
console.log(
|
||||
chalk.white(` docker attach ${containerId.substring(0, 12)}`),
|
||||
);
|
||||
console.log(chalk.yellow("\nOr use docker exec for a new shell:"));
|
||||
console.log(
|
||||
chalk.white(
|
||||
` docker exec -it ${containerId.substring(0, 12)} /bin/bash`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.yellow("\nTo stop the container:"));
|
||||
console.log(
|
||||
chalk.white(` docker stop ${containerId.substring(0, 12)}`),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
"\nThe container will continue running until you stop it manually.",
|
||||
),
|
||||
);
|
||||
}
|
||||
// Always launch web UI
|
||||
this.webServer = new WebUIServer(this.docker);
|
||||
|
||||
// Pass repo info to web server
|
||||
this.webServer.setRepoInfo(process.cwd(), branchName);
|
||||
|
||||
const webUrl = await this.webServer.start();
|
||||
|
||||
// Open browser to the web UI with container ID
|
||||
const fullUrl = `${webUrl}?container=${containerId}`;
|
||||
await this.webServer.openInBrowser(fullUrl);
|
||||
|
||||
console.log(chalk.green(`\n✓ Web UI available at: ${fullUrl}`));
|
||||
console.log(chalk.yellow("Keep this terminal open to maintain the session"));
|
||||
|
||||
// Keep the process running
|
||||
await new Promise(() => {}); // This will keep the process alive
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error:"), error);
|
||||
throw error;
|
||||
|
|
|
@ -7,11 +7,11 @@ export interface VolumeMount {
|
|||
export interface SandboxConfig {
|
||||
dockerImage?: string;
|
||||
dockerfile?: string;
|
||||
detached?: boolean;
|
||||
containerPrefix?: string;
|
||||
autoPush?: boolean;
|
||||
autoCreatePR?: boolean;
|
||||
autoStartClaude?: boolean;
|
||||
defaultShell?: "claude" | "bash";
|
||||
claudeConfigPath?: string;
|
||||
setupCommands?: string[];
|
||||
environment?: Record<string, string>;
|
||||
|
@ -21,7 +21,6 @@ export interface SandboxConfig {
|
|||
allowedTools?: string[];
|
||||
maxThinkingTokens?: number;
|
||||
bashTimeout?: number;
|
||||
webUI?: boolean;
|
||||
includeUntracked?: boolean;
|
||||
targetBranch?: string;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ export class WebUIServer {
|
|||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: ['claude', '--dangerously-skip-permissions'],
|
||||
Cmd: ['/home/claude/start-session.sh'],
|
||||
WorkingDir: '/workspace',
|
||||
User: 'claude',
|
||||
Env: [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue