feat(@scripts): ✨ add claude-rc manager and crc launcher
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
commit
a49ea98efd
7 changed files with 465 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.log
|
||||||
|
*.pid
|
||||||
82
README.md
Normal file
82
README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# claude-rc
|
||||||
|
|
||||||
|
Run **Claude Code Remote Control** (`claude rc`) servers so projects are
|
||||||
|
drivable from [claude.ai/code](https://claude.ai/code) and the Claude mobile app
|
||||||
|
— either as always-on, reboot-surviving services, or as ad-hoc throwaways.
|
||||||
|
|
||||||
|
Two tools:
|
||||||
|
|
||||||
|
| Tool | Purpose | Persistence |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `claude-rc` | central **manager** of always-on servers, supervised by `systemd --user` from a registry | survives reboot (linger) + crash (`Restart=always`) |
|
||||||
|
| `crc` | **ad-hoc** headless launcher for one dir on one host | survives terminal drops; dies on reboot |
|
||||||
|
|
||||||
|
Both run `claude rc` **headless** — detached, logged, with `--spawn` and
|
||||||
|
`--permission-mode` preset so it never blocks on the interactive spawn-mode
|
||||||
|
prompt.
|
||||||
|
|
||||||
|
## Why no tmux
|
||||||
|
|
||||||
|
The obvious approach — park `claude rc` in a tmux session — is a trap: a shared
|
||||||
|
user tmux server can be restarted out from under you (e.g. by another
|
||||||
|
supervisor) and your sessions vanish. `claude rc` needs no TTY (it connects fine
|
||||||
|
with stdin closed), so **systemd alone** gives boot-persistence + crash-restart,
|
||||||
|
and `crc` uses a plain logged background process. No tmux anywhere.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./install.sh # symlink bin/*, install the unit, seed registry
|
||||||
|
sudo loginctl enable-linger "$USER" # once, so units start at boot (Linux)
|
||||||
|
claude-rc sync # bring up everything in the registry
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `claude` (Claude Code) on `$PATH` and a logged-in subscription account.
|
||||||
|
|
||||||
|
## Manager — `claude-rc`
|
||||||
|
|
||||||
|
Registry (`~/.config/claude-rc/projects`) is the single source of truth:
|
||||||
|
|
||||||
|
```
|
||||||
|
name=dir # dir relative to $HOME, or absolute, or ~/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entry → a systemd template instance `claude-rc@<name>.service` running
|
||||||
|
`claude rc --name <name>` in `<dir>`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude-rc list # registry + unit state + dir
|
||||||
|
claude-rc status [name] # state + claude.ai/code URL
|
||||||
|
claude-rc url <name> # just the URL
|
||||||
|
claude-rc add <name> <dir> # register + enable --now
|
||||||
|
claude-rc rm <name> # disable + unregister
|
||||||
|
claude-rc sync # reconcile units to the registry
|
||||||
|
claude-rc logs <name> [-f] # journal
|
||||||
|
claude-rc restart|stop|start <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults (override via env in a unit drop-in):
|
||||||
|
- `CLAUDE_RC_SPAWN=worktree` — isolated git worktree per spawned session.
|
||||||
|
- `CLAUDE_RC_PERM=bypassPermissions` — spawned sessions skip permission prompts.
|
||||||
|
|
||||||
|
## Ad-hoc — `crc`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
crc <host> <dir> # launch headless; prints the URL. host=local for here
|
||||||
|
crc <host> <dir> --status | --log | --stop
|
||||||
|
crc <host> <dir> --spawn same-dir --perm default -- <extra claude rc args>
|
||||||
|
```
|
||||||
|
|
||||||
|
`host` is any ssh target, or `local`. State lives under
|
||||||
|
`~/.local/state/claude-rc/` on the target. Re-running reports the existing
|
||||||
|
server instead of duplicating it.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/claude-rc manager (runs on the host; drive remotely over ssh)
|
||||||
|
bin/crc ad-hoc headless launcher
|
||||||
|
units/claude-rc@.service systemd --user template
|
||||||
|
projects.example registry seed
|
||||||
|
install.sh symlinks + unit install + registry seed
|
||||||
|
```
|
||||||
147
bin/claude-rc
Executable file
147
bin/claude-rc
Executable file
|
|
@ -0,0 +1,147 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# claude-rc — central manager for always-on `claude rc` (Remote Control) servers,
|
||||||
|
# supervised by systemd --user on this host. Runs ON the host (apricot); drive it
|
||||||
|
# from elsewhere over ssh (plum's `rc` function forwards here).
|
||||||
|
#
|
||||||
|
# Single source of truth is the registry:
|
||||||
|
# ~/.config/claude-rc/projects # lines: name=dir ('#' comments)
|
||||||
|
# `dir` is relative to $HOME (or absolute, or ~/...). Each entry maps to a
|
||||||
|
# systemd template instance `claude-rc@<name>.service` running `claude rc --name
|
||||||
|
# <name>` in <dir>. systemd + Linger give boot-persistence and Restart=always.
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# list registry + unit state + dir
|
||||||
|
# status [name] state + claude.ai/code URL (all, or one)
|
||||||
|
# url <name> print the environment URL
|
||||||
|
# add <name> <dir> register + enable --now
|
||||||
|
# rm <name> disable + unregister
|
||||||
|
# sync reconcile units to the registry (boot/after edits)
|
||||||
|
# logs <name> [journalctl-args…]
|
||||||
|
# restart|stop|start <name>
|
||||||
|
# _run <name> internal: exec'd by the template unit
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REG=${CLAUDE_RC_REGISTRY:-$HOME/.config/claude-rc/projects}
|
||||||
|
TPL=claude-rc@ # systemd template prefix
|
||||||
|
|
||||||
|
uc() { systemctl --user "$@"; }
|
||||||
|
|
||||||
|
# name -> absolute dir (resolves rel-to-HOME, ~/ and absolute). Empty if absent.
|
||||||
|
reg_dir() {
|
||||||
|
[ -f "$REG" ] || return 0
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in ''|\#*) continue ;; esac
|
||||||
|
[ "${line%%=*}" = "$1" ] || continue
|
||||||
|
d=${line#*=}
|
||||||
|
case "$d" in
|
||||||
|
/*) printf %s "$d" ;;
|
||||||
|
'~/'*) printf %s "$HOME/${d#\~/}" ;;
|
||||||
|
*) printf %s "$HOME/$d" ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
done < "$REG"
|
||||||
|
}
|
||||||
|
|
||||||
|
reg_names() {
|
||||||
|
[ -f "$REG" ] || return 0
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in ''|\#*) continue ;; esac
|
||||||
|
printf '%s\n' "${line%%=*}"
|
||||||
|
done < "$REG"
|
||||||
|
}
|
||||||
|
|
||||||
|
url_of() {
|
||||||
|
journalctl --user -u "$TPL$1" -o cat -n 120 2>/dev/null \
|
||||||
|
| grep -oE 'env_[A-Za-z0-9]+' | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
require() { [ -n "${1:-}" ] || { echo "claude-rc: missing <name>" >&2; exit 2; }; }
|
||||||
|
|
||||||
|
cmd=${1:-list}; [ $# -gt 0 ] && shift || true
|
||||||
|
case "$cmd" in
|
||||||
|
_run)
|
||||||
|
name=${1:-}; require "$name"
|
||||||
|
dir=$(reg_dir "$name")
|
||||||
|
[ -n "$dir" ] || { echo "claude-rc: '$name' not in $REG" >&2; exit 1; }
|
||||||
|
cd "$dir" || { echo "claude-rc: dir missing: $dir" >&2; exit 1; }
|
||||||
|
# --spawn is mandatory for headless operation: without it `claude rc`
|
||||||
|
# prompts "Choose [1/2]" for spawn mode and blocks forever. Default to
|
||||||
|
# worktree (isolated session per spawn — safe for concurrent agents);
|
||||||
|
# override per-instance with CLAUDE_RC_SPAWN=same-dir|session.
|
||||||
|
# --permission-mode sets the mode for spawned sessions; bypassPermissions
|
||||||
|
# so phone/web sessions run without permission prompts (override with
|
||||||
|
# CLAUDE_RC_PERM=default|acceptEdits|plan|...).
|
||||||
|
exec claude rc --name "$name" \
|
||||||
|
--spawn "${CLAUDE_RC_SPAWN:-worktree}" \
|
||||||
|
--permission-mode "${CLAUDE_RC_PERM:-bypassPermissions}"
|
||||||
|
;;
|
||||||
|
list|ls)
|
||||||
|
printf '%-16s %-10s %s\n' NAME STATE DIR
|
||||||
|
reg_names | while read -r n; do
|
||||||
|
printf '%-16s %-10s %s\n' "$n" "$(uc is-active "$TPL$n" 2>/dev/null || echo -)" "$(reg_dir "$n")"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
status|st)
|
||||||
|
show() {
|
||||||
|
env=$(url_of "$1")
|
||||||
|
printf '%-16s %-10s %s\n' "$1" "$(uc is-active "$TPL$1" 2>/dev/null || echo -)" \
|
||||||
|
"${env:+https://claude.ai/code?environment=$env}"
|
||||||
|
}
|
||||||
|
if [ -n "${1:-}" ]; then show "$1"; else reg_names | while read -r n; do show "$n"; done; fi
|
||||||
|
;;
|
||||||
|
url)
|
||||||
|
name=${1:-}; require "$name"
|
||||||
|
env=$(url_of "$name")
|
||||||
|
[ -n "$env" ] && echo "https://claude.ai/code?environment=$env" || { echo "claude-rc: no URL for $name" >&2; exit 1; }
|
||||||
|
;;
|
||||||
|
add)
|
||||||
|
name=${1:-}; dir=${2:-}
|
||||||
|
[ -n "$name" ] && [ -n "$dir" ] || { echo "usage: claude-rc add <name> <dir>" >&2; exit 2; }
|
||||||
|
mkdir -p "$(dirname "$REG")"; touch "$REG"
|
||||||
|
if reg_names | grep -qx "$name"; then
|
||||||
|
echo "claude-rc: '$name' already registered ($(reg_dir "$name"))"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$name" "$dir" >> "$REG"
|
||||||
|
echo "registered $name=$dir"
|
||||||
|
fi
|
||||||
|
uc enable --now "$TPL$name" && echo "enabled+started $TPL$name"
|
||||||
|
;;
|
||||||
|
rm|remove)
|
||||||
|
name=${1:-}; require "$name"
|
||||||
|
uc disable --now "$TPL$name" 2>/dev/null && echo "disabled $TPL$name" || true
|
||||||
|
if [ -f "$REG" ]; then
|
||||||
|
tmp=$(mktemp); grep -v "^$name=" "$REG" > "$tmp" 2>/dev/null || true; mv "$tmp" "$REG"
|
||||||
|
fi
|
||||||
|
echo "unregistered $name"
|
||||||
|
;;
|
||||||
|
sync)
|
||||||
|
# Bring every registered project up…
|
||||||
|
reg_names | while read -r n; do
|
||||||
|
uc enable --now "$TPL$n" >/dev/null 2>&1 && echo "up: $n" || echo "FAIL: $n"
|
||||||
|
done
|
||||||
|
# …and tear down any enabled instance no longer in the registry.
|
||||||
|
uc list-unit-files "${TPL}*.service" --no-legend 2>/dev/null | awk '{print $1}' | while read -r uf; do
|
||||||
|
inst=${uf#"$TPL"}; inst=${inst%.service}
|
||||||
|
[ -n "$inst" ] || continue
|
||||||
|
if ! reg_names | grep -qx "$inst"; then
|
||||||
|
uc disable --now "$TPL$inst" >/dev/null 2>&1 && echo "down: $inst (orphan)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
name=${1:-}; require "$name"; shift || true
|
||||||
|
set -- "$@"
|
||||||
|
[ $# -gt 0 ] || set -- -n 40 --no-pager
|
||||||
|
exec journalctl --user -u "$TPL$name" "$@"
|
||||||
|
;;
|
||||||
|
restart|stop|start)
|
||||||
|
name=${1:-}; require "$name"
|
||||||
|
uc "$cmd" "$TPL$name" && uc is-active "$TPL$name" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "claude-rc: unknown command '$cmd' (try: claude-rc help)" >&2; exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
151
bin/crc
Executable file
151
bin/crc
Executable file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# crc — launch a non-persistent, HEADLESS `claude rc` (Remote Control) server in
|
||||||
|
# a target dir on a target host, and print its claude.ai/code URL.
|
||||||
|
#
|
||||||
|
# Headless: claude rc runs detached with output logged (no tmux, no tty) and
|
||||||
|
# `--spawn` preset, so it never blocks on the interactive spawn-mode prompt. It
|
||||||
|
# survives terminal/ssh drops but NOT a host reboot (non-persistent). For servers
|
||||||
|
# that must survive reboot, register them with the `claude-rc` manager instead.
|
||||||
|
#
|
||||||
|
# Drive the session from claude.ai/code or the Claude mobile app via the printed
|
||||||
|
# URL. Re-running just reports the existing server (never duplicates it).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# crc # apricot.lan, mirror of $PWD
|
||||||
|
# crc <host> # <host>, mirror of $PWD
|
||||||
|
# crc <host> <dir> # <host>, explicit dir (abs path or ~/...)
|
||||||
|
# crc <host> <dir> --stop # stop that server
|
||||||
|
# crc <host> <dir> --status # status + URL only (don't start)
|
||||||
|
# crc <host> <dir> --log # tail its log
|
||||||
|
# crc ... -- <args> # extra args passed to `claude rc`
|
||||||
|
#
|
||||||
|
# host: any ssh target (alias, user@host, IP), or local/./localhost. When <dir>
|
||||||
|
# is omitted, $PWD is mirrored to the same path under the remote's $HOME.
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --spawn <m> worktree | same-dir | session (default: worktree)
|
||||||
|
# --perm <m> permission mode for spawned sessions:
|
||||||
|
# bypassPermissions | default | acceptEdits | plan | dontAsk | auto
|
||||||
|
# (default: bypassPermissions)
|
||||||
|
#
|
||||||
|
# Env: CRC_HOST default host when none given (default: apricot.lan)
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
host=${CRC_HOST:-apricot.lan}
|
||||||
|
action=launch # launch | stop | status | log
|
||||||
|
spawn=worktree
|
||||||
|
perm=bypassPermissions
|
||||||
|
have_host=0
|
||||||
|
dir_set=0
|
||||||
|
dir=''
|
||||||
|
rc_args=''
|
||||||
|
|
||||||
|
usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--stop) action=stop; shift ;;
|
||||||
|
--status) action=status; shift ;;
|
||||||
|
--log) action=log; shift ;;
|
||||||
|
--spawn) [ $# -ge 2 ] || { echo "crc: --spawn needs a value" >&2; exit 2; }; spawn=$2; shift 2 ;;
|
||||||
|
--perm|--permission-mode) [ $# -ge 2 ] || { echo "crc: $1 needs a value" >&2; exit 2; }; perm=$2; shift 2 ;;
|
||||||
|
--) shift; rc_args=$*; break ;;
|
||||||
|
-*) echo "crc: unknown option: $1" >&2; exit 2 ;;
|
||||||
|
*)
|
||||||
|
if [ $have_host -eq 0 ]; then host=$1; have_host=1
|
||||||
|
elif [ $dir_set -eq 0 ]; then dir=$1; dir_set=1
|
||||||
|
else echo "crc: too many arguments: $1" >&2; exit 2
|
||||||
|
fi
|
||||||
|
shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- resolve dir + a stable session name (slug) ----------------------------
|
||||||
|
rel=''
|
||||||
|
abs=''
|
||||||
|
slug_src=''
|
||||||
|
if [ $dir_set -eq 1 ]; then
|
||||||
|
abs=$dir
|
||||||
|
# Normalize so ~/x, $HOME/x and /abs/$HOME/x map to the same slug (the shell
|
||||||
|
# may have expanded ~ to $HOME before crc saw it).
|
||||||
|
case "$dir" in
|
||||||
|
"$HOME"/*) slug_src=${dir#"$HOME"/} ;;
|
||||||
|
'~/'*) slug_src=${dir#\~/} ;;
|
||||||
|
*) slug_src=$dir ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
case "$PWD" in
|
||||||
|
"$HOME") rel='' ; slug_src='home' ;;
|
||||||
|
"$HOME"/*) rel=${PWD#"$HOME"/} ; slug_src=$rel ;;
|
||||||
|
*) rel='' ; slug_src='home' ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
slug=$(printf %s "$slug_src" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^a-z0-9]\{1,\}#-#g' -e 's#^-##' -e 's#-$##')
|
||||||
|
[ -n "$slug" ] || slug='home'
|
||||||
|
name="crc-${slug}"
|
||||||
|
|
||||||
|
# --- remote bootstrap (base64'd to survive every quoting layer) ------------
|
||||||
|
# Runs on the target (local or via ssh). Manages a detached, logged claude rc
|
||||||
|
# keyed by $name under the state dir; idempotent (won't double-launch).
|
||||||
|
bootf=$(mktemp "${TMPDIR:-/tmp}/crc.XXXXXX")
|
||||||
|
trap 'rm -f "$bootf"' EXIT INT TERM
|
||||||
|
cat > "$bootf" <<BOOT
|
||||||
|
set -eu
|
||||||
|
NAME=$(printf %q "$name")
|
||||||
|
REL=$(printf %q "$rel")
|
||||||
|
ABS=$(printf %q "$abs")
|
||||||
|
SPAWN=$(printf %q "$spawn")
|
||||||
|
PERM=$(printf %q "$perm")
|
||||||
|
ACTION=$(printf %q "$action")
|
||||||
|
RC_ARGS=$(printf %q "$rc_args")
|
||||||
|
|
||||||
|
if [ "\$ABS" = "~" ]; then DIR=\$HOME
|
||||||
|
elif [ -n "\$ABS" ] && [ "\${ABS#~/}" != "\$ABS" ]; then DIR="\$HOME/\${ABS#~/}"
|
||||||
|
elif [ -n "\$ABS" ]; then DIR=\$ABS
|
||||||
|
else DIR="\$HOME\${REL:+/\$REL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SD=\${XDG_STATE_HOME:-\$HOME/.local/state}/claude-rc
|
||||||
|
mkdir -p "\$SD"
|
||||||
|
LOG="\$SD/\$NAME.log"; PIDF="\$SD/\$NAME.pid"
|
||||||
|
|
||||||
|
alive() { [ -f "\$PIDF" ] && kill -0 "\$(cat "\$PIDF" 2>/dev/null)" 2>/dev/null; }
|
||||||
|
envid() { grep -aoE 'env_[A-Za-z0-9]+' "\$LOG" 2>/dev/null | tail -1; }
|
||||||
|
report() {
|
||||||
|
if alive; then
|
||||||
|
e=\$(envid)
|
||||||
|
echo "\$NAME: running (pid \$(cat "\$PIDF")) in \$DIR"
|
||||||
|
if [ -n "\$e" ]; then echo " https://claude.ai/code?environment=\$e"
|
||||||
|
else echo " (URL not in log yet — retry: crc ... --status)"; fi
|
||||||
|
else
|
||||||
|
echo "\$NAME: not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "\$ACTION" in
|
||||||
|
stop)
|
||||||
|
if alive; then kill "\$(cat "\$PIDF")" 2>/dev/null && echo "stopped \$NAME"; else echo "\$NAME not running"; fi
|
||||||
|
rm -f "\$PIDF" ;;
|
||||||
|
status) report ;;
|
||||||
|
log) [ -f "\$LOG" ] && tail -n 40 "\$LOG" || echo "no log: \$LOG" ;;
|
||||||
|
launch)
|
||||||
|
if alive; then
|
||||||
|
echo "crc: \$NAME already running — reporting existing server"
|
||||||
|
else
|
||||||
|
cd "\$DIR" 2>/dev/null || { echo "crc: directory not found: \$DIR" >&2; exit 1; }
|
||||||
|
: > "\$LOG"
|
||||||
|
nohup claude rc --name "\$NAME" --spawn "\$SPAWN" --permission-mode "\$PERM" \$RC_ARGS </dev/null >>"\$LOG" 2>&1 &
|
||||||
|
echo \$! > "\$PIDF"
|
||||||
|
i=0; while [ \$i -lt 20 ] && [ -z "\$(envid)" ] && alive; do sleep 1; i=\$((i+1)); done
|
||||||
|
fi
|
||||||
|
report ;;
|
||||||
|
esac
|
||||||
|
BOOT
|
||||||
|
boot_b64=$(base64 < "$bootf" | tr -d '\n')
|
||||||
|
run_remote="printf %s '${boot_b64}' | base64 -d | sh"
|
||||||
|
|
||||||
|
case "$host" in
|
||||||
|
local|localhost|.) eval "$run_remote" ;;
|
||||||
|
*) ssh "$host" "$run_remote" ;;
|
||||||
|
esac
|
||||||
52
install.sh
Normal file
52
install.sh
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# install.sh — install the claude-rc system on this host (idempotent).
|
||||||
|
#
|
||||||
|
# - symlinks bin/* into the first of ~/.local/bin | ~/bin that is on $PATH
|
||||||
|
# - installs the systemd --user template unit (copied, so `systemctl enable`
|
||||||
|
# can manage its own symlinks cleanly)
|
||||||
|
# - seeds ~/.config/claude-rc/projects from projects.example if absent
|
||||||
|
#
|
||||||
|
# After install: enable linger once (so units start at boot without login):
|
||||||
|
# sudo loginctl enable-linger "$USER"
|
||||||
|
# then bring the registered servers up:
|
||||||
|
# claude-rc sync
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
|
||||||
|
# --- bin symlinks ----------------------------------------------------------
|
||||||
|
target=""
|
||||||
|
for c in "$HOME/.local/bin" "$HOME/bin"; do
|
||||||
|
case ":$PATH:" in *":$c:"*) target=$c; break ;; esac
|
||||||
|
done
|
||||||
|
[ -n "$target" ] || target="$HOME/.local/bin"
|
||||||
|
mkdir -p "$target"
|
||||||
|
for f in "$repo"/bin/*; do
|
||||||
|
ln -sfn "$f" "$target/$(basename "$f")"
|
||||||
|
echo "link: $target/$(basename "$f")"
|
||||||
|
done
|
||||||
|
case ":$PATH:" in *":$target:"*) ;; *) echo "note: add $target to PATH" ;; esac
|
||||||
|
|
||||||
|
# --- systemd --user template unit ------------------------------------------
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
ud="$HOME/.config/systemd/user"
|
||||||
|
mkdir -p "$ud"
|
||||||
|
cp "$repo/units/claude-rc@.service" "$ud/claude-rc@.service"
|
||||||
|
echo "copy: $ud/claude-rc@.service"
|
||||||
|
systemctl --user daemon-reload && echo "ok: systemd --user daemon-reloaded"
|
||||||
|
else
|
||||||
|
echo "note: systemctl --user not available — persistent units are Linux-only"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- registry --------------------------------------------------------------
|
||||||
|
reg="$HOME/.config/claude-rc/projects"
|
||||||
|
if [ ! -f "$reg" ]; then
|
||||||
|
mkdir -p "$(dirname "$reg")"
|
||||||
|
cp "$repo/projects.example" "$reg"
|
||||||
|
echo "seed: $reg (edit it, then: claude-rc sync)"
|
||||||
|
else
|
||||||
|
echo "ok: registry exists: $reg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "done. next: 'sudo loginctl enable-linger $USER' (boot-start) then 'claude-rc sync'"
|
||||||
9
projects.example
Normal file
9
projects.example
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# claude-rc registry — one project per line: name=dir
|
||||||
|
# dir is relative to $HOME (or absolute, or ~/...).
|
||||||
|
# '#' starts a comment; blank lines are ignored.
|
||||||
|
# Edit with `claude-rc add <name> <dir>` / `claude-rc rm <name>`, or by hand
|
||||||
|
# then `claude-rc sync`. install.sh seeds ~/.config/claude-rc/projects from this
|
||||||
|
# file if none exists.
|
||||||
|
magic-civ=Code/@projects/@magic-civilization
|
||||||
|
cocottetech=Code/@projects/@cocottetech
|
||||||
|
lp=Code/@projects/@lilith/lilith-platform.live
|
||||||
22
units/claude-rc@.service
Normal file
22
units/claude-rc@.service
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[Unit]
|
||||||
|
Description=claude rc (Remote Control) server — %i
|
||||||
|
Documentation=man:claude(1)
|
||||||
|
After=network-online.target time-sync.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Centrally managed by `claude-rc` (registry: ~/.config/claude-rc/projects).
|
||||||
|
# Direct under systemd (no tmux — a shared tmux server gets wiped). systemd +
|
||||||
|
# Linger give boot-persistence; Restart=always gives crash-recovery. The
|
||||||
|
# claude.ai/code URL lands in the journal: journalctl --user -u claude-rc@%i
|
||||||
|
Type=simple
|
||||||
|
Environment=PATH=%h/.local/bin:%h/.local/share/pnpm:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin
|
||||||
|
ExecStart=%h/.local/bin/claude-rc _run %i
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=claude-rc-%i
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Loading…
Add table
Reference in a new issue