Spaces:
Paused
Paused
Henry commited on
Commit ·
eaef622
1
Parent(s): c09b231
test
Browse files- Dockerfile +58 -0
- README.md +55 -10
- docker-compose.yml +32 -0
- package.json +15 -0
- public/index.html +101 -0
- public/style.css +52 -0
- requirements.txt +10 -0
- run.sh +2 -0
- scripts/download_claude_backup.sh +343 -0
- scripts/setup.sh +4 -0
- scripts/ssh-setup.sh +58 -0
- scripts/terminal-anywhere-setup.sh +7 -0
- scripts/upload_claude_backup.sh +209 -0
- server.js +137 -0
Dockerfile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node 20 slim base suitable for VPS
|
| 2 |
+
FROM node:20-slim
|
| 3 |
+
|
| 4 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 5 |
+
|
| 6 |
+
# System deps for node-pty (node-gyp toolchain) and useful terminal tools
|
| 7 |
+
RUN apt-get update && \
|
| 8 |
+
apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 install -y --no-install-recommends \
|
| 9 |
+
bash git python3 python3-pip python3-venv python-is-python3 build-essential \
|
| 10 |
+
tmux ncurses-term rclone rsync nano ssh curl && \
|
| 11 |
+
rm -rf /var/lib/apt/lists/* && \
|
| 12 |
+
pip install --no-cache-dir --break-system-packages PyInstaller
|
| 13 |
+
|
| 14 |
+
# Python QoL defaults
|
| 15 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 16 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 17 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 18 |
+
|
| 19 |
+
# Convenience symlinks for common typos
|
| 20 |
+
RUN ln -sf /usr/bin/python3 /usr/local/bin/pyhon && \
|
| 21 |
+
ln -sf /usr/bin/python3 /usr/local/bin/ptyhon
|
| 22 |
+
|
| 23 |
+
# install Claude Code CLI
|
| 24 |
+
RUN npm install -g @anthropic-ai/claude-code
|
| 25 |
+
|
| 26 |
+
WORKDIR /app
|
| 27 |
+
|
| 28 |
+
# Preinstall Python packages used in terminal
|
| 29 |
+
COPY requirements.txt ./
|
| 30 |
+
RUN pip install --no-cache-dir --break-system-packages --timeout=60 -r requirements.txt || true
|
| 31 |
+
|
| 32 |
+
# Node deps
|
| 33 |
+
COPY package.json ./
|
| 34 |
+
RUN npm install --production
|
| 35 |
+
|
| 36 |
+
# App source
|
| 37 |
+
COPY server.js ./
|
| 38 |
+
COPY scripts ./scripts
|
| 39 |
+
COPY public ./public
|
| 40 |
+
|
| 41 |
+
# Make backup scripts available on PATH
|
| 42 |
+
COPY scripts/download_claude_backup.sh /usr/local/bin/download_claude_backup.sh
|
| 43 |
+
COPY scripts/upload_claude_backup.sh /usr/local/bin/upload_claude_backup.sh
|
| 44 |
+
COPY scripts/terminal-anywhere-setup.sh /usr/local/bin/terminal-anywhere-setup.sh
|
| 45 |
+
COPY scripts/setup.sh /usr/local/bin/setup.sh
|
| 46 |
+
COPY scripts/ssh-setup.sh /usr/local/bin/ssh-setup
|
| 47 |
+
RUN chmod +x /usr/local/bin/ssh-setup
|
| 48 |
+
RUN chmod +x /usr/local/bin/download_claude_backup.sh
|
| 49 |
+
RUN chmod +x /usr/local/bin/upload_claude_backup.sh
|
| 50 |
+
RUN chmod +x /usr/local/bin/terminal-anywhere-setup.sh
|
| 51 |
+
RUN chmod +x /usr/local/bin/setup.sh
|
| 52 |
+
|
| 53 |
+
ENV PORT=7860 \
|
| 54 |
+
NODE_ENV=production
|
| 55 |
+
EXPOSE 7860
|
| 56 |
+
|
| 57 |
+
CMD ["sh", "-c", "/usr/local/bin/setup.sh"]
|
| 58 |
+
|
README.md
CHANGED
|
@@ -1,10 +1,55 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**Overview**
|
| 2 |
+
- Purpose: Secure, browser-based terminal you can deploy on a VPS using Docker. Uses `xterm.js` in the browser with a `node-pty` backend.
|
| 3 |
+
- Security: Requires a shared secret token (`TERMINAL_TOKEN`) to open a terminal session.
|
| 4 |
+
|
| 5 |
+
**Quick Start**
|
| 6 |
+
- Build and run with Docker Compose:
|
| 7 |
+
- `cp .env.example .env` and fill in `TERMINAL_TOKEN` (and optional vars).
|
| 8 |
+
- `docker compose up -d --build`
|
| 9 |
+
- Open `http://<your-vps-ip>:<HOST_PORT>` and click Connect (enter token).
|
| 10 |
+
|
| 11 |
+
**Environment**
|
| 12 |
+
- `TERMINAL_TOKEN`: Shared secret required by the UI to connect.
|
| 13 |
+
- `HOST_PORT`: Host port to expose (default 7860).
|
| 14 |
+
- `JOB_WEBHOOK_TOKEN`: Optional. If set, secures `POST /tasks/heartbeat`.
|
| 15 |
+
- `SLACK_BOT_TOKEN`, `SLACK_CHANNEL_ID`: Optional. Send heartbeats to Slack.
|
| 16 |
+
- `PCLOUD_USERNAME`, `PCLOUD_PASSWORD`: Optional. Backup `~/.claude` to pCloud.
|
| 17 |
+
- `HEARTBEAT_LOG_PATH`: Optional. Where heartbeat requests are logged.
|
| 18 |
+
- `INSTALL_CLAUDE_CLI`: Optional `true|false`. Install Claude CLI during build.
|
| 19 |
+
|
| 20 |
+
**Ports**
|
| 21 |
+
- App listens on container port `7860`. Host port is controlled by `HOST_PORT` in `.env`.
|
| 22 |
+
|
| 23 |
+
**Persistent Data**
|
| 24 |
+
- Named volume `claude_data` is mounted at `/root/.claude` to persist Claude CLI tokens/config across restarts.
|
| 25 |
+
|
| 26 |
+
**Docker**
|
| 27 |
+
- Build image: `docker build -t claude-vps-web-terminal .`
|
| 28 |
+
- Run container: `docker run -d --name claude-vps-web-terminal -p 7860:7860 -e TERMINAL_TOKEN=yourtoken -v claude_data:/root/.claude claude-vps-web-terminal`
|
| 29 |
+
|
| 30 |
+
**Reverse Proxy (optional)**
|
| 31 |
+
- Put behind nginx/traefik/caddy and forward WebSocket `Upgrade` headers.
|
| 32 |
+
- Example nginx snippet:
|
| 33 |
+
- `location / { proxy_pass http://127.0.0.1:7860; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; }`
|
| 34 |
+
|
| 35 |
+
**API Endpoints**
|
| 36 |
+
- `GET /health`: Returns `ok`.
|
| 37 |
+
- `POST /tasks/heartbeat`: Sends a message to Slack. If `JOB_WEBHOOK_TOKEN` is set, include header `Authorization: Bearer <token>` or `?token=` query.
|
| 38 |
+
|
| 39 |
+
**Security Notes**
|
| 40 |
+
- Always set a strong `TERMINAL_TOKEN`. Anyone with the token can open a shell in the container.
|
| 41 |
+
- Use a firewall and/or reverse proxy to restrict access by IP if needed.
|
| 42 |
+
- For TLS, terminate at your reverse proxy (nginx/traefik/caddy/Cloudflare) and forward to the container.
|
| 43 |
+
|
| 44 |
+
**Files**
|
| 45 |
+
- `server.js`: Express server and WebSocket bridge to `node-pty`.
|
| 46 |
+
- `public/*`: Minimal terminal UI using `xterm.js`.
|
| 47 |
+
- `scripts/upload_claude_to_pcloud.py`: Optional pCloud backup of `~/.claude` to `/claude-vps/.claude` and `~/.claude.json` to `/claude-vps`.
|
| 48 |
+
- `scripts/download_claude_from_pcloud.py`: Restore backup from `/claude-vps/.claude` to `~/.claude` and `/claude-vps/.claude.json` to `~/.claude.json`. Supports `--overwrite` to replace existing local files.
|
| 49 |
+
- `Dockerfile`: Node 20 base with Python + build tools for `node-pty` and terminal QoL tools (tmux, rclone, rsync).
|
| 50 |
+
- `docker-compose.yml`: One-service stack with env vars and volume for persistence.
|
| 51 |
+
|
| 52 |
+
**Local Dev**
|
| 53 |
+
- `npm install`
|
| 54 |
+
- `npm start` then visit `http://localhost:7860`.
|
| 55 |
+
- Set `TERMINAL_TOKEN=yourtoken npm start` to require a token locally.
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
services:
|
| 3 |
+
web-terminal:
|
| 4 |
+
build:
|
| 5 |
+
context: .
|
| 6 |
+
dockerfile: Dockerfile
|
| 7 |
+
args:
|
| 8 |
+
# set to "true" to include the Claude CLI inside the image
|
| 9 |
+
INSTALL_CLAUDE_CLI: ${INSTALL_CLAUDE_CLI-false}
|
| 10 |
+
image: claude-vps-web-terminal:latest
|
| 11 |
+
container_name: claude-vps-web-terminal
|
| 12 |
+
restart: unless-stopped
|
| 13 |
+
ports:
|
| 14 |
+
- "${HOST_PORT-7860}:7860"
|
| 15 |
+
environment:
|
| 16 |
+
# required to gate access to the terminal UI
|
| 17 |
+
- TERMINAL_TOKEN=${TERMINAL_TOKEN}
|
| 18 |
+
# optional: provide SSH keys via env (used by entrypoint to create /root/.ssh)
|
| 19 |
+
- SSH_PUBLIC_KEY=${SSH_PUBLIC_KEY}
|
| 20 |
+
- SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY}
|
| 21 |
+
- SSH_OPTS=${SSH_OPTS}
|
| 22 |
+
# optional: rsync remote for backup restore
|
| 23 |
+
- REMOTE_HOST=${REMOTE_HOST}
|
| 24 |
+
- REMOTE_PATH=${REMOTE_PATH}
|
| 25 |
+
volumes:
|
| 26 |
+
# persist Claude CLI auth/config across restarts
|
| 27 |
+
- claude_data:/root/.claude
|
| 28 |
+
# optional: persist tmp logs if desired
|
| 29 |
+
# - ./data/tmp:/tmp
|
| 30 |
+
|
| 31 |
+
volumes:
|
| 32 |
+
claude_data: {}
|
package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "vps-web-terminal",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Secure browser terminal for VPS using xterm.js + node-pty",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"express": "^4.19.2",
|
| 11 |
+
"node-pty": "^1.0.0",
|
| 12 |
+
"ws": "^8.18.0"
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
public/index.html
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>VPS Web Terminal</title>
|
| 7 |
+
<link rel="stylesheet" href="https://unpkg.com/xterm/css/xterm.css" />
|
| 8 |
+
<link rel="stylesheet" href="/style.css" />
|
| 9 |
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://unpkg.com 'unsafe-inline'; script-src 'self' https://unpkg.com 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:;">
|
| 10 |
+
<meta http-equiv="Referrer-Policy" content="no-referrer" />
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div class="bar">
|
| 14 |
+
<div class="left">
|
| 15 |
+
<strong>Web Terminal</strong>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="right">
|
| 18 |
+
<label>Token: <input id="token" type="password" placeholder="required if set" /></label>
|
| 19 |
+
<button id="connect">Connect</button>
|
| 20 |
+
<button id="clear">Clear</button>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
<div id="terminal"></div>
|
| 24 |
+
|
| 25 |
+
<script src="https://unpkg.com/xterm/lib/xterm.js"></script>
|
| 26 |
+
<script src="https://unpkg.com/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
| 27 |
+
<script>
|
| 28 |
+
const term = new window.Terminal({
|
| 29 |
+
fontFamily: 'Menlo, Monaco, Consolas, monospace',
|
| 30 |
+
fontSize: 14,
|
| 31 |
+
convertEol: true,
|
| 32 |
+
cursorBlink: true,
|
| 33 |
+
scrollback: 5000,
|
| 34 |
+
theme: { background: '#111111' }
|
| 35 |
+
});
|
| 36 |
+
const fitAddon = new window.FitAddon.FitAddon();
|
| 37 |
+
term.loadAddon(fitAddon);
|
| 38 |
+
|
| 39 |
+
const el = document.getElementById('terminal');
|
| 40 |
+
term.open(el);
|
| 41 |
+
fitAddon.fit();
|
| 42 |
+
|
| 43 |
+
let ws;
|
| 44 |
+
|
| 45 |
+
function connect() {
|
| 46 |
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
| 47 |
+
const token = document.getElementById('token').value.trim();
|
| 48 |
+
const proto = (location.protocol === 'https:') ? 'wss' : 'ws';
|
| 49 |
+
const url = `${proto}://${location.host}/ws${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
| 50 |
+
ws = new WebSocket(url);
|
| 51 |
+
|
| 52 |
+
ws.onopen = () => {
|
| 53 |
+
term.focus();
|
| 54 |
+
sendResize();
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
ws.onmessage = (ev) => {
|
| 58 |
+
try {
|
| 59 |
+
const msg = JSON.parse(ev.data);
|
| 60 |
+
if (msg.type === 'output' && typeof msg.data === 'string') {
|
| 61 |
+
term.write(msg.data);
|
| 62 |
+
}
|
| 63 |
+
} catch (_) {}
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
ws.onclose = (ev) => {
|
| 67 |
+
term.write(`\r\n\x1b[31m[disconnected: ${ev.code}]\x1b[0m\r\n`);
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
ws.onerror = () => {
|
| 71 |
+
term.write('\r\n\x1b[31m[ws error]\x1b[0m\r\n');
|
| 72 |
+
};
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function sendResize() {
|
| 76 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
| 77 |
+
const cols = term.cols;
|
| 78 |
+
const rows = term.rows;
|
| 79 |
+
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// forward user keystrokes to pty
|
| 83 |
+
term.onData(data => {
|
| 84 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 85 |
+
ws.send(JSON.stringify({ type: 'input', data }));
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
window.addEventListener('resize', () => {
|
| 90 |
+
fitAddon.fit();
|
| 91 |
+
sendResize();
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
document.getElementById('connect').addEventListener('click', connect);
|
| 95 |
+
document.getElementById('clear').addEventListener('click', () => term.clear());
|
| 96 |
+
|
| 97 |
+
// initial hint
|
| 98 |
+
term.write('\x1b[32m[ready]\x1b[0m type to begin — click Connect first.\r\n');
|
| 99 |
+
</script>
|
| 100 |
+
</body>
|
| 101 |
+
</html>
|
public/style.css
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html, body {
|
| 2 |
+
height: 100%;
|
| 3 |
+
margin: 0;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
body {
|
| 7 |
+
background: #111;
|
| 8 |
+
color: #eee;
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.bar {
|
| 13 |
+
display: flex;
|
| 14 |
+
align-items: center;
|
| 15 |
+
justify-content: space-between;
|
| 16 |
+
padding: 8px 12px;
|
| 17 |
+
background: #1b1b1b;
|
| 18 |
+
border-bottom: 1px solid #2a2a2a;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.right label {
|
| 22 |
+
margin-right: 8px;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
#terminal {
|
| 26 |
+
position: absolute;
|
| 27 |
+
left: 0;
|
| 28 |
+
right: 0;
|
| 29 |
+
top: 44px;
|
| 30 |
+
bottom: 0;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
input[type="password"] {
|
| 34 |
+
background: #222;
|
| 35 |
+
color: #eee;
|
| 36 |
+
border: 1px solid #333;
|
| 37 |
+
padding: 4px 6px;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
button {
|
| 41 |
+
background: #333;
|
| 42 |
+
color: #eee;
|
| 43 |
+
border: 1px solid #444;
|
| 44 |
+
padding: 4px 8px;
|
| 45 |
+
margin-left: 6px;
|
| 46 |
+
cursor: pointer;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
button:hover {
|
| 50 |
+
background: #3a3a3a;
|
| 51 |
+
}
|
| 52 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mcp
|
| 2 |
+
pcloud
|
| 3 |
+
ptyprocess
|
| 4 |
+
websockets
|
| 5 |
+
gradio
|
| 6 |
+
plotly
|
| 7 |
+
rich
|
| 8 |
+
textual
|
| 9 |
+
asyncio
|
| 10 |
+
psutil
|
run.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
docker compose up -d --build
|
| 2 |
+
docker compose logs -f
|
scripts/download_claude_backup.sh
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Claude Configuration Restore Script
|
| 4 |
+
# Downloads ~/.claude.json and ~/.claude/ folder from rsync.net storage
|
| 5 |
+
# Usage: ./download_claude_backup.sh [OPTIONS]
|
| 6 |
+
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
|
| 9 |
+
# Allow environment overrides while keeping sane defaults
|
| 10 |
+
REMOTE_HOST="${REMOTE_HOST:-zh5486@zh5486.rsync.net}"
|
| 11 |
+
REMOTE_PATH="${REMOTE_PATH:-vps/claude_code/host_1}"
|
| 12 |
+
# SSH options: accept new host keys automatically to avoid interactive prompt
|
| 13 |
+
# You can override via SSH_OPTS env (e.g., to pin known_hosts instead)
|
| 14 |
+
SSH_OPTS="${SSH_OPTS:--o StrictHostKeyChecking=accept-new}"
|
| 15 |
+
LOG_DIR="$HOME/.claude_backup_logs"
|
| 16 |
+
LOG_FILE="$LOG_DIR/download_$(date +%Y%m%d_%H%M%S).log"
|
| 17 |
+
|
| 18 |
+
# Default values
|
| 19 |
+
DEFAULT_DEST="$HOME"
|
| 20 |
+
DEST_DIR="$DEFAULT_DEST"
|
| 21 |
+
MODE="override"
|
| 22 |
+
DRY_RUN=false
|
| 23 |
+
|
| 24 |
+
# Colors for output
|
| 25 |
+
RED='\033[0;31m'
|
| 26 |
+
GREEN='\033[0;32m'
|
| 27 |
+
YELLOW='\033[1;33m'
|
| 28 |
+
BLUE='\033[0;34m'
|
| 29 |
+
NC='\033[0m' # No Color
|
| 30 |
+
|
| 31 |
+
# Function to log messages
|
| 32 |
+
log() {
|
| 33 |
+
local level=$1
|
| 34 |
+
shift
|
| 35 |
+
local message="$*"
|
| 36 |
+
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
| 37 |
+
|
| 38 |
+
# Create log directory if it doesn't exist
|
| 39 |
+
mkdir -p "$LOG_DIR"
|
| 40 |
+
|
| 41 |
+
# Write to log file
|
| 42 |
+
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
| 43 |
+
|
| 44 |
+
# Print to console with colors
|
| 45 |
+
case $level in
|
| 46 |
+
"ERROR") echo -e "${RED}[ERROR]${NC} $message" ;;
|
| 47 |
+
"SUCCESS") echo -e "${GREEN}[SUCCESS]${NC} $message" ;;
|
| 48 |
+
"WARNING") echo -e "${YELLOW}[WARNING]${NC} $message" ;;
|
| 49 |
+
"INFO") echo -e "${BLUE}[INFO]${NC} $message" ;;
|
| 50 |
+
*) echo "[$level] $message" ;;
|
| 51 |
+
esac
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# Function to show usage
|
| 55 |
+
show_help() {
|
| 56 |
+
cat << EOF
|
| 57 |
+
Claude Configuration Restore Script
|
| 58 |
+
|
| 59 |
+
Usage: $0 [OPTIONS]
|
| 60 |
+
|
| 61 |
+
OPTIONS:
|
| 62 |
+
--override Overwrite existing files (default)
|
| 63 |
+
--skip Skip existing files, only download new/missing files
|
| 64 |
+
--dest PATH Destination directory (default: $DEFAULT_DEST)
|
| 65 |
+
--dry-run Show what would be downloaded without actually doing it
|
| 66 |
+
--help Show this help message
|
| 67 |
+
|
| 68 |
+
MODES:
|
| 69 |
+
--override: Downloads all files and overwrites existing ones
|
| 70 |
+
Creates backup of existing files before overwriting
|
| 71 |
+
|
| 72 |
+
--skip: Only downloads files that don't exist locally
|
| 73 |
+
Preserves all existing local files
|
| 74 |
+
|
| 75 |
+
EXAMPLES:
|
| 76 |
+
$0 # Download to ~/ with override mode
|
| 77 |
+
$0 --skip # Download missing files only
|
| 78 |
+
$0 --dest /tmp/claude_restore # Download to custom location
|
| 79 |
+
$0 --skip --dest /backup/claude # Download missing files to custom location
|
| 80 |
+
$0 --dry-run # Preview what would be downloaded
|
| 81 |
+
|
| 82 |
+
This script restores:
|
| 83 |
+
- .claude.json
|
| 84 |
+
- .claude/ folder
|
| 85 |
+
|
| 86 |
+
From: $REMOTE_HOST:$REMOTE_PATH/
|
| 87 |
+
|
| 88 |
+
Requirements:
|
| 89 |
+
- SSH key authentication must be set up for $REMOTE_HOST
|
| 90 |
+
- rsync must be installed
|
| 91 |
+
|
| 92 |
+
Logs are saved to: $LOG_DIR/
|
| 93 |
+
EOF
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
# Parse command line arguments
|
| 97 |
+
while [[ $# -gt 0 ]]; do
|
| 98 |
+
case $1 in
|
| 99 |
+
--override)
|
| 100 |
+
MODE="override"
|
| 101 |
+
shift
|
| 102 |
+
;;
|
| 103 |
+
--skip)
|
| 104 |
+
MODE="skip"
|
| 105 |
+
shift
|
| 106 |
+
;;
|
| 107 |
+
--dest)
|
| 108 |
+
if [[ -z "${2:-}" ]]; then
|
| 109 |
+
log "ERROR" "--dest requires a path argument"
|
| 110 |
+
exit 1
|
| 111 |
+
fi
|
| 112 |
+
DEST_DIR="$2"
|
| 113 |
+
shift 2
|
| 114 |
+
;;
|
| 115 |
+
--dry-run)
|
| 116 |
+
DRY_RUN=true
|
| 117 |
+
shift
|
| 118 |
+
;;
|
| 119 |
+
--help|-h)
|
| 120 |
+
show_help
|
| 121 |
+
exit 0
|
| 122 |
+
;;
|
| 123 |
+
*)
|
| 124 |
+
log "ERROR" "Unknown option: $1"
|
| 125 |
+
show_help
|
| 126 |
+
exit 1
|
| 127 |
+
;;
|
| 128 |
+
esac
|
| 129 |
+
done
|
| 130 |
+
|
| 131 |
+
# Validate and create destination directory
|
| 132 |
+
setup_destination() {
|
| 133 |
+
# Expand tilde if present
|
| 134 |
+
DEST_DIR="${DEST_DIR/#\~/$HOME}"
|
| 135 |
+
|
| 136 |
+
log "INFO" "Destination directory: $DEST_DIR"
|
| 137 |
+
log "INFO" "Mode: $MODE"
|
| 138 |
+
|
| 139 |
+
if $DRY_RUN; then
|
| 140 |
+
log "INFO" "[DRY RUN] Would use destination: $DEST_DIR"
|
| 141 |
+
return
|
| 142 |
+
fi
|
| 143 |
+
|
| 144 |
+
if [[ ! -d "$DEST_DIR" ]]; then
|
| 145 |
+
log "INFO" "Creating destination directory: $DEST_DIR"
|
| 146 |
+
if ! mkdir -p "$DEST_DIR"; then
|
| 147 |
+
log "ERROR" "Failed to create destination directory: $DEST_DIR"
|
| 148 |
+
exit 1
|
| 149 |
+
fi
|
| 150 |
+
fi
|
| 151 |
+
|
| 152 |
+
if [[ ! -w "$DEST_DIR" ]]; then
|
| 153 |
+
log "ERROR" "Destination directory is not writable: $DEST_DIR"
|
| 154 |
+
exit 1
|
| 155 |
+
fi
|
| 156 |
+
|
| 157 |
+
log "SUCCESS" "Destination directory ready: $DEST_DIR"
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
# Test SSH connection and check remote files
|
| 161 |
+
test_remote_connection() {
|
| 162 |
+
log "INFO" "Testing SSH connection to $REMOTE_HOST..."
|
| 163 |
+
|
| 164 |
+
if ! ssh $SSH_OPTS -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_HOST" "echo 'SSH connection successful'" >/dev/null 2>&1; then
|
| 165 |
+
log "ERROR" "Failed to connect to $REMOTE_HOST"
|
| 166 |
+
log "ERROR" "Please ensure:"
|
| 167 |
+
log "ERROR" " 1. SSH key authentication is set up"
|
| 168 |
+
log "ERROR" " 2. The host is reachable"
|
| 169 |
+
log "ERROR" " 3. Your SSH key is loaded (ssh-add -l)"
|
| 170 |
+
exit 1
|
| 171 |
+
fi
|
| 172 |
+
|
| 173 |
+
log "SUCCESS" "SSH connection successful"
|
| 174 |
+
|
| 175 |
+
# Check if remote backup directory exists
|
| 176 |
+
log "INFO" "Checking remote backup directory..."
|
| 177 |
+
if ! ssh $SSH_OPTS "$REMOTE_HOST" "test -d '$REMOTE_PATH'"; then
|
| 178 |
+
log "ERROR" "Remote backup directory does not exist: $REMOTE_PATH"
|
| 179 |
+
log "ERROR" "Please run the upload script first to create a backup"
|
| 180 |
+
exit 1
|
| 181 |
+
fi
|
| 182 |
+
|
| 183 |
+
log "SUCCESS" "Remote backup directory found: $REMOTE_PATH"
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
# Create backup of existing files before override
|
| 187 |
+
backup_existing_files() {
|
| 188 |
+
if [[ "$MODE" != "override" ]]; then
|
| 189 |
+
return
|
| 190 |
+
fi
|
| 191 |
+
|
| 192 |
+
local backup_dir="$DEST_DIR/.claude_backup_$(date +%Y%m%d_%H%M%S)"
|
| 193 |
+
local files_to_backup=()
|
| 194 |
+
|
| 195 |
+
# Check what exists locally
|
| 196 |
+
if [[ -f "$DEST_DIR/.claude.json" ]]; then
|
| 197 |
+
files_to_backup+=(".claude.json")
|
| 198 |
+
fi
|
| 199 |
+
|
| 200 |
+
if [[ -d "$DEST_DIR/.claude" ]]; then
|
| 201 |
+
files_to_backup+=(".claude/")
|
| 202 |
+
fi
|
| 203 |
+
|
| 204 |
+
if [[ ${#files_to_backup[@]} -eq 0 ]]; then
|
| 205 |
+
log "INFO" "No existing files to backup"
|
| 206 |
+
return
|
| 207 |
+
fi
|
| 208 |
+
|
| 209 |
+
if $DRY_RUN; then
|
| 210 |
+
log "INFO" "[DRY RUN] Would backup existing files to: $backup_dir"
|
| 211 |
+
for file in "${files_to_backup[@]}"; do
|
| 212 |
+
log "INFO" "[DRY RUN] Would backup: $DEST_DIR/$file"
|
| 213 |
+
done
|
| 214 |
+
return
|
| 215 |
+
fi
|
| 216 |
+
|
| 217 |
+
log "INFO" "Creating backup of existing files..."
|
| 218 |
+
mkdir -p "$backup_dir"
|
| 219 |
+
|
| 220 |
+
for file in "${files_to_backup[@]}"; do
|
| 221 |
+
log "INFO" "Backing up existing: $file"
|
| 222 |
+
if cp -r "$DEST_DIR/$file" "$backup_dir/"; then
|
| 223 |
+
log "SUCCESS" "Backed up: $file to $backup_dir/"
|
| 224 |
+
else
|
| 225 |
+
log "WARNING" "Failed to backup: $file"
|
| 226 |
+
fi
|
| 227 |
+
done
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
# Download files using rsync
|
| 231 |
+
download_files() {
|
| 232 |
+
local rsync_opts="-avz --progress"
|
| 233 |
+
|
| 234 |
+
case $MODE in
|
| 235 |
+
"override")
|
| 236 |
+
# Default rsync behavior - overwrites existing files
|
| 237 |
+
;;
|
| 238 |
+
"skip")
|
| 239 |
+
# Skip existing files
|
| 240 |
+
rsync_opts="$rsync_opts --ignore-existing"
|
| 241 |
+
;;
|
| 242 |
+
esac
|
| 243 |
+
|
| 244 |
+
if $DRY_RUN; then
|
| 245 |
+
rsync_opts="$rsync_opts --dry-run"
|
| 246 |
+
log "INFO" "[DRY RUN] Showing what would be downloaded..."
|
| 247 |
+
else
|
| 248 |
+
log "INFO" "Starting download from $REMOTE_HOST:$REMOTE_PATH"
|
| 249 |
+
log "INFO" "Using mode: $MODE"
|
| 250 |
+
fi
|
| 251 |
+
|
| 252 |
+
# Download .claude.json if it exists remotely
|
| 253 |
+
log "INFO" "Checking for .claude.json on remote server..."
|
| 254 |
+
if ssh $SSH_OPTS "$REMOTE_HOST" "test -f '$REMOTE_PATH/.claude.json'"; then
|
| 255 |
+
log "INFO" "Downloading .claude.json..."
|
| 256 |
+
if rsync -e "ssh $SSH_OPTS" $rsync_opts "$REMOTE_HOST:$REMOTE_PATH/.claude.json" "$DEST_DIR/"; then
|
| 257 |
+
if ! $DRY_RUN; then
|
| 258 |
+
log "SUCCESS" ".claude.json downloaded successfully"
|
| 259 |
+
fi
|
| 260 |
+
else
|
| 261 |
+
log "ERROR" "Failed to download .claude.json"
|
| 262 |
+
exit 1
|
| 263 |
+
fi
|
| 264 |
+
else
|
| 265 |
+
log "WARNING" ".claude.json not found on remote server"
|
| 266 |
+
fi
|
| 267 |
+
|
| 268 |
+
# Download .claude folder if it exists remotely
|
| 269 |
+
log "INFO" "Checking for .claude/ folder on remote server..."
|
| 270 |
+
if ssh $SSH_OPTS "$REMOTE_HOST" "test -d '$REMOTE_PATH/claude'"; then
|
| 271 |
+
log "INFO" "Downloading .claude/ folder..."
|
| 272 |
+
|
| 273 |
+
# Create local .claude directory if it doesn't exist
|
| 274 |
+
if ! $DRY_RUN && [[ ! -d "$DEST_DIR/.claude" ]]; then
|
| 275 |
+
mkdir -p "$DEST_DIR/.claude"
|
| 276 |
+
fi
|
| 277 |
+
|
| 278 |
+
if rsync -e "ssh $SSH_OPTS" $rsync_opts "$REMOTE_HOST:$REMOTE_PATH/claude/" "$DEST_DIR/.claude/"; then
|
| 279 |
+
if ! $DRY_RUN; then
|
| 280 |
+
log "SUCCESS" ".claude/ folder downloaded successfully"
|
| 281 |
+
fi
|
| 282 |
+
else
|
| 283 |
+
log "ERROR" "Failed to download .claude/ folder"
|
| 284 |
+
exit 1
|
| 285 |
+
fi
|
| 286 |
+
else
|
| 287 |
+
log "WARNING" ".claude/ folder not found on remote server"
|
| 288 |
+
fi
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
# Show summary of what was downloaded/would be downloaded
|
| 292 |
+
show_summary() {
|
| 293 |
+
log "INFO" "Download summary:"
|
| 294 |
+
log "INFO" " Source: $REMOTE_HOST:$REMOTE_PATH"
|
| 295 |
+
log "INFO" " Destination: $DEST_DIR"
|
| 296 |
+
log "INFO" " Mode: $MODE"
|
| 297 |
+
|
| 298 |
+
if $DRY_RUN; then
|
| 299 |
+
log "INFO" " Status: DRY RUN - no files were actually transferred"
|
| 300 |
+
else
|
| 301 |
+
log "INFO" " Status: Completed successfully"
|
| 302 |
+
|
| 303 |
+
# Show what was actually downloaded
|
| 304 |
+
if [[ -f "$DEST_DIR/.claude.json" ]]; then
|
| 305 |
+
log "INFO" " Downloaded: .claude.json"
|
| 306 |
+
fi
|
| 307 |
+
|
| 308 |
+
if [[ -d "$DEST_DIR/.claude" ]]; then
|
| 309 |
+
log "INFO" " Downloaded: .claude/ folder"
|
| 310 |
+
fi
|
| 311 |
+
fi
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
# Main execution
|
| 315 |
+
main() {
|
| 316 |
+
log "INFO" "Starting Claude configuration restore..."
|
| 317 |
+
log "INFO" "Log file: $LOG_FILE"
|
| 318 |
+
|
| 319 |
+
if $DRY_RUN; then
|
| 320 |
+
log "INFO" "Running in DRY RUN mode - no files will be transferred"
|
| 321 |
+
fi
|
| 322 |
+
|
| 323 |
+
setup_destination
|
| 324 |
+
test_remote_connection
|
| 325 |
+
backup_existing_files
|
| 326 |
+
download_files
|
| 327 |
+
show_summary
|
| 328 |
+
|
| 329 |
+
if $DRY_RUN; then
|
| 330 |
+
log "INFO" "[DRY RUN] Restore simulation completed"
|
| 331 |
+
else
|
| 332 |
+
log "SUCCESS" "Restore completed successfully!"
|
| 333 |
+
fi
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
# Check if rsync is available
|
| 337 |
+
if ! command -v rsync &> /dev/null; then
|
| 338 |
+
log "ERROR" "rsync is not installed. Please install rsync first."
|
| 339 |
+
exit 1
|
| 340 |
+
fi
|
| 341 |
+
|
| 342 |
+
# Run main function
|
| 343 |
+
main "$@"
|
scripts/setup.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
echo "test"
|
| 2 |
+
/usr/local/bin/ssh-setup 2>&1
|
| 3 |
+
/usr/local/bin/terminal-anywhere-setup.sh 2>&1
|
| 4 |
+
node server
|
scripts/ssh-setup.sh
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env sh
|
| 2 |
+
set -eu
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if [ -f /.env ]; then
|
| 6 |
+
export $(grep -v '^#' /.env | xargs)
|
| 7 |
+
fi
|
| 8 |
+
# Write SSH keys from env vars into /root/.ssh with correct permissions.
|
| 9 |
+
# Handles both real newlines and literal \n sequences in SSH_PRIVATE_KEY.
|
| 10 |
+
|
| 11 |
+
HOME_DIR="${HOME:-/root}"
|
| 12 |
+
SSH_DIR="$HOME_DIR/.ssh"
|
| 13 |
+
|
| 14 |
+
mkdir -p "$SSH_DIR"
|
| 15 |
+
chmod 700 "$SSH_DIR"
|
| 16 |
+
|
| 17 |
+
echo "created folder $SSH_DIR"
|
| 18 |
+
|
| 19 |
+
# Public key (optional)
|
| 20 |
+
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
|
| 21 |
+
printf "%s\n" "$SSH_PUBLIC_KEY" > "$SSH_DIR/id_rsa.pub"
|
| 22 |
+
echo "Created id_rsa.pub"
|
| 23 |
+
chmod 644 "$SSH_DIR/id_rsa.pub"
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# Private key (optional)
|
| 27 |
+
if [ -n "${SSH_PRIVATE_KEY:-}" ]; then
|
| 28 |
+
# If the key contains literal \n sequences, convert them to newlines.
|
| 29 |
+
if printf "%s" "$SSH_PRIVATE_KEY" | grep -q '\\n'; then
|
| 30 |
+
printf "%s" "$SSH_PRIVATE_KEY" | sed 's/\\n/\n/g' > "$SSH_DIR/id_rsa"
|
| 31 |
+
echo "created id_rsa"
|
| 32 |
+
else
|
| 33 |
+
printf "%s" "$SSH_PRIVATE_KEY" > "$SSH_DIR/id_rsa"
|
| 34 |
+
fi
|
| 35 |
+
chmod 600 "$SSH_DIR/id_rsa"
|
| 36 |
+
fi
|
| 37 |
+
|
| 38 |
+
# Prime known_hosts to avoid host key prompts for common git hosts
|
| 39 |
+
if command -v ssh-keyscan >/dev/null 2>&1; then
|
| 40 |
+
for host in github.com gitlab.com; do
|
| 41 |
+
ssh-keyscan -H "$host" 2>/dev/null >> "$SSH_DIR/known_hosts" || true
|
| 42 |
+
done
|
| 43 |
+
# Optionally pre-seed remote backup host if provided via REMOTE_HOST (user@host)
|
| 44 |
+
if [ -n "${REMOTE_HOST:-}" ]; then
|
| 45 |
+
remote_host_domain="$(printf "%s" "$REMOTE_HOST" | awk -F'@' '{print $NF}')"
|
| 46 |
+
if [ -n "$remote_host_domain" ]; then
|
| 47 |
+
ssh-keyscan -H "$remote_host_domain" 2>/dev/null >> "$SSH_DIR/known_hosts" || true
|
| 48 |
+
fi
|
| 49 |
+
fi
|
| 50 |
+
chmod 644 "$SSH_DIR/known_hosts" || true
|
| 51 |
+
fi
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
#rm /app/scripts/*.sh
|
| 55 |
+
#rm ~/.ssh/id_rsa
|
| 56 |
+
#rm ~/.ssh/id_rsa.pub
|
| 57 |
+
|
| 58 |
+
exec "$@"
|
scripts/terminal-anywhere-setup.sh
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
echo "setup terminal-anywhere"
|
| 2 |
+
echo "clone repo"
|
| 3 |
+
cd ~/
|
| 4 |
+
rm -f -r terminal-anywhere-source
|
| 5 |
+
/usr/bin/git clone git@github.com:scantrancher/terminal-anywhere-source.git
|
| 6 |
+
cd ~/terminal-anywhere-source/
|
| 7 |
+
/usr/bin/bash run.sh
|
scripts/upload_claude_backup.sh
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Claude Configuration Backup Script
|
| 4 |
+
# Backs up ~/.claude.json and ~/.claude/ folder to rsync.net storage
|
| 5 |
+
# Usage: ./upload_claude_backup.sh [--dry-run]
|
| 6 |
+
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
|
| 9 |
+
REMOTE_HOST="zh5486@zh5486.rsync.net"
|
| 10 |
+
REMOTE_PATH="vps/claude_code/host_1"
|
| 11 |
+
LOG_DIR="$HOME/.claude_backup_logs"
|
| 12 |
+
LOG_FILE="$LOG_DIR/upload_$(date +%Y%m%d_%H%M%S).log"
|
| 13 |
+
|
| 14 |
+
# Colors for output
|
| 15 |
+
RED='\033[0;31m'
|
| 16 |
+
GREEN='\033[0;32m'
|
| 17 |
+
YELLOW='\033[1;33m'
|
| 18 |
+
BLUE='\033[0;34m'
|
| 19 |
+
NC='\033[0m' # No Color
|
| 20 |
+
|
| 21 |
+
# Function to log messages
|
| 22 |
+
log() {
|
| 23 |
+
local level=$1
|
| 24 |
+
shift
|
| 25 |
+
local message="$*"
|
| 26 |
+
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
| 27 |
+
|
| 28 |
+
# Create log directory if it doesn't exist
|
| 29 |
+
mkdir -p "$LOG_DIR"
|
| 30 |
+
|
| 31 |
+
# Write to log file
|
| 32 |
+
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
| 33 |
+
|
| 34 |
+
# Print to console with colors
|
| 35 |
+
case $level in
|
| 36 |
+
"ERROR") echo -e "${RED}[ERROR]${NC} $message" ;;
|
| 37 |
+
"SUCCESS") echo -e "${GREEN}[SUCCESS]${NC} $message" ;;
|
| 38 |
+
"WARNING") echo -e "${YELLOW}[WARNING]${NC} $message" ;;
|
| 39 |
+
"INFO") echo -e "${BLUE}[INFO]${NC} $message" ;;
|
| 40 |
+
*) echo "[$level] $message" ;;
|
| 41 |
+
esac
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# Function to show usage
|
| 45 |
+
show_help() {
|
| 46 |
+
cat << EOF
|
| 47 |
+
Claude Configuration Backup Script
|
| 48 |
+
|
| 49 |
+
Usage: $0 [OPTIONS]
|
| 50 |
+
|
| 51 |
+
OPTIONS:
|
| 52 |
+
--dry-run Show what would be backed up without actually doing it
|
| 53 |
+
--help Show this help message
|
| 54 |
+
|
| 55 |
+
This script backs up:
|
| 56 |
+
- ~/.claude.json
|
| 57 |
+
- ~/.claude/ folder
|
| 58 |
+
|
| 59 |
+
To: $REMOTE_HOST:$REMOTE_PATH/
|
| 60 |
+
|
| 61 |
+
Requirements:
|
| 62 |
+
- SSH key authentication must be set up for $REMOTE_HOST
|
| 63 |
+
- rsync must be installed
|
| 64 |
+
|
| 65 |
+
Logs are saved to: $LOG_DIR/
|
| 66 |
+
EOF
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
# Parse command line arguments
|
| 70 |
+
DRY_RUN=false
|
| 71 |
+
while [[ $# -gt 0 ]]; do
|
| 72 |
+
case $1 in
|
| 73 |
+
--dry-run)
|
| 74 |
+
DRY_RUN=true
|
| 75 |
+
shift
|
| 76 |
+
;;
|
| 77 |
+
--help|-h)
|
| 78 |
+
show_help
|
| 79 |
+
exit 0
|
| 80 |
+
;;
|
| 81 |
+
*)
|
| 82 |
+
log "ERROR" "Unknown option: $1"
|
| 83 |
+
show_help
|
| 84 |
+
exit 1
|
| 85 |
+
;;
|
| 86 |
+
esac
|
| 87 |
+
done
|
| 88 |
+
|
| 89 |
+
# Check if required files/folders exist
|
| 90 |
+
check_source_files() {
|
| 91 |
+
local missing_files=()
|
| 92 |
+
|
| 93 |
+
if [[ ! -f "$HOME/.claude.json" ]]; then
|
| 94 |
+
missing_files+=("~/.claude.json")
|
| 95 |
+
fi
|
| 96 |
+
|
| 97 |
+
if [[ ! -d "$HOME/.claude" ]]; then
|
| 98 |
+
missing_files+=("~/.claude/")
|
| 99 |
+
fi
|
| 100 |
+
|
| 101 |
+
if [[ ${#missing_files[@]} -gt 0 ]]; then
|
| 102 |
+
log "WARNING" "Some Claude configuration files are missing:"
|
| 103 |
+
for file in "${missing_files[@]}"; do
|
| 104 |
+
log "WARNING" " - $file"
|
| 105 |
+
done
|
| 106 |
+
log "WARNING" "Backup will proceed with available files only."
|
| 107 |
+
fi
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
# Test SSH connection
|
| 111 |
+
test_ssh_connection() {
|
| 112 |
+
log "INFO" "Testing SSH connection to $REMOTE_HOST..."
|
| 113 |
+
|
| 114 |
+
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_HOST" "echo 'SSH connection successful'" >/dev/null 2>&1; then
|
| 115 |
+
log "ERROR" "Failed to connect to $REMOTE_HOST"
|
| 116 |
+
log "ERROR" "Please ensure:"
|
| 117 |
+
log "ERROR" " 1. SSH key authentication is set up"
|
| 118 |
+
log "ERROR" " 2. The host is reachable"
|
| 119 |
+
log "ERROR" " 3. Your SSH key is loaded (ssh-add -l)"
|
| 120 |
+
exit 1
|
| 121 |
+
fi
|
| 122 |
+
|
| 123 |
+
log "SUCCESS" "SSH connection successful"
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
# Create remote directory if it doesn't exist
|
| 127 |
+
create_remote_directory() {
|
| 128 |
+
log "INFO" "Creating remote directory structure..."
|
| 129 |
+
|
| 130 |
+
if $DRY_RUN; then
|
| 131 |
+
log "INFO" "[DRY RUN] Would create directory: $REMOTE_HOST:$REMOTE_PATH"
|
| 132 |
+
return
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
if ssh "$REMOTE_HOST" "mkdir -p '$REMOTE_PATH'"; then
|
| 136 |
+
log "SUCCESS" "Remote directory ready: $REMOTE_PATH"
|
| 137 |
+
else
|
| 138 |
+
log "ERROR" "Failed to create remote directory: $REMOTE_PATH"
|
| 139 |
+
exit 1
|
| 140 |
+
fi
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
# Backup files using rsync
|
| 144 |
+
backup_files() {
|
| 145 |
+
local rsync_opts="-avz --progress --delete"
|
| 146 |
+
|
| 147 |
+
if $DRY_RUN; then
|
| 148 |
+
rsync_opts="$rsync_opts --dry-run"
|
| 149 |
+
log "INFO" "[DRY RUN] Showing what would be backed up..."
|
| 150 |
+
else
|
| 151 |
+
log "INFO" "Starting backup to $REMOTE_HOST:$REMOTE_PATH"
|
| 152 |
+
fi
|
| 153 |
+
|
| 154 |
+
# Backup .claude.json if it exists
|
| 155 |
+
if [[ -f "$HOME/.claude.json" ]]; then
|
| 156 |
+
log "INFO" "Backing up ~/.claude.json..."
|
| 157 |
+
if rsync $rsync_opts "$HOME/.claude.json" "$REMOTE_HOST:$REMOTE_PATH/"; then
|
| 158 |
+
if ! $DRY_RUN; then
|
| 159 |
+
log "SUCCESS" "~/.claude.json backed up successfully"
|
| 160 |
+
fi
|
| 161 |
+
else
|
| 162 |
+
log "ERROR" "Failed to backup ~/.claude.json"
|
| 163 |
+
exit 1
|
| 164 |
+
fi
|
| 165 |
+
fi
|
| 166 |
+
|
| 167 |
+
# Backup .claude folder if it exists
|
| 168 |
+
if [[ -d "$HOME/.claude" ]]; then
|
| 169 |
+
log "INFO" "Backing up ~/.claude/ folder..."
|
| 170 |
+
if rsync $rsync_opts "$HOME/.claude/" "$REMOTE_HOST:$REMOTE_PATH/claude/"; then
|
| 171 |
+
if ! $DRY_RUN; then
|
| 172 |
+
log "SUCCESS" "~/.claude/ folder backed up successfully"
|
| 173 |
+
fi
|
| 174 |
+
else
|
| 175 |
+
log "ERROR" "Failed to backup ~/.claude/ folder"
|
| 176 |
+
exit 1
|
| 177 |
+
fi
|
| 178 |
+
fi
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Main execution
|
| 182 |
+
main() {
|
| 183 |
+
log "INFO" "Starting Claude configuration backup..."
|
| 184 |
+
log "INFO" "Log file: $LOG_FILE"
|
| 185 |
+
|
| 186 |
+
if $DRY_RUN; then
|
| 187 |
+
log "INFO" "Running in DRY RUN mode - no files will be transferred"
|
| 188 |
+
fi
|
| 189 |
+
|
| 190 |
+
check_source_files
|
| 191 |
+
test_ssh_connection
|
| 192 |
+
create_remote_directory
|
| 193 |
+
backup_files
|
| 194 |
+
|
| 195 |
+
if $DRY_RUN; then
|
| 196 |
+
log "INFO" "[DRY RUN] Backup simulation completed"
|
| 197 |
+
else
|
| 198 |
+
log "SUCCESS" "Backup completed successfully!"
|
| 199 |
+
log "INFO" "Remote location: $REMOTE_HOST:$REMOTE_PATH"
|
| 200 |
+
fi
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
# Check if rsync is available
|
| 204 |
+
if ! command -v rsync &> /dev/null; then
|
| 205 |
+
log "ERROR" "rsync is not installed. Please install rsync first."
|
| 206 |
+
exit 1
|
| 207 |
+
fi
|
| 208 |
+
|
| 209 |
+
# Run main function
|
server.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const http = require('http');
|
| 2 |
+
const express = require('express');
|
| 3 |
+
const path = require('path');
|
| 4 |
+
const fs = require('fs');
|
| 5 |
+
const { WebSocketServer } = require('ws');
|
| 6 |
+
const { spawn } = require('child_process');
|
| 7 |
+
const pty = require('node-pty');
|
| 8 |
+
|
| 9 |
+
const app = express();
|
| 10 |
+
const server = http.createServer(app);
|
| 11 |
+
|
| 12 |
+
const PORT = process.env.PORT || 7860;
|
| 13 |
+
const TERMINAL_TOKEN = process.env.TERMINAL_TOKEN || ""; // set in env for security
|
| 14 |
+
const JOB_WEBHOOK_TOKEN = process.env.JOB_WEBHOOK_TOKEN || ""; // bearer token for webhook auth
|
| 15 |
+
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || ""; // xoxb- token from Slack app
|
| 16 |
+
const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID || ""; // channel ID to post heartbeat
|
| 17 |
+
const HEARTBEAT_LOG_PATH = process.env.HEARTBEAT_LOG_PATH || '/tmp/heartbeat.log';
|
| 18 |
+
|
| 19 |
+
// If behind a reverse proxy (nginx/traefik), trust X-Forwarded-* for req.ip
|
| 20 |
+
app.set('trust proxy', true);
|
| 21 |
+
|
| 22 |
+
// Serve static client
|
| 23 |
+
app.use(express.static(path.join(__dirname, 'public')));
|
| 24 |
+
// JSON parser for webhook
|
| 25 |
+
app.use(express.json());
|
| 26 |
+
|
| 27 |
+
// Basic liveness endpoint
|
| 28 |
+
app.get('/health', (_req, res) => res.status(200).send('ok'));
|
| 29 |
+
|
| 30 |
+
// Minimal webhook to send a heartbeat message to Slack
|
| 31 |
+
app.post('/tasks/heartbeat', async (req, res) => {
|
| 32 |
+
try {
|
| 33 |
+
const authHeader = req.headers['authorization'] || '';
|
| 34 |
+
const bearer = authHeader.startsWith('Bearer ')
|
| 35 |
+
? authHeader.slice('Bearer '.length)
|
| 36 |
+
: (req.query.token || '');
|
| 37 |
+
// Enforce only when JOB_WEBHOOK_TOKEN is set
|
| 38 |
+
if (JOB_WEBHOOK_TOKEN && bearer !== JOB_WEBHOOK_TOKEN) {
|
| 39 |
+
return res.sendStatus(401);
|
| 40 |
+
}
|
| 41 |
+
if (!SLACK_BOT_TOKEN || !SLACK_CHANNEL_ID) {
|
| 42 |
+
return res.status(500).json({ error: 'Slack not configured' });
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const now = new Date().toISOString();
|
| 46 |
+
const host = req.headers.host || 'unknown-host';
|
| 47 |
+
const text = `Heartbeat: VPS web terminal alive at ${now} (host: ${host})`;
|
| 48 |
+
|
| 49 |
+
const resp = await fetch('https://slack.com/api/chat.postMessage', {
|
| 50 |
+
method: 'POST',
|
| 51 |
+
headers: {
|
| 52 |
+
'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
|
| 53 |
+
'Content-Type': 'application/json'
|
| 54 |
+
},
|
| 55 |
+
body: JSON.stringify({ channel: SLACK_CHANNEL_ID, text })
|
| 56 |
+
});
|
| 57 |
+
const data = await resp.json().catch(() => ({}));
|
| 58 |
+
const ip = req.ip;
|
| 59 |
+
const ua = req.headers['user-agent'] || '';
|
| 60 |
+
const logLine = `${now}\tip=${ip}\tua=${JSON.stringify(ua)}\tstatus=${data && data.ok ? 'ok' : 'error'}\n`;
|
| 61 |
+
try {
|
| 62 |
+
const dir = path.dirname(HEARTBEAT_LOG_PATH);
|
| 63 |
+
fs.mkdirSync(dir, { recursive: true });
|
| 64 |
+
fs.appendFileSync(HEARTBEAT_LOG_PATH, logLine, 'utf8');
|
| 65 |
+
} catch (_) { /* ignore log failures */ }
|
| 66 |
+
|
| 67 |
+
// Kick off pCloud backup: ~/.claude -> /claude-vps/.claude and ~/.claude.json -> /claude-vps (non-blocking)
|
| 68 |
+
try {
|
| 69 |
+
const child = spawn('python', ['scripts/upload_claude_to_pcloud.py'], {
|
| 70 |
+
cwd: __dirname,
|
| 71 |
+
stdio: 'inherit',
|
| 72 |
+
env: process.env,
|
| 73 |
+
detached: false
|
| 74 |
+
});
|
| 75 |
+
// no await; run in background
|
| 76 |
+
} catch (_) { /* ignore spawn errors */ }
|
| 77 |
+
|
| 78 |
+
if (!data.ok) {
|
| 79 |
+
return res.status(502).json({ error: 'Slack API error', details: data });
|
| 80 |
+
}
|
| 81 |
+
return res.status(202).json({ ok: true, ts: data.ts, channel: data.channel });
|
| 82 |
+
} catch (err) {
|
| 83 |
+
return res.status(500).json({ error: 'server_error' });
|
| 84 |
+
}
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
// WebSocket for terminal
|
| 88 |
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
| 89 |
+
|
| 90 |
+
wss.on('connection', (ws, req) => {
|
| 91 |
+
try {
|
| 92 |
+
// Simple shared-secret check via query string: /ws?token=...
|
| 93 |
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
| 94 |
+
const token = url.searchParams.get('token') || '';
|
| 95 |
+
if (TERMINAL_TOKEN && token !== TERMINAL_TOKEN) {
|
| 96 |
+
ws.close(1008, 'Invalid token'); // Policy Violation
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
const shell = process.env.SHELL || '/bin/bash';
|
| 101 |
+
|
| 102 |
+
const ptyProcess = pty.spawn(shell, ['-l'], {
|
| 103 |
+
name: 'xterm-256color',
|
| 104 |
+
cols: 80,
|
| 105 |
+
rows: 24,
|
| 106 |
+
cwd: process.env.HOME || process.cwd(),
|
| 107 |
+
env: process.env
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
ptyProcess.onData(data => {
|
| 111 |
+
if (ws.readyState === ws.OPEN) {
|
| 112 |
+
ws.send(JSON.stringify({ type: 'output', data }));
|
| 113 |
+
}
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
ws.on('message', (msg) => {
|
| 117 |
+
try {
|
| 118 |
+
const { type, data, cols, rows } = JSON.parse(msg.toString());
|
| 119 |
+
if (type === 'input' && typeof data === 'string') {
|
| 120 |
+
ptyProcess.write(data);
|
| 121 |
+
} else if (type === 'resize' && Number.isInteger(cols) && Number.isInteger(rows)) {
|
| 122 |
+
ptyProcess.resize(cols, rows);
|
| 123 |
+
}
|
| 124 |
+
} catch (_) {/* ignore malformed */}
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
ws.on('close', () => {
|
| 128 |
+
try { ptyProcess.kill(); } catch (_) {}
|
| 129 |
+
});
|
| 130 |
+
} catch (err) {
|
| 131 |
+
try { ws.close(1011, 'Server error'); } catch (_) {}
|
| 132 |
+
}
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
server.listen(PORT, '0.0.0.0', () => {
|
| 136 |
+
console.log(`Web terminal listening on :${PORT}`);
|
| 137 |
+
});
|