feat: live Repo/CI panel, env.local overrides (0.3.0)
- `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:
+70
-2
@@ -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 "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user