Guro dee63efcf4
feat: add ttyd and cloudflared tunnel integration
Add support for exposing Claude's terminal interface via browser using ttyd and cloudflared tunnel.

- Add cloudflare_tunnel_token input parameter to both action.yml files
- Spawn ttyd process on port 7681 to serve Claude's CLI interface
- Start cloudflared tunnel process with provided token to expose via web
- Implement proper process cleanup in finally block
- Add 3-second startup delay for process initialization

When cloudflare_tunnel_token is provided, users can access Claude's interactive terminal through their browser via the cloudflared tunnel.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 12:53:44 -07:00

96 lines
2.9 KiB
TypeScript

#!/usr/bin/env bun
import * as core from "@actions/core";
import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env";
import { spawn } from "child_process";
async function run() {
try {
validateEnvironmentVariables();
await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS,
undefined, // homeDir
process.env.INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR,
);
const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
promptFile: process.env.INPUT_PROMPT_FILE || "",
});
// Setup ttyd and cloudflared tunnel if token provided
let ttydProcess: any = null;
let cloudflaredProcess: any = null;
if (process.env.INPUT_CLOUDFLARE_TUNNEL_TOKEN) {
console.log("Setting up ttyd and cloudflared tunnel...");
// Start ttyd process in background
ttydProcess = spawn("ttyd", ["-p", "7681", "-i", "0.0.0.0", "claude"], {
stdio: "inherit",
detached: true,
});
ttydProcess.on("error", (error: Error) => {
console.warn(`ttyd process error: ${error.message}`);
});
// Start cloudflared tunnel
cloudflaredProcess = spawn("cloudflared", ["tunnel", "run", "--token", process.env.INPUT_CLOUDFLARE_TUNNEL_TOKEN], {
stdio: "inherit",
detached: true,
});
cloudflaredProcess.on("error", (error: Error) => {
console.warn(`cloudflared process error: ${error.message}`);
});
// Give processes time to start up
await new Promise(resolve => setTimeout(resolve, 3000));
console.log("ttyd and cloudflared tunnel started");
}
try {
await runClaude(promptConfig.path, {
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS,
mcpConfig: process.env.INPUT_MCP_CONFIG,
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
claudeEnv: process.env.INPUT_CLAUDE_ENV,
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
model: process.env.ANTHROPIC_MODEL,
});
} finally {
// Clean up processes
if (ttydProcess) {
try {
ttydProcess.kill("SIGTERM");
} catch (e) {
console.warn("Failed to terminate ttyd process");
}
}
if (cloudflaredProcess) {
try {
cloudflaredProcess.kill("SIGTERM");
} catch (e) {
console.warn("Failed to terminate cloudflared process");
}
}
}
} catch (error) {
core.setFailed(`Action failed with error: ${error}`);
core.setOutput("conclusion", "failure");
process.exit(1);
}
}
if (import.meta.main) {
run();
}