feat: live Repo/CI panel, env.local overrides (0.3.0)
build-deb / Lint, test, package (push) Successful in 1m46s
build-deb / Notify on failure (push) Skipped

- `devdesk attach --repo NAME` / `devdesk repo NAME`: git state of the session's
  folder (branch, ahead/behind, uncommitted changes) plus the Gitea commit
  status of HEAD and open PR count. Uses the anonymous API for public repos,
  or /etc/devdesk/gitea-token when present. Runs over the ttyd channel, so no
  sidecar is needed. The panel links to the repo, the CI run and PRs, and a
  notification fires when HEAD's CI finishes while the tab is in the background.
- /etc/devdesk/env.local is read after the packaged env and never shipped, so
  upgrades no longer stop at a conffile prompt.
- Hooks and status line only record for panes on DevDesk's own tmux server; the
  Claude wrapper re-reads env before every launch (so the Remote Control toggle
  applies on the next Claude restart).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:09:32 -04:00
co-authored by Claude Opus 5.5
parent ae8b5d843f
commit 9026238024
15 changed files with 256 additions and 20 deletions
+70 -2
View File
@@ -5,10 +5,11 @@
# devdesk new NAME [PROJECT] create session NAME (detached) in PROJECT's folder, or the default folder
# devdesk rename OLD NEW rename a session (running or dormant)
# devdesk end NAME stop session NAME and forget it
# devdesk repo NAME git + Gitea CI status of the session's folder (JSON)
# devdesk ls list sessions
# devdesk pick interactive picker
#
# The web UI calls `devdesk attach --list | --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
# The web UI calls `devdesk attach --list | --repo NAME | --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
# through ttyd; those print JSON.
set -euo pipefail
@@ -16,7 +17,7 @@ set -euo pipefail
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
usage() {
sed -n '2,10s/^# \{0,1\}//p' "$0"
sed -n '2,11s/^# \{0,1\}//p' "$0"
}
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
@@ -144,6 +145,67 @@ cmd_end() {
dd_registry_remove "$1"
}
# GET $DEVDESK_GITEA_URL/api/v1<path>; token optional (private repos only).
gitea_get() {
local auth=()
if [ -r "$DEVDESK_GITEA_TOKEN_FILE" ]; then
auth=(-H "Authorization: token $(tr -d '[:space:]' <"$DEVDESK_GITEA_TOKEN_FILE")")
fi
curl -fsS --max-time 4 "${auth[@]}" "$DEVDESK_GITEA_URL/api/v1$1" 2>/dev/null
}
# owner/repo when REMOTE points at our Gitea (https or ssh, any configured host), else nothing.
gitea_slug() {
local remote=$1 host hosts
hosts="${DEVDESK_GITEA_URL#*://} $DEVDESK_GITEA_ALIASES"
for host in $hosts; do
host=${host%%/*}
host=${host%%:*}
if [[ $remote =~ [@/]${host//./\.}(:[0-9]+)?[:/]+([^/]+/[^/]+)$ ]]; then
printf '%s\n' "${BASH_REMATCH[2]%.git}"
return
fi
done
}
cmd_repo() {
[ $# -eq 1 ] || dd_die "usage: devdesk repo NAME"
dd_valid_name "$1" || dd_die "invalid session name: $1"
local dir g branch sha="" dirty ahead=null behind=null upstream=false remote slug="" ci=null prs=null
dir=$(dd_resolve_dir "$1")
g=(git -c safe.directory='*' -C "$dir")
if ! "${g[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
jq -cMn --arg s "$1" --arg dir "$dir" '{session: $s, dir: $dir, git: false}'
return
fi
branch=$("${g[@]}" symbolic-ref --short -q HEAD || echo "(detached)")
sha=$("${g[@]}" rev-parse -q --verify HEAD 2>/dev/null || true)
dirty=$("${g[@]}" status --porcelain 2>/dev/null | wc -l)
if "${g[@]}" rev-parse -q --verify '@{u}' >/dev/null 2>&1; then
upstream=true
read -r behind ahead < <("${g[@]}" rev-list --left-right --count '@{u}...HEAD')
fi
remote=$("${g[@]}" remote get-url origin 2>/dev/null || true)
[ -n "$remote" ] && slug=$(gitea_slug "$remote")
if [ -n "$slug" ] && [ -n "$sha" ]; then
ci=$(gitea_get "/repos/$slug/commits/$sha/status" |
jq -cM '{state, total: .total_count,
url: (.statuses[0]?.target_url | if . then sub("/jobs/[0-9]+$"; "") else null end),
jobs: [.statuses[]? | {name: .context, status}]}' 2>/dev/null) || ci=null
prs=$(gitea_get "/repos/$slug/pulls?state=open&limit=50" | jq -M 'length' 2>/dev/null) || prs=null
fi
jq -cMn --arg s "$1" --arg dir "$dir" --arg branch "$branch" --arg sha "${sha:0:7}" \
--argjson dirty "$dirty" --argjson ahead "$ahead" --argjson behind "$behind" \
--argjson upstream "$upstream" --arg remote "$remote" --arg slug "$slug" \
--arg web "$DEVDESK_GITEA_URL" --argjson ci "${ci:-null}" --argjson prs "${prs:-null}" '
{session: $s, dir: $dir, git: true, branch: $branch, sha: $sha, dirty: $dirty,
ahead: $ahead, behind: $behind, upstream: $upstream, remote: $remote,
repo: (if $slug == "" then null else $slug end),
url: (if $slug == "" then null else "\($web)/\($slug)" end),
ci: (if $ci == null then null else $ci + {url: (if $ci.url then $web + $ci.url else null end)} end),
prs: $prs}'
}
ok_json() {
printf '{"ok":true}\n'
}
@@ -158,6 +220,11 @@ cmd_attach() {
cmd_list
return
;;
--repo)
shift
cmd_repo "$@"
return
;;
--new | --rename | --end)
local action=${1#--}
shift
@@ -195,6 +262,7 @@ case $cmd in
attach | a) cmd_attach "$@" ;;
new) cmd_new "$@" ;;
rename | mv) cmd_rename "$@" ;;
repo) cmd_repo "$@" ;;
ls | list) cmd_ls ;;
--list) cmd_list ;;
end) cmd_end "$@" ;;