Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74927a7640 |
@@ -157,6 +157,8 @@ jobs:
|
||||
steps:
|
||||
# CT 126's webhook listener runs /usr/lib/devdesk/devdesk-deploy, which
|
||||
# waits for this version in the apt registry and upgrades the package.
|
||||
# The hook answers when the install is done (HTTP 500 if it failed), so
|
||||
# this job's result is the deploy's result.
|
||||
- name: Trigger devdesk-deploy webhook
|
||||
env:
|
||||
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
|
||||
@@ -164,12 +166,15 @@ jobs:
|
||||
run: |
|
||||
PAYLOAD="{\"ref\":\"${GIT_REF}\"}"
|
||||
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
|
||||
curl -sf --connect-timeout 10 \
|
||||
code=$(curl -sS --connect-timeout 10 --max-time 900 -o /tmp/deploy.out -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Gitea-Signature: ${SIG}" \
|
||||
-d "$PAYLOAD" \
|
||||
"http://10.10.10.58:9000/hooks/devdesk-deploy"
|
||||
"http://10.10.10.58:9000/hooks/devdesk-deploy") || code=000
|
||||
cat /tmp/deploy.out 2>/dev/null || true
|
||||
echo "webhook: HTTP $code"
|
||||
[ "$code" = 200 ]
|
||||
|
||||
notify-failure:
|
||||
name: Notify on failure
|
||||
|
||||
+10
-4
@@ -141,11 +141,17 @@ cmd_rename() {
|
||||
if dd_server_up && dd_tmux has-session -t "=$old" 2>/dev/null; then running=1; fi
|
||||
[ "$running" = 1 ] || [ -n "$(dd_registry_get "$old" dir)" ] || dd_die "no session named '$old'"
|
||||
! session_exists "$new" || dd_die "a session named '$new' already exists"
|
||||
if [ "$running" = 1 ]; then
|
||||
dd_tmux rename-session -t "=$old" "$new"
|
||||
dd_tmux set-environment -t "=$new" DEVDESK_SESSION "$new"
|
||||
dd_registry_locked rename_locked "$old" "$new" "$running" || dd_die "could not rename '$old'"
|
||||
}
|
||||
|
||||
# tmux and the registry change together, under the registry lock (see
|
||||
# devdesk-session-hook).
|
||||
rename_locked() {
|
||||
if [ "$3" = 1 ]; then
|
||||
dd_tmux rename-session -t "=$1" "$2" || return 1
|
||||
dd_tmux set-environment -t "=$2" DEVDESK_SESSION "$2"
|
||||
fi
|
||||
dd_registry_rename "$old" "$new"
|
||||
dd_registry_rename "$1" "$2"
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
|
||||
Vendored
+25
@@ -1,3 +1,28 @@
|
||||
devdesk (0.9.1) bookworm; urgency=medium
|
||||
|
||||
* Robustness (#12):
|
||||
- A resume that fails (an early Ctrl+C, a network error) no longer
|
||||
replaces the session's recorded conversation with a new one: Claude
|
||||
starts fresh only when the conversation's file is really gone;
|
||||
otherwise the menu offers to try again.
|
||||
- The SessionStart hook reads the session name under the registry lock
|
||||
that rename holds, so renaming during /clear or a restart can't leave
|
||||
the conversation id under the old name.
|
||||
- Env files are parsed the way systemd parses them instead of being
|
||||
sourced: values are literal (no $ expansion or command substitution),
|
||||
and bad lines are reported and skipped.
|
||||
- Deploys wait for the dpkg lock (120 s), log an explicit "deploy
|
||||
FAILED" line, and the webhook answers with the result, so CI's deploy
|
||||
job fails (and alerts) when the install fails. The deploy log is
|
||||
rotated monthly.
|
||||
- postinst removes the duplicate tmux terminal-features entries left by
|
||||
upgrades before 0.3.1.
|
||||
- Temp files of hooks killed mid-write are swept.
|
||||
- NPM now sends X-Frame-Options DENY and CSP frame-ancestors 'none' for
|
||||
dev.lotusguild.org (proxy config, not part of the package).
|
||||
|
||||
-- Jared Vititoe <jjvititoe1@gmail.com> Sun, 11 Oct 2026 01:01:33 -0400
|
||||
|
||||
devdesk (0.9.0) bookworm; urgency=medium
|
||||
|
||||
* Search past conversations (#24): every Claude conversation on this host
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/var/log/devdesk-deploy.log {
|
||||
monthly
|
||||
rotate 6
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
Vendored
+7
@@ -13,6 +13,13 @@ configure)
|
||||
# Apply tmux.conf changes to a running server without restarting it.
|
||||
if [ -S /run/devdesk/tmux.sock ]; then
|
||||
tmux -N -S /run/devdesk/tmux.sock source-file /usr/share/devdesk/tmux.conf >/dev/null 2>&1 || true
|
||||
# Before 0.3.1 every upgrade appended another copy of DevDesk's
|
||||
# terminal-features entry; tmux.conf now sets index 90. Drop the copies.
|
||||
tmux -N -S /run/devdesk/tmux.sock show -s terminal-features 2>/dev/null |
|
||||
awk '$2 == "xterm-256color:RGB" && $1 != "terminal-features[90]" { print $1 }' |
|
||||
while read -r opt; do
|
||||
tmux -N -S /run/devdesk/tmux.sock set -su "$opt" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
+5
-1
@@ -37,7 +37,9 @@ Then run `authelia validate-config --config /etc/authelia/configuration.yml && s
|
||||
- **Pi-hole:** add `"10.10.10.27 dev.lotusguild.org"` to `dns.hosts` in `pve-infra/containers/163-pihole/etc/pihole/pihole.toml`. A push auto-deploys it, and LAN clients then reach NPM directly instead of hairpinning.
|
||||
|
||||
## 5. Auto-deploy on release
|
||||
A `v*` tag makes CI publish the package, then send a signed webhook (the house HMAC pattern) to CT 126's existing `webhook` listener on :9000. The listener runs `/usr/lib/devdesk/devdesk-deploy <ref>`, which waits for that version in the apt registry and upgrades only `devdesk` with `--force-confold`. Running Claude sessions are untouched. The log is `/var/log/devdesk-deploy.log`.
|
||||
A `v*` tag makes CI publish the package, then send a signed webhook (the house HMAC pattern) to CT 126's existing `webhook` listener on :9000. The listener runs `/usr/lib/devdesk/devdesk-deploy <ref>`, which waits for that version in the apt registry and upgrades only `devdesk` with `--force-confold`. Running Claude sessions are untouched. The log is `/var/log/devdesk-deploy.log` (rotated monthly).
|
||||
|
||||
The hook answers when the deploy has finished, with the script's last line, and with HTTP 500 if it failed. CI's **Deploy to CT 126** job therefore fails when the install fails, and the failure alert goes to Matrix.
|
||||
|
||||
**Setup, done once:**
|
||||
1. Generate a secret.
|
||||
@@ -48,6 +50,8 @@ A `v*` tag makes CI publish the package, then send a signed webhook (the house H
|
||||
"execute-command": "/usr/lib/devdesk/devdesk-deploy",
|
||||
"command-working-directory": "/",
|
||||
"pass-arguments-to-command": [{"source": "payload", "name": "ref"}],
|
||||
"include-command-output-in-response": true,
|
||||
"include-command-output-in-response-on-error": true,
|
||||
"trigger-rule": {"and": [
|
||||
{"match": {"type": "payload-hash-sha256", "secret": "<WEBHOOK_SECRET>",
|
||||
"parameter": {"source": "header", "name": "X-Gitea-Signature"}}},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# DevDesk defaults, shipped by the package. Don't edit: put your changes in
|
||||
# /etc/devdesk/env.local (same KEY=value format, read after this file), so
|
||||
# package upgrades never prompt about this file.
|
||||
# Read by systemd (EnvironmentFile=) and by the devdesk scripts. Plain KEY=value
|
||||
# lines only: systemd does not support inline comments.
|
||||
# Read by systemd (EnvironmentFile=) and by the devdesk scripts, the same way:
|
||||
# plain KEY=value lines, values taken literally (no $VAR, no $(…)), optionally
|
||||
# in quotes. No inline comments. Other lines are ignored with a warning.
|
||||
# The ttyd credential lives in /etc/devdesk/secret.env (generated on install).
|
||||
|
||||
# Port ttyd listens on (NPM proxies dev.lotusguild.org here).
|
||||
|
||||
+10
-11
@@ -42,12 +42,21 @@ run_new() {
|
||||
}
|
||||
|
||||
run_resume() {
|
||||
local id args started rc fork=()
|
||||
local id args fork=()
|
||||
id=$(dd_registry_get "$NAME" claude_session_id)
|
||||
if [ -z "$id" ]; then
|
||||
run_new
|
||||
return
|
||||
fi
|
||||
# Start fresh only when the conversation's file is really gone. Any other
|
||||
# failure (an early Ctrl+C, a network error) leaves the recorded id alone
|
||||
# and ends at the menu, where ⏎ tries again (devdesk#12).
|
||||
if [ -z "$(dd_convo_file "$id")" ]; then
|
||||
printf "%sConversation %s is no longer in Claude's history — starting a new one.%s\n" "$MUTED" "$id" "$RESET"
|
||||
sleep 1
|
||||
run_new
|
||||
return
|
||||
fi
|
||||
mapfile -d '' args < <(common_args)
|
||||
# "Fork" from conversation search (devdesk#24): continue as a new
|
||||
# conversation, once; the SessionStart hook then records the new id.
|
||||
@@ -56,17 +65,7 @@ run_resume() {
|
||||
# shellcheck disable=SC2016 # $n is a jq variable
|
||||
dd_registry_update 'del(.sessions[$n].fork)' --arg n "$NAME" 2>/dev/null || true
|
||||
fi
|
||||
started=$SECONDS
|
||||
"$CLAUDE_BIN" --resume "$id" "${fork[@]}" "${args[@]}"
|
||||
rc=$?
|
||||
# A resume that fails straight away means the transcript is gone; start fresh.
|
||||
if [ "$rc" -ne 0 ] && [ $((SECONDS - started)) -lt 10 ]; then
|
||||
printf '%sCould not resume conversation %s — starting a new one.%s\n' "$MUTED" "$id" "$RESET"
|
||||
sleep 1
|
||||
run_new
|
||||
return
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
menu() {
|
||||
|
||||
+32
-9
@@ -3,6 +3,9 @@
|
||||
# listener (hook "devdesk-deploy") when CI publishes a v* tag; the tag's ref is
|
||||
# passed as $1. Safe for running sessions: package upgrades restart only
|
||||
# devdesk.service (ttyd), never devdesk-tmux.service where Claude lives.
|
||||
#
|
||||
# The hook runs this synchronously and returns its output, with HTTP 500 when
|
||||
# it fails, so CI's "Deploy to CT 126" job shows the real result (devdesk#12).
|
||||
set -euo pipefail
|
||||
|
||||
LOG=${DEVDESK_DEPLOY_LOG:-/var/log/devdesk-deploy.log}
|
||||
@@ -12,11 +15,28 @@ log() {
|
||||
printf '%s %s\n' "$(date '+%F %T')" "$*" >>"$LOG"
|
||||
}
|
||||
|
||||
exec 9>/run/devdesk-deploy.lock
|
||||
flock -w 900 9 || {
|
||||
log "another deploy is still running; giving up"
|
||||
# The outcome goes to the log and to stdout (the webhook response).
|
||||
say() {
|
||||
log "$*"
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
fail() {
|
||||
say "deploy FAILED: $*"
|
||||
exit 1
|
||||
}
|
||||
on_error() {
|
||||
local rc=$?
|
||||
say "deploy FAILED: line $1 exited $rc (details in $LOG)"
|
||||
exit "$rc"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
# Wait for dpkg/apt rather than fail when unattended-upgrades or a manual apt
|
||||
# run holds the lock.
|
||||
APT_OPTS=(-o DPkg::Lock::Timeout=120 -o Dpkg::Use-Pty=0)
|
||||
|
||||
exec 9>/run/devdesk-deploy.lock
|
||||
flock -w 900 9 || fail "another deploy is still running"
|
||||
|
||||
want=${1:-}
|
||||
want=${want#refs/tags/}
|
||||
@@ -25,7 +45,7 @@ before=$(dpkg-query -W -f='${Version}' devdesk 2>/dev/null || echo none)
|
||||
log "=== deploy: installed $before, release ${want:-latest}"
|
||||
|
||||
refresh() {
|
||||
apt-get update -q -o Dir::Etc::sourcelist="$SOURCES" -o Dir::Etc::sourceparts=- \
|
||||
apt-get update -q "${APT_OPTS[@]}" -o Dir::Etc::sourcelist="$SOURCES" -o Dir::Etc::sourceparts=- \
|
||||
-o APT::Get::List-Cleanup=0 >>"$LOG" 2>&1
|
||||
}
|
||||
|
||||
@@ -41,14 +61,17 @@ for _ in $(seq 1 18); do
|
||||
done
|
||||
|
||||
if [ -n "$want" ] && [ "$candidate" != "$want" ]; then
|
||||
log "registry still offers $candidate, not $want; giving up"
|
||||
exit 1
|
||||
fail "the registry still offers ${candidate:-nothing}, not $want"
|
||||
fi
|
||||
if [ "$candidate" = "$before" ]; then
|
||||
log "already at $before; nothing to do"
|
||||
say "already at $before; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -q --only-upgrade \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -q --only-upgrade "${APT_OPTS[@]}" \
|
||||
-o Dpkg::Options::=--force-confold devdesk </dev/null >>"$LOG" 2>&1
|
||||
log "devdesk $before -> $(dpkg-query -W -f='${Version}' devdesk)"
|
||||
after=$(dpkg-query -W -f='${Version}' devdesk)
|
||||
if [ -n "$want" ] && [ "$after" != "$want" ]; then
|
||||
fail "installed $after, not $want"
|
||||
fi
|
||||
say "devdesk $before -> $after"
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
# Past conversations (devdesk#24) and client presence (devdesk#26), sourced by
|
||||
# bin/devdesk and reached through `devdesk attach --search|--resume-convo|--presence`.
|
||||
|
||||
dd_projects_dir() {
|
||||
printf '%s/projects\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
}
|
||||
|
||||
dd_convo_file() {
|
||||
[[ $1 =~ ^[0-9a-f-]{36}$ ]] || return 1
|
||||
compgen -G "$(dd_projects_dir)/*/$1.jsonl" | head -n1
|
||||
}
|
||||
|
||||
# --search QUERY: full-text search over every Claude conversation on this host
|
||||
# (DevDesk and code-server alike). Case-insensitive, plain text. Newest first;
|
||||
# per conversation: title, folder, dates and up to 3 snippets (what you asked,
|
||||
|
||||
@@ -50,8 +50,7 @@ cmd_answer() {
|
||||
dd_transcript() {
|
||||
local id
|
||||
id=$(dd_registry_get "$1" claude_session_id)
|
||||
[[ $id =~ ^[0-9a-f-]{36}$ ]] || return 1
|
||||
compgen -G "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/*/$id.jsonl" | head -n1
|
||||
dd_convo_file "$id"
|
||||
}
|
||||
|
||||
# --peek SESSION: what the session is doing and the last exchange, from the end
|
||||
|
||||
+74
-20
@@ -8,13 +8,34 @@ DEVDESK_ENV=${DEVDESK_ENV:-/etc/devdesk/env}
|
||||
# Local overrides go in env.local: it is never shipped, so upgrades don't prompt about it.
|
||||
DEVDESK_ENV_LOCAL=${DEVDESK_ENV_LOCAL:-${DEVDESK_ENV}.local}
|
||||
|
||||
# The env files are read by systemd (EnvironmentFile=) as well, so they are
|
||||
# parsed here the way systemd parses them, never sourced: KEY=value lines, the
|
||||
# value taken literally (no $ expansion, no command substitution), optionally
|
||||
# in matching quotes. Anything else is reported and skipped (devdesk#12).
|
||||
dd_load_env() {
|
||||
local f
|
||||
for f in "$DEVDESK_ENV" "$DEVDESK_ENV_LOCAL"; do
|
||||
if [ -r "$f" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$f"
|
||||
fi
|
||||
local _dd_f _dd_line _dd_n _dd_key _dd_val
|
||||
for _dd_f in "$DEVDESK_ENV" "$DEVDESK_ENV_LOCAL"; do
|
||||
[ -r "$_dd_f" ] || continue
|
||||
_dd_n=0
|
||||
while IFS= read -r _dd_line || [ -n "$_dd_line" ]; do
|
||||
_dd_n=$((_dd_n + 1))
|
||||
_dd_line=${_dd_line%$'\r'}
|
||||
[[ $_dd_line =~ ^[[:space:]]*([#\;].*)?$ ]] && continue
|
||||
if ! [[ $_dd_line =~ ^[[:space:]]*([A-Z_][A-Z0-9_]*)=(.*)$ ]]; then
|
||||
printf 'devdesk: %s:%s: not a KEY=value line, ignored\n' "$_dd_f" "$_dd_n" >&2
|
||||
continue
|
||||
fi
|
||||
_dd_key=${BASH_REMATCH[1]}
|
||||
_dd_val=${BASH_REMATCH[2]}
|
||||
_dd_val=${_dd_val%"${_dd_val##*[![:space:]]}"} # trailing blanks, like systemd
|
||||
if [[ $_dd_val =~ ^\"(.*)\"$ || $_dd_val =~ ^\'(.*)\'$ ]]; then
|
||||
_dd_val=${BASH_REMATCH[1]}
|
||||
elif [[ $_dd_val == [\"\']* ]]; then
|
||||
printf 'devdesk: %s:%s: unmatched quote, ignored\n' "$_dd_f" "$_dd_n" >&2
|
||||
continue
|
||||
fi
|
||||
printf -v "$_dd_key" '%s' "$_dd_val"
|
||||
done <"$_dd_f"
|
||||
done
|
||||
}
|
||||
dd_load_env
|
||||
@@ -101,26 +122,59 @@ dd_registry_get() {
|
||||
jq -Mr --arg n "$1" --arg f "$2" '.sessions[$n][$f] // empty' "$DEVDESK_REGISTRY" 2>/dev/null
|
||||
}
|
||||
|
||||
# dd_registry_update JQ_FILTER [jq args...] — read-modify-write under flock.
|
||||
dd_registry_update() {
|
||||
local filter=$1
|
||||
shift
|
||||
# dd_registry_locked CMD [ARGS...] — run CMD holding the registry lock, so a
|
||||
# step that reads tmux and then writes the registry (the SessionStart hook,
|
||||
# rename) can't interleave with another. Nests: an update inside reuses it.
|
||||
dd_registry_locked() {
|
||||
if [ -n "${DD_REGISTRY_LOCKED:-}" ]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$DEVDESK_STATE_DIR"
|
||||
(
|
||||
flock -w 5 9 || exit 1
|
||||
[ -s "$DEVDESK_REGISTRY" ] || printf '{"sessions":{}}\n' >"$DEVDESK_REGISTRY"
|
||||
local tmp
|
||||
tmp=$(mktemp "$DEVDESK_REGISTRY.XXXXXX") || exit 1
|
||||
if jq "$@" "$filter" "$DEVDESK_REGISTRY" >"$tmp"; then
|
||||
chmod 600 "$tmp"
|
||||
mv -f "$tmp" "$DEVDESK_REGISTRY"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
exit 1
|
||||
fi
|
||||
DD_REGISTRY_LOCKED=1
|
||||
"$@"
|
||||
) 9>"$DEVDESK_REGISTRY.lock"
|
||||
}
|
||||
|
||||
# dd_registry_update JQ_FILTER [jq args...] — read-modify-write under flock.
|
||||
dd_registry_update() {
|
||||
dd_registry_locked _dd_registry_apply "$@"
|
||||
}
|
||||
|
||||
_dd_registry_apply() {
|
||||
local filter=$1 tmp
|
||||
shift
|
||||
[ -s "$DEVDESK_REGISTRY" ] || printf '{"sessions":{}}\n' >"$DEVDESK_REGISTRY"
|
||||
tmp=$(mktemp "$DEVDESK_REGISTRY.XXXXXX") || return 1
|
||||
if jq "$@" "$filter" "$DEVDESK_REGISTRY" >"$tmp"; then
|
||||
chmod 600 "$tmp"
|
||||
mv -f "$tmp" "$DEVDESK_REGISTRY"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Claude's conversation files: ~/.claude/projects/<folder>/<id>.jsonl.
|
||||
dd_projects_dir() {
|
||||
printf '%s/projects\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
}
|
||||
|
||||
dd_convo_file() {
|
||||
[[ $1 =~ ^[0-9a-f-]{36}$ ]] || return 1
|
||||
compgen -G "$(dd_projects_dir)/*/$1.jsonl" | head -n1
|
||||
}
|
||||
|
||||
# Temp files of writers that were killed mid-write (hooks run under Claude's
|
||||
# timeouts): .<session>.XXXXXX and registry.json.XXXXXX, older than 10 minutes.
|
||||
dd_sweep_tmp() {
|
||||
find "$DEVDESK_CONTEXT_DIR" "$DEVDESK_STATUS_DIR" -maxdepth 1 -type f -name '.*.??????' -mmin +10 -delete 2>/dev/null
|
||||
find "$DEVDESK_STATE_DIR" -maxdepth 1 -type f -name 'registry.json.??????' -mmin +10 -delete 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
dd_registry_set_dir() {
|
||||
dd_registry_update '.sessions[$n] = ((.sessions[$n] // {}) + {dir: $d, updated: $t})' \
|
||||
--arg n "$1" --arg d "$2" --arg t "$(date -Is)"
|
||||
|
||||
@@ -7,14 +7,20 @@
|
||||
# shellcheck source=libexec/devdesk-lib.sh
|
||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh" || exit 0
|
||||
|
||||
session=$(dd_current_session)
|
||||
[ -n "$session" ] || exit 0
|
||||
dd_valid_name "$session" || exit 0
|
||||
|
||||
input=$(cat)
|
||||
id=$(jq -r '.session_id // empty' <<<"$input" 2>/dev/null)
|
||||
source=$(jq -r '.source // empty' <<<"$input" 2>/dev/null)
|
||||
[ -n "$id" ] || exit 0
|
||||
|
||||
dd_registry_set_claude "$session" "$id" "$source" 2>/dev/null
|
||||
# The session's name is read under the registry lock: a rename holds it too,
|
||||
# so the id can't land under the old name after the entry moved.
|
||||
# shellcheck disable=SC2317 # called through dd_registry_locked
|
||||
record() {
|
||||
local session
|
||||
session=$(dd_current_session)
|
||||
if [ -z "$session" ] || ! dd_valid_name "$session"; then return 0; fi
|
||||
dd_registry_set_claude "$session" "$id" "$source"
|
||||
}
|
||||
dd_registry_locked record 2>/dev/null
|
||||
dd_sweep_tmp
|
||||
exit 0
|
||||
|
||||
+84
-3
@@ -15,12 +15,14 @@ setup() {
|
||||
export DEVDESK_MATRIX_WEBHOOK_FILE="$TMP/no-webhook" # never the real room
|
||||
export FAKE_LOG="$TMP/claude-args"
|
||||
export CLAUDE_BIN="$TMP/fake-claude"
|
||||
export CLAUDE_CONFIG_DIR="$TMP/claude" # never the real conversation history
|
||||
export PATH="$REPO/bin:$PATH"
|
||||
mkdir -p "$DEVDESK_PROJECTS_ROOT/alpha" "$DEVDESK_PROJECTS_ROOT/test code" "$TMP/outside" "$FAKE_LOG"
|
||||
ln -s "$TMP/outside" "$DEVDESK_PROJECTS_ROOT/escape"
|
||||
cat >"$CLAUDE_BIN" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$FAKE_LOG/$DEVDESK_SESSION"
|
||||
[ -z "${FAKE_EXIT:-}" ] || exit "$FAKE_EXIT"
|
||||
exec sleep 300
|
||||
EOF
|
||||
chmod +x "$CLAUDE_BIN"
|
||||
@@ -440,12 +442,91 @@ posts() { [ -f "$TMP/posts" ] && wc -l <"$TMP/posts" || echo 0; }
|
||||
|
||||
@test "session hook records the Claude session and the next start resumes it" {
|
||||
start_server
|
||||
echo '{"session_id":"abc-123","source":"startup"}' | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
|
||||
id=abcabcab-1234-1234-1234-123412341234
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/projects/-p" && touch "$CLAUDE_CONFIG_DIR/projects/-p/$id.jsonl"
|
||||
echo "{\"session_id\":\"$id\",\"source\":\"startup\"}" | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
|
||||
run jq -r '.sessions.alpha.claude_session_id' "$DEVDESK_STATE_DIR/registry.json"
|
||||
[ "$output" = "abc-123" ]
|
||||
[ "$output" = "$id" ]
|
||||
devdesk new alpha
|
||||
run claude_args alpha
|
||||
[ "$output" = "--resume abc-123 --settings $REPO/share/claude-settings.json" ]
|
||||
[ "$output" = "--resume $id --settings $REPO/share/claude-settings.json" ]
|
||||
}
|
||||
|
||||
@test "a conversation whose file is gone starts fresh; a failed resume keeps the id (#12)" {
|
||||
start_server
|
||||
id=abcabcab-1234-1234-1234-123412341234
|
||||
echo "{\"session_id\":\"$id\"}" | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
|
||||
devdesk new alpha
|
||||
run claude_args alpha
|
||||
[ "$output" = "-n alpha --settings $REPO/share/claude-settings.json" ]
|
||||
tmux -S "$DEVDESK_SOCKET" kill-server
|
||||
# The file exists, but Claude exits at once (Ctrl+C, network): the menu, not a new conversation.
|
||||
mkdir -p "$CLAUDE_CONFIG_DIR/projects/-p" && touch "$CLAUDE_CONFIG_DIR/projects/-p/$id.jsonl"
|
||||
rm -f "$FAKE_LOG/alpha"
|
||||
FAKE_EXIT=1 start_server
|
||||
devdesk new alpha
|
||||
run claude_args alpha
|
||||
[ "$output" = "--resume $id --settings $REPO/share/claude-settings.json" ]
|
||||
for _ in $(seq 1 30); do
|
||||
tmux -S "$DEVDESK_SOCKET" capture-pane -p -t =alpha:claude | grep -q "Claude exited" && break
|
||||
sleep 0.1
|
||||
done
|
||||
tmux -S "$DEVDESK_SOCKET" capture-pane -p -t =alpha:claude | grep -q "Claude exited"
|
||||
[ "$(wc -l <"$FAKE_LOG/alpha")" = 1 ]
|
||||
[ "$(jq -r '.sessions.alpha.claude_session_id' "$DEVDESK_STATE_DIR/registry.json")" = "$id" ]
|
||||
}
|
||||
|
||||
@test "env files are parsed like systemd does, never run (#12)" {
|
||||
cat >"$TMP/env" <<'ENV'
|
||||
# comment
|
||||
; also a comment
|
||||
DEVDESK_DEFAULT_DIR=/srv/ops
|
||||
CLAUDE_ARGS=--model opus --verbose
|
||||
DEVDESK_PUBLIC_URL="https://dev.example.org"
|
||||
DEVDESK_GRAFANA_URL='http://g:3000'
|
||||
DEVDESK_MATRIX_DELAY=$(touch /tmp/devdesk-env-pwned)
|
||||
lowercase=x
|
||||
not a line
|
||||
DEVDESK_BIND="unterminated
|
||||
ENV
|
||||
run env DEVDESK_ENV="$TMP/env" DEVDESK_ENV_LOCAL=/dev/null bash -c '. "$1/libexec/devdesk-lib.sh"; printf "%s|" "$DEVDESK_DEFAULT_DIR" "$CLAUDE_ARGS" "$DEVDESK_PUBLIC_URL" "$DEVDESK_GRAFANA_URL" "$DEVDESK_MATRIX_DELAY" "${lowercase:-unset}" "${DEVDESK_BIND:-unset}"' _ "$REPO"
|
||||
[ ! -e /tmp/devdesk-env-pwned ]
|
||||
[[ $output == *'/srv/ops|--model opus --verbose|https://dev.example.org|http://g:3000|$(touch /tmp/devdesk-env-pwned)|unset|unset|'* ]]
|
||||
[[ $output == *"env:8: not a KEY=value line"* ]]
|
||||
[[ $output == *"env:10: unmatched quote"* ]]
|
||||
}
|
||||
|
||||
@test "the SessionStart hook reads the session name under the rename lock (#12)" {
|
||||
start_server
|
||||
devdesk new alpha
|
||||
claude_args alpha >/dev/null
|
||||
pane=$(tmux -S "$DEVDESK_SOCKET" display -p -t =alpha:claude '#{pane_id}')
|
||||
# A rename holds the lock; the hook fires meanwhile, while tmux still says "alpha".
|
||||
(
|
||||
flock 9
|
||||
sleep 0.5
|
||||
tmux -S "$DEVDESK_SOCKET" rename-session -t =alpha beta
|
||||
DEVDESK_ENV=/dev/null bash -c '. "$1/libexec/devdesk-lib.sh"; DD_REGISTRY_LOCKED=1 dd_registry_rename alpha beta' _ "$REPO"
|
||||
) 9>"$DEVDESK_STATE_DIR/registry.json.lock" 3>&- &
|
||||
renamer=$!
|
||||
sleep 0.1
|
||||
echo '{"session_id":"ffffffff-0000-0000-0000-000000000000","source":"clear"}' |
|
||||
TMUX="$DEVDESK_SOCKET,1,0" TMUX_PANE=$pane "$REPO/libexec/devdesk-session-hook"
|
||||
wait "$renamer"
|
||||
[ "$(jq -r '.sessions | keys | join(",")' "$DEVDESK_STATE_DIR/registry.json")" = beta ]
|
||||
[ "$(jq -r '.sessions.beta.claude_session_id' "$DEVDESK_STATE_DIR/registry.json")" = ffffffff-0000-0000-0000-000000000000 ]
|
||||
}
|
||||
|
||||
@test "leftover temp files of killed writers are swept (#12)" {
|
||||
mkdir -p "$DEVDESK_STATE_DIR/state" "$DEVDESK_STATE_DIR/context"
|
||||
touch -d '20 minutes ago' "$DEVDESK_STATE_DIR/state/.alpha.Ab12Cd" "$DEVDESK_STATE_DIR/context/.alpha.Xy34Zw" "$DEVDESK_STATE_DIR/registry.json.Qq11Rr"
|
||||
touch "$DEVDESK_STATE_DIR/state/.beta.Ab12Cd" "$DEVDESK_STATE_DIR/state/alpha.json"
|
||||
echo '{"session_id":"x"}' | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
|
||||
[ ! -e "$DEVDESK_STATE_DIR/state/.alpha.Ab12Cd" ]
|
||||
[ ! -e "$DEVDESK_STATE_DIR/context/.alpha.Xy34Zw" ]
|
||||
[ ! -e "$DEVDESK_STATE_DIR/registry.json.Qq11Rr" ]
|
||||
[ -e "$DEVDESK_STATE_DIR/state/.beta.Ab12Cd" ]
|
||||
[ -e "$DEVDESK_STATE_DIR/state/alpha.json" ]
|
||||
}
|
||||
|
||||
@test "session hook ignores sessions it does not own" {
|
||||
|
||||
Reference in New Issue
Block a user