Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
531b640ae9 | ||
|
|
9026238024 | ||
|
|
ae8b5d843f |
@@ -149,10 +149,32 @@ jobs:
|
|||||||
"$api/releases/$id/assets?name=$name" >/dev/null
|
"$api/releases/$id/assets?name=$name" >/dev/null
|
||||||
echo "release $TAG: $GITEA_URL/$REPO/releases/tag/$TAG"
|
echo "release $TAG: $GITEA_URL/$REPO/releases/tag/$TAG"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to CT 126
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
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.
|
||||||
|
- name: Trigger devdesk-deploy webhook
|
||||||
|
env:
|
||||||
|
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
|
||||||
|
GIT_REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
PAYLOAD="{\"ref\":\"${GIT_REF}\"}"
|
||||||
|
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
|
||||||
|
curl -sf --connect-timeout 10 \
|
||||||
|
-X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Gitea-Signature: ${SIG}" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"http://10.10.10.58:9000/hooks/devdesk-deploy"
|
||||||
|
|
||||||
notify-failure:
|
notify-failure:
|
||||||
name: Notify on failure
|
name: Notify on failure
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build]
|
needs: [build, deploy]
|
||||||
if: failure() && github.event_name == 'push'
|
if: failure() && github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
- name: Send Matrix alert
|
- name: Send Matrix alert
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ install: all
|
|||||||
install -m 0755 bin/devdesk $(DESTDIR)$(PREFIX)/bin/devdesk
|
install -m 0755 bin/devdesk $(DESTDIR)$(PREFIX)/bin/devdesk
|
||||||
install -m 0644 libexec/devdesk-lib.sh $(DESTDIR)$(LIBDIR)/devdesk-lib.sh
|
install -m 0644 libexec/devdesk-lib.sh $(DESTDIR)$(LIBDIR)/devdesk-lib.sh
|
||||||
install -m 0755 libexec/devdesk-picker libexec/devdesk-claude-run libexec/devdesk-session-hook \
|
install -m 0755 libexec/devdesk-picker libexec/devdesk-claude-run libexec/devdesk-session-hook \
|
||||||
libexec/devdesk-tmux-ready libexec/devdesk-serve libexec/devdesk-statusline $(DESTDIR)$(LIBDIR)/
|
libexec/devdesk-tmux-ready libexec/devdesk-serve libexec/devdesk-statusline \
|
||||||
|
libexec/devdesk-deploy $(DESTDIR)$(LIBDIR)/
|
||||||
install -m 0755 build/ttyd $(DESTDIR)$(LIBDIR)/bin/ttyd
|
install -m 0755 build/ttyd $(DESTDIR)$(LIBDIR)/bin/ttyd
|
||||||
install -m 0644 dist/index.html share/tmux.conf share/claude-settings.json $(DESTDIR)$(SHAREDIR)/
|
install -m 0644 dist/index.html share/tmux.conf share/claude-settings.json $(DESTDIR)$(SHAREDIR)/
|
||||||
install -m 0644 etc/env $(DESTDIR)$(SYSCONFDIR)/devdesk/env
|
install -m 0644 etc/env $(DESTDIR)$(SYSCONFDIR)/devdesk/env
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ The tmux server runs in its own unit, so every Claude process lives in **that**
|
|||||||
Use the **Full** button for fullscreen. In Chromium it also captures Ctrl+W/Ctrl+T, which browsers otherwise keep for themselves.
|
Use the **Full** button for fullscreen. In Chromium it also captures Ctrl+W/Ctrl+T, which browsers otherwise keep for themselves.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
- `/etc/devdesk/env` (conffile):
|
- **Put your changes in `/etc/devdesk/env.local`.** It's read after the packaged `env` and never shipped, so `apt upgrade` never asks about it. Example: `DEVDESK_REMOTE_CONTROL=1`.
|
||||||
|
- `/etc/devdesk/env` holds the packaged defaults; don't edit it. Its keys:
|
||||||
- port, bind address, projects root and `DEVDESK_DEFAULT_DIR`
|
- port, bind address, projects root and `DEVDESK_DEFAULT_DIR`
|
||||||
|
- `DEVDESK_GITEA_URL` / `DEVDESK_GITEA_ALIASES` for the Repo/CI panel. Public repos need no token; for private ones put a `read:repository` token in `/etc/devdesk/gitea-token`
|
||||||
- `CLAUDE_BIN` and `CLAUDE_ARGS`
|
- `CLAUDE_BIN` and `CLAUDE_ARGS`
|
||||||
- `DEVDESK_REMOTE_CONTROL=1`, which also starts each session with Claude Remote Control so it can be continued from the Claude app
|
- `DEVDESK_REMOTE_CONTROL=1`, which also starts each session with Claude Remote Control so it can be continued from the Claude app
|
||||||
- `/etc/devdesk/secret.env` is generated on install. It holds the ttyd credential that NPM must inject; see [docs/DEPLOY.md](docs/DEPLOY.md).
|
- `/etc/devdesk/secret.env` is generated on install. It holds the ttyd credential that NPM must inject; see [docs/DEPLOY.md](docs/DEPLOY.md).
|
||||||
@@ -69,8 +71,8 @@ apt install ../devdesk_*_amd64.deb
|
|||||||
|
|
||||||
CI (`.gitea/workflows/build-deb.yml`):
|
CI (`.gitea/workflows/build-deb.yml`):
|
||||||
- Every push lints, tests, builds and smoke-tests the package and uploads it as an artifact.
|
- Every push lints, tests, builds and smoke-tests the package and uploads it as an artifact.
|
||||||
- A `v*` tag also publishes to `https://code.lotusguild.org/api/packages/LotusGuild/debian` and creates a release.
|
- A `v*` tag also publishes to `https://code.lotusguild.org/api/packages/LotusGuild/debian`, creates a release, and auto-deploys to CT 126. A signed webhook runs `devdesk-deploy`, which upgrades the package; see [docs/DEPLOY.md](docs/DEPLOY.md).
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- **v1.1:** `devdesk-api` sidecar for the Repo/CI panel, Matrix notifications when you're away, and auto-deploy.
|
- **Next:** Matrix notifications when you're away (needs the Ci-Alert hookshot URL).
|
||||||
- **v2:** Ops Alerts (Grafana) with "Ask Claude" prompts, and file drop.
|
- **v2:** Ops Alerts (Grafana) with "Ask Claude" prompts, and file drop.
|
||||||
|
|||||||
+70
-2
@@ -5,10 +5,11 @@
|
|||||||
# devdesk new NAME [PROJECT] create session NAME (detached) in PROJECT's folder, or the default folder
|
# 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 rename OLD NEW rename a session (running or dormant)
|
||||||
# devdesk end NAME stop session NAME and forget it
|
# 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 ls list sessions
|
||||||
# devdesk pick interactive picker
|
# 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.
|
# through ttyd; those print JSON.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ set -euo pipefail
|
|||||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
||||||
|
|
||||||
usage() {
|
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.
|
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
|
||||||
@@ -144,6 +145,67 @@ cmd_end() {
|
|||||||
dd_registry_remove "$1"
|
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() {
|
ok_json() {
|
||||||
printf '{"ok":true}\n'
|
printf '{"ok":true}\n'
|
||||||
}
|
}
|
||||||
@@ -158,6 +220,11 @@ cmd_attach() {
|
|||||||
cmd_list
|
cmd_list
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
--repo)
|
||||||
|
shift
|
||||||
|
cmd_repo "$@"
|
||||||
|
return
|
||||||
|
;;
|
||||||
--new | --rename | --end)
|
--new | --rename | --end)
|
||||||
local action=${1#--}
|
local action=${1#--}
|
||||||
shift
|
shift
|
||||||
@@ -195,6 +262,7 @@ case $cmd in
|
|||||||
attach | a) cmd_attach "$@" ;;
|
attach | a) cmd_attach "$@" ;;
|
||||||
new) cmd_new "$@" ;;
|
new) cmd_new "$@" ;;
|
||||||
rename | mv) cmd_rename "$@" ;;
|
rename | mv) cmd_rename "$@" ;;
|
||||||
|
repo) cmd_repo "$@" ;;
|
||||||
ls | list) cmd_ls ;;
|
ls | list) cmd_ls ;;
|
||||||
--list) cmd_list ;;
|
--list) cmd_list ;;
|
||||||
end) cmd_end "$@" ;;
|
end) cmd_end "$@" ;;
|
||||||
|
|||||||
Vendored
+32
@@ -1,3 +1,35 @@
|
|||||||
|
devdesk (0.3.1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Auto-deploy: tagged releases trigger CT 126's webhook listener (house HMAC
|
||||||
|
pattern), which runs /usr/lib/devdesk/devdesk-deploy to upgrade the
|
||||||
|
package from the apt registry. Running sessions are untouched.
|
||||||
|
|
||||||
|
-- Jared Vititoe <jjvititoe1@gmail.com> Wed, 07 Oct 2026 13:20:58 -0400
|
||||||
|
|
||||||
|
devdesk (0.3.0) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Repo / CI panel is live: branch, ahead/behind, uncommitted changes, the
|
||||||
|
Gitea CI status of HEAD (linked to the run) and open PRs for the current
|
||||||
|
session's folder, via `devdesk attach --repo` over the ttyd channel. No
|
||||||
|
sidecar needed; public repos need no token (optional
|
||||||
|
/etc/devdesk/gitea-token for private ones). A notification fires when CI
|
||||||
|
for HEAD finishes while the tab is in the background.
|
||||||
|
* Remote Control verified with resume (same claude.ai session link).
|
||||||
|
* Local settings now go in /etc/devdesk/env.local (read after the packaged
|
||||||
|
env, never shipped), so upgrades no longer stop at a conffile prompt.
|
||||||
|
|
||||||
|
-- Jared Vititoe <jjvititoe1@gmail.com> Wed, 07 Oct 2026 12:57:04 -0400
|
||||||
|
|
||||||
|
devdesk (0.2.1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Session hook and status line only record for panes on DevDesk's own
|
||||||
|
tmux server (Claude run with these settings elsewhere no longer writes
|
||||||
|
into DevDesk's state).
|
||||||
|
* The Claude wrapper re-reads /etc/devdesk/env before every Claude launch,
|
||||||
|
so Remote Control and CLAUDE_ARGS changes apply on the next restart.
|
||||||
|
|
||||||
|
-- Jared Vititoe <jjvititoe1@gmail.com> Wed, 07 Oct 2026 12:54:06 -0400
|
||||||
|
|
||||||
devdesk (0.2.0) bookworm; urgency=medium
|
devdesk (0.2.0) bookworm; urgency=medium
|
||||||
|
|
||||||
* Sessions no longer need a project folder: any name works and starts in
|
* Sessions no longer need a project folder: any name works and starts in
|
||||||
|
|||||||
Vendored
+1
@@ -11,6 +11,7 @@ User=root
|
|||||||
Environment=HOME=/root USER=root LOGNAME=root SHELL=/bin/bash LANG=C.UTF-8
|
Environment=HOME=/root USER=root LOGNAME=root SHELL=/bin/bash LANG=C.UTF-8
|
||||||
Environment=PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
EnvironmentFile=-/etc/devdesk/env
|
EnvironmentFile=-/etc/devdesk/env
|
||||||
|
EnvironmentFile=-/etc/devdesk/env.local
|
||||||
WorkingDirectory=-/root
|
WorkingDirectory=-/root
|
||||||
RuntimeDirectory=devdesk
|
RuntimeDirectory=devdesk
|
||||||
RuntimeDirectoryMode=0700
|
RuntimeDirectoryMode=0700
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ "$1" = purge ]; then
|
if [ "$1" = purge ]; then
|
||||||
rm -f /etc/devdesk/secret.env
|
rm -f /etc/devdesk/secret.env /etc/devdesk/env.local
|
||||||
rm -rf /var/lib/devdesk
|
rm -rf /var/lib/devdesk
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -10,6 +10,7 @@ User=root
|
|||||||
Environment=HOME=/root LANG=C.UTF-8
|
Environment=HOME=/root LANG=C.UTF-8
|
||||||
Environment=PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
EnvironmentFile=/etc/devdesk/env
|
EnvironmentFile=/etc/devdesk/env
|
||||||
|
EnvironmentFile=-/etc/devdesk/env.local
|
||||||
EnvironmentFile=/etc/devdesk/secret.env
|
EnvironmentFile=/etc/devdesk/secret.env
|
||||||
ExecStart=/usr/lib/devdesk/devdesk-serve
|
ExecStart=/usr/lib/devdesk/devdesk-serve
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
+24
-1
@@ -36,7 +36,30 @@ Then run `authelia validate-config --config /etc/authelia/configuration.yml && s
|
|||||||
- **Cloudflare:** add a CNAME `dev` → apex, DNS-only (grey cloud).
|
- **Cloudflare:** add a CNAME `dev` → apex, DNS-only (grey cloud).
|
||||||
- **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.
|
- **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. Optional hardening
|
## 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`.
|
||||||
|
|
||||||
|
**Setup, done once:**
|
||||||
|
1. Generate a secret.
|
||||||
|
2. Add this hook to `/etc/webhook/hooks.json`, next to `126-config-deploy`. The file is local and untracked because it holds secrets.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "devdesk-deploy",
|
||||||
|
"execute-command": "/usr/lib/devdesk/devdesk-deploy",
|
||||||
|
"command-working-directory": "/",
|
||||||
|
"pass-arguments-to-command": [{"source": "payload", "name": "ref"}],
|
||||||
|
"trigger-rule": {"and": [
|
||||||
|
{"match": {"type": "payload-hash-sha256", "secret": "<WEBHOOK_SECRET>",
|
||||||
|
"parameter": {"source": "header", "name": "X-Gitea-Signature"}}},
|
||||||
|
{"match": {"type": "regex", "regex": "^refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+$",
|
||||||
|
"parameter": {"source": "payload", "name": "ref"}}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Run `systemctl restart webhook`.
|
||||||
|
4. Store the same secret as the devdesk repo's Actions secret `WEBHOOK_SECRET`.
|
||||||
|
|
||||||
|
## 6. Optional hardening
|
||||||
Enable the Proxmox firewall on CT 126 (`firewall=1` on net0) and allow tcp/7681 only from 10.10.10.27. The same rule suits code-server on :8680.
|
Enable the Proxmox firewall on CT 126 (`firewall=1` on net0) and allow tcp/7681 only from 10.10.10.27. The same rule suits code-server on :8680.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# DevDesk configuration. Read by systemd (EnvironmentFile=) and by the devdesk
|
# DevDesk defaults, shipped by the package. Don't edit: put your changes in
|
||||||
# scripts. Plain KEY=value lines only: systemd does not support inline comments.
|
# /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.
|
||||||
# The ttyd credential lives in /etc/devdesk/secret.env (generated on install).
|
# The ttyd credential lives in /etc/devdesk/secret.env (generated on install).
|
||||||
|
|
||||||
# Port ttyd listens on (NPM proxies dev.lotusguild.org here).
|
# Port ttyd listens on (NPM proxies dev.lotusguild.org here).
|
||||||
@@ -15,6 +18,11 @@ DEVDESK_DEFAULT_SESSION=main
|
|||||||
# Where sessions that are not a project folder start (e.g. ops sessions); empty = projects root.
|
# Where sessions that are not a project folder start (e.g. ops sessions); empty = projects root.
|
||||||
DEVDESK_DEFAULT_DIR=
|
DEVDESK_DEFAULT_DIR=
|
||||||
|
|
||||||
|
# Gitea for the Repo/CI panel. Public repos need no token; for private ones put a
|
||||||
|
# read:repository token in /etc/devdesk/gitea-token (mode 600).
|
||||||
|
DEVDESK_GITEA_URL=https://code.lotusguild.org
|
||||||
|
DEVDESK_GITEA_ALIASES=10.10.10.63
|
||||||
|
|
||||||
# Claude Code binary and extra arguments for every session.
|
# Claude Code binary and extra arguments for every session.
|
||||||
CLAUDE_BIN=/root/.local/bin/claude
|
CLAUDE_BIN=/root/.local/bin/claude
|
||||||
CLAUDE_ARGS=
|
CLAUDE_ARGS=
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ set -uo pipefail
|
|||||||
NAME=$(dd_current_session)
|
NAME=$(dd_current_session)
|
||||||
[ -n "$NAME" ] || dd_die "not running inside a DevDesk session"
|
[ -n "$NAME" ] || dd_die "not running inside a DevDesk session"
|
||||||
|
|
||||||
CLAUDE_BIN=${CLAUDE_BIN:-claude}
|
|
||||||
SETTINGS="$DEVDESK_SHARE/claude-settings.json"
|
SETTINGS="$DEVDESK_SHARE/claude-settings.json"
|
||||||
read -r -a EXTRA <<<"${CLAUDE_ARGS:-}"
|
|
||||||
|
# Re-read /etc/devdesk/env before every Claude launch, so changes (Remote
|
||||||
|
# Control, CLAUDE_ARGS, …) apply on the next restart of Claude in a session.
|
||||||
|
reload_config() {
|
||||||
|
dd_load_env
|
||||||
|
CLAUDE_BIN=${CLAUDE_BIN:-claude}
|
||||||
|
read -r -a EXTRA <<<"${CLAUDE_ARGS:-}"
|
||||||
|
}
|
||||||
|
|
||||||
ORANGE=$'\033[38;2;255;107;0m'
|
ORANGE=$'\033[38;2;255;107;0m'
|
||||||
CYAN=$'\033[38;2;0;212;255m'
|
CYAN=$'\033[38;2;0;212;255m'
|
||||||
@@ -66,6 +72,7 @@ printf '\033]2;%s\033\134' "$NAME" # pane title (ST = ESC \\)
|
|||||||
mode=resume
|
mode=resume
|
||||||
while :; do
|
while :; do
|
||||||
NAME=$(dd_current_session) # follow renames
|
NAME=$(dd_current_session) # follow renames
|
||||||
|
reload_config
|
||||||
if [ "$mode" = new ]; then run_new; else run_resume; fi
|
if [ "$mode" = new ]; then run_new; else run_resume; fi
|
||||||
rc=$?
|
rc=$?
|
||||||
NAME=$(dd_current_session)
|
NAME=$(dd_current_session)
|
||||||
|
|||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Upgrade DevDesk from the LotusGuild apt registry. Run by the host's webhook
|
||||||
|
# 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.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG=${DEVDESK_DEPLOY_LOG:-/var/log/devdesk-deploy.log}
|
||||||
|
SOURCES=${DEVDESK_APT_SOURCES:-sources.list.d/lotusguild.list}
|
||||||
|
|
||||||
|
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"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
want=${1:-}
|
||||||
|
want=${want#refs/tags/}
|
||||||
|
want=${want#v}
|
||||||
|
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=- \
|
||||||
|
-o APT::Get::List-Cleanup=0 >>"$LOG" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The registry can lag the release by a few seconds: wait for the tagged version.
|
||||||
|
candidate=""
|
||||||
|
for _ in $(seq 1 18); do
|
||||||
|
refresh || true
|
||||||
|
candidate=$(apt-cache policy devdesk | awk '/Candidate:/ {print $2}')
|
||||||
|
if [ -z "$want" ] || [ "$candidate" = "$want" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$want" ] && [ "$candidate" != "$want" ]; then
|
||||||
|
log "registry still offers $candidate, not $want; giving up"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$candidate" = "$before" ]; then
|
||||||
|
log "already at $before; nothing to do"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -q --only-upgrade \
|
||||||
|
-o Dpkg::Options::=--force-confold devdesk </dev/null >>"$LOG" 2>&1
|
||||||
|
log "devdesk $before -> $(dpkg-query -W -f='${Version}' devdesk)"
|
||||||
+20
-6
@@ -5,10 +5,19 @@
|
|||||||
# against a throwaway tmux server and state directory.
|
# against a throwaway tmux server and state directory.
|
||||||
|
|
||||||
DEVDESK_ENV=${DEVDESK_ENV:-/etc/devdesk/env}
|
DEVDESK_ENV=${DEVDESK_ENV:-/etc/devdesk/env}
|
||||||
if [ -r "$DEVDESK_ENV" ]; then
|
# Local overrides go in env.local: it is never shipped, so upgrades don't prompt about it.
|
||||||
# shellcheck source=/dev/null
|
DEVDESK_ENV_LOCAL=${DEVDESK_ENV_LOCAL:-${DEVDESK_ENV}.local}
|
||||||
. "$DEVDESK_ENV"
|
|
||||||
fi
|
dd_load_env() {
|
||||||
|
local f
|
||||||
|
for f in "$DEVDESK_ENV" "$DEVDESK_ENV_LOCAL"; do
|
||||||
|
if [ -r "$f" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
dd_load_env
|
||||||
|
|
||||||
: "${DEVDESK_PROJECTS_ROOT:=/root/code}"
|
: "${DEVDESK_PROJECTS_ROOT:=/root/code}"
|
||||||
: "${DEVDESK_DEFAULT_DIR:=$DEVDESK_PROJECTS_ROOT}"
|
: "${DEVDESK_DEFAULT_DIR:=$DEVDESK_PROJECTS_ROOT}"
|
||||||
@@ -18,6 +27,9 @@ fi
|
|||||||
: "${DEVDESK_LIBEXEC:=/usr/lib/devdesk}"
|
: "${DEVDESK_LIBEXEC:=/usr/lib/devdesk}"
|
||||||
: "${DEVDESK_SHARE:=/usr/share/devdesk}"
|
: "${DEVDESK_SHARE:=/usr/share/devdesk}"
|
||||||
: "${DEVDESK_TMUX_UNIT:=devdesk-tmux.service}"
|
: "${DEVDESK_TMUX_UNIT:=devdesk-tmux.service}"
|
||||||
|
: "${DEVDESK_GITEA_URL:=https://code.lotusguild.org}"
|
||||||
|
: "${DEVDESK_GITEA_ALIASES:=10.10.10.63}"
|
||||||
|
: "${DEVDESK_GITEA_TOKEN_FILE:=/etc/devdesk/gitea-token}"
|
||||||
|
|
||||||
DEVDESK_REGISTRY="$DEVDESK_STATE_DIR/registry.json"
|
DEVDESK_REGISTRY="$DEVDESK_STATE_DIR/registry.json"
|
||||||
DEVDESK_CONTEXT_DIR="$DEVDESK_STATE_DIR/context"
|
DEVDESK_CONTEXT_DIR="$DEVDESK_STATE_DIR/context"
|
||||||
@@ -152,8 +164,10 @@ dd_resolve_dir() {
|
|||||||
# Name of the DevDesk session this process runs in. Asks tmux, so it stays
|
# Name of the DevDesk session this process runs in. Asks tmux, so it stays
|
||||||
# right after a rename; falls back to the DEVDESK_SESSION set at creation.
|
# right after a rename; falls back to the DEVDESK_SESSION set at creation.
|
||||||
dd_current_session() {
|
dd_current_session() {
|
||||||
local name=""
|
local name="" outer=${TMUX:-}
|
||||||
if [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ]; then
|
# Only DevDesk's own tmux server: a Claude started with these settings in
|
||||||
|
# some other tmux must not write into DevDesk's state.
|
||||||
|
if [ "${outer%%,*}" = "$DEVDESK_SOCKET" ] && [ -n "${TMUX_PANE:-}" ]; then
|
||||||
name=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
name=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "${name:-${DEVDESK_SESSION:-}}"
|
printf '%s\n' "${name:-${DEVDESK_SESSION:-}}"
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
input=$(cat)
|
input=$(cat)
|
||||||
|
|
||||||
session=""
|
# Only record for panes on DevDesk's own tmux server.
|
||||||
if [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ]; then
|
session="" outer=${TMUX:-}
|
||||||
|
if [ "${outer%%,*}" = "${DEVDESK_SOCKET:-/run/devdesk/tmux.sock}" ] && [ -n "${TMUX_PANE:-}" ]; then
|
||||||
session=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
session=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
||||||
|
session=${session:-${DEVDESK_SESSION:-}}
|
||||||
fi
|
fi
|
||||||
session=${session:-${DEVDESK_SESSION:-}}
|
|
||||||
|
|
||||||
if [[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]]; then
|
if [[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]]; then
|
||||||
dir="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/context"
|
dir="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/context"
|
||||||
|
|||||||
@@ -197,6 +197,55 @@ claude_args() {
|
|||||||
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.model' <<<"$output")" = Opus ]
|
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.model' <<<"$output")" = Opus ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "hooks ignore Claude running in some other tmux server" {
|
||||||
|
start_server
|
||||||
|
other="$TMP/other.sock"
|
||||||
|
tmux -S "$other" new-session -d -s stranger 'sleep 60'
|
||||||
|
pane=$(tmux -S "$other" display -p -t =stranger '#{pane_id}')
|
||||||
|
echo '{"session_id":"nope","source":"startup"}' |
|
||||||
|
TMUX="$other,1,0" TMUX_PANE="$pane" "$REPO/libexec/devdesk-session-hook"
|
||||||
|
echo '{"model":{"display_name":"Opus"},"context_window":{}}' |
|
||||||
|
TMUX="$other,1,0" TMUX_PANE="$pane" "$REPO/libexec/devdesk-statusline" >/dev/null
|
||||||
|
tmux -S "$other" kill-server
|
||||||
|
[ ! -e "$DEVDESK_STATE_DIR/registry.json" ] || [ "$(jq -r '.sessions | has("stranger")' "$DEVDESK_STATE_DIR/registry.json")" = false ]
|
||||||
|
[ ! -e "$DEVDESK_STATE_DIR/context/stranger.json" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "repo reports git state and recognises Gitea remotes" {
|
||||||
|
export DEVDESK_GITEA_URL=http://127.0.0.1:9 # nothing listens: CI lookups fail fast
|
||||||
|
repo="$DEVDESK_PROJECTS_ROOT/alpha"
|
||||||
|
git -C "$repo" init -q -b main
|
||||||
|
git -C "$repo" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init
|
||||||
|
git -C "$repo" remote add origin gitea@10.10.10.63:LotusGuild/alpha.git
|
||||||
|
touch "$repo/new-file"
|
||||||
|
run devdesk attach --repo alpha
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$(jq -r '.git' <<<"$output")" = true ]
|
||||||
|
[ "$(jq -r '.branch' <<<"$output")" = main ]
|
||||||
|
[ "$(jq -r '.dirty' <<<"$output")" = 1 ]
|
||||||
|
[ "$(jq -r '.repo' <<<"$output")" = LotusGuild/alpha ]
|
||||||
|
[ "$(jq -r '.ci' <<<"$output")" = null ]
|
||||||
|
git -C "$repo" remote set-url origin https://127.0.0.1:9/LotusGuild/alpha-https.git
|
||||||
|
run devdesk repo alpha
|
||||||
|
[ "$(jq -r '.repo' <<<"$output")" = LotusGuild/alpha-https ]
|
||||||
|
git -C "$repo" remote set-url origin https://github.com/someone/else.git
|
||||||
|
run devdesk repo alpha
|
||||||
|
[ "$(jq -r '.repo' <<<"$output")" = null ]
|
||||||
|
run devdesk repo main
|
||||||
|
[ "$(jq -r '.git' <<<"$output")" = false ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "env.local overrides the packaged env" {
|
||||||
|
mkdir -p "$TMP/etc" "$DEVDESK_PROJECTS_ROOT/alpha/ops"
|
||||||
|
printf 'DEVDESK_DEFAULT_DIR=%s\n' "$DEVDESK_PROJECTS_ROOT" >"$TMP/etc/env"
|
||||||
|
printf 'DEVDESK_DEFAULT_DIR=%s\n' "$DEVDESK_PROJECTS_ROOT/alpha/ops" >"$TMP/etc/env.local"
|
||||||
|
export DEVDESK_ENV="$TMP/etc/env"
|
||||||
|
start_server
|
||||||
|
devdesk new adhoc
|
||||||
|
run tmux -S "$DEVDESK_SOCKET" display -p -t =adhoc:claude '#{pane_current_path}'
|
||||||
|
[ "$output" = "$DEVDESK_PROJECTS_ROOT/alpha/ops" ]
|
||||||
|
}
|
||||||
|
|
||||||
@test "web actions run through attach and answer with JSON" {
|
@test "web actions run through attach and answer with JSON" {
|
||||||
start_server
|
start_server
|
||||||
run devdesk attach --new web1 alpha
|
run devdesk attach --new web1 alpha
|
||||||
|
|||||||
@@ -89,8 +89,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="lt-frame dd-widget" id="widget-repo" data-state="stub">
|
<section class="lt-frame dd-widget" id="widget-repo" data-state="empty">
|
||||||
<div class="lt-section-header">Repo / CI <span class="lt-badge lt-badge-sm dd-soon">v1.1</span></div>
|
<div class="lt-section-header">Repo / CI</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<div class="lt-kv-grid">
|
<div class="lt-kv-grid">
|
||||||
<span class="lt-kv-key">Repo</span><span class="lt-kv-val" data-field="repo">—</span>
|
<span class="lt-kv-key">Repo</span><span class="lt-kv-val" data-field="repo">—</span>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<span class="lt-kv-key">CI · HEAD</span><span class="lt-kv-val"><span class="lt-status lt-status-pending" data-field="ci">no data</span></span>
|
<span class="lt-kv-key">CI · HEAD</span><span class="lt-kv-val"><span class="lt-status lt-status-pending" data-field="ci">no data</span></span>
|
||||||
<span class="lt-kv-key">Open PRs</span><span class="lt-kv-val" data-field="prs">—</span>
|
<span class="lt-kv-key">Open PRs</span><span class="lt-kv-val" data-field="prs">—</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="dd-muted dd-stub-note">Gitea status for this session's repo arrives with devdesk-api.</p>
|
<p class="dd-muted dd-stub-note dd-repo-note"></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ export async function fetchSessionList() {
|
|||||||
return parseListOutput(await runCommand([LIST_ARG]));
|
return parseListOutput(await runCommand([LIST_ARG]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Git + Gitea CI status for a session's folder (`devdesk attach --repo NAME`).
|
||||||
|
export async function fetchRepo(session) {
|
||||||
|
return parseListOutput(await runCommand(['--repo', session]));
|
||||||
|
}
|
||||||
|
|
||||||
// `devdesk attach --new|--rename|--end …`: resolves on {"ok":true}, otherwise
|
// `devdesk attach --new|--rename|--end …`: resolves on {"ok":true}, otherwise
|
||||||
// rejects with devdesk's own error message.
|
// rejects with devdesk's own error message.
|
||||||
export async function sessionAction(action, ...args) {
|
export async function sessionAction(action, ...args) {
|
||||||
|
|||||||
+35
-2
@@ -1,9 +1,10 @@
|
|||||||
/* global __DEVDESK_VERSION__ */
|
/* global __DEVDESK_VERSION__ */
|
||||||
import { createTerminal, loadFonts } from './term.js';
|
import { createTerminal, loadFonts } from './term.js';
|
||||||
import { TtydConnection, fetchToken, AuthExpiredError, UnauthorizedError } from './connection.js';
|
import { TtydConnection, fetchRepo, fetchToken, AuthExpiredError, UnauthorizedError } from './connection.js';
|
||||||
import { parseTitle, sessionFromSearch } from './protocol.js';
|
import { parseTitle, sessionFromSearch } from './protocol.js';
|
||||||
import { SessionsPanel, rememberSession } from './sessions.js';
|
import { SessionsPanel, rememberSession } from './sessions.js';
|
||||||
import { renderContext } from './context.js';
|
import { renderContext } from './context.js';
|
||||||
|
import { renderRepo } from './repo.js';
|
||||||
import { bindAskClaude } from './widgets.js';
|
import { bindAskClaude } from './widgets.js';
|
||||||
import * as notify from './notify.js';
|
import * as notify from './notify.js';
|
||||||
|
|
||||||
@@ -96,6 +97,34 @@ function showContext() {
|
|||||||
renderContext($('widget-context'), state.session, current?.context ?? null);
|
renderContext($('widget-context'), state.session, current?.context ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Repo / CI panel for the current session; notifies when HEAD's CI finishes.
|
||||||
|
let lastCi = { key: '', state: '' };
|
||||||
|
let repoBusy = false;
|
||||||
|
async function refreshRepo() {
|
||||||
|
const session = state.session;
|
||||||
|
if (!session) {
|
||||||
|
renderRepo($('widget-repo'), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (repoBusy) return;
|
||||||
|
repoBusy = true;
|
||||||
|
try {
|
||||||
|
const repo = await fetchRepo(session);
|
||||||
|
if (session !== state.session) return;
|
||||||
|
renderRepo($('widget-repo'), repo);
|
||||||
|
const key = `${session}@${repo.sha}`;
|
||||||
|
const ci = repo.ci?.total ? repo.ci.state : '';
|
||||||
|
if (key === lastCi.key && lastCi.state === 'pending' && ['success', 'failure', 'error'].includes(ci)) {
|
||||||
|
notify.ring(session, `CI ${ci === 'success' ? 'passed' : 'failed'} on ${repo.branch} @ ${repo.sha}`);
|
||||||
|
}
|
||||||
|
lastCi = { key, state: ci };
|
||||||
|
} catch {
|
||||||
|
// Keep the last rendering; the next refresh retries.
|
||||||
|
} finally {
|
||||||
|
repoBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let refreshTimer = 0;
|
let refreshTimer = 0;
|
||||||
function refreshSoon(delay = 1500) {
|
function refreshSoon(delay = 1500) {
|
||||||
clearTimeout(refreshTimer);
|
clearTimeout(refreshTimer);
|
||||||
@@ -133,6 +162,7 @@ function connect(session) {
|
|||||||
setWsState('connected', 'live');
|
setWsState('connected', 'live');
|
||||||
setSso('ok');
|
setSso('ok');
|
||||||
if (session) rememberSession(session);
|
if (session) rememberSession(session);
|
||||||
|
refreshRepo();
|
||||||
fit.fit();
|
fit.fit();
|
||||||
term.focus();
|
term.focus();
|
||||||
},
|
},
|
||||||
@@ -394,7 +424,10 @@ async function boot() {
|
|||||||
ui.sessionChip.textContent = to;
|
ui.sessionChip.textContent = to;
|
||||||
sessionsPanel.setCurrent(to);
|
sessionsPanel.setCurrent(to);
|
||||||
},
|
},
|
||||||
onData: () => showContext(),
|
onData: () => {
|
||||||
|
showContext();
|
||||||
|
refreshRepo();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
bindAskClaude($('sidebar'), (text) => {
|
bindAskClaude($('sidebar'), (text) => {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Repo / CI panel: git state of the current session's folder plus the Gitea CI
|
||||||
|
// status of its HEAD commit (`devdesk attach --repo <session>`).
|
||||||
|
|
||||||
|
const CI = {
|
||||||
|
success: ['passing', 'lt-status-completed'],
|
||||||
|
failure: ['failed', 'lt-status-failed'],
|
||||||
|
error: ['error', 'lt-status-failed'],
|
||||||
|
pending: ['running', 'lt-status-running'],
|
||||||
|
warning: ['warning', 'lt-status-pending'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function link(href, text) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.rel = 'noopener';
|
||||||
|
a.textContent = text;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderRepo(root, repo) {
|
||||||
|
const field = (name) => root.querySelector(`[data-field="${name}"]`);
|
||||||
|
const set = (name, value) => field(name).replaceChildren(value);
|
||||||
|
const note = root.querySelector('.dd-repo-note');
|
||||||
|
|
||||||
|
if (!repo || !repo.git) {
|
||||||
|
root.dataset.state = 'empty';
|
||||||
|
for (const f of ['repo', 'branch', 'dirty', 'prs']) set(f, '—');
|
||||||
|
const ci = field('ci');
|
||||||
|
ci.className = 'lt-status lt-status-pending';
|
||||||
|
ci.textContent = 'no data';
|
||||||
|
note.textContent = repo
|
||||||
|
? `${repo.dir} is not a git repository.`
|
||||||
|
: 'Open a session to see its repository.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.dataset.state = 'live';
|
||||||
|
set('repo', repo.url ? link(repo.url, repo.repo) : (repo.remote || 'no remote'));
|
||||||
|
const sync = repo.upstream
|
||||||
|
? [repo.ahead ? `↑${repo.ahead}` : '', repo.behind ? `↓${repo.behind}` : ''].filter(Boolean).join(' ') || 'in sync'
|
||||||
|
: 'no upstream';
|
||||||
|
set('branch', `${repo.branch} @ ${repo.sha || '—'} · ${sync}`);
|
||||||
|
set('dirty', repo.dirty ? `${repo.dirty} changed` : 'clean');
|
||||||
|
|
||||||
|
const ci = field('ci');
|
||||||
|
const [label, cls] = repo.ci?.total ? (CI[repo.ci.state] || [repo.ci.state, 'lt-status-pending'])
|
||||||
|
: [repo.ahead ? 'not pushed' : 'no CI', 'lt-status-pending'];
|
||||||
|
ci.className = `lt-status ${cls}`;
|
||||||
|
ci.replaceChildren(repo.ci?.url ? link(repo.ci.url, label) : label);
|
||||||
|
|
||||||
|
set('prs', repo.url && repo.prs != null
|
||||||
|
? link(`${repo.url}/pulls`, `${repo.prs} open`)
|
||||||
|
: '—');
|
||||||
|
note.textContent = repo.repo ? '' : 'Not a Gitea repository; CI status unavailable.';
|
||||||
|
}
|
||||||
+5
-1
@@ -288,7 +288,11 @@ body.dd-sidebar-open .dd-sidebar {
|
|||||||
.dd-context-grid { margin-top: var(--space-sm); }
|
.dd-context-grid { margin-top: var(--space-sm); }
|
||||||
|
|
||||||
[data-state="stub"] .lt-section-body,
|
[data-state="stub"] .lt-section-body,
|
||||||
#widget-context[data-state="empty"] .lt-kv-grid { opacity: 0.6; }
|
#widget-context[data-state="empty"] .lt-kv-grid,
|
||||||
|
#widget-repo[data-state="empty"] .lt-kv-grid { opacity: 0.6; }
|
||||||
|
|
||||||
|
.lt-status a { color: inherit; text-shadow: none; }
|
||||||
|
.dd-widget .lt-kv-val { overflow-wrap: anywhere; }
|
||||||
|
|
||||||
/* ---- overlay ----------------------------------------------------------------- */
|
/* ---- overlay ----------------------------------------------------------------- */
|
||||||
.dd-overlay {
|
.dd-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user