feat: 导出 SproutClaw .sproutclaw 配置

包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。
排除 node_modules、npm 缓存、sessions 等运行时数据。
This commit is contained in:
2026-06-26 15:48:56 +08:00
commit 50edff80f5
13904 changed files with 411646 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
---
# Linux SSH Operator
## Overview
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login, tunnels).
## Workflow
1. Confirm authorization and the target (host, port, user).
2. Prefer SSH keys (recommended) for non-interactive runs; avoid storing passwords in files or chat logs.
3. Start with read-only checks, then apply changes, then verify.
4. If a password prompt or interactive tool is required, run the SSH command in a real terminal/TTY (or enable TTY in your runner).
## Quick Start
### Set up SSH keys (recommended)
Generate a key (ed25519):
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
Install the public key on the server (example uses port 22):
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
Optional: create an alias in `~/.ssh/config`:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
Then connect:
```bash
ssh my-server
```
### Bootstrap alias + passwordless login
When the user provides an alias, host/IP, and password and wants `ssh alias` to work immediately:
1. Write or update the alias with the helper script:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
```
2. If `ssh-copy-id` prompts for a password, run in a TTY and enter the password once.
3. Verify passwordless login:
```bash
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
```
4. Remind the user that `scp` and `rsync` can reuse the same alias:
```bash
scp ./file.txt my-server:/root/
rsync -av ./dir/ my-server:/root/dir/
```
Notes:
- `scripts/ssh_alias_setup.sh` auto-generates `~/.ssh/id_ed25519` if it does not exist.
- It updates `~/.ssh/config` idempotently and defaults to `root`, port `22`, and `StrictHostKeyChecking=accept-new`.
- For tests, set `SSH_CONFIG_FILE=/path/to/temp-config`.
### Run remote commands
- Direct:
```bash
ssh my-server uname -a
```
- With sudo (often needs a TTY):
```bash
ssh -tt my-server sudo systemctl status nginx --no-pager
```
- Via wrapper script (consistent options; supports env defaults like `REMOTE_USER`, `REMOTE_PORT`, `REMOTE_KEY`):
- If you installed this Skill globally in `~/.claude/skills/`, use the absolute paths below (recommended so both Claude Code + OpenCode can find it).
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
```
### Transfer files
Upload (scp):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
Download (scp):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
Incremental sync (rsync — preferred for directories or large files):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
```
### SSH Tunneling / Port Forwarding
Local forward (access remote service on a local port):
```bash
# Access remote MySQL (3306) at localhost:13306
ssh -L 13306:127.0.0.1:3306 my-server -N
```
Remote forward (expose a local port on the server):
```bash
# Make local :8080 accessible on server's :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
Jump host (bastion → target):
```bash
ssh -J bastion-user@bastion:22 target-user@target-host
```
Or configure in `~/.ssh/config`:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
### SSH Multiplexing (faster repeated connections)
Add to `~/.ssh/config` to reuse the same TCP connection:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
The first `ssh my-server` opens a master connection; subsequent connections reuse it without re-handshaking.
## Common Ops (snippets)
- Disk: `df -h`, `du -sh /path/* | sort -h`
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
- Services: `systemctl status|restart|stop|enable SERVICE`
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
- Docker: `docker ps`, `docker logs -f CONTAINER`, `docker exec -it CONTAINER bash`
- Config test: `nginx -t`, `sshd -T`
- Open files: `lsof -i :PORT`, `fuser PORT/tcp`
## Safety
- Never store or paste passwords in repo files or chat logs.
- Avoid `StrictHostKeyChecking=no`; prefer verifying host keys (or use `accept-new` only when appropriate).
- For destructive commands (rm, shutdown, firewall changes), ask for explicit user confirmation and show the exact command first.
## References
- SSH security + troubleshooting: `references/ssh-playbook.md`
## Scripts
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
- `scripts/ssh_run.sh`: run remote commands with consistent options (supports `--compress`, `--tty`, `--sudo`).
- `scripts/ssh_copy.sh`: push/pull files via scp or rsync (use `--rsync` for incremental transfers).

View File

