feat(claude-code): support binary distribution without Node.js dependency (#332)

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: kyle <kyle@coder.com>
Co-authored-by: 35C4n0r <work.jaykumar@gmail.com>
This commit is contained in:
blink-so[bot] 2025-08-18 17:00:34 +05:00 committed by GitHub
parent c2bc5cd314
commit 4dcaea7bf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 64 additions and 80 deletions

View File

@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.7"
version = "2.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@ -28,7 +28,6 @@ module "claude-code" {
## Prerequisites
- Node.js and npm must be installed in your workspace to install Claude Code
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
@ -84,7 +83,7 @@ resource "coder_agent" "main" {
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.7"
version = "2.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@ -102,7 +101,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.7"
version = "2.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true

View File

@ -111,7 +111,7 @@ locals {
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.js"))
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
claude_code_app_slug = "ccw"
}
@ -129,6 +129,21 @@ resource "coder_script" "claude_code" {
command -v "$1" >/dev/null 2>&1
}
function install_claude_code_cli() {
echo "Installing Claude Code via official installer"
set +e
curl -fsSL claude.ai/install.sh | bash -s -- "${var.claude_code_version}" 2>&1
CURL_EXIT=$${PIPESTATUS[0]}
set -e
if [ $CURL_EXIT -ne 0 ]; then
echo "Claude Code installer failed with exit code $$CURL_EXIT"
fi
# Ensure binaries are discoverable.
export PATH="~/.local/bin:$PATH"
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
}
if [ ! -d "${local.workdir}" ]; then
echo "Warning: The specified folder '${local.workdir}' does not exist."
echo "Creating the folder..."
@ -143,37 +158,7 @@ resource "coder_script" "claude_code" {
fi
if [ "${var.install_claude_code}" = "true" ]; then
if ! command_exists npm; then
echo "npm not found, checking for Node.js installation..."
if ! command_exists node; then
echo "Node.js not found, installing Node.js via NVM..."
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
echo "Node.js installed: $(node --version)"
echo "npm installed: $(npm --version)"
else
echo "Node.js is installed but npm is not available. Please install npm manually."
exit 1
fi
fi
echo "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
fi
if ! command_exists node; then
echo "Error: Node.js is not installed. Please install Node.js manually."
exit 1
install_claude_code_cli
fi
# Install AgentAPI if enabled
@ -214,7 +199,7 @@ resource "coder_script" "claude_code" {
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.js"
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.sh"
chmod +x "$module_path/scripts/agentapi-start.sh"
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
@ -292,4 +277,4 @@ resource "coder_ai_task" "claude_code" {
sidebar_app {
id = coder_app.claude_code_web.id
}
}
}

View File

@ -19,10 +19,10 @@ if [ -f "$log_file_path" ]; then
mv "$log_file_path" "$log_file_path"".$(date +%s)"
fi
# see the remove-last-session-id.js script for details
# see the remove-last-session-id.sh script for details
# about why we need it
# avoid exiting if the script fails
node "$scripts_dir/remove-last-session-id.js" "$(pwd)" || true
bash "$scripts_dir/remove-last-session-id.sh" "$(pwd)" 2>/dev/null || true
# we'll be manually handling errors from this point on
set +o errexit

View File

@ -1,40 +0,0 @@
// If lastSessionId is present in .claude.json, claude --continue will start a
// conversation starting from that session. The problem is that lastSessionId
// doesn't always point to the last session. The field is updated by claude only
// at the point of normal CLI exit. If Claude exits with an error, or if the user
// restarts the Coder workspace, lastSessionId will be stale, and claude --continue
// will start from an old session.
//
// If lastSessionId is missing, claude seems to accurately figure out where to
// start using the conversation history - even if the CLI previously exited with
// an error.
//
// This script removes the lastSessionId field from .claude.json.
const path = require("path")
const fs = require("fs")
const workingDirArg = process.argv[2]
if (!workingDirArg) {
console.log("No working directory provided - it must be the first argument")
process.exit(1)
}
const workingDir = path.resolve(workingDirArg)
console.log("workingDir", workingDir)
const claudeJsonPath = path.join(process.env.HOME, ".claude.json")
console.log(".claude.json path", claudeJsonPath)
if (!fs.existsSync(claudeJsonPath)) {
console.log("No .claude.json file found")
process.exit(0)
}
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, "utf8"))
if ("projects" in claudeJson && workingDir in claudeJson.projects && "lastSessionId" in claudeJson.projects[workingDir]) {
delete claudeJson.projects[workingDir].lastSessionId
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2))
console.log("Removed lastSessionId from .claude.json")
} else {
console.log("No lastSessionId found in .claude.json - nothing to do")
}

View File

@ -0,0 +1,40 @@
# If lastSessionId is present in .claude.json, claude --continue will start a
# conversation starting from that session. The problem is that lastSessionId
# doesn't always point to the last session. The field is updated by claude only
# at the point of normal CLI exit. If Claude exits with an error, or if the user
# restarts the Coder workspace, lastSessionId will be stale, and claude --continue
# will start from an old session.
#
# If lastSessionId is missing, claude seems to accurately figure out where to
# start using the conversation history - even if the CLI previously exited with
# an error.
#
# This script removes the lastSessionId field from .claude.json.
if [ $# -eq 0 ]; then
echo "No working directory provided - it must be the first argument"
exit 1
fi
# Get absolute path of working directory
working_dir=$(realpath "$1")
echo "workingDir $working_dir"
# Path to .claude.json
claude_json_path="$HOME/.claude.json"
echo ".claude.json path $claude_json_path"
# Check if .claude.json exists
if [ ! -f "$claude_json_path" ]; then
echo "No .claude.json file found"
exit 0
fi
# Use jq to check if lastSessionId exists for the working directory and remove it
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
# Remove lastSessionId and update the file
jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"
echo "Removed lastSessionId from .claude.json"
else
echo "No lastSessionId found in .claude.json - nothing to do"
fi