Deploy Julian
Deploy Julian to an exe.xyz VM. Two paths: provision a new VM or update an existing one. The instance registry at deploy/instances.json tracks which VMs have been provisioned.
Target VM
Determine the target VM name:
- •If
$ARGUMENTSis provided, use it as the VM name (e.g.,/julian:deploy screen-test) - •If no arguments, derive from current git branch:
julian-<branch>(e.g., branchscreen→julian-screen) - •Strip any characters not valid in hostnames (keep alphanumeric and hyphens)
PRODUCTION SAFETY: If the resolved VM name is exactly julian (the production instance), STOP and warn the user before proceeding. Only proceed after explicit confirmation.
Routing: Provision or Update?
Read deploy/instances.json. If the target VM name exists in the registry, run the Update path. Otherwise, run the Provision path.
If deploy/instances.json doesn't exist, create it as {}.
Path A: Provision (New VM)
Full first-time setup. Run all steps in order.
Pre-flight
- •Get current git branch:
git rev-parse --abbrev-ref HEAD - •Pull Julian's changes locally:
git pull(stop on merge conflicts) - •Check for uncommitted changes:
git status --porcelain(warn but don't block) - •Push to GitHub:
git push - •Print target: VM name and URL (
https://<vmname>.exe.xyz/)
Clerk Pre-flight
Read the local .env file and check for VITE_CLERK_PUBLISHABLE_KEY:
- •If present (matches
pk_(test|live)_*): Extract the value for later. Proceed. - •If missing or invalid: STOP and guide the user:
- •Option A: Run
/vibes:connectto set up Clerk + Connect end-to-end - •Option B: Manually add
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...to.env - •Remind them to create the
with-emailJWT template in Clerk Dashboard:- •Go to Clerk Dashboard → Configure → JWT Templates
- •Create a new template named
with-email - •Set custom claims JSON (the
|| ''fallbacks are required — Fireproof Studio rejects null names):json{ "params": { "email": "{{user.primary_email_address}}", "email_verified": "{{user.email_verified}}", "external_id": "{{user.external_id}}", "first": "{{user.first_name || ''}}", "last": "{{user.last_name || ''}}", "name": "{{user.full_name || ''}}", "image_url": "{{user.image_url}}", "public_meta": "{{user.public_metadata}}" }, "role": "authenticated", "userId": "{{user.id}}" }
- •Option A: Run
Step P1: Create VM
IMPORTANT: All SSH commands targeting the VM must include -o StrictHostKeyChecking=accept-new.
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 <vmname>.exe.xyz echo ok
If unreachable, create it:
ssh exe.dev new --name=<vmname> ssh exe.dev share set-public <vmname>
Wait for boot (up to 90 seconds):
for i in $(seq 1 9); do ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 <vmname>.exe.xyz echo ok && break echo "Attempt $i failed, retrying in 10s..." sleep 10 done
Step P2: Install system dependencies
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "curl -fsSL https://bun.sh/install | bash && sudo apt-get update -qq && sudo apt-get install -y npm inotify-tools"
Step P3: Set up directory structure
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "sudo mkdir -p /opt/julian && sudo chown exedev:exedev /opt/julian && mkdir -p /home/exedev/mailbox"
Step P4: Generate deploy key and clone repo
Generate an SSH key for push access:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "ssh-keygen -t ed25519 -f ~/.ssh/julian-deploy -N '' -C '<vmname>-deploy'"
Configure SSH to use it for GitHub:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "mkdir -p ~/.ssh && cat >> ~/.ssh/config << 'SSHEOF' Host github.com IdentityFile ~/.ssh/julian-deploy StrictHostKeyChecking accept-new SSHEOF"
Add the deploy key to GitHub with write access:
DEPLOY_KEY=$(ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cat ~/.ssh/julian-deploy.pub") gh repo deploy-key add - --repo popmechanic/Julian --title "<vmname>-deploy" --allow-write <<< "$DEPLOY_KEY"
If the key title already exists, skip — it's fine.
Clone the repo and configure git identity:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "git clone git@github.com:popmechanic/Julian.git /opt/julian" ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && git config user.name 'Julian' && git config user.email 'julian@exe.xyz'"
Step P5: Install dependencies
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && /home/exedev/.bun/bin/bun install"
Step P6: Create .env
Use the VITE_CLERK_PUBLISHABLE_KEY from pre-flight (do NOT hardcode):
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cat > /opt/julian/.env << 'ENVEOF' VITE_CLERK_PUBLISHABLE_KEY=<value from local .env> ALLOWED_ORIGIN=https://<vmname>.exe.xyz ENVEOF"
Step P6b: Configure Claude Code settings
Enable Agent Teams (disabled by default) so Julian can spawn and manage agent teammates:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "mkdir -p /home/exedev/.claude && cat > /home/exedev/.claude/settings.json << 'SETTINGSEOF'
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
SETTINGSEOF"
Step P7: Install and start systemd services
scp deploy/julian.service <vmname>.exe.xyz:/tmp/ scp deploy/julian-screen.service <vmname>.exe.xyz:/tmp/ ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "sudo cp /tmp/julian.service /etc/systemd/system/ && \ sudo cp /tmp/julian-screen.service /etc/systemd/system/ && \ sudo systemctl daemon-reload && \ sudo systemctl enable --now julian julian-screen"
Step P8: Register instance
Add the VM to deploy/instances.json:
{
"<vmname>": {
"url": "https://<vmname>.exe.xyz",
"provisioned": "<ISO 8601 timestamp>",
"branch": "<git branch used for first deploy>"
}
}
Read the existing file, merge the new entry, write it back. Commit and push the updated registry so other machines know about it:
git add deploy/instances.json git commit -m "Register <vmname> instance" git push
Step P9: Verify
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "systemctl is-active julian julian-screen" curl -sf https://<vmname>.exe.xyz/ | head -5 curl -sf https://<vmname>.exe.xyz/api/health
Report: URL, service status, and remind user that Anthropic credentials need one-time setup on new instances.
Path B: Update (Existing VM)
Fast path — just sync code and restart. This is the common case.
Pre-flight
- •Pull Julian's changes locally:
git pull(stop on merge conflicts) - •Check for uncommitted changes:
git status --porcelain(warn but don't block) - •Push to GitHub:
git push - •Print target: VM name and URL
Change analysis
Before deploying, assess the scope of changes. Get the server's current commit and diff it against what you're about to deploy:
SERVER_HEAD=$(ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && git rev-parse HEAD") git diff --stat $SERVER_HEAD HEAD git diff --name-only $SERVER_HEAD HEAD
Classify the deploy based on what changed:
Content only (soul/, memory/, catalog.xml, docs/):
- •Safe. Tell the user: "Content-only update — safe to deploy directly."
- •Proceed without prompting.
Small code change (1-3 files changed in server/ or frontend, under ~100 lines total):
- •Low risk. Tell the user: "Small code update — deploying to <vmname>."
- •Proceed without prompting.
Large code change (4+ files changed, or 200+ lines, or structural changes to server.ts):
- •Higher risk. Tell the user the scope, e.g.: "This is a larger change — 8 files, ~350 lines, including server.ts changes."
- •If the target is production (
julian), suggest: "Want to deploy to a fresh test VM first? I can provision one with/julian:deploy test." - •If the target is already a non-production VM, proceed — that's what test VMs are for.
Dependency change (package.json modified):
- •Note it: "package.json changed — will run bun install."
- •If combined with large code changes on production, reinforce the test VM suggestion.
No changes (server is already on the same commit):
- •Tell the user: "Server is already up to date (commit <hash>). Nothing to deploy."
- •Skip the deploy entirely.
Step U1: Pull latest code
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && git pull"
If git pull fails because Julian has uncommitted changes:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && git stash && git pull && git stash pop"
If there are merge conflicts after stash pop, report them to the user.
Step U2: Install dependencies (if needed)
Check if package.json changed in the pull:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && git diff HEAD~1 --name-only 2>/dev/null | grep -q package.json && echo changed || echo unchanged"
If changed (or if in doubt), run:
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "cd /opt/julian && /home/exedev/.bun/bin/bun install"
Step U3: Restart services
ssh -o StrictHostKeyChecking=accept-new <vmname>.exe.xyz "sudo systemctl restart julian julian-screen"
Step U4: Verify
curl -sf https://<vmname>.exe.xyz/api/health
Confirm the version field in the health response matches the current git hash. Report the URL and version.
Error Recovery
- •DNS not resolving after 90 seconds: Run
ssh exe.dev listto verify VM exists. If it does, wait longer or check exe.dev status. - •Service won't start: Usually missing Bun. Check
ssh <vmname>.exe.xyz "/home/exedev/.bun/bin/bun --version". - •Connection refused on port 8000: Check logs:
ssh <vmname>.exe.xyz "journalctl -u julian -n 20 --no-pager". Common causes: missing Bun, missingjosedependency. - •git pull/push auth error: Deploy key issue. Check
ssh <vmname>.exe.xyz "ssh -T git@github.com". Re-run Step P4 if needed. - •git pull merge conflict: Julian has uncommitted changes. Stash first (see Step U1).
- •Instance in registry but VM gone: Remove the entry from
deploy/instances.jsonand re-run — it will take the Provision path. - •401 on
/tokens/with-email: Missing Clerk JWT template. Createwith-emailin Clerk Dashboard → Configure → JWT Templates with the claims JSON from the Clerk Pre-flight section. - •VM creation fails: Check exe.dev status, retry once.