|
1 | | -import { execSync, spawnSync } from "node:child_process"; |
| 1 | +import { execFileSync, execSync, spawnSync } from "node:child_process"; |
2 | 2 | import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs"; |
3 | 3 | import { join } from "node:path"; |
4 | 4 | import { homedir, userInfo } from "node:os"; |
@@ -127,11 +127,25 @@ export async function runCloudRun(): Promise<void> { |
127 | 127 | const existing = existingSandboxes.find((m) => m.app_name === choice); |
128 | 128 | if (existing?.app_url) { |
129 | 129 | const devUrl = existing.app_url.replace(/\/$/, "") + "/dev/"; |
130 | | - p.log.info(`URL: ${pc.cyan(devUrl)}`); |
131 | | - openInBrowser(devUrl); |
| 130 | + p.log.info(`Dev UI: ${pc.cyan(devUrl)}`); |
132 | 131 | } |
133 | 132 | p.log.info(`Status: ${pc.bold(existing?.fly_state ?? "unknown")}`); |
134 | | - p.outro(pc.green("Sandbox ready.")); |
| 133 | + |
| 134 | + // Open dev UI and register MCP + launch local Claude Code |
| 135 | + if (existing?.app_url) openInBrowser(existing.app_url.replace(/\/$/, "") + "/dev/"); |
| 136 | + |
| 137 | + const s = p.spinner(); |
| 138 | + s.start("Registering sandbox MCP server..."); |
| 139 | + const ok = await registerSandboxMcp(choice as string); |
| 140 | + if (ok) { |
| 141 | + s.stop(pc.green("MCP server registered")); |
| 142 | + p.log.info("Launching Claude Code with sandbox access..."); |
| 143 | + p.outro(""); |
| 144 | + launchClaude(); |
| 145 | + } else { |
| 146 | + s.stop(pc.yellow("MCP registration failed")); |
| 147 | + p.outro(pc.green("Sandbox ready.")); |
| 148 | + } |
135 | 149 | return; |
136 | 150 | } |
137 | 151 |
|
@@ -235,9 +249,21 @@ export async function runCloudRun(): Promise<void> { |
235 | 249 | const baseUrl = statusResult.url ?? createResult.appUrl; |
236 | 250 | const devUrl = baseUrl.replace(/\/$/, "") + "/dev/"; |
237 | 251 | s.stop(pc.green("Sandbox is running!")); |
238 | | - p.log.info(`URL: ${pc.cyan(devUrl)}`); |
| 252 | + p.log.info(`Dev UI: ${pc.cyan(devUrl)}`); |
239 | 253 | openInBrowser(devUrl); |
240 | | - p.outro(pc.green("Cloud dev environment is ready!")); |
| 254 | + |
| 255 | + // Register MCP and launch local Claude Code |
| 256 | + s.start("Registering sandbox MCP server..."); |
| 257 | + const ok = await registerSandboxMcp(appName as string); |
| 258 | + if (ok) { |
| 259 | + s.stop(pc.green("MCP server registered")); |
| 260 | + p.log.info("Launching Claude Code with sandbox access..."); |
| 261 | + p.outro(""); |
| 262 | + launchClaude(); |
| 263 | + } else { |
| 264 | + s.stop(pc.yellow("MCP registration failed")); |
| 265 | + p.outro(pc.green("Cloud dev environment is ready!")); |
| 266 | + } |
241 | 267 | return; |
242 | 268 | } |
243 | 269 |
|
@@ -515,6 +541,86 @@ export async function handleClaude(extraArgs: string[] = []): Promise<void> { |
515 | 541 | process.exit(exitCode); |
516 | 542 | } |
517 | 543 |
|
| 544 | +// ── MCP sandbox registration ──────────────────────────────── |
| 545 | + |
| 546 | +const MCP_SERVER_NAME = "crayon-sandbox"; |
| 547 | + |
| 548 | +async function registerSandboxMcp(appName: string): Promise<boolean> { |
| 549 | + let sshInfo: SSHKeyInfo; |
| 550 | + try { |
| 551 | + sshInfo = await getSSHKey(appName); |
| 552 | + } catch (err) { |
| 553 | + p.log.error( |
| 554 | + `Failed to get SSH key: ${err instanceof Error ? err.message : String(err)}`, |
| 555 | + ); |
| 556 | + return false; |
| 557 | + } |
| 558 | + |
| 559 | + // Ensure key is cached on disk (getSSHKey already does this) |
| 560 | + const keyPath = getCachedKeyPath(appName); |
| 561 | + |
| 562 | + const sshArgs = [ |
| 563 | + "-i", keyPath, |
| 564 | + "-p", String(sshInfo.port), |
| 565 | + "-o", "StrictHostKeyChecking=no", |
| 566 | + "-o", "UserKnownHostsFile=/dev/null", |
| 567 | + "-o", "LogLevel=ERROR", |
| 568 | + "-o", "IdentitiesOnly=yes", |
| 569 | + `${sshInfo.linuxUser}@${sshInfo.host}`, |
| 570 | + "crayon", "mcp", "sandbox", |
| 571 | + ]; |
| 572 | + |
| 573 | + // Register via claude mcp add (removes old one first if exists) |
| 574 | + try { |
| 575 | + execSync(`claude mcp remove ${MCP_SERVER_NAME} 2>/dev/null`, { |
| 576 | + stdio: "ignore", |
| 577 | + }); |
| 578 | + } catch { |
| 579 | + // May not exist yet — ignore |
| 580 | + } |
| 581 | + |
| 582 | + try { |
| 583 | + execFileSync("claude", [ |
| 584 | + "mcp", "add", |
| 585 | + "--transport", "stdio", |
| 586 | + MCP_SERVER_NAME, |
| 587 | + "--", |
| 588 | + "ssh", |
| 589 | + ...sshArgs, |
| 590 | + ], { stdio: "ignore" }); |
| 591 | + return true; |
| 592 | + } catch (err) { |
| 593 | + p.log.error( |
| 594 | + `Failed to register MCP server: ${err instanceof Error ? err.message : String(err)}`, |
| 595 | + ); |
| 596 | + return false; |
| 597 | + } |
| 598 | +} |
| 599 | + |
| 600 | +function launchClaude(): void { |
| 601 | + const result = spawnSync("claude", [], { stdio: "inherit" }); |
| 602 | + process.exit(result.status ?? 0); |
| 603 | +} |
| 604 | + |
| 605 | +export async function handleMcp(appNameArg?: string): Promise<void> { |
| 606 | + await ensureAuth(); |
| 607 | + const appName = appNameArg ?? (await selectMachine({ excludeStopped: true })); |
| 608 | + |
| 609 | + const s = p.spinner(); |
| 610 | + s.start("Registering sandbox MCP server..."); |
| 611 | + |
| 612 | + const ok = await registerSandboxMcp(appName); |
| 613 | + if (!ok) { |
| 614 | + s.stop(pc.red("Failed to register MCP server")); |
| 615 | + process.exit(1); |
| 616 | + } |
| 617 | + |
| 618 | + s.stop(pc.green(`MCP server "${MCP_SERVER_NAME}" registered for ${appName}`)); |
| 619 | + p.log.info("Launching Claude Code with sandbox access..."); |
| 620 | + p.outro(""); |
| 621 | + launchClaude(); |
| 622 | +} |
| 623 | + |
518 | 624 | export async function handleSSH(): Promise<void> { |
519 | 625 | await ensureAuth(); |
520 | 626 | const appName = await selectMachine({ excludeStopped: true }); |
|
0 commit comments