Skip to content

Commit bfe08b3

Browse files
cevianclaude
andcommitted
fix(cloud-dev): native Claude install via skel, fix plugin registration, disable symlinks
- Install Claude Code via native installer (curl) as temp user instead of npm, copy to /etc/skel so each runtime user gets their own installation - Fix relative symlink for claude binary so non-root users can access it - Add temp /usr/local/bin/claude symlink before 0pflow install so claude is on PATH - Fix plugin registration: replace hardcoded /home/_setup paths with __HOMEDIR__ placeholder in skel, substitute actual home dir in entrypoint - Add build-time verification that settings.json exists (plugin registered) - Merge .claude.json config instead of overwriting (preserves skel plugin state) - Disable node_modules symlink scaffold (npm treats symlinks as linked packages and runs lifecycle scripts, causing husky-not-found errors) - Remove 0pflow plugin install step from scripts/install.sh (no longer needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 08c4ccb commit bfe08b3

File tree

3 files changed

+82
-97
lines changed

3 files changed

+82
-97
lines changed

packages/core/docker/Dockerfile.cloud-dev

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ RUN apt-get update && apt-get install -y python3 make g++ git curl openssh-serve
1111
# This means users can run `npm install` without extra flags and still hit the cache.
1212
RUN npm config set cache /npm-cache --global
1313

14-
# Claude Code CLI (global so PTY manager can find it via `which claude`)
15-
RUN npm install -g @anthropic-ai/claude-code
16-
1714
# 0pflow — install from local tarball or npm depending on build arg.
1815
# In dev mode, run: npm pack --pack-destination docker/ (from packages/core)
1916
# The wildcard COPY uses a dot file as fallback so it never fails when no .tgz exists.
@@ -45,6 +42,24 @@ RUN GLOBAL_MODULES="$(npm root -g)" && \
4542
RUN chmod -R a+rwx /npm-cache
4643
ENV PATH=/node_modules/.bin:$PATH
4744

45+
# Pre-run claude install + 0pflow install as a temp user, then save to /etc/skel.
46+
# useradd -m copies /etc/skel into new home dirs, so runtime users get Claude Code
47+
# + plugin/marketplace registration without the slow setup on every boot.
48+
RUN groupadd -f devs && \
49+
useradd -m -s /bin/bash -g devs _setup && \
50+
su -s /bin/bash _setup -c "curl -fsSL https://claude.ai/install.sh | bash" && \
51+
ln -sf /home/_setup/.local/bin/claude /usr/local/bin/claude && \
52+
OPFLOW="$(npm prefix -g)/bin/0pflow" && \
53+
su -s /bin/bash _setup -c "$OPFLOW install --force" && \
54+
test -f /home/_setup/.claude/settings.json || { echo "ERROR: 0pflow plugin not registered"; exit 1; } && \
55+
cp -a /home/_setup/. /etc/skel/ && \
56+
rm -f /etc/skel/.claude/.credentials.json && \
57+
sed -i 's|/home/_setup|__HOMEDIR__|g' /etc/skel/.claude/plugins/known_marketplaces.json /etc/skel/.claude/plugins/installed_plugins.json && \
58+
CLAUDE_VER=$(basename "$(readlink /etc/skel/.local/bin/claude)") && \
59+
ln -sf "../share/claude/versions/$CLAUDE_VER" /etc/skel/.local/bin/claude && \
60+
ln -sf /etc/skel/.local/bin/claude /usr/local/bin/claude && \
61+
userdel -r _setup 2>/dev/null || true
62+
4863
COPY entrypoint.sh /entrypoint.sh
4964
RUN chmod +x /entrypoint.sh
5065

packages/core/docker/entrypoint.sh

Lines changed: 60 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ if ! id "$DEV_USER" &>/dev/null; then
1717
fi
1818
DEV_HOME=$(eval echo "~$DEV_USER")
1919

20+
# Fix up plugin paths — skel has __HOMEDIR__ placeholders from build time
21+
for f in "$DEV_HOME/.claude/plugins/known_marketplaces.json" "$DEV_HOME/.claude/plugins/installed_plugins.json"; do
22+
[ -f "$f" ] && sed -i "s|__HOMEDIR__|$DEV_HOME|g" "$f"
23+
done
24+
2025
# ── Set up Claude Code config + credentials in user's home ────
2126
log "Setting up credentials..."
2227
mkdir -p "$DEV_HOME/.claude"
@@ -29,38 +34,33 @@ elif [ -f "$DEV_HOME/.claude/.credentials.json" ]; then
2934
log " OAuth credentials already exist, skipping"
3035
fi
3136

32-
# Write .claude.json on first boot only — Claude Code may update it at runtime
33-
if [ ! -f "$DEV_HOME/.claude.json" ]; then
34-
API_KEY_FIELD=""
35-
if [ -n "$CLAUDE_API_KEY" ]; then
36-
API_KEY_FIELD="\"primaryApiKey\": \"$CLAUDE_API_KEY\","
37-
log " Including API key in config"
38-
fi
39-
cat > "$DEV_HOME/.claude.json" <<CJSON
40-
{
41-
$API_KEY_FIELD
42-
"numStartups": 1,
43-
"installMethod": "npm",
44-
"autoUpdates": false,
45-
"hasCompletedOnboarding": true,
46-
"effortCalloutDismissed": true,
47-
"bypassPermissionsModeAccepted": true,
48-
"projects": {
49-
"$APP_DIR": {
50-
"allowedTools": [],
51-
"mcpContextUris": [],
52-
"mcpServers": {},
53-
"enabledMcpjsonServers": [],
54-
"disabledMcpjsonServers": [],
55-
"hasTrustDialogAccepted": true
56-
}
57-
}
58-
}
59-
CJSON
60-
log " Wrote .claude.json config"
61-
else
62-
log " .claude.json already exists, skipping"
63-
fi
37+
# Merge required fields into .claude.json (skel provides plugin registrations;
38+
# we overlay credentials, onboarding flags, and project trust on every boot).
39+
log "Updating .claude.json..."
40+
node -e "
41+
const fs = require('fs');
42+
const path = '$DEV_HOME/.claude.json';
43+
let cfg = {};
44+
try { cfg = JSON.parse(fs.readFileSync(path, 'utf-8')); } catch {}
45+
const apiKey = process.env.CLAUDE_API_KEY || '';
46+
if (apiKey) cfg.primaryApiKey = apiKey;
47+
Object.assign(cfg, {
48+
hasCompletedOnboarding: true,
49+
effortCalloutDismissed: true,
50+
bypassPermissionsModeAccepted: true,
51+
});
52+
cfg.projects = cfg.projects || {};
53+
cfg.projects['$APP_DIR'] = Object.assign(cfg.projects['$APP_DIR'] || {}, {
54+
allowedTools: [],
55+
mcpContextUris: [],
56+
mcpServers: {},
57+
enabledMcpjsonServers: [],
58+
disabledMcpjsonServers: [],
59+
hasTrustDialogAccepted: true,
60+
});
61+
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
62+
"
63+
log " .claude.json updated"
6464

6565
if [ -z "$CLAUDE_OAUTH_CREDENTIALS" ] && [ -z "$CLAUDE_API_KEY" ]; then
6666
log " WARNING: No Claude credentials found (CLAUDE_OAUTH_CREDENTIALS / CLAUDE_API_KEY not set)"
@@ -77,35 +77,34 @@ if [ ! -f "$APP_DIR/package.json" ]; then
7777
su -s /bin/bash "$DEV_USER" -c "cd '$APP_DIR' && "$OPFLOW" init '$APP_NAME' --dir . --no-install"
7878

7979
# Symlink base packages into /data/app/node_modules (one-time, persists on volume).
80-
# npm sees symlinks as installed packages — subsequent `npm install <pkg>` only writes
81-
# the new package. Uninstalls remove symlinks and they stay gone (not recreated on boot).
82-
# Upgrades: npm replaces symlinks with real dirs as needed.
83-
log " Symlinking base packages..."
84-
mkdir -p "$APP_DIR/node_modules/.bin"
85-
for pkg in /node_modules/*; do
86-
name="$(basename "$pkg")"
87-
[[ "$name" == .* ]] && continue # skip .bin, .package-lock.json, .cache, etc.
88-
if [[ "$name" == @* ]] && [ -d "$pkg" ]; then
89-
# Scoped package dir (e.g. @scope) — link individual packages so npm can
90-
# still add new packages under the same scope without hitting a symlink.
91-
mkdir -p "$APP_DIR/node_modules/$name"
92-
for scoped in "$pkg"/*; do
93-
sname="$(basename "$scoped")"
94-
[ ! -e "$APP_DIR/node_modules/$name/$sname" ] && \
95-
ln -s "$scoped" "$APP_DIR/node_modules/$name/$sname"
96-
done
97-
else
98-
[ ! -e "$APP_DIR/node_modules/$name" ] && \
99-
ln -s "$pkg" "$APP_DIR/node_modules/$name"
100-
fi
101-
done
102-
# Symlink .bin entries so npm scripts work without a local install
103-
for cmd in /node_modules/.bin/*; do
104-
cname="$(basename "$cmd")"
105-
[ ! -e "$APP_DIR/node_modules/.bin/$cname" ] && \
106-
ln -s "$cmd" "$APP_DIR/node_modules/.bin/$cname"
107-
done
108-
chown -R "$DEV_USER:devs" "$APP_DIR/node_modules"
80+
# DISABLED: npm treats symlinks as linked packages and runs their lifecycle scripts,
81+
# which fails (e.g. husky not found). Base packages at /node_modules/ are still
82+
# found by Node.js via parent-directory resolution from /data/app.
83+
# TODO: revisit — either run `npm install` during scaffold or find a symlink-compatible approach.
84+
#
85+
# log " Symlinking base packages..."
86+
# mkdir -p "$APP_DIR/node_modules/.bin"
87+
# for pkg in /node_modules/*; do
88+
# name="$(basename "$pkg")"
89+
# [[ "$name" == .* ]] && continue
90+
# if [[ "$name" == @* ]] && [ -d "$pkg" ]; then
91+
# mkdir -p "$APP_DIR/node_modules/$name"
92+
# for scoped in "$pkg"/*; do
93+
# sname="$(basename "$scoped")"
94+
# [ ! -e "$APP_DIR/node_modules/$name/$sname" ] && \
95+
# ln -s "$scoped" "$APP_DIR/node_modules/$name/$sname"
96+
# done
97+
# else
98+
# [ ! -e "$APP_DIR/node_modules/$name" ] && \
99+
# ln -s "$pkg" "$APP_DIR/node_modules/$name"
100+
# fi
101+
# done
102+
# for cmd in /node_modules/.bin/*; do
103+
# cname="$(basename "$cmd")"
104+
# [ ! -e "$APP_DIR/node_modules/.bin/$cname" ] && \
105+
# ln -s "$cmd" "$APP_DIR/node_modules/.bin/$cname"
106+
# done
107+
# chown -R "$DEV_USER:devs" "$APP_DIR/node_modules"
109108
log " Scaffold complete"
110109
fi
111110

@@ -123,14 +122,6 @@ mkdir -p "$APP_DIR/node_modules"
123122
ln -sfn /node_modules/0pflow "$APP_DIR/node_modules/0pflow"
124123
chown -h "$DEV_USER:devs" "$APP_DIR/node_modules" "$APP_DIR/node_modules/0pflow" 2>/dev/null || true
125124

126-
# ── Complete Claude Code native installer migration (suppresses startup prompt) ──
127-
log "Completing Claude Code native install for $DEV_USER..."
128-
su -s /bin/bash "$DEV_USER" -c "HOME='$DEV_HOME' claude install" 2>/dev/null || true
129-
130-
# ── Register 0pflow Claude Code plugin for DEV_USER ────────────
131-
log "Installing 0pflow plugin for $DEV_USER..."
132-
su -s /bin/bash "$DEV_USER" -c "HOME='$DEV_HOME' "$OPFLOW" install" 2>/dev/null || true
133-
134125
# ── SSH server setup ──────────────────────────────────────────
135126
# Persist host keys on volume so they survive redeployments (avoids "host key changed" warnings)
136127
if [ ! -f /data/ssh_host_keys/ssh_host_ed25519_key ]; then

scripts/install.sh

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ detect_os() {
5656
# ── Step 1: Node.js ─────────────────────────────────────────────────────────
5757

5858
install_node() {
59-
step "Step 1/4: Node.js"
59+
step "Step 1/3: Node.js"
6060

6161
if has_cmd node; then
6262
local node_version node_major
@@ -92,7 +92,7 @@ install_node() {
9292
# ── Step 2: Claude Code CLI ─────────────────────────────────────────────────
9393

9494
install_claude() {
95-
step "Step 2/4: Claude Code CLI"
95+
step "Step 2/3: Claude Code CLI"
9696

9797
if has_cmd claude; then
9898
success "Claude Code CLI found"
@@ -120,7 +120,7 @@ install_claude() {
120120
# ── Step 3: Tiger CLI ───────────────────────────────────────────────────────
121121

122122
install_tiger() {
123-
step "Step 3/4: Tiger CLI"
123+
step "Step 3/3: Tiger CLI"
124124

125125
if has_cmd tiger; then
126126
success "Tiger CLI found"
@@ -145,26 +145,6 @@ install_tiger() {
145145
fi
146146
}
147147

148-
# ── Step 4: 0pflow plugin ───────────────────────────────────────────────────
149-
150-
install_0pflow() {
151-
step "Step 4/4: 0pflow plugin"
152-
153-
if [ -n "${CLAUDECODE:-}" ]; then
154-
warn "Running inside a Claude Code session."
155-
warn "Please run this install script from a regular terminal instead."
156-
fatal "Cannot install 0pflow plugin from inside Claude Code."
157-
fi
158-
159-
info "Installing 0pflow plugin for Claude Code..."
160-
# Suppress inner output — our script provides its own progress messages
161-
if ! npx --loglevel=error -y --prefer-online 0pflow@dev install --force > /dev/null 2>&1; then
162-
fatal "0pflow plugin installation failed. Try running: npx -y 0pflow@dev install --force"
163-
fi
164-
165-
success "0pflow plugin installed"
166-
}
167-
168148
# ── Shell alias ──────────────────────────────────────────────────────────────
169149

170150
setup_alias() {
@@ -227,7 +207,6 @@ main() {
227207
install_node
228208
install_claude
229209
install_tiger
230-
install_0pflow
231210
setup_alias
232211

233212
# Determine which rc file to source
@@ -239,7 +218,7 @@ main() {
239218
printf "\n"
240219
printf "${GREEN}${BOLD} Installation complete!${RESET}\n\n" >&2
241220
printf "${BOLD} To get started, run:${RESET}\n\n" >&2
242-
printf "${CYAN} source ${rc_file} && 0pflow run${RESET}\n\n" >&2
221+
printf "${CYAN} source ${rc_file} && 0pflow cloud run${RESET}\n\n" >&2
243222
}
244223

245224
main "$@"

0 commit comments

Comments
 (0)