Skip to content

Commit 67f0751

Browse files
committed
Initial commit.
0 parents  commit 67f0751

7 files changed

Lines changed: 513 additions & 0 deletions

File tree

.bash_aliases

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
alias ll='ls -l'

.github/workflows/push.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: build
2+
on:
3+
push:
4+
jobs:
5+
lint:
6+
permissions:
7+
contents: read
8+
runs-on: ubuntu-24.04
9+
steps:
10+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
11+
- run: shellcheck *.sh
12+
push:
13+
needs: [lint]
14+
uses: libops/.github/.github/workflows/build-push-ghcr.yaml@main
15+
permissions:
16+
contents: read
17+
packages: write
18+
secrets: inherit

Dockerfile

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
FROM node:20
2+
3+
ARG TZ
4+
ENV TZ="$TZ"
5+
6+
# install go
7+
WORKDIR /go
8+
COPY download.sh /usr/local/bin
9+
ARG \
10+
TARGETARCH=amd64 \
11+
# renovate: datasource=github-tags depName=golang packageName=golang/go versioning=go-mod-directive
12+
GO_VERSION=go1.25.3 \
13+
GO_BASE_URL="https://go.dev/dl/${GO_VERSION}" \
14+
GO_AMD64=linux-amd64.tar.gz \
15+
GO_AMD64_SHA256="0335f314b6e7bfe08c3d0cfaa7c19db961b7b99fb20be62b0a826c992ad14e0f" \
16+
GO_ARM64=linux-arm64.tar.gz \
17+
GO_ARM64_SHA256="1d42ebc84999b5e2069f5e31b67d6fc5d67308adad3e178d5a2ee2c9ff2001f5"
18+
19+
RUN --mount=type=cache,id=base-downloads-${TARGETARCH},sharing=locked,target=/opt/downloads \
20+
if [ "${TARGETARCH}" = "amd64" ]; \
21+
then \
22+
download.sh \
23+
--url "${GO_BASE_URL}.${GO_AMD64}" \
24+
--sha256 "${GO_AMD64_SHA256}" \
25+
--dest /usr/local ; \
26+
else \
27+
download.sh \
28+
--url "${GO_BASE_URL}.${GO_ARM64}" \
29+
--sha256 "${GO_ARM64_SHA256}" \
30+
--dest /usr/local ; \
31+
fi
32+
33+
34+
# Install basic development tools and iptables/ipset
35+
RUN apt-get update && apt-get install -y --no-install-recommends \
36+
less \
37+
git \
38+
procps \
39+
sudo \
40+
fzf \
41+
zsh \
42+
man-db \
43+
unzip \
44+
gnupg2 \
45+
gh \
46+
iptables \
47+
ipset \
48+
iproute2 \
49+
dnsutils \
50+
aggregate \
51+
jq \
52+
nano \
53+
vim \
54+
make \
55+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
56+
57+
# Ensure default node user has access to /usr/local/share
58+
RUN mkdir -p /usr/local/share/npm-global && \
59+
chown -R node:node /usr/local/share && \
60+
mkdir -p /workspace /home/node/.claude && \
61+
chown -R node:node /workspace /home/node/.claude
62+
63+
WORKDIR /workspace
64+
65+
ARG GIT_DELTA_VERSION=0.18.2
66+
RUN ARCH=$(dpkg --print-architecture) && \
67+
wget --progress=dot:giga "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
68+
dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
69+
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
70+
71+
# Set up non-root user
72+
USER node
73+
74+
# Install global packages
75+
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
76+
ENV PATH=$PATH:/usr/local/share/npm-global/bin
77+
78+
# Set the default shell to zsh rather than sh
79+
ENV SHELL=/bin/zsh
80+
81+
# Set the default editor and visual
82+
ENV EDITOR=nano
83+
ENV VISUAL=nano
84+
85+
# Default powerline10k theme
86+
ARG ZSH_IN_DOCKER_VERSION=1.2.0
87+
RUN sh -c "$(wget --progress=dot:giga -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
88+
-p git \
89+
-p fzf \
90+
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
91+
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
92+
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
93+
-x
94+
95+
# Install Claude and Gemini
96+
RUN npm install -g @anthropic-ai/claude-code@v2.0.59
97+
RUN npm install -g @google/gemini-cli@v0.19.4
98+
99+
# Copy and set up firewall script
100+
COPY init-firewall.sh /usr/local/bin/
101+
USER root
102+
RUN chmod +x /usr/local/bin/init-firewall.sh && \
103+
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
104+
chmod 0440 /etc/sudoers.d/node-firewall
105+
USER node
106+
ENV \
107+
NODE_OPTIONS="--max-old-space-size=4096" \
108+
CLAUDE_CONFIG_DIR="/home/node/.claude" \
109+
POWERLEVEL9K_DISABLE_GITSTATUS="true"
110+
111+
COPY docker-entrypoint.sh /docker-entrypoint.sh
112+
COPY .bash_aliases /home/node/
113+
114+
ENTRYPOINT [ "/docker-entrypoint.sh" ]

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# cli-sandbox
2+
3+
Run `gemini` and `claude` in a docker container
4+
5+
## Usage
6+
7+
```
8+
cd /path/to/codebase
9+
docker run \
10+
-v $HOME/.gemini:/home/node/.gemini \
11+
-v $HOME/.claude:/home/node/.claude \
12+
--cap-add=NET_ADMIN --cap-add=NET_RAW \
13+
-v ./:/workspace \
14+
-w /workspace \
15+
--rm -it ghcr.io/joecorall/cli-sandbox:main
16+
# chit chat
17+
```
18+
19+
## Attribution
20+
21+
`Dockerfile` and `init-firewall.sh` forked from [anthropics/claude-code](https://github.com/anthropics/claude-code/tree/main/.devcontainer). Added gemini support

docker-entrypoint.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
set -eou pipefail
4+
5+
sudo /usr/local/bin/init-firewall.sh \
6+
|| (
7+
echo "Unable to set firewall" \
8+
echo "Make sure you pass these flags to docker run: --cap-add=NET_ADMIN --cap-add=NET_RAW" \
9+
&& exit 1
10+
)
11+
12+
if [ "$#" -eq 0 ]; then
13+
exec /bin/bash
14+
else
15+
exec "$@"
16+
fi

download.sh

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ARGS=("$@")
5+
PROGNAME=$(basename "$0")
6+
readonly ARGS PROGNAME
7+
8+
function usage {
9+
cat <<-EOF
10+
usage: $PROGNAME options
11+
12+
Downloads the file at the given url to the download cache folder.
13+
14+
Does not re-download the file it already exists and matches the given checksum.
15+
16+
Unpacks the file if the destination option is given.
17+
18+
Download is placed in the directory ${DOWNLOAD_CACHE_DIRECTORY}.
19+
20+
OPTIONS:
21+
-u --url The url of the file to download.
22+
-c --sha256 The sha256 checksum to use to validate the download.
23+
-d --dest The location to unpack file into (optional).
24+
-s --strip Exclude the root folder when unpacking (optional, not supported with gzip or jar).
25+
-h --help Show this help.
26+
-x --debug Debug this script.
27+
28+
Examples:
29+
$PROGNAME \\
30+
--url https://github.com/just-containers/s6-overlay/releases/download/v1.22.1.0/s6-overlay-amd64.tar.gz
31+
--sha256 7f3aba1d803543dd1df3944d014f055112cf8dadf0a583c76dd5f46578ebe3c2 \\
32+
--dest /opt/s6-overlay
33+
EOF
34+
}
35+
36+
function cmdline {
37+
local arg=
38+
local args=
39+
for arg; do
40+
local delim=""
41+
case "$arg" in
42+
# Translate --gnu-long-options to -g (short options)
43+
--url) args="${args}-u " ;;
44+
--sha256) args="${args}-c " ;;
45+
--dest) args="${args}-d " ;;
46+
--strip) args="${args}-s " ;;
47+
--help) args="${args}-h " ;;
48+
--debug) args="${args}-x " ;;
49+
# Pass through anything else
50+
*)
51+
[[ "${arg:0:1}" == "-" ]] || delim="\""
52+
args="${args}${delim}${arg}${delim} "
53+
;;
54+
esac
55+
done
56+
57+
# Reset the positional parameters to the short options
58+
eval set -- "${args}"
59+
60+
while getopts "u:c:d:shx" OPTION; do
61+
case $OPTION in
62+
u)
63+
readonly URL=${OPTARG}
64+
;;
65+
c)
66+
readonly CHECKSUM=${OPTARG}
67+
;;
68+
d)
69+
readonly DEST=${OPTARG}
70+
;;
71+
s)
72+
readonly STRIP=true
73+
;;
74+
h)
75+
usage
76+
exit 0
77+
;;
78+
x)
79+
set -x
80+
;;
81+
*)
82+
echo "Invalid Option: $OPTION" >&2
83+
usage
84+
exit 1
85+
;;
86+
esac
87+
done
88+
89+
if [[ -z $URL || -z $CHECKSUM ]]; then
90+
echo "Missing one or more required options: --url --sha256"
91+
exit 1
92+
fi
93+
94+
# All remaning parameters are files to be removed from the installation.
95+
shift $((OPTIND-1))
96+
readonly REMOVE=("$@")
97+
98+
return 0
99+
}
100+
101+
function validate {
102+
local file=${1}
103+
sha256sum "${file}" | cut -f1 -d' ' | xargs test "${CHECKSUM}" ==
104+
}
105+
106+
function unpack {
107+
local file="${1}"
108+
local dest="${2}"
109+
local args=()
110+
local filename=
111+
mkdir -p "${dest}"
112+
if [[ -v STRIP ]]; then
113+
args+=("--strip-components" "1")
114+
fi
115+
filename=$(basename "${file}")
116+
case "${file}" in
117+
*.tar.xz | *.txz)
118+
tar -xf "${file}" -C "${dest}" "${args[@]}"
119+
;;
120+
*.tar.gz | *.tgz)
121+
tar -xzf "${file}" -C "${dest}" "${args[@]}"
122+
;;
123+
*.gz | *.gzip)
124+
gunzip "${file}" -f -c > "${dest}/${filename%.*}"
125+
;;
126+
*.zip | *.war)
127+
if [[ -v STRIP ]]; then
128+
mkdir -p /tmp/unpack
129+
unzip "${file}" -d /tmp/unpack
130+
mv "$(find /tmp/unpack/ -type d -mindepth 1 -maxdepth 1)"/* "${dest}"
131+
rm -fr /tmp/unpack
132+
else
133+
unzip "${file}" -d "${dest}"
134+
fi
135+
;;
136+
*.jar)
137+
cp "${file}" "${dest}"
138+
;;
139+
*)
140+
echo "Unable to unpack ${file} please update script to support additional formats." >&2
141+
exit 1
142+
;;
143+
esac
144+
# Remove extraneous files.
145+
for i in "${REMOVE[@]}"; do
146+
rm -fr "${dest:?}/${i}"
147+
done
148+
}
149+
150+
function main {
151+
local file
152+
cmdline "${ARGS[@]}"
153+
154+
DOWNLOAD_CACHE_DIRECTORY=${DOWNLOAD_CACHE_DIRECTORY:-/tmp}
155+
file="${DOWNLOAD_CACHE_DIRECTORY:?}/$(basename "${URL}")"
156+
# Remove the downloaded file if it exist and does not match the checksum so that it can be downloaded again.
157+
if [ -f "${file}" ] && ! validate "${file}"; then
158+
rm "${file}"
159+
fi
160+
wget -N -P "${DOWNLOAD_CACHE_DIRECTORY}" "${URL}"
161+
# Return non-zero if the checksum does not match the downloaded file.
162+
validate "${file}"
163+
if [[ -v DEST ]]; then
164+
unpack "${file}" "${DEST}"
165+
fi
166+
}
167+
main

0 commit comments

Comments
 (0)