@@ -0,0 +1,270 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
---
# Linux SSH 运维助手(中文说明)
> 本文件是 `SKILL.md` 的中文说明版,便于阅读与维护;技能实际触发仍以 `SKILL.md` 为准。
## 概述
这个 Skill 用于通过 SSH 安全地连接 Linux 服务器,并执行可重复、可验证的远程操作,包括:
- 执行远程命令
- 查看系统与服务日志
- 管理 `systemd` 服务
- 上传和下载文件scp 或 rsync
- 配置 SSH 别名
- 安装公钥并启用免密登录
- SSH 隧道 / 端口转发 / 跳板机
- 进行常见运维排障
适合以下场景:
- 登录远程 Linux 服务器
- 在服务器上运行命令
- 查看 `journalctl` / 应用日志
- 启动、停止、重启服务
- 把文件部署到服务器scp 或 rsync 增量同步)
- 从服务器拉取日志、配置或产物文件
- 按"别名 + IP + 密码"快速配置一键 SSH 免密登录
- 配置端口转发访问内网服务
- 通过跳板机/堡垒机连接内网服务器
## 工作流程
1. 先确认授权,以及目标信息:主机、端口、用户名。
2. 优先使用 SSH 密钥进行非交互连接,避免在聊天记录或文件里保存密码。
3. 优先先做只读检查,再执行修改,最后进行验证。
4. 如果需要密码提示或交互式工具,使用真实终端或启用 TTY。
## 快速开始
### 配置 SSH 密钥(推荐)
生成一把 `ed25519` 密钥:
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
把公钥安装到目标服务器(示例端口为 `22`
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
可选:在 `~/.ssh/config` 中配置主机别名:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
之后可直接连接:
```bash
ssh my-server
```
### 一键配置别名 + 免密登录
当用户提供"别名、主机/IP、密码",并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
1. 使用辅助脚本写入或更新 SSH 别名:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
```
2. 如果 `ssh-copy-id` 提示输入密码,请在 TTY 中输入一次密码。
3. 安装完成后验证免密登录:
```bash
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
```
4. 告诉用户 `scp``rsync` 也可以直接复用这个别名:
```bash
scp ./file.txt my-server:/root/
rsync -av ./dir/ my-server:/root/dir/
```
补充说明:
- `scripts/ssh_alias_setup.sh` 在本机没有 `~/.ssh/id_ed25519` 时会自动生成。
- 它会幂等更新 `~/.ssh/config`,默认使用 `root`、端口 `22``StrictHostKeyChecking=accept-new`
- 如果要做临时测试,可以设置 `SSH_CONFIG_FILE=/path/to/temp-config`
### 运行远程命令
直接执行:
```bash
ssh my-server uname -a
```
`sudo` 执行(通常需要 TTY
```bash
ssh -tt my-server sudo systemctl status nginx --no-pager
```
使用封装脚本执行(统一参数风格,支持环境变量默认值如 `REMOTE_USER``REMOTE_PORT``REMOTE_KEY`
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
```
### 传输文件
上传文件scp
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
下载文件scp
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
增量同步rsync推荐用于目录或大文件
```bash
# 上传目录
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
# 上传并删除远端多余文件
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
# 下载目录
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
```
### SSH 隧道 / 端口转发
本地转发(将远程服务映射到本地端口):
```bash
# 把远程 MySQL(3306) 映射到本地 13306
ssh -L 13306:127.0.0.1:3306 my-server -N
```
远程转发(将本地服务暴露到服务器端口):
```bash
# 把本地 :8080 映射到服务器 :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
跳板机 / 堡垒机:
```bash
ssh -J bastion-user@bastion:22 target-user@target-host
```
或在 `~/.ssh/config` 中配置:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
### SSH 多路复用(加速重复连接)
`~/.ssh/config` 中添加,复用同一 TCP 连接:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
第一次 `ssh my-server` 建立主连接,后续连接直接复用,无需重新握手,速度更快。
## 常见运维操作
- 磁盘空间:`df -h`
- 目录占用:`du -sh /path/* | sort -h`
- 内存查看:`free -h`
- 进程查看:`ps aux --sort=-%mem | head`
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
- 日志跟踪:`tail -n 200 -f /path/log`
- 服务管理:`systemctl status|restart|stop|enable SERVICE`
- 网络排查:`ss -lntp``ip a``ip r``curl -v http://127.0.0.1:PORT/`
- Docker`docker ps``docker logs -f CONTAINER``docker exec -it CONTAINER bash`
- 配置测试:`nginx -t``sshd -T`
- 端口占用:`lsof -i :PORT``fuser PORT/tcp`
## 安全注意事项
- 不要把密码保存到仓库文件、脚本或聊天记录中。
- 尽量避免使用 `StrictHostKeyChecking=no`
- 首次连接建议核验主机指纹;只有在临时环境自动化场景下,才考虑 `accept-new`
- 对于破坏性命令(如删除、关机、防火墙调整),必须先确认再执行,并展示将要运行的确切命令。
## 附带脚本
### `scripts/ssh_alias_setup.sh`
用于创建或更新 SSH 别名,并可选执行 `ssh-copy-id`,支持:
- 指定别名、主机、用户、端口、私钥
- 自动生成默认的 `~/.ssh/id_ed25519`
- 幂等更新 `~/.ssh/config`
- 可选直接运行 `ssh-copy-id` 安装公钥
- 使用 `--dry-run` 预览即将执行的动作
### `scripts/ssh_run.sh`
用于通过 SSH 执行远程命令,支持:
- 指定用户、端口、私钥
- 设置连接超时
- 启用 TTY`--tty`
- 启用 SSH 压缩(`--compress`,适合慢速链路)
- 使用 `sudo``sudo -n`
- 使用 `--accept-new` 处理首次连接主机
- 使用 `--dry-run` 预览实际将执行的命令
默认可读取这些环境变量:
- `REMOTE_USER`
- `REMOTE_PORT`
- `REMOTE_KEY`
- `REMOTE_CONNECT_TIMEOUT`
### `scripts/ssh_copy.sh`
用于通过 `scp``rsync` 上传或下载文件,支持:
- `push` 上传文件到远程服务器
- `pull` 从远程服务器下载文件
- `--rsync`:使用 rsync 增量同步(显示进度,适合目录和大文件)
- `--delete`:配合 `--rsync push` 删除远端多余文件
- 递归复制目录scp 模式用 `-r`
- 指定用户、端口、私钥
- 使用 `--accept-new`
- 使用 `--dry-run` 预览命令
默认可读取这些环境变量:
- `REMOTE_USER`
- `REMOTE_PORT`
- `REMOTE_KEY`
## 参考资料
- `references/ssh-playbook.md`SSH 安全建议、常见命令、主机指纹处理与故障排查说明。

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Linux SSH Operator"
short_description: "Operate Linux servers, SSH aliases, and passwordless login"

View File

@@ -0,0 +1,227 @@
# SSH playbook (Linux server ops)
## Defaults and conventions
- Prefer SSH keys (ed25519) and `~/.ssh/config` aliases for repeatable runs.
- Avoid putting passwords in files, prompts, or chat logs. If password auth is required, use an interactive terminal/TTY.
- Start with read-only inspection, then apply changes, then verify.
Recommended env vars for wrappers:
- `REMOTE_USER`: default SSH user
- `REMOTE_PORT`: default SSH port (usually 22)
- `REMOTE_KEY`: path to identity file (private key)
- `REMOTE_CONNECT_TIMEOUT`: connect timeout seconds
## SSH key setup (recommended)
Generate a new key:
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
Copy the public key to the server:
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
Add a host alias:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
## Common tasks
### Connectivity and OS info
```bash
ssh my-server "whoami && hostname && uname -a"
ssh my-server "cat /etc/os-release"
```
### Disk and memory
```bash
ssh my-server "df -h"
ssh my-server "free -h"
ssh my-server "du -sh /var/log/* | sort -h | tail"
```
### Processes and ports
```bash
ssh my-server "ps aux --sort=-%mem | head"
ssh my-server "ss -lntp"
ssh my-server "lsof -i :8080"
```
### Logs (systemd)
```bash
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
ssh my-server "journalctl -u SERVICE -f --no-pager"
ssh my-server "journalctl -u SERVICE --since '1 hour ago' --no-pager"
```
### Services (systemd)
Status:
```bash
ssh my-server "systemctl status SERVICE --no-pager"
```
Restart (often needs sudo and TTY):
```bash
ssh -tt my-server "sudo systemctl restart SERVICE"
```
Non-interactive sudo (fails if a password prompt would be required):
```bash
ssh my-server "sudo -n systemctl restart SERVICE"
```
### Docker
```bash
ssh my-server "docker ps"
ssh my-server "docker logs --tail=200 -f CONTAINER"
ssh my-server "docker exec -it CONTAINER bash"
ssh my-server "docker stats --no-stream"
```
### File editing shortcuts
```bash
# View a config file
ssh my-server "cat /etc/nginx/nginx.conf"
# Quick in-place line replacement
ssh my-server "sed -i 's/old/new/g' /path/to/file"
# Append a line
ssh my-server "echo 'new line' >> /path/to/file"
```
## SSH Tunneling / Port Forwarding
### Local forward
Access a remote service on a local port (server-side port does not need to be publicly open):
```bash
# remote MySQL → localhost:13306
ssh -L 13306:127.0.0.1:3306 my-server -N
# remote Redis → localhost:16379
ssh -L 16379:127.0.0.1:6379 my-server -N
```
Add `-f` to background the tunnel:
```bash
ssh -fNL 13306:127.0.0.1:3306 my-server
```
### Remote forward
Expose a local service on the server (useful for demos or webhooks):
```bash
# local :8080 → server :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
### Jump host / bastion
```bash
# Single jump
ssh -J bastion user@target
# Multi-hop
ssh -J jump1,jump2 user@target
```
`~/.ssh/config` version:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
## SSH Multiplexing
Reuse the same TCP connection for multiple SSH sessions — avoids repeated handshakes:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
Check active master sockets:
```bash
ls ~/.ssh/ctrl-*
```
Close a master socket manually:
```bash
ssh -O exit my-server
```
## Safer host key handling
- Prefer verifying the host key fingerprint out-of-band on first connect.
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
Remove a stale known_hosts entry:
```bash
ssh-keygen -R SERVER_IP
```
## Troubleshooting quick hits
| Symptom | Likely cause | Fix |
|---|---|---|
| `Permission denied (publickey)` | Wrong user/key, pub key not on server, sshd config | Check `~/.ssh/authorized_keys` on server; verify key matches |
| `Connection timed out` | Firewall/security group, wrong port, server down | Check port, security group, ping |
| `No route to host` | Network path missing (VPN, ACL, subnet) | Check VPN, routing table |
| `Host key changed` warning | Server rebuilt, or MITM | Verify with out-of-band channel; `ssh-keygen -R HOST` if expected |
| `Too many authentication failures` | SSH agent offering too many keys | Use `-o IdentitiesOnly=yes -i ~/.ssh/id_ed25519` |
| `sudo: a terminal is required` | No TTY allocated | Use `ssh -tt` or pass `-t` to wrapper |
### Debug connection verbosely
```bash
ssh -vvv my-server
```
### Check sshd config on server
```bash
ssh my-server "sudo sshd -T | grep -E 'pubkeyauthentication|passwordauthentication|permitrootlogin'"
```
### Check authorized_keys permissions (common cause of publickey failure)
```bash
ssh my-server "ls -la ~/.ssh/ && cat ~/.ssh/authorized_keys"
# .ssh should be 700, authorized_keys should be 600
```

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Configure an SSH alias and optionally install the current public key for passwordless login.
Usage:
ssh_alias_setup.sh [options] ALIAS HOST
Options:
-u, --user USER SSH user (default: root)
-p, --port PORT SSH port (default: 22)
-i, --key PATH Identity file (default: ~/.ssh/id_ed25519)
--install-key Run ssh-copy-id after writing the alias
--no-accept-new Disable StrictHostKeyChecking=accept-new
--dry-run Print planned actions without changing files
-h, --help Show help
Environment:
SSH_CONFIG_FILE Override SSH config path for testing
REMOTE_KEY Default key path
REMOTE_PORT Default SSH port
Examples:
ssh_alias_setup.sh bigmengya 192.168.1.233 --install-key
ssh_alias_setup.sh -u ubuntu -p 2222 devbox 10.0.0.8
USAGE
}
expand_path() {
local value="$1"
case "$value" in
~) printf '%s\n' "$HOME" ;;
~/*) printf '%s/%s\n' "$HOME" "${value#~/}" ;;
*) printf '%s\n' "$value" ;;
esac
}
user="root"
port="${REMOTE_PORT:-22}"
key="${REMOTE_KEY:-$HOME/.ssh/id_ed25519}"
accept_new=true
install_key=false
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
--install-key)
install_key=true
shift
;;
--no-accept-new)
accept_new=false
shift
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
if [[ $# -lt 2 ]]; then
usage >&2
exit 2
fi
alias_name="$1"
host="$2"
key="$(expand_path "$key")"
config_file="$(expand_path "${SSH_CONFIG_FILE:-$HOME/.ssh/config}")"
ssh_dir="$(dirname "$config_file")"
key_pub="${key}.pub"
key_dir="$(dirname "$key")"
if $dry_run; then
echo "Would ensure directory: $ssh_dir"
echo "Would ensure key directory: $key_dir"
else
mkdir -p "$ssh_dir" "$key_dir"
chmod 700 "$ssh_dir" "$key_dir"
fi
if [[ ! -f "$key" ]]; then
if $dry_run; then
printf 'Would generate key: ssh-keygen -t ed25519 -C %q -f %q -N %q\n' "codex" "$key" ""
else
ssh-keygen -t ed25519 -C "codex" -f "$key" -N ""
fi
fi
if ! $dry_run && [[ ! -f "$key_pub" ]]; then
echo "Public key not found: $key_pub" >&2
exit 1
fi
if $dry_run; then
cat <<DRYRUN
Would write/update SSH alias:
Host $alias_name
HostName $host
User $user
Port $port
ServerAliveInterval 30
ServerAliveCountMax 3
ConnectTimeout 10
StrictHostKeyChecking $( $accept_new && printf 'accept-new' || printf 'ask' )
IdentityFile $key
IdentitiesOnly yes
DRYRUN
else
python3 - "$config_file" "$alias_name" "$host" "$user" "$port" "$key" "$accept_new" <<'PY'
from pathlib import Path
import sys
config_path = Path(sys.argv[1])
alias_name = sys.argv[2]
host = sys.argv[3]
user = sys.argv[4]
port = sys.argv[5]
key = sys.argv[6]
accept_new = sys.argv[7].lower() == 'true'
existing = config_path.read_text(encoding='utf-8') if config_path.exists() else ''
lines = existing.splitlines()
out: list[str] = []
skip = False
for line in lines:
stripped = line.strip()
if stripped.lower().startswith('host '):
host_names = stripped[5:].split()
if alias_name in host_names:
skip = True
continue
skip = False
out.append(line)
continue
if skip:
continue
out.append(line)
block = [
f'Host {alias_name}',
f' HostName {host}',
f' User {user}',
f' Port {port}',
' ServerAliveInterval 30',
' ServerAliveCountMax 3',
' ConnectTimeout 10',
f' StrictHostKeyChecking {"accept-new" if accept_new else "ask"}',
f' IdentityFile {key}',
' IdentitiesOnly yes',
]
text = '\n'.join(out).strip()
if text:
text += '\n\n'
text += '\n'.join(block) + '\n'
config_path.write_text(text, encoding='utf-8')
PY
chmod 600 "$config_file"
fi
printf 'Alias ready: ssh %s\n' "$alias_name"
printf 'Config file: %s\n' "$config_file"
if $install_key; then
if ! command -v ssh-copy-id >/dev/null 2>&1; then
echo 'ssh-copy-id is required but not installed.' >&2
exit 1
fi
if ! [ -t 0 ]; then
echo 'Warning: no TTY detected; password prompt may fail. Prefer running with a terminal/TTY.' >&2
fi
if $dry_run; then
printf 'Would run: ssh-copy-id -i %q %q\n' "$key_pub" "$alias_name"
else
ssh-copy-id -i "$key_pub" "$alias_name"
fi
fi

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Copy files via scp or rsync with consistent options.
Usage:
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
ssh_copy.sh [options] pull HOST REMOTE_PATH LOCAL_PATH
Options:
-u, --user USER Override SSH user (or set REMOTE_USER)
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-r, --recursive Copy directories recursively (scp mode only; rsync always recurses)
--rsync Use rsync instead of scp (incremental, shows progress)
--delete With --rsync push: delete remote files not present in source
--accept-new Set StrictHostKeyChecking=accept-new
--dry-run Print the command that would run
-h, --help Show help
Environment defaults:
REMOTE_USER, REMOTE_PORT, REMOTE_KEY
Examples:
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
USAGE
}
port="${REMOTE_PORT:-22}"
user="${REMOTE_USER:-}"
key="${REMOTE_KEY:-}"
recursive=false
use_rsync=false
delete_mode=false
accept_new=false
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user) user="${2:-}"; shift 2 ;;
-p|--port) port="${2:-}"; shift 2 ;;
-i|--key) key="${2:-}"; shift 2 ;;
-r|--recursive) recursive=true; shift ;;
--rsync) use_rsync=true; shift ;;
--delete) delete_mode=true; shift ;;
--accept-new) accept_new=true; shift ;;
--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*) break ;;
esac
done
if [[ $# -lt 4 ]]; then
usage >&2
exit 2
fi
direction="$1"; shift
host="$1"; shift
dest_host="$host"
if [[ -n "$user" ]]; then
host_no_user="${host#*@}"
dest_host="${user}@${host_no_user}"
fi
if $use_rsync; then
if ! command -v rsync >/dev/null 2>&1; then
echo "rsync is required but not installed." >&2
exit 1
fi
ssh_parts=("ssh" "-p" "$port")
if [[ -n "$key" ]]; then
ssh_parts+=("-i" "$key" "-o" "IdentitiesOnly=yes")
fi
if $accept_new; then
ssh_parts+=("-o" "StrictHostKeyChecking=accept-new")
fi
rsync_opts=(-avz --progress -e "${ssh_parts[*]}")
case "$direction" in
push)
local_path="$1"
remote_path="$2"
if $delete_mode; then
rsync_opts+=(--delete)
fi
full_cmd=(rsync "${rsync_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(rsync "${rsync_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
else
scp_opts=(-P "$port" -p)
if [[ -n "$key" ]]; then
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $recursive; then
scp_opts+=(-r)
fi
if $accept_new; then
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
case "$direction" in
push)
local_path="$1"
remote_path="$2"
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
fi
if $dry_run; then
printf '%q ' "${full_cmd[@]}"
printf '\n'
exit 0
fi
"${full_cmd[@]}"

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Run a remote command over SSH with consistent, script-friendly options.
Usage:
ssh_run.sh [options] HOST -- COMMAND [ARG...]
ssh_run.sh [options] HOST # interactive shell
Options:
-u, --user USER Override SSH user (or set REMOTE_USER)
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
-C, --compress Enable SSH compression (useful on slow links)
--accept-new Set StrictHostKeyChecking=accept-new
--sudo Prefix command with sudo --
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
--connect-timeout SEC Connect timeout (default: REMOTE_CONNECT_TIMEOUT or 10)
--dry-run Print the ssh command that would run
-h, --help Show help
Environment defaults:
REMOTE_USER, REMOTE_PORT, REMOTE_KEY, REMOTE_CONNECT_TIMEOUT
Examples:
ssh_run.sh --user ubuntu 10.0.0.1 -- uname -a
ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
USAGE
}
port="${REMOTE_PORT:-22}"
user="${REMOTE_USER:-}"
key="${REMOTE_KEY:-}"
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
tty=false
compress=false
accept_new=false
sudo_mode=""
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
-t|--tty)
tty=true
shift
;;
-C|--compress)
compress=true
shift
;;
--accept-new)
accept_new=true
shift
;;
--sudo)
sudo_mode="sudo"
shift
;;
--sudo-non-interactive)
sudo_mode="sudo-n"
shift
;;
--connect-timeout)
connect_timeout="${2:-}"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
host="$1"
shift
dest="$host"
if [[ -n "$user" ]]; then
host_no_user="${host#*@}"
dest="${user}@${host_no_user}"
fi
ssh_opts=(
-p "$port"
-o "ConnectTimeout=${connect_timeout}"
-o "ServerAliveInterval=30"
-o "ServerAliveCountMax=3"
)
if [[ -n "$key" ]]; then
ssh_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $accept_new; then
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
if $compress; then
ssh_opts+=(-C)
fi
if $tty; then
ssh_opts+=(-tt)
fi
cmd=("$@")
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
if [[ "$sudo_mode" == "sudo-n" ]]; then
cmd=("sudo" "-n" "--" "${cmd[@]}")
else
cmd=("sudo" "--" "${cmd[@]}")
fi
fi
full_cmd=(ssh "${ssh_opts[@]}" "$dest")
if [[ ${#cmd[@]} -gt 0 ]]; then
full_cmd+=("${cmd[@]}")
fi
if $dry_run; then
printf '%q ' "${full_cmd[@]}"
printf '\n'
exit 0
fi
"${full_cmd[@]}"