detached 1f5bcd2

feat(demo): add VEV_ENV sandbox with two remote containers

commit
1f5bcd2be970e5dc83954456ec952a1c9cb153b8
author
brice · · 2026-09-22 20:09 UTC
parent
0125c1b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +423 -0
M .gitignore +1 -0
@@ -9,3 +9,4 @@ docs/superpowers/
99 .worktrees/
1010 scripts/demo/out/
1111 /.worktrees/
12+/build/
@@ -9,3 +9,4 @@ docs/superpowers/
9 .worktrees/9 .worktrees/
10 scripts/demo/out/10 scripts/demo/out/
11 /.worktrees/11 /.worktrees/
12+/build/
A scripts/demo/README.md +80 -0
@@ -0,0 +1,80 @@
1+# Demo and sandbox scripts
2+
3+- `run.sh`, `demo.tape`, `smoke.tape`: VHS demo recording.
4+- `sandbox-run.sh`: automated acceptance. Client and two remotes all run in
5+ containers on an internal network.
6+- `test-remotes.sh`: two containerised remotes for manual testing from your own
7+ terminal.
8+
9+All of them build `build/vev` and bake it into the `scripts/demo/Dockerfile`
10+image, so local and remote vev are the same binary. Docker must be rootless.
11+
12+## Manual remote testing
13+
14+```sh
15+scripts/demo/test-remotes.sh up # build, start remote-a/remote-b, register both hosts
16+scripts/demo/test-remotes.sh shell # print the client command
17+scripts/demo/test-remotes.sh status # containers, ports, vev host list, vev ls --all
18+scripts/demo/test-remotes.sh broker-restart # stop only the test broker; the next vev call respawns it
19+scripts/demo/test-remotes.sh down # stop the test broker/daemon, remove containers and keys
20+```
21+
22+Run the printed command from a real terminal, or `scripts/demo/test-remotes.sh
23+shell --exec [vev args]`. It sets:
24+
25+- `VEV_ENV=test` and `VEV_ENV_ROOT=<checkout>/.dev`: all vev state lives in
26+ `.dev/test/`, apart from your installed vev.
27+- `PATH=.dev/test/bin:$PATH`: vev runs `ssh` from `PATH` and has no ssh config
28+ option, so `.dev/test/bin/ssh` wraps the real client with
29+ `-F .dev/test/ssh/config`. The key, `known_hosts` and config are generated in
30+ `.dev/test/ssh/`. `~/.ssh` and your agent are never used.
31+- `VEV_REMOTE_TRANSPORT=stdio`: hosts are registered with the SSH-only carriage.
32+ QUIC cannot work here. The remote proxy listens on a random UDP port, and
33+ the client dials that port on the SSH target name (`remote-a`), which does
34+ not resolve to the loopback port mapping.
35+- `-u VEV`: a client started from a pane of your real vev is not treated as
36+ nested.
37+
38+sshd is published on `127.0.0.1:2222` (remote-a) and `127.0.0.1:2223`
39+(remote-b). Override the ports with `VEV_TEST_SSH_PORT_A` and
40+`VEV_TEST_SSH_PORT_B`, and the environment name with `VEV_TEST_ENV`. Plain
41+shell access: `.dev/test/bin/ssh remote-a`.
42+
43+`up` is idempotent: it recreates the containers, reinstalls the key and
44+skips hosts that are already registered. A `VEV_ENV=test` broker started by an
45+older build keeps running until it is stopped, and `up` warns when it finds
46+one. Run `down` then `up` after a rebuild. `down` stops **every**
47+`VEV_ENV=test` daemon of this checkout, including sessions you opened
48+there by hand.
49+
50+Remote sessions live in the containers and disappear on `down`.
51+
52+### Checklist
53+
54+Open a few sessions first, with at least two tabs in one of them, local and on
55+each remote: `vev new work`, `vev new ra remote-a`, `vev new rb remote-b`
56+(each run with the environment above).
57+
58+- [ ] Picker over a session (`Alt+Space`, `SSP`): opens over the live
59+ session; `Esc` returns to the same session, tab and pane.
60+- [ ] Move pane (`MFP`) and move tab (`MAT`): the destination picker lists
61+ local and remote sessions and tabs; the move lands where you chose.
62+- [ ] `x` in the picker kills the selected session (local and remote); the
63+ row disappears and `vev ls --all` agrees.
64+- [ ] Tab rows: expand a session and see one row per tab, with the right names
65+ and active marker. Selecting a tab row lands on that tab.
66+- [ ] MRU order: switch A → B → C, reopen the picker, and check the order is
67+ C, B, A.
68+- [ ] Bell icons: in a background tab or session run `sleep 2; printf '\a'`,
69+ switch away, and check that the bell icon shows on that session and tab
70+ row. It clears once you visit it. Try it on a remote session too.
71+- [ ] Live preview: moving the cursor over a local session, then over a remote
72+ one, shows its current screen. Run `ticker` there to check that the
73+ preview updates live.
74+- [ ] Remote attach and switch: `vev attach demo@remote-a:<session>` from
75+ the shell (this also adds the learned target `demo@remote-a`), then
76+ switch between local, remote-a and remote-b sessions from the picker
77+ without dropping input or output.
78+- [ ] Broker restart: `scripts/demo/test-remotes.sh broker-restart` stops only
79+ the test broker. Reopen the picker from a running client and check that
80+ hosts and sessions come back.
@@ -0,0 +1,80 @@
1+# Demo and sandbox scripts
2+
3+- `run.sh`, `demo.tape`, `smoke.tape`: VHS demo recording.
4+- `sandbox-run.sh`: automated acceptance. Client and two remotes all run in
5+ containers on an internal network.
6+- `test-remotes.sh`: two containerised remotes for manual testing from your own
7+ terminal.
8+
9+All of them build `build/vev` and bake it into the `scripts/demo/Dockerfile`
10+image, so local and remote vev are the same binary. Docker must be rootless.
11+
12+## Manual remote testing
13+
14+```sh
15+scripts/demo/test-remotes.sh up # build, start remote-a/remote-b, register both hosts
16+scripts/demo/test-remotes.sh shell # print the client command
17+scripts/demo/test-remotes.sh status # containers, ports, vev host list, vev ls --all
18+scripts/demo/test-remotes.sh broker-restart # stop only the test broker; the next vev call respawns it
19+scripts/demo/test-remotes.sh down # stop the test broker/daemon, remove containers and keys
20+```
21+
22+Run the printed command from a real terminal, or `scripts/demo/test-remotes.sh
23+shell --exec [vev args]`. It sets:
24+
25+- `VEV_ENV=test` and `VEV_ENV_ROOT=<checkout>/.dev`: all vev state lives in
26+ `.dev/test/`, apart from your installed vev.
27+- `PATH=.dev/test/bin:$PATH`: vev runs `ssh` from `PATH` and has no ssh config
28+ option, so `.dev/test/bin/ssh` wraps the real client with
29+ `-F .dev/test/ssh/config`. The key, `known_hosts` and config are generated in
30+ `.dev/test/ssh/`. `~/.ssh` and your agent are never used.
31+- `VEV_REMOTE_TRANSPORT=stdio`: hosts are registered with the SSH-only carriage.
32+ QUIC cannot work here. The remote proxy listens on a random UDP port, and
33+ the client dials that port on the SSH target name (`remote-a`), which does
34+ not resolve to the loopback port mapping.
35+- `-u VEV`: a client started from a pane of your real vev is not treated as
36+ nested.
37+
38+sshd is published on `127.0.0.1:2222` (remote-a) and `127.0.0.1:2223`
39+(remote-b). Override the ports with `VEV_TEST_SSH_PORT_A` and
40+`VEV_TEST_SSH_PORT_B`, and the environment name with `VEV_TEST_ENV`. Plain
41+shell access: `.dev/test/bin/ssh remote-a`.
42+
43+`up` is idempotent: it recreates the containers, reinstalls the key and
44+skips hosts that are already registered. A `VEV_ENV=test` broker started by an
45+older build keeps running until it is stopped, and `up` warns when it finds
46+one. Run `down` then `up` after a rebuild. `down` stops **every**
47+`VEV_ENV=test` daemon of this checkout, including sessions you opened
48+there by hand.
49+
50+Remote sessions live in the containers and disappear on `down`.
51+
52+### Checklist
53+
54+Open a few sessions first, with at least two tabs in one of them, local and on
55+each remote: `vev new work`, `vev new ra remote-a`, `vev new rb remote-b`
56+(each run with the environment above).
57+
58+- [ ] Picker over a session (`Alt+Space`, `SSP`): opens over the live
59+ session; `Esc` returns to the same session, tab and pane.
60+- [ ] Move pane (`MFP`) and move tab (`MAT`): the destination picker lists
61+ local and remote sessions and tabs; the move lands where you chose.
62+- [ ] `x` in the picker kills the selected session (local and remote); the
63+ row disappears and `vev ls --all` agrees.
64+- [ ] Tab rows: expand a session and see one row per tab, with the right names
65+ and active marker. Selecting a tab row lands on that tab.
66+- [ ] MRU order: switch A → B → C, reopen the picker, and check the order is
67+ C, B, A.
68+- [ ] Bell icons: in a background tab or session run `sleep 2; printf '\a'`,
69+ switch away, and check that the bell icon shows on that session and tab
70+ row. It clears once you visit it. Try it on a remote session too.
71+- [ ] Live preview: moving the cursor over a local session, then over a remote
72+ one, shows its current screen. Run `ticker` there to check that the
73+ preview updates live.
74+- [ ] Remote attach and switch: `vev attach demo@remote-a:<session>` from
75+ the shell (this also adds the learned target `demo@remote-a`), then
76+ switch between local, remote-a and remote-b sessions from the picker
77+ without dropping input or output.
78+- [ ] Broker restart: `scripts/demo/test-remotes.sh broker-restart` stops only
79+ the test broker. Reopen the picker from a running client and check that
80+ hosts and sessions come back.
A scripts/demo/compose.test.yaml +38 -0
@@ -0,0 +1,38 @@
1+# Two SSH remotes for manual testing from the host terminal.
2+# Driven by scripts/demo/test-remotes.sh; not used by sandbox-run.sh.
3+#
4+# sshd is published on loopback only. QUIC is not published: the remote QUIC
5+# proxy listens on a random UDP port per bootstrap and the client dials the
6+# SSH target name, so hosts are registered with VEV_REMOTE_TRANSPORT=stdio.
7+# The project name is derived from VEV_ENV/VEV_TEST_ENV so distinct sandbox
8+# environments (see scripts/demo/test-remotes.sh) never share one compose
9+# project's containers or network.
10+name: vev-remotes-${VEV_TEST_ENV:-test}
11+
12+services:
13+ remote-a:
14+ image: vev-sandbox:${VEV_TEST_TAG:-test}
15+ hostname: remote-a
16+ user: root
17+ entrypoint: ["/usr/bin/sshd", "-D", "-e"]
18+ networks: [remotes]
19+ ports:
20+ - "127.0.0.1:${VEV_TEST_SSH_PORT_A:-2222}:22/tcp"
21+ tmpfs:
22+ - /run
23+ - /tmp
24+
25+ remote-b:
26+ image: vev-sandbox:${VEV_TEST_TAG:-test}
27+ hostname: remote-b
28+ user: root
29+ entrypoint: ["/usr/bin/sshd", "-D", "-e"]
30+ networks: [remotes]
31+ ports:
32+ - "127.0.0.1:${VEV_TEST_SSH_PORT_B:-2223}:22/tcp"
33+ tmpfs:
34+ - /run
35+ - /tmp
36+
37+networks:
38+ remotes: {}
@@ -0,0 +1,38 @@
1+# Two SSH remotes for manual testing from the host terminal.
2+# Driven by scripts/demo/test-remotes.sh; not used by sandbox-run.sh.
3+#
4+# sshd is published on loopback only. QUIC is not published: the remote QUIC
5+# proxy listens on a random UDP port per bootstrap and the client dials the
6+# SSH target name, so hosts are registered with VEV_REMOTE_TRANSPORT=stdio.
7+# The project name is derived from VEV_ENV/VEV_TEST_ENV so distinct sandbox
8+# environments (see scripts/demo/test-remotes.sh) never share one compose
9+# project's containers or network.
10+name: vev-remotes-${VEV_TEST_ENV:-test}
11+
12+services:
13+ remote-a:
14+ image: vev-sandbox:${VEV_TEST_TAG:-test}
15+ hostname: remote-a
16+ user: root
17+ entrypoint: ["/usr/bin/sshd", "-D", "-e"]
18+ networks: [remotes]
19+ ports:
20+ - "127.0.0.1:${VEV_TEST_SSH_PORT_A:-2222}:22/tcp"
21+ tmpfs:
22+ - /run
23+ - /tmp
24+
25+ remote-b:
26+ image: vev-sandbox:${VEV_TEST_TAG:-test}
27+ hostname: remote-b
28+ user: root
29+ entrypoint: ["/usr/bin/sshd", "-D", "-e"]
30+ networks: [remotes]
31+ ports:
32+ - "127.0.0.1:${VEV_TEST_SSH_PORT_B:-2223}:22/tcp"
33+ tmpfs:
34+ - /run
35+ - /tmp
36+
37+networks:
38+ remotes: {}
A scripts/demo/test-remotes.sh +304 -0
@@ -0,0 +1,304 @@
1+#!/usr/bin/env bash
2+# Two containerised SSH remotes for manual vev testing from the host terminal.
3+#
4+# scripts/demo/test-remotes.sh up|status|down|broker-restart|shell
5+#
6+# Everything vev-side runs with VEV_ENV=test, so config, state, runtime and
7+# hosts live in .dev/test/ of this checkout. SSH material lives in
8+# .dev/test/ssh/; ~/.ssh is never read or written. vev runs plain `ssh` from
9+# PATH, so .dev/test/bin/ssh wraps the real ssh with `-F .dev/test/ssh/config`.
10+#
11+# Environment:
12+# VEV_TEST_SSH_PORT_A / VEV_TEST_SSH_PORT_B loopback sshd ports (2222/2223)
13+# VEV_TEST_TAG image tag (test)
14+# VEV_TEST_ENV VEV_ENV name (test)
15+# DOCKER_HOST honoured as-is
16+set -euo pipefail
17+
18+cd "$(dirname "$0")/../.."
19+root=$(pwd -P)
20+
21+export VEV_TEST_TAG=${VEV_TEST_TAG:-test}
22+env_name=${VEV_TEST_ENV:-test}
23+# Exported so compose.test.yaml's `name:` interpolates the same resolved
24+# value: a distinct VEV_ENV name must get a distinct compose project, or two
25+# sandboxes running side by side under different VEV_TEST_ENV values collide
26+# on the same containers/network.
27+export VEV_TEST_ENV="$env_name"
28+export VEV_TEST_SSH_PORT_A=${VEV_TEST_SSH_PORT_A:-2222}
29+export VEV_TEST_SSH_PORT_B=${VEV_TEST_SSH_PORT_B:-2223}
30+
31+image="vev-sandbox:${VEV_TEST_TAG}"
32+compose=(docker compose -f scripts/demo/compose.test.yaml)
33+hosts=(remote-a remote-b)
34+env_root="$root/.dev"
35+test_dir="$env_root/$env_name"
36+ssh_dir="$test_dir/ssh"
37+bin_dir="$test_dir/bin"
38+vev_bin="$root/build/vev"
39+
40+die() { echo "test-remotes: error: $*" >&2; exit 1; }
41+log() { echo "test-remotes: $*" >&2; }
42+
43+port_for() {
44+ case "$1" in
45+ remote-a) echo "$VEV_TEST_SSH_PORT_A" ;;
46+ remote-b) echo "$VEV_TEST_SSH_PORT_B" ;;
47+ *) die "unknown host $1" ;;
48+ esac
49+}
50+
51+# vev_env runs a command with the sandbox vev environment. VEV is unset so a
52+# command launched from inside a real vev pane does not look nested.
53+vev_env() {
54+ env -u VEV -u VEV_ENV_ROOT \
55+ VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
56+ PATH="$bin_dir:$PATH" "$@"
57+}
58+
59+need_docker() {
60+ command -v docker >/dev/null || die "docker not found"
61+ docker info >/dev/null 2>&1 || die "docker daemon unreachable (DOCKER_HOST=${DOCKER_HOST:-unset})"
62+}
63+
64+real_ssh() {
65+ local path
66+ path=$(PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vxF "$bin_dir" | paste -sd:) command -v ssh) \
67+ || die "ssh client not found"
68+ echo "$path"
69+}
70+
71+write_ssh_material() {
72+ local dir
73+ for dir in "$env_root" "$test_dir" "$ssh_dir" "$bin_dir"; do
74+ [[ -d $dir ]] || mkdir -m 700 "$dir"
75+ done
76+ if [[ ! -f "$ssh_dir/id_ed25519" ]]; then
77+ ssh-keygen -q -t ed25519 -N '' -C vev-test-remotes -f "$ssh_dir/id_ed25519"
78+ fi
79+ {
80+ echo "# Generated by scripts/demo/test-remotes.sh. Sandbox only."
81+ local host
82+ for host in "${hosts[@]}"; do
83+ cat <<EOF
84+
85+Host $host
86+ HostName 127.0.0.1
87+ Port $(port_for "$host")
88+ User demo
89+ HostKeyAlias $host
90+ IdentityFile $ssh_dir/id_ed25519
91+ IdentitiesOnly yes
92+ IdentityAgent none
93+ UserKnownHostsFile $ssh_dir/known_hosts
94+ GlobalKnownHostsFile /dev/null
95+ StrictHostKeyChecking yes
96+ ControlMaster no
97+ ControlPath none
98+ LogLevel ERROR
99+EOF
100+ done
101+ } >"$ssh_dir/config"
102+ chmod 600 "$ssh_dir/config"
103+ cat >"$bin_dir/ssh" <<EOF
104+#!/bin/sh
105+# Sandbox ssh: vev runs \`ssh\` from PATH; force the sandbox config.
106+exec $(real_ssh) -F "$ssh_dir/config" "\$@"
107+EOF
108+ chmod 700 "$bin_dir/ssh"
109+}
110+
111+container_for() {
112+ "${compose[@]}" ps -q "$1"
113+}
114+
115+install_keys() {
116+ local host cid pub
117+ pub=$(cat "$ssh_dir/id_ed25519.pub")
118+ : >"$ssh_dir/known_hosts.tmp"
119+ for host in "${hosts[@]}"; do
120+ cid=$(container_for "$host")
121+ [[ -n $cid ]] || die "$host container is not running"
122+ docker exec -i "$cid" sh -c 'umask 077; cat > /home/demo/.ssh/authorized_keys && chown demo:demo /home/demo/.ssh/authorized_keys' <<<"$pub"
123+ printf '%s ' "$host" >>"$ssh_dir/known_hosts.tmp"
124+ docker exec "$cid" cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub >>"$ssh_dir/known_hosts.tmp"
125+ done
126+ mv "$ssh_dir/known_hosts.tmp" "$ssh_dir/known_hosts"
127+ chmod 600 "$ssh_dir/known_hosts"
128+}
129+
130+wait_ssh() {
131+ local host
132+ for host in "${hosts[@]}"; do
133+ local ok=
134+ for _ in $(seq 1 40); do
135+ if timeout 5 "$bin_dir/ssh" -o ConnectTimeout=2 -o BatchMode=yes "$host" true >/dev/null 2>&1; then
136+ ok=1; break
137+ fi
138+ sleep 0.25
139+ done
140+ [[ -n $ok ]] || die "SSH readiness timeout for $host (try: $bin_dir/ssh -v $host true)"
141+ timeout 10 "$bin_dir/ssh" -o BatchMode=yes "$host" 'vev --version' >/dev/null \
142+ || die "vev is not runnable on $host"
143+ log "ssh ready: $host"
144+ done
145+}
146+
147+register_hosts() {
148+ local host listed
149+ listed=$(vev_env timeout 30 "$vev_bin" host list 2>&1) || die "vev host list failed: $listed"
150+ for host in "${hosts[@]}"; do
151+ if grep -qw -- "$host" <<<"$listed"; then
152+ log "host already registered: $host"
153+ continue
154+ fi
155+ vev_env timeout 30 "$vev_bin" host add "$host" || die "vev host add $host failed"
156+ log "host registered: $host"
157+ done
158+}
159+
160+cmd_up() {
161+ need_docker
162+ log "building build/vev"
163+ go build -o build/vev .
164+ log "building $image"
165+ docker build -q --label vev.sandbox.binary-sha256="$(sha256sum build/vev | cut -d' ' -f1)" \
166+ -t "$image" -f scripts/demo/Dockerfile . >/dev/null
167+ local stale
168+ stale=$(vev_pids | while read -r pid; do
169+ exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
170+ [[ $exe == "$vev_bin" ]] || echo "$pid"
171+ done | paste -sd' ')
172+ if [[ -n $stale ]]; then
173+ log "warning: VEV_ENV=$env_name broker/daemon from an older build is still running (pids $stale)."
174+ log "warning: it keeps serving until stopped; run '$0 down' then '$0 up' to test the new build."
175+ fi
176+ write_ssh_material
177+ # --force-recreate picks up a rebuilt image and restarts remote daemons.
178+ local out
179+ out=$("${compose[@]}" up -d --force-recreate --remove-orphans remote-a remote-b 2>&1) \
180+ || die "compose up failed: $out"
181+ install_keys
182+ wait_ssh
183+ register_hosts
184+ echo
185+ cmd_shell_hint
186+}
187+
188+cmd_status() {
189+ need_docker
190+ "${compose[@]}" ps --format 'table {{.Service}}\t{{.State}}\t{{.Ports}}'
191+ echo
192+ if [[ ! -x $vev_bin ]]; then
193+ echo "build/vev missing; run: $0 up"
194+ return 0
195+ fi
196+ echo "== vev host list"
197+ vev_env timeout 15 "$vev_bin" host list || echo "(host list failed: $?)"
198+ echo "== vev ls --all"
199+ vev_env timeout 30 "$vev_bin" ls --all || echo "(ls --all failed: $?)"
200+}
201+
202+# vev_pids prints vev processes of this checkout's sandbox environment: the
203+# executable lives inside this checkout, and the environment carries both
204+# VEV_ENV=<name> and VEV_ENV_ROOT=<this checkout>/.dev. The owner's installed
205+# vev and other VEV_ENV names never match. Pane shells are not signalled; they
206+# end with their daemon.
207+vev_pids() {
208+ local environ pid exe vars
209+ for environ in /proc/[0-9]*/environ; do
210+ [[ -r $environ ]] || continue
211+ pid=${environ#/proc/}; pid=${pid%/environ}
212+ exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
213+ [[ ${exe% (deleted)} == "$root"/* ]] || continue
214+ vars=$(tr '\0' '\n' 2>/dev/null <"$environ") || continue
215+ if grep -qxF "VEV_ENV=$env_name" <<<"$vars" && grep -qxF "VEV_ENV_ROOT=$env_root" <<<"$vars"; then
216+ echo "$pid"
217+ fi
218+ done
219+ return 0
220+}
221+
222+cmd_down() {
223+ if [[ -x $vev_bin && -d $test_dir ]]; then
224+ local host
225+ # Forget the registrations so a later plain VEV_ENV=test client does not
226+ # keep dialing hosts that no longer exist; `up` re-adds them.
227+ for host in "${hosts[@]}"; do
228+ vev_env timeout 15 "$vev_bin" host rm "$host" >/dev/null 2>&1 || true
229+ done
230+ vev_env timeout 10 "$vev_bin" kill --daemon >/dev/null 2>&1 || true
231+ fi
232+ # vev has no broker stop command: TERM the sandbox broker and any leftover
233+ # sandbox roles, then KILL whatever ignores it.
234+ local pids
235+ pids=$(vev_pids | paste -sd' ')
236+ if [[ -n $pids ]]; then
237+ log "stopping VEV_ENV=$env_name vev processes: $pids"
238+ # shellcheck disable=SC2086
239+ kill -TERM $pids 2>/dev/null || true
240+ for _ in $(seq 1 20); do
241+ [[ -z $(vev_pids) ]] && break
242+ sleep 0.1
243+ done
244+ pids=$(vev_pids | paste -sd' ')
245+ # shellcheck disable=SC2086
246+ [[ -z $pids ]] || kill -KILL $pids 2>/dev/null || true
247+ fi
248+ if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
249+ "${compose[@]}" down -v --remove-orphans --timeout 5 >/dev/null 2>&1 || true
250+ else
251+ log "docker unreachable; containers were not removed (DOCKER_HOST=${DOCKER_HOST:-unset})"
252+ fi
253+ rm -rf "$ssh_dir" "$bin_dir"
254+ log "down"
255+}
256+
257+# broker_restart stops only this sandbox's broker. The daemon and its sessions
258+# stay up; the next vev invocation respawns the broker.
259+cmd_broker_restart() {
260+ local pid pids=()
261+ for pid in $(vev_pids); do
262+ if tr '\0' ' ' 2>/dev/null </proc/"$pid"/cmdline | grep -q '_broker-production-serve'; then
263+ pids+=("$pid")
264+ fi
265+ done
266+ [[ ${#pids[@]} -gt 0 ]] || die "no VEV_ENV=$env_name broker is running"
267+ log "stopping broker: ${pids[*]}"
268+ kill -TERM "${pids[@]}"
269+ for _ in $(seq 1 30); do
270+ kill -0 "${pids[@]}" 2>/dev/null || break
271+ sleep 0.1
272+ done
273+ vev_env timeout 30 "$vev_bin" host list
274+}
275+
276+cmd_shell_hint() {
277+ cat <<EOF
278+Sandbox ready. Launch the client from any terminal with:
279+
280+ cd $root && env -u VEV VEV_ENV=$env_name VEV_ENV_ROOT=$env_root VEV_REMOTE_TRANSPORT=stdio PATH=$bin_dir:\$PATH ./build/vev
281+
282+or: $0 shell --exec
283+Plain ssh: $bin_dir/ssh remote-a
284+EOF
285+}
286+
287+cmd_shell() {
288+ [[ -x $bin_dir/ssh ]] || die "sandbox is not up; run: $0 up"
289+ if [[ ${1:-} == --exec ]]; then
290+ shift
291+ exec env -u VEV -u VEV_ENV_ROOT VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
292+ PATH="$bin_dir:$PATH" "$vev_bin" "$@"
293+ fi
294+ cmd_shell_hint
295+}
296+
297+case "${1:-}" in
298+ up) cmd_up ;;
299+ status) cmd_status ;;
300+ down) cmd_down ;;
301+ broker-restart) cmd_broker_restart ;;
302+ shell) shift; cmd_shell "$@" ;;
303+ *) echo "usage: $0 up|status|down|broker-restart|shell [--exec [vev args]]" >&2; exit 2 ;;
304+esac
@@ -0,0 +1,304 @@
1+#!/usr/bin/env bash
2+# Two containerised SSH remotes for manual vev testing from the host terminal.
3+#
4+# scripts/demo/test-remotes.sh up|status|down|broker-restart|shell
5+#
6+# Everything vev-side runs with VEV_ENV=test, so config, state, runtime and
7+# hosts live in .dev/test/ of this checkout. SSH material lives in
8+# .dev/test/ssh/; ~/.ssh is never read or written. vev runs plain `ssh` from
9+# PATH, so .dev/test/bin/ssh wraps the real ssh with `-F .dev/test/ssh/config`.
10+#
11+# Environment:
12+# VEV_TEST_SSH_PORT_A / VEV_TEST_SSH_PORT_B loopback sshd ports (2222/2223)
13+# VEV_TEST_TAG image tag (test)
14+# VEV_TEST_ENV VEV_ENV name (test)
15+# DOCKER_HOST honoured as-is
16+set -euo pipefail
17+
18+cd "$(dirname "$0")/../.."
19+root=$(pwd -P)
20+
21+export VEV_TEST_TAG=${VEV_TEST_TAG:-test}
22+env_name=${VEV_TEST_ENV:-test}
23+# Exported so compose.test.yaml's `name:` interpolates the same resolved
24+# value: a distinct VEV_ENV name must get a distinct compose project, or two
25+# sandboxes running side by side under different VEV_TEST_ENV values collide
26+# on the same containers/network.
27+export VEV_TEST_ENV="$env_name"
28+export VEV_TEST_SSH_PORT_A=${VEV_TEST_SSH_PORT_A:-2222}
29+export VEV_TEST_SSH_PORT_B=${VEV_TEST_SSH_PORT_B:-2223}
30+
31+image="vev-sandbox:${VEV_TEST_TAG}"
32+compose=(docker compose -f scripts/demo/compose.test.yaml)
33+hosts=(remote-a remote-b)
34+env_root="$root/.dev"
35+test_dir="$env_root/$env_name"
36+ssh_dir="$test_dir/ssh"
37+bin_dir="$test_dir/bin"
38+vev_bin="$root/build/vev"
39+
40+die() { echo "test-remotes: error: $*" >&2; exit 1; }
41+log() { echo "test-remotes: $*" >&2; }
42+
43+port_for() {
44+ case "$1" in
45+ remote-a) echo "$VEV_TEST_SSH_PORT_A" ;;
46+ remote-b) echo "$VEV_TEST_SSH_PORT_B" ;;
47+ *) die "unknown host $1" ;;
48+ esac
49+}
50+
51+# vev_env runs a command with the sandbox vev environment. VEV is unset so a
52+# command launched from inside a real vev pane does not look nested.
53+vev_env() {
54+ env -u VEV -u VEV_ENV_ROOT \
55+ VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
56+ PATH="$bin_dir:$PATH" "$@"
57+}
58+
59+need_docker() {
60+ command -v docker >/dev/null || die "docker not found"
61+ docker info >/dev/null 2>&1 || die "docker daemon unreachable (DOCKER_HOST=${DOCKER_HOST:-unset})"
62+}
63+
64+real_ssh() {
65+ local path
66+ path=$(PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vxF "$bin_dir" | paste -sd:) command -v ssh) \
67+ || die "ssh client not found"
68+ echo "$path"
69+}
70+
71+write_ssh_material() {
72+ local dir
73+ for dir in "$env_root" "$test_dir" "$ssh_dir" "$bin_dir"; do
74+ [[ -d $dir ]] || mkdir -m 700 "$dir"
75+ done
76+ if [[ ! -f "$ssh_dir/id_ed25519" ]]; then
77+ ssh-keygen -q -t ed25519 -N '' -C vev-test-remotes -f "$ssh_dir/id_ed25519"
78+ fi
79+ {
80+ echo "# Generated by scripts/demo/test-remotes.sh. Sandbox only."
81+ local host
82+ for host in "${hosts[@]}"; do
83+ cat <<EOF
84+
85+Host $host
86+ HostName 127.0.0.1
87+ Port $(port_for "$host")
88+ User demo
89+ HostKeyAlias $host
90+ IdentityFile $ssh_dir/id_ed25519
91+ IdentitiesOnly yes
92+ IdentityAgent none
93+ UserKnownHostsFile $ssh_dir/known_hosts
94+ GlobalKnownHostsFile /dev/null
95+ StrictHostKeyChecking yes
96+ ControlMaster no
97+ ControlPath none
98+ LogLevel ERROR
99+EOF
100+ done
101+ } >"$ssh_dir/config"
102+ chmod 600 "$ssh_dir/config"
103+ cat >"$bin_dir/ssh" <<EOF
104+#!/bin/sh
105+# Sandbox ssh: vev runs \`ssh\` from PATH; force the sandbox config.
106+exec $(real_ssh) -F "$ssh_dir/config" "\$@"
107+EOF
108+ chmod 700 "$bin_dir/ssh"
109+}
110+
111+container_for() {
112+ "${compose[@]}" ps -q "$1"
113+}
114+
115+install_keys() {
116+ local host cid pub
117+ pub=$(cat "$ssh_dir/id_ed25519.pub")
118+ : >"$ssh_dir/known_hosts.tmp"
119+ for host in "${hosts[@]}"; do
120+ cid=$(container_for "$host")
121+ [[ -n $cid ]] || die "$host container is not running"
122+ docker exec -i "$cid" sh -c 'umask 077; cat > /home/demo/.ssh/authorized_keys && chown demo:demo /home/demo/.ssh/authorized_keys' <<<"$pub"
123+ printf '%s ' "$host" >>"$ssh_dir/known_hosts.tmp"
124+ docker exec "$cid" cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub >>"$ssh_dir/known_hosts.tmp"
125+ done
126+ mv "$ssh_dir/known_hosts.tmp" "$ssh_dir/known_hosts"
127+ chmod 600 "$ssh_dir/known_hosts"
128+}
129+
130+wait_ssh() {
131+ local host
132+ for host in "${hosts[@]}"; do
133+ local ok=
134+ for _ in $(seq 1 40); do
135+ if timeout 5 "$bin_dir/ssh" -o ConnectTimeout=2 -o BatchMode=yes "$host" true >/dev/null 2>&1; then
136+ ok=1; break
137+ fi
138+ sleep 0.25
139+ done
140+ [[ -n $ok ]] || die "SSH readiness timeout for $host (try: $bin_dir/ssh -v $host true)"
141+ timeout 10 "$bin_dir/ssh" -o BatchMode=yes "$host" 'vev --version' >/dev/null \
142+ || die "vev is not runnable on $host"
143+ log "ssh ready: $host"
144+ done
145+}
146+
147+register_hosts() {
148+ local host listed
149+ listed=$(vev_env timeout 30 "$vev_bin" host list 2>&1) || die "vev host list failed: $listed"
150+ for host in "${hosts[@]}"; do
151+ if grep -qw -- "$host" <<<"$listed"; then
152+ log "host already registered: $host"
153+ continue
154+ fi
155+ vev_env timeout 30 "$vev_bin" host add "$host" || die "vev host add $host failed"
156+ log "host registered: $host"
157+ done
158+}
159+
160+cmd_up() {
161+ need_docker
162+ log "building build/vev"
163+ go build -o build/vev .
164+ log "building $image"
165+ docker build -q --label vev.sandbox.binary-sha256="$(sha256sum build/vev | cut -d' ' -f1)" \
166+ -t "$image" -f scripts/demo/Dockerfile . >/dev/null
167+ local stale
168+ stale=$(vev_pids | while read -r pid; do
169+ exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
170+ [[ $exe == "$vev_bin" ]] || echo "$pid"
171+ done | paste -sd' ')
172+ if [[ -n $stale ]]; then
173+ log "warning: VEV_ENV=$env_name broker/daemon from an older build is still running (pids $stale)."
174+ log "warning: it keeps serving until stopped; run '$0 down' then '$0 up' to test the new build."
175+ fi
176+ write_ssh_material
177+ # --force-recreate picks up a rebuilt image and restarts remote daemons.
178+ local out
179+ out=$("${compose[@]}" up -d --force-recreate --remove-orphans remote-a remote-b 2>&1) \
180+ || die "compose up failed: $out"
181+ install_keys
182+ wait_ssh
183+ register_hosts
184+ echo
185+ cmd_shell_hint
186+}
187+
188+cmd_status() {
189+ need_docker
190+ "${compose[@]}" ps --format 'table {{.Service}}\t{{.State}}\t{{.Ports}}'
191+ echo
192+ if [[ ! -x $vev_bin ]]; then
193+ echo "build/vev missing; run: $0 up"
194+ return 0
195+ fi
196+ echo "== vev host list"
197+ vev_env timeout 15 "$vev_bin" host list || echo "(host list failed: $?)"
198+ echo "== vev ls --all"
199+ vev_env timeout 30 "$vev_bin" ls --all || echo "(ls --all failed: $?)"
200+}
201+
202+# vev_pids prints vev processes of this checkout's sandbox environment: the
203+# executable lives inside this checkout, and the environment carries both
204+# VEV_ENV=<name> and VEV_ENV_ROOT=<this checkout>/.dev. The owner's installed
205+# vev and other VEV_ENV names never match. Pane shells are not signalled; they
206+# end with their daemon.
207+vev_pids() {
208+ local environ pid exe vars
209+ for environ in /proc/[0-9]*/environ; do
210+ [[ -r $environ ]] || continue
211+ pid=${environ#/proc/}; pid=${pid%/environ}
212+ exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
213+ [[ ${exe% (deleted)} == "$root"/* ]] || continue
214+ vars=$(tr '\0' '\n' 2>/dev/null <"$environ") || continue
215+ if grep -qxF "VEV_ENV=$env_name" <<<"$vars" && grep -qxF "VEV_ENV_ROOT=$env_root" <<<"$vars"; then
216+ echo "$pid"
217+ fi
218+ done
219+ return 0
220+}
221+
222+cmd_down() {
223+ if [[ -x $vev_bin && -d $test_dir ]]; then
224+ local host
225+ # Forget the registrations so a later plain VEV_ENV=test client does not
226+ # keep dialing hosts that no longer exist; `up` re-adds them.
227+ for host in "${hosts[@]}"; do
228+ vev_env timeout 15 "$vev_bin" host rm "$host" >/dev/null 2>&1 || true
229+ done
230+ vev_env timeout 10 "$vev_bin" kill --daemon >/dev/null 2>&1 || true
231+ fi
232+ # vev has no broker stop command: TERM the sandbox broker and any leftover
233+ # sandbox roles, then KILL whatever ignores it.
234+ local pids
235+ pids=$(vev_pids | paste -sd' ')
236+ if [[ -n $pids ]]; then
237+ log "stopping VEV_ENV=$env_name vev processes: $pids"
238+ # shellcheck disable=SC2086
239+ kill -TERM $pids 2>/dev/null || true
240+ for _ in $(seq 1 20); do
241+ [[ -z $(vev_pids) ]] && break
242+ sleep 0.1
243+ done
244+ pids=$(vev_pids | paste -sd' ')
245+ # shellcheck disable=SC2086
246+ [[ -z $pids ]] || kill -KILL $pids 2>/dev/null || true
247+ fi
248+ if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
249+ "${compose[@]}" down -v --remove-orphans --timeout 5 >/dev/null 2>&1 || true
250+ else
251+ log "docker unreachable; containers were not removed (DOCKER_HOST=${DOCKER_HOST:-unset})"
252+ fi
253+ rm -rf "$ssh_dir" "$bin_dir"
254+ log "down"
255+}
256+
257+# broker_restart stops only this sandbox's broker. The daemon and its sessions
258+# stay up; the next vev invocation respawns the broker.
259+cmd_broker_restart() {
260+ local pid pids=()
261+ for pid in $(vev_pids); do
262+ if tr '\0' ' ' 2>/dev/null </proc/"$pid"/cmdline | grep -q '_broker-production-serve'; then
263+ pids+=("$pid")
264+ fi
265+ done
266+ [[ ${#pids[@]} -gt 0 ]] || die "no VEV_ENV=$env_name broker is running"
267+ log "stopping broker: ${pids[*]}"
268+ kill -TERM "${pids[@]}"
269+ for _ in $(seq 1 30); do
270+ kill -0 "${pids[@]}" 2>/dev/null || break
271+ sleep 0.1
272+ done
273+ vev_env timeout 30 "$vev_bin" host list
274+}
275+
276+cmd_shell_hint() {
277+ cat <<EOF
278+Sandbox ready. Launch the client from any terminal with:
279+
280+ cd $root && env -u VEV VEV_ENV=$env_name VEV_ENV_ROOT=$env_root VEV_REMOTE_TRANSPORT=stdio PATH=$bin_dir:\$PATH ./build/vev
281+
282+or: $0 shell --exec
283+Plain ssh: $bin_dir/ssh remote-a
284+EOF
285+}
286+
287+cmd_shell() {
288+ [[ -x $bin_dir/ssh ]] || die "sandbox is not up; run: $0 up"
289+ if [[ ${1:-} == --exec ]]; then
290+ shift
291+ exec env -u VEV -u VEV_ENV_ROOT VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
292+ PATH="$bin_dir:$PATH" "$vev_bin" "$@"
293+ fi
294+ cmd_shell_hint
295+}
296+
297+case "${1:-}" in
298+ up) cmd_up ;;
299+ status) cmd_status ;;
300+ down) cmd_down ;;
301+ broker-restart) cmd_broker_restart ;;
302+ shell) shift; cmd_shell "$@" ;;
303+ *) echo "usage: $0 up|status|down|broker-restart|shell [--exec [vev args]]" >&2; exit 2 ;;
304+esac

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close