@vibelord/lanes 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/bin/lanes-install.mjs +158 -0
- package/lib/plan.mjs +65 -0
- package/package.json +37 -0
- package/payload/lanes/README.md +152 -0
- package/payload/lanes/VERSION +1 -0
- package/payload/lanes/bin/lanes +1035 -0
- package/payload/lanes/client/ghostty/common.conf +22 -0
- package/payload/lanes/client/ghostty/macos.conf +6 -0
- package/payload/lanes/client/ghostty/omarchy.conf +19 -0
- package/payload/lanes/client/install.sh +16 -0
- package/payload/lanes/client/lanes-launch +315 -0
- package/payload/lanes/client/lanes-upload +147 -0
- package/payload/lanes/client/lanes-window +36 -0
- package/payload/lanes/client/macos-helper.applescript +14 -0
- package/payload/lanes/client/omarchy/discover.sh +56 -0
- package/payload/lanes/client/omarchy/lanes.conf +5 -0
- package/payload/lanes/client/omarchy/lanes.desktop +9 -0
- package/payload/lanes/client/omarchy/lanes.lua +14 -0
- package/payload/lanes/client/ssh.stanza.in +54 -0
- package/payload/lanes/container/env.sh +16 -0
- package/payload/lanes/container/lane +268 -0
- package/payload/lanes/container/lane-run +34 -0
- package/payload/lanes/docs/ARCHITECTURE.md +93 -0
- package/payload/lanes/docs/HOST-GUARD.md +108 -0
- package/payload/lanes/docs/INSTALL.md +192 -0
- package/payload/lanes/docs/RUNBOOK.md +385 -0
- package/payload/lanes/docs/SHARING.md +124 -0
- package/payload/lanes/docs/ZMX-RING0.md +51 -0
- package/payload/lanes/docs/images/desktop.png +0 -0
- package/payload/lanes/docs/images/iphone.png +0 -0
- package/payload/lanes/holders/exec.sh +21 -0
- package/payload/lanes/holders/tmux.sh +111 -0
- package/payload/lanes/holders/zmx.sh +22 -0
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-phone.sh +43 -0
- package/payload/lanes/install/enable-web.sh +16 -0
- package/payload/lanes/install/host-install.sh +72 -0
- package/payload/lanes/install/services.py +61 -0
- package/payload/lanes/install.sh +603 -0
- package/payload/lanes/libexec/cursor_commit.py +159 -0
- package/payload/lanes/libexec/cursor_sync.py +166 -0
- package/payload/lanes/libexec/guard.py +231 -0
- package/payload/lanes/libexec/lanes_lib.py +610 -0
- package/payload/lanes/libexec/legacy_sessions.py +130 -0
- package/payload/lanes/libexec/session_actions.py +276 -0
- package/payload/lanes/libexec/sidebar +8 -0
- package/payload/lanes/libexec/sidebar.py +872 -0
- package/payload/lanes/libexec/statusd.py +764 -0
- package/payload/lanes/libexec/tmpscan.py +921 -0
- package/payload/lanes/libexec/view.sh +179 -0
- package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
- package/payload/lanes/site/README.md +28 -0
- package/payload/lanes/site/beyondwork/known_hosts +1 -0
- package/payload/lanes/site/beyondwork/site.conf +11 -0
- package/payload/lanes/site/site.example.conf +10 -0
- package/payload/lanes/sudo/host-guard.sh +356 -0
- package/payload/lanes/sudo/one-time-root.sh +32 -0
- package/payload/lanes/systemd/lanes-ensure.service +7 -0
- package/payload/lanes/systemd/lanes-ensure.timer +10 -0
- package/payload/lanes/systemd/lanes-gc.service +7 -0
- package/payload/lanes/systemd/lanes-gc.timer +9 -0
- package/payload/lanes/systemd/lanes-guard.service +8 -0
- package/payload/lanes/systemd/lanes-guard.timer +10 -0
- package/payload/lanes/systemd/lanes-mobile.service +23 -0
- package/payload/lanes/systemd/lanes-statusd.service +15 -0
- package/payload/lanes/systemd/lanes-sync.service +10 -0
- package/payload/lanes/systemd/lanes-sync.timer +10 -0
- package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
- package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
- package/payload/lanes/systemd/lanes-web.service +13 -0
- package/payload/lanes/systemd/lanes.slice +11 -0
- package/payload/lanes/web/Caddyfile +37 -0
- package/payload/lanes/web/README.md +35 -0
- package/payload/lanes/web/icon-192.png +0 -0
- package/payload/lanes/web/icon.svg +4 -0
- package/payload/lanes/web/index.html +30 -0
- package/payload/lanes/web/manifest.webmanifest +15 -0
- package/payload/lanes/web/mobile.css +301 -0
- package/payload/lanes/web/mobile.js +367 -0
- package/payload/lanes/workspaces.example/README.md +27 -0
- package/payload/lanes/workspaces.example/catalog.json +7 -0
- package/payload/lanes/workspaces.example/commit-targets.json +8 -0
- package/payload/lanes/workspaces.example/terminals.example.json +19 -0
- package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
- package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
- package/payload/lanes/zellij/config.kdl +360 -0
- package/payload/manifest.json +31 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Attach this terminal to an EXISTING in-container tmux lane, straight over its unix socket.
|
|
3
|
+
# usage: tmux.sh <container> <socket> <session>
|
|
4
|
+
#
|
|
5
|
+
# Guards (all mandatory, see docs/ARCHITECTURE.md):
|
|
6
|
+
# -N never start a tmux server (a plain attach on a missing socket would start a phantom one)
|
|
7
|
+
# env -i tmux's update-environment must not copy this host's SSH_* / DISPLAY into the lane
|
|
8
|
+
# init-pid the socket is reached through /proc/<container-init-pid>/root, verified to be a docker cgroup
|
|
9
|
+
# pty guard the lane's client-attached hook detaches any client whose tty NAME equals the lane's
|
|
10
|
+
# pane_tty; host and container pty numbers can collide, so re-wrap in a fresh pty on a match
|
|
11
|
+
# ignore-size while the old viewers coexist, never resize a live agent's window
|
|
12
|
+
set -u
|
|
13
|
+
state=${LANES_STATE:-$HOME/.local/state/lanes}
|
|
14
|
+
container=${1:-} socket=${2:-} session=${3:-}
|
|
15
|
+
fail() { echo "$2" >&2; exit "$1"; } # the view shows the last stderr line on its status screen
|
|
16
|
+
case "$container" in ''|-*|*[!A-Za-z0-9_.-]*) fail 64 'invalid container' ;; esac
|
|
17
|
+
case "$socket" in ''|-*|*[!A-Za-z0-9_.-]*) fail 64 'invalid socket' ;; esac
|
|
18
|
+
# The session gate admits what TMUX itself can hold, not what a lane may be DECLARED with. statusd lists every
|
|
19
|
+
# live session, its name outside the declaration grammar or not (the `<name> (drain)` twins, a hand-made
|
|
20
|
+
# `Planning Notes 2`), and the sidebar offers each as a 👻 row — so a gate copied from
|
|
21
|
+
# `lanes_lib.TMUX_SESSION` refused `invalid session` (64) on exactly those rows and view.sh retried that refusal
|
|
22
|
+
# for ever under a name the lane itself answers to (DEF-891). MEASURED (tmux 3.4): `new-session -s` accepts
|
|
23
|
+
# `(`, `)`, `/`, `,`, `$`, a backtick, a quote and a leading `-`; only `:` and `.` are rewritten to `_`, so
|
|
24
|
+
# such a name simply has no session and fails below with the precise "no such tmux session". The name never
|
|
25
|
+
# reaches a shell: it is one argv element behind `-t "$target"` (an EXACT match, so a leading `-` is inert),
|
|
26
|
+
# and the pty re-wrap below hands it to `script` through the ENVIRONMENT, never inside a quoted command string.
|
|
27
|
+
# Control characters stay refused: no listing round-trips them.
|
|
28
|
+
case "$session" in ''|*[[:cntrl:]]*) fail 64 'invalid session' ;; esac
|
|
29
|
+
# Every target carries the WINDOW suffix `:`. A trailing `;` ends a tmux command in ANY argv element, so a bare
|
|
30
|
+
# `-t "=$session"` on a live session named `foo;` resolved `=foo`: MEASURED (tmux 3.4, scratch server) an
|
|
31
|
+
# `attach-session -t '=foo;'` put the client on `foo` — a lane the operator did not select — while with `foo`
|
|
32
|
+
# gone `has-session -t '=foo;'` failed "can't find session: foo" although `foo;` was listed. With the suffix
|
|
33
|
+
# the same three calls resolve `foo;` (`attach-session -t '=foo;:'` landed on `foo;`, `-f ignore-size` intact).
|
|
34
|
+
# `:` alone is the session's CURRENT window, which is what a plain attach selects anyway (DEF-917).
|
|
35
|
+
target="=$session:"
|
|
36
|
+
|
|
37
|
+
is_container_pid() {
|
|
38
|
+
[ "${1:-0}" -gt 0 ] 2>/dev/null && [ -r "/proc/$1/cgroup" ] && grep -q docker "/proc/$1/cgroup" 2>/dev/null
|
|
39
|
+
}
|
|
40
|
+
pid=$(cat "$state/containers/$container.pid" 2>/dev/null || echo 0)
|
|
41
|
+
if ! is_container_pid "$pid"; then
|
|
42
|
+
pid=$(docker inspect -f '{{.State.Pid}}' "$container" 2>/dev/null || echo 0)
|
|
43
|
+
is_container_pid "$pid" || fail 69 "container $container is not running"
|
|
44
|
+
fi
|
|
45
|
+
path=/proc/$pid/root/tmp/tmux-${LANES_CONTAINER_UID:-1000}/$socket
|
|
46
|
+
if [ ! -S "$path" ]; then
|
|
47
|
+
# statusd saw the server PROCESS alive while its socket file is gone = somebody emptied the container's /tmp.
|
|
48
|
+
# 75 (try again), not 69: the lane exists, so the view keeps its "THE LANE IS STILL RUNNING" headline.
|
|
49
|
+
if grep -q -x -F "$container/$socket" "$state/flags/sockets-lost" 2>/dev/null; then
|
|
50
|
+
fail 75 "the lane runs, but its tmux socket file was deleted (emptied /tmp?) — on the server: lanes sockets repair"
|
|
51
|
+
fi
|
|
52
|
+
fail 69 "tmux server '$socket' is not running in $container"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Viewer marker: a user-level terminfo alias of xterm-256color (install/host-install.sh). tmux 3.4 reads terminfo
|
|
56
|
+
# on the client side and exposes the name as #{client_termname}; `xterm*` keeps tmux's default xterm features.
|
|
57
|
+
# (#{client_cwd} is empty in tmux 3.4, so the working directory cannot serve as the marker.)
|
|
58
|
+
view_term=xterm-256color
|
|
59
|
+
if infocmp xterm-lanesview >/dev/null 2>&1; then view_term=xterm-lanesview; fi
|
|
60
|
+
lanes_tmux() {
|
|
61
|
+
env -i PATH=/usr/bin:/bin HOME="$HOME" TERM="$view_term" LANG=C.UTF-8 COLORTERM=truecolor \
|
|
62
|
+
tmux -N -u -2 -T RGB,hyperlinks -S "$path" "$@"
|
|
63
|
+
}
|
|
64
|
+
lanes_tmux has-session -t "$target" 2>/dev/null || fail 66 "lane $socket / $session is not provisioned (no such tmux session)"
|
|
65
|
+
# ... and the session that ANSWERED is the one asked for. `$<n>`, `@<n>` and `%<n>` are tmux OBJECT IDs, and
|
|
66
|
+
# neither the `=` exact-match prefix nor the `:` window suffix stops the id lookup: MEASURED (tmux 3.4, scratch
|
|
67
|
+
# server holding `plain`=$0, `other`=$1 and a session literally NAMED `$0`) `display-message -p -t '=$0:'`
|
|
68
|
+
# answered `plain|$0`, `has-session` and `list-clients` succeeded against `plain`, and a real
|
|
69
|
+
# `attach-session -f ignore-size -t '=$0:'` put the client on `plain` — a lane the operator did not select,
|
|
70
|
+
# whose editor clients the size pass below would then put into ignore-size. The gate above admits such a name
|
|
71
|
+
# on purpose (statusd lists every live session and the sidebar opens it, DEF-891), so the ANSWER is what is
|
|
72
|
+
# checked, exactly as the Host's channel preflight checks it (`studio_host::channel`, `LaneMissing`). DEF-968.
|
|
73
|
+
answering=$(lanes_tmux display-message -p -t "$target" '#{session_name}' 2>/dev/null || echo '')
|
|
74
|
+
[ "$answering" = "$session" ] || fail 66 "lane $socket / $session is not provisioned (that target resolves the session '$answering')"
|
|
75
|
+
|
|
76
|
+
flags=
|
|
77
|
+
if [ -e "$state/flags/coexist" ] && [ ! -e "$state/flags/sized.${LANES_WS:-}" ]; then flags=ignore-size; fi
|
|
78
|
+
if [ -e "$state/flags/readonly" ]; then flags=${flags:+$flags,}read-only; fi
|
|
79
|
+
|
|
80
|
+
my_tty=$(tty 2>/dev/null || echo none)
|
|
81
|
+
pane_tty=$(lanes_tmux display-message -p -t "$target" '#{pane_tty}' 2>/dev/null || echo unknown)
|
|
82
|
+
if [ "$my_tty" = "$pane_tty" ] && [ -z "${LANES_REWRAPPED:-}" ]; then
|
|
83
|
+
# Same pty NAME in two mount namespaces: move this viewer onto a fresh pty. The three arguments travel in
|
|
84
|
+
# the ENVIRONMENT and the command string is single-quoted, so the inner `sh -c` expands them as values: a
|
|
85
|
+
# session name carrying a `'` (which tmux accepts) used to close the quoting and run the rest as a command.
|
|
86
|
+
LANES_REWRAPPED=1 LANES_STATE="$state" LANES_WS="${LANES_WS:-}" \
|
|
87
|
+
LANES_SELF="$0" LANES_ARG_CONTAINER="$container" LANES_ARG_SOCKET="$socket" LANES_ARG_SESSION="$session" \
|
|
88
|
+
exec script -qfc 'exec "$LANES_SELF" "$LANES_ARG_CONTAINER" "$LANES_ARG_SOCKET" "$LANES_ARG_SESSION"' /dev/null
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# The lanes view owns the size of the lane it shows: put editor terminals (Cursor / VS Code, ad-hoc attaches)
|
|
92
|
+
# already on this lane into ignore-size now. statusd reconciles continuously and gives them the size back once
|
|
93
|
+
# no viewer shows the lane.
|
|
94
|
+
if [ -z "$flags" ] && [ "$view_term" = xterm-lanesview ]; then
|
|
95
|
+
lanes_tmux list-clients -t "$target" -F '#{client_name}|#{client_termname}|#{client_flags}' 2>/dev/null |
|
|
96
|
+
while IFS='|' read -r c_name c_term c_flags; do
|
|
97
|
+
[ "$c_term" = xterm-lanesview ] && continue
|
|
98
|
+
case ",$c_flags," in *,ignore-size,*) continue ;; esac
|
|
99
|
+
[ "$c_name" = "$my_tty" ] && continue
|
|
100
|
+
lanes_tmux refresh-client -t "$c_name" -f ignore-size 2>/dev/null || :
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# exec: the supervisor's child pid must BE the tmux client, so a lane switch (SIGTERM) detaches it cleanly
|
|
105
|
+
# instead of orphaning a client on this pty.
|
|
106
|
+
if [ -n "$flags" ]; then
|
|
107
|
+
exec env -i PATH=/usr/bin:/bin HOME="$HOME" TERM="$view_term" LANG=C.UTF-8 COLORTERM=truecolor \
|
|
108
|
+
tmux -N -u -2 -T RGB,hyperlinks -S "$path" attach-session -f "$flags" -t "$target"
|
|
109
|
+
fi
|
|
110
|
+
exec env -i PATH=/usr/bin:/bin HOME="$HOME" TERM="$view_term" LANG=C.UTF-8 COLORTERM=truecolor \
|
|
111
|
+
tmux -N -u -2 -T RGB,hyperlinks -S "$path" attach-session -t "$target"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Attach this terminal to an EXISTING zmx-held lane inside a container, straight over its unix socket.
|
|
3
|
+
# usage: zmx.sh <container> <lane-name> <zmx-version>
|
|
4
|
+
#
|
|
5
|
+
# * zmx has no attach-only flag: a missing session would be CREATED (here, on the host). Guard twice:
|
|
6
|
+
# the socket must exist, and the create-command is /bin/false so an accidental creation dies at once.
|
|
7
|
+
# * zmx has no IPC version handshake: run the very binary the lane's daemon runs (from the home volume).
|
|
8
|
+
# * sun_path is 108 bytes and the volume path alone is 74: reach the sockets through the short symlink
|
|
9
|
+
# ~/.lz/<container> -> <home volume>/.lz (zmx does not canonicalise paths).
|
|
10
|
+
# * ZMX_NO_DETACH_KEY: Ctrl-\ belongs to the agent; leaving a lane is the view's job.
|
|
11
|
+
set -u
|
|
12
|
+
container=${1:-} name=${2:-} version=${3:-}
|
|
13
|
+
fail() { echo "$2" >&2; exit "$1"; }
|
|
14
|
+
case "$container" in ''|-*|*[!A-Za-z0-9_.-]*) fail 64 'invalid container' ;; esac
|
|
15
|
+
case "$name" in ''|-*|*[!A-Za-z0-9_.-]*) fail 64 'invalid lane name' ;; esac
|
|
16
|
+
case "$version" in ''|*[!0-9.]*) fail 64 'invalid zmx version' ;; esac
|
|
17
|
+
link=$HOME/.lz/$container
|
|
18
|
+
zmx=$link/bin/zmx-$version
|
|
19
|
+
[ -x "$zmx" ] || fail 69 "zmx $version is not installed for $container"
|
|
20
|
+
[ -S "$link/s/$name" ] || fail 66 "lane $name is not running"
|
|
21
|
+
exec env -i PATH=/usr/bin:/bin HOME="$HOME" TERM=xterm-256color LANG=C.UTF-8 \
|
|
22
|
+
ZMX_DIR="$link/s" ZMX_NO_DETACH_KEY=1 "$zmx" attach "$name" /bin/false
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Pinned binaries. install/host-install.sh refuses anything whose sha256 differs.
|
|
2
|
+
# name version sha256(tarball) url
|
|
3
|
+
# zellij 0.45.1: tarball digest matches GitHub's asset digest; inner binary d006c521…19dd matches the published .sha256sum.
|
|
4
|
+
zellij 0.45.1 40bcc2e03f5d5ae8e054e39f676081fe12ab70871506996ba595834c3718eefc https://github.com/zellij-org/zellij/releases/download/v0.45.1/zellij-x86_64-unknown-linux-musl.tar.gz
|
|
5
|
+
zmx 0.8.1 dfd75720b942466f28870731cc86dbc07afa72fb8f3bd5eeb4ff707e4eecebe8 https://github.com/neurosnap/zmx/releases/download/v0.8.1/zmx-0.8.1-linux-x86_64.tar.gz
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Key-authenticated client entry point and local server setup. Tokens stay in the caller's terminal.
|
|
3
|
+
set -eu
|
|
4
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
5
|
+
host= device=phone local=0 named=0
|
|
6
|
+
while [ "$#" -gt 0 ]; do
|
|
7
|
+
case "$1" in
|
|
8
|
+
--host) host=${2:?--host SSH_ALIAS}; shift ;;
|
|
9
|
+
--local) local=1 ;;
|
|
10
|
+
-h|--help)
|
|
11
|
+
echo 'usage: enable-phone.sh [--host SSH_ALIAS | --local] [device]'
|
|
12
|
+
echo 'Client: --host lanes uses the SSH key from ./install.sh. Server: --local.'
|
|
13
|
+
exit 0 ;;
|
|
14
|
+
-*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
15
|
+
*) [ "$named" = 0 ] || { echo 'only one device name is allowed' >&2; exit 2; }; device=$1; named=1 ;;
|
|
16
|
+
esac
|
|
17
|
+
shift
|
|
18
|
+
done
|
|
19
|
+
case "$device" in ''|-*|*[!A-Za-z0-9_.-]*) echo 'invalid device name' >&2; exit 2 ;; esac
|
|
20
|
+
case "$host" in -*|*[!A-Za-z0-9_.@:-]*) echo 'invalid SSH host' >&2; exit 2 ;; esac
|
|
21
|
+
[ "$local" = 0 ] || [ -z "$host" ] || { echo '--local and --host are mutually exclusive' >&2; exit 2; }
|
|
22
|
+
if [ "$local" = 0 ] && [ "$(uname -s)" = Darwin ] && [ -z "$host" ]; then host=lanes; fi
|
|
23
|
+
[ -t 0 ] && [ -t 1 ] || { echo 'Run enable-phone.sh in your own terminal; it displays a private login token.' >&2; exit 64; }
|
|
24
|
+
if [ -n "$host" ]; then
|
|
25
|
+
exec ssh -t -o BatchMode=yes -o PreferredAuthentications=publickey -o ForwardAgent=no "$host" \
|
|
26
|
+
"exec sh \"\$HOME/.local/share/lanes/current/install/enable-phone.sh\" --local '$device'"
|
|
27
|
+
fi
|
|
28
|
+
command -v tailscale >/dev/null 2>&1 || { echo 'Install and connect Tailscale on the server first.' >&2; exit 1; }
|
|
29
|
+
sh "$repo/install/enable-web.sh" "$device"
|
|
30
|
+
if ! tailscale serve --bg --https=8443 http://127.0.0.1:8083; then
|
|
31
|
+
echo 'The server admin must allow this account to manage Tailscale Serve:' >&2
|
|
32
|
+
printf ' sudo tailscale set --operator=%s\n' "$(id -un)" >&2
|
|
33
|
+
echo 'Then run this command again. See docs/INSTALL.md for HTTPS setup.' >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
dns=$(tailscale status --json | python3 -c 'import json,sys; print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))')
|
|
37
|
+
[ -n "$dns" ] || { echo 'This node needs Tailscale MagicDNS and HTTPS enabled.' >&2; exit 1; }
|
|
38
|
+
printf '\nOn your phone, connect Tailscale and open:\n\n https://%s:8443/%s\n\n' "$dns" "$device"
|
|
39
|
+
printf 'The last line is NAME: TOKEN. Paste only TOKEN into the sign-in screen.\n'
|
|
40
|
+
printf 'In Safari, Share > Add to Home Screen installs the app.\n\n'
|
|
41
|
+
# Zellij 0.45.1 requires --create-token by itself; --token-name cannot accompany it.
|
|
42
|
+
exec "${LANES_SHARE:-$HOME/.local/share/lanes}/bin/zellij-0.45.1" \
|
|
43
|
+
--config-dir "$repo/zellij" --config "$repo/zellij/config.kdl" web --create-token
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Enable loopback web services as the installed workspace user. Does not print a token.
|
|
3
|
+
set -eu
|
|
4
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
5
|
+
device=${1:-phone}
|
|
6
|
+
[ "$#" -le 1 ] || { echo 'usage: enable-web.sh [device]' >&2; exit 2; }
|
|
7
|
+
case "$device" in ''|-*|*[!A-Za-z0-9_.-]*) echo 'invalid device name' >&2; exit 2 ;; esac
|
|
8
|
+
command -v caddy >/dev/null 2>&1 || { echo 'Install Caddy first (see docs/INSTALL.md).' >&2; exit 1; }
|
|
9
|
+
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
|
10
|
+
"$repo/bin/lanes" prepare "$device" --mobile
|
|
11
|
+
python3 "$repo/install/services.py"
|
|
12
|
+
systemctl --user daemon-reload
|
|
13
|
+
systemctl --user enable --now lanes-web.service lanes-mobile.service
|
|
14
|
+
systemctl --user is-active --quiet lanes-web.service lanes-mobile.service
|
|
15
|
+
printf '\nPhone frontend: 127.0.0.1:8083; Zellij: 127.0.0.1:8082 (loopback only).\n'
|
|
16
|
+
printf 'For private HTTPS and a login token, run in your terminal:\n sh "%s/install/enable-phone.sh" --local %s\n' "$repo" "$device"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Linux server install/update. Called by ./install.sh --server. Never runs as root.
|
|
3
|
+
set -eu
|
|
4
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
5
|
+
start=1 check=0
|
|
6
|
+
for argument in "$@"; do
|
|
7
|
+
case "$argument" in
|
|
8
|
+
--no-start) start=0 ;;
|
|
9
|
+
--check) check=1 ;;
|
|
10
|
+
-h|--help)
|
|
11
|
+
echo 'usage: ./install.sh --server [--check | --no-start]'
|
|
12
|
+
echo 'Installs pinned binaries and user services. --check changes nothing; --no-start only installs files.'
|
|
13
|
+
exit 0 ;;
|
|
14
|
+
*) echo "unknown server option: $argument" >&2; exit 2 ;;
|
|
15
|
+
esac
|
|
16
|
+
done
|
|
17
|
+
[ "$(uname -s)/$(uname -m)" = Linux/x86_64 ] || {
|
|
18
|
+
echo 'The pinned server binaries require Linux x86_64. macOS/Linux desktops use ./install.sh --site FILE.' >&2; exit 1;
|
|
19
|
+
}
|
|
20
|
+
[ "$(id -u)" != 0 ] || { echo 'Run as the workspace user, not root.' >&2; exit 1; }
|
|
21
|
+
for command in python3 curl tar sha256sum infocmp tic install systemctl docker tmux; do
|
|
22
|
+
command -v "$command" >/dev/null 2>&1 || { echo "Missing prerequisite: $command (see docs/INSTALL.md)" >&2; exit 1; }
|
|
23
|
+
done
|
|
24
|
+
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else "Python 3.9+ is required")'
|
|
25
|
+
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
|
26
|
+
if [ "$start" = 1 ]; then
|
|
27
|
+
systemctl --user show-environment >/dev/null
|
|
28
|
+
python3 -c 'import subprocess; subprocess.run(["docker", "info", "--format", "{{.ServerVersion}}"], check=True, timeout=15)'
|
|
29
|
+
fi
|
|
30
|
+
if [ "$check" = 1 ]; then
|
|
31
|
+
echo 'Server prerequisites: ready'
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
share=${LANES_SHARE:-$HOME/.local/share/lanes}
|
|
35
|
+
state=${LANES_STATE:-$HOME/.local/state/lanes}
|
|
36
|
+
mkdir -p "$share/bin" "$share/dl" "$state/flags"
|
|
37
|
+
grep -v '^#' "$repo/holders.lock" | while read -r name version sha url; do
|
|
38
|
+
[ -n "$name" ] || continue
|
|
39
|
+
target=$share/bin/$name-$version
|
|
40
|
+
if [ -x "$target" ]; then echo "ok $name $version"; continue; fi
|
|
41
|
+
archive=$share/dl/$(basename "$url")
|
|
42
|
+
curl -fsSL --retry 3 -o "$archive" "$url"
|
|
43
|
+
actual=$(sha256sum "$archive" | cut -d' ' -f1)
|
|
44
|
+
[ "$actual" = "$sha" ] || { echo "sha256 mismatch for $name" >&2; exit 1; }
|
|
45
|
+
temporary=$(mktemp -d)
|
|
46
|
+
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
|
|
47
|
+
tar -xzf "$archive" -C "$temporary"
|
|
48
|
+
install -m 0755 "$temporary/$name" "$target"
|
|
49
|
+
rm -rf "$temporary"
|
|
50
|
+
trap - EXIT HUP INT TERM
|
|
51
|
+
echo "installed $target"
|
|
52
|
+
done
|
|
53
|
+
if ! infocmp xterm-lanesview >/dev/null 2>&1; then
|
|
54
|
+
infocmp -x xterm-256color | sed '1,3s/^xterm-256color|/xterm-lanesview|/' > "$share/dl/xterm-lanesview.ti"
|
|
55
|
+
tic -x -o "$HOME/.terminfo" "$share/dl/xterm-lanesview.ti"
|
|
56
|
+
fi
|
|
57
|
+
[ -e "$state/registry.json" ] || touch "$state/flags/coexist"
|
|
58
|
+
python3 "$repo/install/services.py"
|
|
59
|
+
catalog=$HOME/.config/lanes/workspaces/catalog.json
|
|
60
|
+
if [ ! -e "$catalog" ] && [ ! -d "$repo/workspaces" ] && [ -z "${LANES_WORKSPACES:-}" ]; then
|
|
61
|
+
mkdir -p "$(dirname "$catalog")"
|
|
62
|
+
printf '{"version":1,"workspaces":[]}\n' > "$catalog"
|
|
63
|
+
fi
|
|
64
|
+
if [ "$start" = 1 ]; then
|
|
65
|
+
systemctl --user daemon-reload
|
|
66
|
+
"$repo/bin/lanes" sync
|
|
67
|
+
systemctl --user enable --now lanes-statusd.service lanes-sync.timer lanes-ensure.timer lanes-gc.timer
|
|
68
|
+
fi
|
|
69
|
+
printf '\nServer installed from %s\n' "$repo"
|
|
70
|
+
echo 'Configure boards in ~/.config/lanes/workspaces/; see workspaces.example/README.md.'
|
|
71
|
+
echo 'Next: lanes install-container CONTAINER; lanes sync; lanes doctor'
|
|
72
|
+
echo 'Web access and host cleanup/guard timers remain opt-in; see docs/INSTALL.md.'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Register this checkout's user services without starting or restarting them."""
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def unit_value(value, command=False):
|
|
10
|
+
value = str(value)
|
|
11
|
+
if any(c in value for c in '\r\n\0'):
|
|
12
|
+
raise ValueError('installation paths must not contain control characters')
|
|
13
|
+
value = value.replace('\\', '\\\\').replace('"', '\\"').replace('%', '%%')
|
|
14
|
+
return value.replace('$', '$$') if command else value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(repo, home, share, state, workspaces=None):
|
|
18
|
+
repo, home, share, state = (Path(p).resolve() for p in (repo, home, share, state))
|
|
19
|
+
current = home / '.local/share/lanes/current'
|
|
20
|
+
links = ((current, repo), (home / '.local/bin/lanes', repo / 'bin/lanes'))
|
|
21
|
+
for link, _ in links:
|
|
22
|
+
if link.exists() and not link.is_symlink():
|
|
23
|
+
raise ValueError(f'{link} already exists and is not a symlink')
|
|
24
|
+
settings = [('LANES_REPO', repo), ('LANES_SHARE', share), ('LANES_STATE', state)]
|
|
25
|
+
if workspaces:
|
|
26
|
+
settings.append(('LANES_WORKSPACES', Path(workspaces).resolve()))
|
|
27
|
+
environment = ''.join(f'Environment="{name}={unit_value(value)}"\n' for name, value in settings)
|
|
28
|
+
rendered = {}
|
|
29
|
+
for source in sorted((repo / 'systemd').iterdir()):
|
|
30
|
+
if source.name.startswith('.') or source.suffix not in ('.service', '.timer', '.slice') or not source.is_file():
|
|
31
|
+
continue
|
|
32
|
+
content = source.read_text().replace('@REPO@', unit_value(repo, command=True))
|
|
33
|
+
content = content.replace('@SHARE@', unit_value(share, command=True))
|
|
34
|
+
content = content.replace('@CADDY@', unit_value(shutil.which('caddy') or '/usr/bin/caddy', command=True))
|
|
35
|
+
if '[Service]\n' in content:
|
|
36
|
+
content = content.replace('[Service]\n', '[Service]\n' + environment, 1)
|
|
37
|
+
rendered[source.name] = '# Generated by lanes/install/services.py; re-run the server installer to update.\n' + content
|
|
38
|
+
units = home / '.config/systemd/user'
|
|
39
|
+
units.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
for name, content in rendered.items():
|
|
41
|
+
fd, temporary = tempfile.mkstemp(prefix='.lanes-', dir=units)
|
|
42
|
+
try:
|
|
43
|
+
with os.fdopen(fd, 'w') as handle:
|
|
44
|
+
handle.write(content)
|
|
45
|
+
os.chmod(temporary, 0o644)
|
|
46
|
+
os.replace(temporary, units / name) # replace old symlinks, never write through them
|
|
47
|
+
finally:
|
|
48
|
+
if os.path.exists(temporary):
|
|
49
|
+
os.unlink(temporary)
|
|
50
|
+
for link, target in links:
|
|
51
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
if link.is_symlink():
|
|
53
|
+
link.unlink()
|
|
54
|
+
link.symlink_to(target)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
register(Path(__file__).resolve().parents[1], Path.home(),
|
|
59
|
+
os.environ.get('LANES_SHARE', Path.home() / '.local/share/lanes'),
|
|
60
|
+
os.environ.get('LANES_STATE', Path.home() / '.local/state/lanes'),
|
|
61
|
+
os.environ.get('LANES_WORKSPACES'))
|