250 lines
9.9 KiB
Go
250 lines
9.9 KiB
Go
package lifecycle
|
|
|
|
// DefaultPrepareScript returns the default prepare script for a given executor type string.
|
|
func DefaultPrepareScript(executorType string) string {
|
|
switch executorType {
|
|
case "local":
|
|
return defaultLocalPrepareScript
|
|
case "worktree":
|
|
return defaultWorktreePrepareScript
|
|
case "local_docker", "remote_docker":
|
|
return defaultDockerPrepareScript
|
|
case "sprites":
|
|
return defaultSpritesPrepareScript
|
|
case "ssh":
|
|
return defaultSSHPrepareScript
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// KandevBranchCheckoutPostlude returns a kandev-managed shell snippet that
|
|
// guarantees the session's feature branch is checked out inside the
|
|
// workspace, no matter what the user's stored prepare_script does.
|
|
//
|
|
// Why a postlude instead of just relying on the default script: profiles
|
|
// created in the UI snapshot the *then-current* default into their stored
|
|
// prepare_script field. When kandev's default is updated to add a new
|
|
// kandev-managed step (like the worktree-branch checkout), older profiles
|
|
// silently miss it forever. Making the checkout an invariant — appended
|
|
// after the user's script — keeps the contract regardless of which default
|
|
// the user happens to have stored.
|
|
//
|
|
// The snippet is wrapped in a subshell + `|| true` so any failure (e.g. the
|
|
// user's script never produced /workspace, or the branch is the same as the
|
|
// base) is benign and doesn't block agentctl from starting.
|
|
//
|
|
//nolint:dupword // two `fi` tokens close two distinct shell blocks.
|
|
func KandevBranchCheckoutPostlude() string {
|
|
return `
|
|
|
|
# ---- kandev-managed: ensure session feature branch is checked out ----
|
|
# Appended automatically after the user's prepare script. Idempotent and
|
|
# non-destructive: prefer an existing local branch (which may carry unpushed
|
|
# work after a container resume), then fall through to a fresh tracking
|
|
# branch off origin, and only as a last resort create the branch off HEAD.
|
|
# The previous "git checkout -B feature origin/feature" form was destructive
|
|
# for the resume case — overwriting local commits with the remote tip.
|
|
#
|
|
# SECURITY: the data placeholders below are referenced BARE (no surrounding
|
|
# quotes). The scriptengine providers substitute a fully single-quoted,
|
|
# self-contained shell token (see scriptengine.shellQuote), so a branch name
|
|
# containing shell metacharacters (e.g. "$(...)", backticks, ";") resolves to a
|
|
# quoted literal that cannot inject commands. Do NOT wrap these in double
|
|
# quotes — double quotes would re-expose $(...) command substitution. Do NOT
|
|
# assume they are unquoted data either; the value carries its own quoting.
|
|
(
|
|
if [ -d {{workspace.path}}/.git ] \
|
|
&& [ -n {{worktree.branch}} ] \
|
|
&& [ {{worktree.branch}} != {{repository.branch}} ]; then
|
|
cd {{workspace.path}} || exit 0
|
|
if git rev-parse --verify {{worktree.branch}} >/dev/null 2>&1; then
|
|
git checkout {{worktree.branch}}
|
|
elif git fetch --depth=1 origin {{worktree.branch}} 2>/dev/null; then
|
|
git checkout -b {{worktree.branch}} origin/{{worktree.branch}}
|
|
else
|
|
git checkout -b {{worktree.branch}}
|
|
fi
|
|
fi
|
|
) || true
|
|
`
|
|
}
|
|
|
|
const defaultLocalPrepareScript = `#!/bin/bash
|
|
# Prepare local environment
|
|
# Runs before launching the local agent runtime.
|
|
# The script executes with working directory set to {{workspace.path}}.
|
|
# Use {{repository.path}} when you need the canonical repository root path.
|
|
|
|
# ---- Repository setup (if configured) ----
|
|
{{repository.setup_script}}
|
|
`
|
|
|
|
const defaultWorktreePrepareScript = `#!/bin/bash
|
|
# Prepare worktree environment
|
|
# Runs after the worktree has already been created/reused by Kandev.
|
|
# The script executes with working directory set to {{worktree.path}}.
|
|
# Use {{repository.path}} if you need to run commands in the main repository.
|
|
|
|
# ---- Repository setup (if configured) ----
|
|
{{repository.setup_script}}
|
|
`
|
|
|
|
const defaultDockerPrepareScript = `#!/bin/sh
|
|
# Prepare Docker container environment (kandev/multi-agent image)
|
|
# git, node, and agentctl are already installed in the image
|
|
|
|
set -eu
|
|
|
|
# ---- Git identity (optional) ----
|
|
{{git.identity_setup}}
|
|
|
|
# Mounted local remotes and workspaces can be owned by a host UID that does
|
|
# not match the container user.
|
|
git config --global --add safe.directory '*'
|
|
|
|
# ---- Configure git/gh for HTTPS auth ----
|
|
git config --global url."https://github.com/".insteadOf "git@github.com:"
|
|
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
|
|
|
|
# Configure GitHub token for gh CLI and git operations
|
|
{{github.auth_setup}}
|
|
|
|
# ---- Clone repository ----
|
|
# The kandev-managed feature-branch checkout is appended as an invariant
|
|
# postlude (see KandevBranchCheckoutPostlude) — keep it out of the default
|
|
# so old profiles snapshotting this script and the postlude never disagree.
|
|
# SECURITY: the providers substitute fully single-quoted tokens (shellQuote) for
|
|
# repository.branch / repository.clone_url / workspace.path, so a hostile branch
|
|
# name or URL cannot break out of the git clone argument even though the
|
|
# placeholders are referenced bare here. Do not add double quotes around them.
|
|
git clone --depth=1 --branch {{repository.branch}} {{repository.clone_url}} {{workspace.path}}
|
|
cd {{workspace.path}}
|
|
|
|
# Strip embedded token from remote URL to avoid persisting credentials in .git/config
|
|
git remote set-url origin "$(git remote get-url origin | sed 's|https://[^@]*@github.com/|https://github.com/|')" 2>/dev/null || true
|
|
|
|
# ---- Repository setup (if configured) ----
|
|
{{repository.setup_script}}
|
|
`
|
|
|
|
const defaultSpritesPrepareScript = `#!/bin/bash
|
|
# Prepare Sprites.dev cloud sandbox
|
|
#
|
|
# Pre-installed tools (no need to install):
|
|
# git, curl, wget, gh (GitHub CLI), node, python, go,
|
|
# build-essential, openssh-client, ca-certificates
|
|
|
|
set -euo pipefail
|
|
|
|
# ---- Add SSH host keys (prevent "Host key verification failed") ----
|
|
mkdir -p ~/.ssh
|
|
ssh-keyscan -t ed25519 github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null
|
|
|
|
# ---- Configure git/gh for HTTPS auth (token-based, no SSH keys needed) ----
|
|
# Rewrite SSH URLs to HTTPS so git clone git@github.com:... works via token auth
|
|
git config --global url."https://github.com/".insteadOf "git@github.com:"
|
|
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
|
|
|
|
# Configure GitHub token for gh CLI and git operations
|
|
# GH_TOKEN is the primary env var for gh CLI authentication
|
|
{{github.auth_setup}}
|
|
|
|
# ---- Install pnpm globally ----
|
|
curl -fsSL https://github.com/pnpm/pnpm/releases/download/v10.32.1/pnpm-linux-x64 -o /usr/local/bin/pnpm
|
|
chmod +x /usr/local/bin/pnpm
|
|
|
|
# ---- Git identity ----
|
|
{{git.identity_setup}}
|
|
|
|
# ---- Clone repository ----
|
|
# The kandev-managed feature-branch checkout is appended as an invariant
|
|
# postlude (see KandevBranchCheckoutPostlude) — keep it out of the default
|
|
# so old profiles snapshotting this script and the postlude never disagree.
|
|
# SECURITY: the providers substitute fully single-quoted tokens (shellQuote) for
|
|
# these placeholders. printf takes them as separate arguments, NOT inside a
|
|
# double-quoted string (which would re-expand a hostile URL/branch). Do not
|
|
# reintroduce an echo of these placeholders inside a double-quoted string.
|
|
printf 'Cloning %s (branch: %s)...\n' {{repository.clone_url}} {{repository.branch}}
|
|
git clone --depth=1 --quiet --branch {{repository.branch}} {{repository.clone_url}} {{workspace.path}}
|
|
cd {{workspace.path}}
|
|
|
|
# Strip embedded token from remote URL to avoid persisting credentials in .git/config
|
|
git remote set-url origin "$(git remote get-url origin | sed 's|https://[^@]*@github.com/|https://github.com/|')" 2>/dev/null || true
|
|
|
|
# ---- Repository setup (if configured) ----
|
|
{{repository.setup_script}}
|
|
|
|
# ---- Pre-install agent CLI(s) ----
|
|
{{kandev.agents.install}}
|
|
|
|
# ---- Install and start Kandev agent controller ----
|
|
echo "Starting agent controller..."
|
|
{{kandev.agentctl.install}}
|
|
{{kandev.agentctl.start}}
|
|
echo "Prepare complete."
|
|
`
|
|
|
|
//nolint:dupword // repeated shell `fi` tokens close distinct control-flow blocks.
|
|
const defaultSSHPrepareScript = `#!/bin/bash
|
|
# Prepare an SSH task workspace on the remote host.
|
|
# The script runs before the per-session agentctl instance starts.
|
|
|
|
set -euo pipefail
|
|
|
|
workspace={{workspace.path}}
|
|
repository_url={{repository.clone_url}}
|
|
repository_branch={{repository.branch}}
|
|
|
|
# ---- Configure git/gh authentication ----
|
|
{{github.auth_setup}}
|
|
|
|
# ---- Git identity ----
|
|
{{git.identity_setup}}
|
|
|
|
mkdir -p "$workspace"
|
|
|
|
# ---- Materialize or reuse the primary repository ----
|
|
# The task directory already contains .kandev session files, so initialize the
|
|
# repository in place rather than using git clone into a non-empty directory.
|
|
if [ -n "$repository_url" ]; then
|
|
if git -C "$workspace" rev-parse --git-dir >/dev/null 2>&1; then
|
|
configured_url=$(git -C "$workspace" config --get remote.origin.url 2>/dev/null || true)
|
|
if [ -z "$configured_url" ]; then
|
|
git -C "$workspace" remote add origin "$repository_url"
|
|
elif [ "$configured_url" != "$repository_url" ]; then
|
|
echo 'kandev: target origin identity conflict' >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
git init -q "$workspace"
|
|
git -C "$workspace" remote add origin "$repository_url"
|
|
fi
|
|
|
|
git_dir=$(git -C "$workspace" rev-parse --absolute-git-dir)
|
|
exclude_file="$git_dir/info/exclude"
|
|
mkdir -p "$(dirname "$exclude_file")"
|
|
touch "$exclude_file"
|
|
grep -Fqx '/.kandev/' "$exclude_file" || printf '%s\n' '/.kandev/' >>"$exclude_file"
|
|
|
|
if [ -z "$repository_branch" ]; then
|
|
echo 'kandev: target repository has no base branch' >&2
|
|
exit 1
|
|
fi
|
|
if ! git -C "$workspace" fetch --no-tags origin "+refs/heads/$repository_branch:refs/remotes/origin/$repository_branch" >/dev/null 2>&1; then
|
|
echo 'kandev: target base branch is unavailable' >&2
|
|
exit 1
|
|
fi
|
|
# A reused checkout keeps its current branch, local commits, and untracked
|
|
# files. Only an empty repository needs its initial branch created.
|
|
if ! git -C "$workspace" rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
git -C "$workspace" checkout -b "$repository_branch" "origin/$repository_branch"
|
|
fi
|
|
fi
|
|
|
|
cd "$workspace"
|
|
|
|
# ---- Repository setup (if configured) ----
|
|
{{repository.setup_script}}
|
|
`
|