@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/bin/lanes-install.mjs +158 -0
  4. package/lib/plan.mjs +65 -0
  5. package/package.json +37 -0
  6. package/payload/lanes/README.md +152 -0
  7. package/payload/lanes/VERSION +1 -0
  8. package/payload/lanes/bin/lanes +1035 -0
  9. package/payload/lanes/client/ghostty/common.conf +22 -0
  10. package/payload/lanes/client/ghostty/macos.conf +6 -0
  11. package/payload/lanes/client/ghostty/omarchy.conf +19 -0
  12. package/payload/lanes/client/install.sh +16 -0
  13. package/payload/lanes/client/lanes-launch +315 -0
  14. package/payload/lanes/client/lanes-upload +147 -0
  15. package/payload/lanes/client/lanes-window +36 -0
  16. package/payload/lanes/client/macos-helper.applescript +14 -0
  17. package/payload/lanes/client/omarchy/discover.sh +56 -0
  18. package/payload/lanes/client/omarchy/lanes.conf +5 -0
  19. package/payload/lanes/client/omarchy/lanes.desktop +9 -0
  20. package/payload/lanes/client/omarchy/lanes.lua +14 -0
  21. package/payload/lanes/client/ssh.stanza.in +54 -0
  22. package/payload/lanes/container/env.sh +16 -0
  23. package/payload/lanes/container/lane +268 -0
  24. package/payload/lanes/container/lane-run +34 -0
  25. package/payload/lanes/docs/ARCHITECTURE.md +93 -0
  26. package/payload/lanes/docs/HOST-GUARD.md +108 -0
  27. package/payload/lanes/docs/INSTALL.md +192 -0
  28. package/payload/lanes/docs/RUNBOOK.md +385 -0
  29. package/payload/lanes/docs/SHARING.md +124 -0
  30. package/payload/lanes/docs/ZMX-RING0.md +51 -0
  31. package/payload/lanes/docs/images/desktop.png +0 -0
  32. package/payload/lanes/docs/images/iphone.png +0 -0
  33. package/payload/lanes/holders/exec.sh +21 -0
  34. package/payload/lanes/holders/tmux.sh +111 -0
  35. package/payload/lanes/holders/zmx.sh +22 -0
  36. package/payload/lanes/holders.lock +5 -0
  37. package/payload/lanes/install/enable-phone.sh +43 -0
  38. package/payload/lanes/install/enable-web.sh +16 -0
  39. package/payload/lanes/install/host-install.sh +72 -0
  40. package/payload/lanes/install/services.py +61 -0
  41. package/payload/lanes/install.sh +603 -0
  42. package/payload/lanes/libexec/cursor_commit.py +159 -0
  43. package/payload/lanes/libexec/cursor_sync.py +166 -0
  44. package/payload/lanes/libexec/guard.py +231 -0
  45. package/payload/lanes/libexec/lanes_lib.py +610 -0
  46. package/payload/lanes/libexec/legacy_sessions.py +130 -0
  47. package/payload/lanes/libexec/session_actions.py +276 -0
  48. package/payload/lanes/libexec/sidebar +8 -0
  49. package/payload/lanes/libexec/sidebar.py +872 -0
  50. package/payload/lanes/libexec/statusd.py +764 -0
  51. package/payload/lanes/libexec/tmpscan.py +921 -0
  52. package/payload/lanes/libexec/view.sh +179 -0
  53. package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
  54. package/payload/lanes/site/README.md +28 -0
  55. package/payload/lanes/site/beyondwork/known_hosts +1 -0
  56. package/payload/lanes/site/beyondwork/site.conf +11 -0
  57. package/payload/lanes/site/site.example.conf +10 -0
  58. package/payload/lanes/sudo/host-guard.sh +356 -0
  59. package/payload/lanes/sudo/one-time-root.sh +32 -0
  60. package/payload/lanes/systemd/lanes-ensure.service +7 -0
  61. package/payload/lanes/systemd/lanes-ensure.timer +10 -0
  62. package/payload/lanes/systemd/lanes-gc.service +7 -0
  63. package/payload/lanes/systemd/lanes-gc.timer +9 -0
  64. package/payload/lanes/systemd/lanes-guard.service +8 -0
  65. package/payload/lanes/systemd/lanes-guard.timer +10 -0
  66. package/payload/lanes/systemd/lanes-mobile.service +23 -0
  67. package/payload/lanes/systemd/lanes-statusd.service +15 -0
  68. package/payload/lanes/systemd/lanes-sync.service +10 -0
  69. package/payload/lanes/systemd/lanes-sync.timer +10 -0
  70. package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
  71. package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
  72. package/payload/lanes/systemd/lanes-web.service +13 -0
  73. package/payload/lanes/systemd/lanes.slice +11 -0
  74. package/payload/lanes/web/Caddyfile +37 -0
  75. package/payload/lanes/web/README.md +35 -0
  76. package/payload/lanes/web/icon-192.png +0 -0
  77. package/payload/lanes/web/icon.svg +4 -0
  78. package/payload/lanes/web/index.html +30 -0
  79. package/payload/lanes/web/manifest.webmanifest +15 -0
  80. package/payload/lanes/web/mobile.css +301 -0
  81. package/payload/lanes/web/mobile.js +367 -0
  82. package/payload/lanes/workspaces.example/README.md +27 -0
  83. package/payload/lanes/workspaces.example/catalog.json +7 -0
  84. package/payload/lanes/workspaces.example/commit-targets.json +8 -0
  85. package/payload/lanes/workspaces.example/terminals.example.json +19 -0
  86. package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
  87. package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
  88. package/payload/lanes/zellij/config.kdl +360 -0
  89. package/payload/manifest.json +31 -0
@@ -0,0 +1,22 @@
1
+ # lanes window — shared by macOS and Omarchy. Loaded by a SEPARATE Ghostty instance
2
+ # (`--config-default-files=false --config-file=…`), so the everyday Ghostty config is never modified.
3
+ # The everyday config is inherited first (fonts, theme, padding); `?` = optional include.
4
+ config-file = ?~/.config/ghostty/config.ghostty
5
+ config-file = ?~/.config/ghostty/config
6
+
7
+ # The window IS the client: closing it just detaches; nothing on the server stops.
8
+ confirm-close-surface = false
9
+ shell-integration = none
10
+ window-save-state = never
11
+ quit-after-last-window-closed = true
12
+ # Last backstop. lanes-launch holds and reattaches by itself and never ends; should it end anyway (killed, a usage
13
+ # error), the window stays open with the reason on screen — a window that vanishes reads as "my lanes are gone".
14
+ wait-after-command = true
15
+
16
+ # zellij on the server owns copy/paste through OSC 52; tmux/zmx own lane history.
17
+ clipboard-write = allow
18
+ clipboard-read = ask
19
+ copy-on-select = clipboard
20
+ mouse-shift-capture = never
21
+ scrollback-limit = 10000000
22
+ link-url = true
@@ -0,0 +1,6 @@
1
+ # macOS specifics for the lanes window.
2
+ # Ghostty 1.3.1: CVDisplayLink init failures surface as OutOfMemory; keep the proven workaround.
3
+ window-vsync = false
4
+ macos-titlebar-style = transparent
5
+ # Left Option is Alt for the Ctrl-Alt zellij scheme; right Option still types special characters.
6
+ macos-option-as-alt = left
@@ -0,0 +1,19 @@
1
+ # Omarchy / Hyprland specifics for the lanes window (Ghostty GTK, dedicated instance).
2
+ # Look & feel is inherited from ~/.config/ghostty/config (Omarchy's default: theme include, JetBrainsMono Nerd
3
+ # Font, shift+enter as CSI-u, control+insert / shift+insert copy & paste), so Omarchy theme switches follow.
4
+ # Do NOT include the theme file here as well: Ghostty reports a second include of the same file as
5
+ # "config-file …: cycle detected" and opens its configuration-errors window.
6
+
7
+ # org.omarchy.* is matched by Omarchy's `+terminal` window tag (default/hypr/apps/terminals.lua), so Super+C /
8
+ # Super+V send Ctrl+Insert / Shift+Insert here. Without the tag Omarchy would send Ctrl+C — into an agent.
9
+ class = org.omarchy.lanes
10
+ # A dedicated instance must load its own config: single-instance mode would reuse a running Ghostty's config.
11
+ gtk-single-instance = false
12
+ window-decoration = none
13
+
14
+ # GTK defaults bind ctrl+alt+arrows to goto_split:* (performable, so they pass through without splits);
15
+ # unbinding keeps the zellij scheme deterministic: Ctrl-Alt-←/→ = workspace, Ctrl-Alt-↑/↓ = lane.
16
+ keybind = ctrl+alt+up=unbind
17
+ keybind = ctrl+alt+down=unbind
18
+ keybind = ctrl+alt+left=unbind
19
+ keybind = ctrl+alt+right=unbind
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # Former entry point, kept for muscle memory and old notes. The installer now lives one level up: ../install.sh
3
+ # client/install.sh [--parallel] [device] -> ../install.sh --link [--device device]
4
+ # client/install.sh --switch -> ../install.sh --link --coder-launch
5
+ # client/install.sh --rollback -> put a previous coder-launch back
6
+ set -eu
7
+ root=$(cd "$(dirname "$0")/.." && pwd)
8
+ case "${1:-}" in
9
+ --switch) exec "$root/install.sh" --link --coder-launch ;;
10
+ --rollback)
11
+ bin=$HOME/.local/bin
12
+ if [ -e "$bin/coder-launch-legacy" ]; then rm -f "$bin/coder-launch"; mv "$bin/coder-launch-legacy" "$bin/coder-launch"; echo ' coder-launch is the previous client again'
13
+ else echo ' nothing to roll back'; fi ;;
14
+ --parallel) shift; exec "$root/install.sh" --link ${1:+--device "$1"} ;;
15
+ *) exec "$root/install.sh" --link ${1:+--device "$1"} ;;
16
+ esac
@@ -0,0 +1,315 @@
1
+ #!/bin/sh
2
+ # lanes-launch — client for the lanes workspace server (macOS and Linux; POSIX sh, no dependencies).
3
+ # lanes-launch [device] attach this terminal to the device's view session (default: see below)
4
+ # lanes-launch <workspace> same, and bring that workspace's tab to the front (e.g. `coder-launch worlds`)
5
+ # lanes-launch <device> --viewports N a wide view session: N (2-3) viewports side by side per workspace
6
+ # lanes-launch lane <lane-id> attach straight to ONE lane, no multiplexer in between
7
+ # lanes-launch ls list lanes with live status
8
+ # lanes-launch --helper <device> [--service | --while-pid PID] run only the upload helper
9
+ # (--service: the per-machine background service that install.sh sets up)
10
+ # It never ends by itself: after network loss, sleep, or a server that dropped or refused it, it says so, waits and
11
+ # reattaches (Enter = now). Closing the window (or Ctrl-C) ends it. Log: ~/.local/state/lanes-client/launch-<target>.log
12
+ set -u
13
+ host=${LANES_HOST:-lanes}
14
+ primary=$host
15
+ fallback=${LANES_FALLBACK_HOST:-lanes-public}
16
+ remote='$HOME/.local/bin/lanes'
17
+ config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
18
+ state=${XDG_STATE_HOME:-$HOME/.local/state}/lanes-client # logs and helper.alive; `install.sh --check` reads them
19
+
20
+ default_device() {
21
+ if [ -r "$config/device" ]; then head -n 1 "$config/device"; return; fi
22
+ name=$(hostname -s 2>/dev/null || hostname)
23
+ printf '%s' "$name" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9_.-' '-' | sed 's/-*$//'
24
+ }
25
+
26
+ while_pid=
27
+ service=
28
+ extra=
29
+ case "${1:-}" in
30
+ --helper)
31
+ mode=helper target=${2:?device}; shift 2
32
+ while [ $# -gt 0 ]; do
33
+ case "$1" in
34
+ --service) service=1; shift ;;
35
+ --while-pid) while_pid=${2:?pid}; shift 2 ;;
36
+ # Said out loud: an option that is dropped silently is how a client ran "as a service" without being one.
37
+ *) echo "lanes-launch: ignoring unknown helper option $1" >&2; shift ;;
38
+ esac
39
+ done ;;
40
+ ls) exec ssh -o RequestTTY=no "$host" "$remote ls" ;;
41
+ lane)
42
+ [ -n "${2:-}" ] || { echo 'usage: lanes-launch lane <lane-id>' >&2; exit 64; }
43
+ mode=open target=$2 ;;
44
+ -h|--help) sed -n '2,11p' "$0"; exit 0 ;;
45
+ *) mode=attach target=${1:-$(default_device)} ;;
46
+ esac
47
+ if [ "$mode" = attach ] && [ "${2:-}" = --viewports ]; then
48
+ case "${3:-}" in 1|2|3) extra="--viewports $3" ;; *) echo '--viewports takes 1, 2 or 3' >&2; exit 64 ;; esac
49
+ fi
50
+ case "$target" in ''|-*|*[!A-Za-z0-9_.-]*) echo "invalid name: $target" >&2; exit 64 ;; esac
51
+ client="$(hostname -s 2>/dev/null || echo client)-$$"
52
+ client=$(printf '%s' "$client" | tr -c 'A-Za-z0-9_.-' '-')
53
+
54
+ # ---- log: what a window that showed nothing never left behind ------------------------------------------------------
55
+ # One line per connection event, rotated to .1 at 256 KiB. The service writes to stderr instead: launchd / journald
56
+ # keep that, and `install.sh --check` points there. Logging must never be the reason an attach fails.
57
+ logfile=
58
+ mkdir -p "$state" 2>/dev/null && logfile=$state/launch-$target.log
59
+ log() {
60
+ if [ -n "$service" ]; then printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; return 0; fi
61
+ [ -n "$logfile" ] || return 0
62
+ size=0; [ ! -f "$logfile" ] || size=$(wc -c < "$logfile")
63
+ if [ "$((size))" -gt 262144 ]; then mv -f "$logfile" "$logfile.1" 2>/dev/null || :; fi
64
+ printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$$" "$*" >> "$logfile" 2>/dev/null || :
65
+ }
66
+
67
+ # Run ssh as an async child so HUP/TERM (window closed, logout) end it immediately; a foreground child
68
+ # would defer the trap until ssh returned. Keep the terminal on fd 9: dash gives async jobs /dev/null.
69
+ child=
70
+ reader=
71
+ holding=
72
+ now=
73
+ limit=
74
+ helper_pid=
75
+ stop() {
76
+ trap - HUP TERM INT
77
+ exec 2>/dev/null # bash reports every child it sees die of a signal: noise in a terminal that stays open
78
+ if [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
79
+ if [ -n "$reader" ]; then kill -TERM "$reader" 2>/dev/null || :; fi
80
+ end_helper
81
+ log 'stopped: window closed, or a signal'
82
+ exit 0
83
+ }
84
+ end_helper() {
85
+ [ -n "${helper_pid:-}" ] || return 0
86
+ pkill -TERM -P "$helper_pid" 2>/dev/null || :
87
+ kill -TERM "$helper_pid" 2>/dev/null || :
88
+ }
89
+ # INT too: in a plain terminal Ctrl-C is the way out, now that nothing else ends the client.
90
+ trap stop HUP TERM INT
91
+ # With $limit set, a watchdog ends a child that the server leaves hanging (macOS has no timeout(1)).
92
+ run() {
93
+ exec 9<&0
94
+ "$@" <&9 9<&- &
95
+ child=$!
96
+ exec 9<&-
97
+ dog=
98
+ if [ -n "$limit" ]; then ( sleep "$limit"; kill -TERM "$child" ) >/dev/null 2>&1 & dog=$!; fi
99
+ rc=0
100
+ wait "$child" || rc=$?
101
+ child=
102
+ if [ -n "$dog" ]; then kill -TERM "$dog" 2>/dev/null || :; wait "$dog" 2>/dev/null || :; fi
103
+ return "$rc"
104
+ }
105
+ # hold <seconds> [jitter]: wait, but Enter continues at once. No raw mode, so there is no terminal state to restore
106
+ # when the window closes mid-wait: a line reader signals this shell instead. End of input (no terminal) is no Enter.
107
+ trap 'if [ -n "$holding" ]; then now=1; [ -z "$child" ] || { kill -TERM "$child"; wait "$child"; } 2>/dev/null; fi' USR1
108
+ hold() {
109
+ now=; holding=1
110
+ exec 9<&0
111
+ ( IFS= read -r line <&9 && kill -USR1 "$$" ) 2>/dev/null &
112
+ reader=$!
113
+ exec 9<&-
114
+ run sleep "$1" || :
115
+ # Jitter is a second sleep: should its number ever be malformed, the whole wait is still served — no tight loop.
116
+ if [ -z "$now" ] && [ -n "${2:-}" ]; then run sleep "$2" || :; fi
117
+ holding=
118
+ kill -TERM "$reader" 2>/dev/null || :
119
+ wait "$reader" 2>/dev/null || :
120
+ reader=
121
+ }
122
+ # The fallback host counts only if the ssh config really defines it (an undefined alias resolves to itself).
123
+ fallback_defined() {
124
+ resolved=$(ssh -G "$fallback" 2>/dev/null | awk '$1 == "hostname" { print $2; exit }')
125
+ [ -n "$resolved" ] && [ "$resolved" != "$fallback" ]
126
+ }
127
+ # ---- upload helper: serves the sidebar's upload box ------------------------------------------------------------
128
+ # A drop on the terminal only gives the SERVER a local path string; the file lives here. This helper waits for
129
+ # such requests and uploads the files with lanes-upload. It is the security boundary: it reads ONLY from folders
130
+ # listed in ~/.config/lanes/upload-allow (asks once per new folder on macOS) and never from credential folders.
131
+ allowed_path() {
132
+ real=$(cd "$(dirname "$1")" 2>/dev/null && pwd -P)/$(basename "$1")
133
+ case "$real" in
134
+ "$HOME"/.ssh/*|"$HOME"/.gnupg/*|"$HOME"/.aws/*|"$HOME"/.kube/*|"$HOME"/.config/*|"$HOME"/.docker/*|\
135
+ "$HOME"/Library/Keychains/*|"$HOME"/Library/Application\ Support/*[Kk]eychain*|*/.git/*|/etc/*|/private/etc/*) return 1 ;;
136
+ esac
137
+ [ -e "$real" ] || return 1
138
+ while IFS= read -r dir; do
139
+ [ -n "$dir" ] || continue
140
+ case "$real" in "$dir"/*) return 0 ;; esac
141
+ done < "$config/upload-allow" 2>/dev/null
142
+ folder=$(dirname "$real")
143
+ if [ "$(uname -s)" = Darwin ]; then
144
+ answer=$(/usr/bin/osascript -e "display dialog \"The lanes upload box wants to send a file from:\n\n$folder\n\nAllow uploads from this folder?\" with title \"Lanes upload\" buttons {\"Deny\", \"Allow once\", \"Always allow\"} default button \"Allow once\" giving up after 60" -e 'button returned of result' 2>/dev/null)
145
+ case "$answer" in
146
+ 'Always allow') printf '%s\n' "$folder" >> "$config/upload-allow"; return 0 ;;
147
+ 'Allow once') return 0 ;;
148
+ esac
149
+ fi
150
+ return 1
151
+ }
152
+ # Epoch mtime. GNU form first: GNU `stat -f` means "file system" and prints that before failing; BSD rejects -c silently.
153
+ mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
154
+ # The machine's helper SERVICE touches helper.alive on every READY/PING line (one every 5 s): serving = under 90 s old.
155
+ service_serving() {
156
+ seen=$(mtime "$state/helper.alive") || return 1
157
+ [ -n "$seen" ] && [ $(( $(date +%s) - seen )) -lt 90 ]
158
+ }
159
+ upload_helper() {
160
+ [ -x "$HOME/.local/bin/lanes-upload" ] || return 0
161
+ dflt=$(default_device)
162
+ # One helper serves the machine: the server spools the requests of a suffixed view (mac-2, mac-w2) under the BASE
163
+ # device and remembers the asking view itself, so the line protocol and `--device` below need nothing new.
164
+ case "$1" in "$dflt"-*) helper_target=$dflt ;; *) helper_target=$1 ;; esac
165
+ mkdir -p "$config"
166
+ [ -e "$config/upload-allow" ] || for dir in "$HOME/Desktop" "$HOME/Downloads" "$HOME/Pictures" "$HOME/Documents" "$HOME/Lanes Uploads" \
167
+ /var/folders /private/var/folders /tmp /private/tmp; do [ -d "$dir" ] && (cd "$dir" && pwd -P) >> "$config/upload-allow"; done
168
+ errfile=/dev/null; [ ! -d "$state" ] || errfile=$state/helper.err # ssh's last complaint only: bounded by design
169
+ up= # '' unknown, 1 serving, 0 down: only the transitions are logged (nobody rotates the launchd log)
170
+ pause=3
171
+ while :; do
172
+ if [ -n "$while_pid" ] && ! kill -0 "$while_pid" 2>/dev/null; then exit 0; fi # the window it serves is gone
173
+ # A window's own helper is the fallback only. While the service serves this machine it opens no connection
174
+ # (each one is a long-lived process on the server); when the service goes quiet it takes over within ~2 min.
175
+ if [ -z "$service" ] && [ "$helper_target" = "$dflt" ] && service_serving; then sleep 30; continue; fi
176
+ if ssh -o RequestTTY=no -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=4 "$primary" "$remote upload-requests $helper_target --default-device $dflt" 2>"$errfile" |
177
+ ( heard=1
178
+ while IFS= read -r line; do
179
+ case "$line" in
180
+ READY|PING)
181
+ heard=0
182
+ [ -z "$service" ] || touch "$state/helper.alive" 2>/dev/null
183
+ [ "$up" = 1 ] || { up=1; log "upload helper: serving '$helper_target' via $primary"; } ;;
184
+ BEGIN\ *) set -- $line; req=$2; req_board=$3; req_paste=$4; list=$(mktemp) ;;
185
+ P\ *) printf '%s\n' "${line#P }" >> "$list" ;;
186
+ END)
187
+ refused=; set --
188
+ while IFS= read -r path; do
189
+ if allowed_path "$path"; then set -- "$@" "$path"; else refused="$refused $(basename "$path")"; fi
190
+ done < "$list"
191
+ rm -f "$list"
192
+ if [ $# -gt 0 ] && [ -z "$refused" ]; then
193
+ flag=; [ "$req_paste" = 1 ] && flag=--paste
194
+ upload_err=$(mktemp)
195
+ log "upload $req: sending $# item(s) to $req_board"
196
+ LANES_DEFAULT_DEVICE=$dflt "$HOME/.local/bin/lanes-upload" -w "$req_board" $flag --device "$helper_target" --request "$req" "$@" </dev/null >/dev/null 2>"$upload_err" &
197
+ upload_pid=$!
198
+ while kill -0 "$upload_pid" 2>/dev/null; do
199
+ # A large transfer (or a macOS access prompt) must not make a serving helper look dead.
200
+ [ -z "$service" ] || touch "$state/helper.alive" 2>/dev/null
201
+ sleep 1
202
+ done
203
+ if wait "$upload_pid"; then
204
+ log "upload $req: complete"
205
+ else
206
+ detail=$(head -n 1 "$upload_err")
207
+ [ -n "$detail" ] || detail='upload failed on the client'
208
+ log "upload $req failed: $detail"
209
+ case "$detail" in
210
+ *'Operation not permitted'*) detail='macOS denied folder access: allow Lanes Helper in System Settings > Privacy & Security > Files and Folders' ;;
211
+ esac
212
+ # Diagnostics contain filenames: quote them as one remote argument, never shell code.
213
+ quoted=$(printf '%s' "$detail" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")
214
+ ssh -n -o RequestTTY=no -o BatchMode=yes "$primary" "$remote upload-result $helper_target $req $quoted --default-device $dflt" 2>/dev/null
215
+ fi
216
+ rm -f "$upload_err"
217
+ else
218
+ quoted=$(printf '%s' "not allowed on this machine:$refused - run: lanes-upload --allow FOLDER" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")
219
+ ssh -n -o RequestTTY=no -o BatchMode=yes "$primary" "$remote upload-result $helper_target $req $quoted --default-device $dflt" 2>/dev/null
220
+ fi ;;
221
+ esac
222
+ done
223
+ exit "$heard"
224
+ ); then
225
+ up=1; pause=3 # the server ends every stream after 15 min by design: come straight back
226
+ else
227
+ [ "$up" = 0 ] || log "upload helper: no connection to $primary ($(head -n 1 "$errfile" 2>/dev/null)); retrying, at most once a minute"
228
+ up=0
229
+ fi
230
+ sleep "$pause"
231
+ # Back off while the server never answers. As a service this loop runs around the clock, also against a key
232
+ # that is refused or an sshd that is thrashing: 3 s forever would be load on exactly the host that is in trouble.
233
+ if [ "$up" = 0 ]; then pause=$((pause * 2)); [ "$pause" -le 60 ] || pause=60; fi
234
+ done
235
+ }
236
+
237
+ # Real jitter. awk's bare srand() seeds from the clock's SECOND, so windows that woke together all drew the same
238
+ # number; the pid differs per window. Up to 1 s + 25 % of the wait. (LC_ALL=C: `sleep` does not take a decimal comma.)
239
+ jitter() {
240
+ LC_ALL=C awk -v seed="$(( $$ + $(date +%s) ))" -v d="$1" 'BEGIN { srand(seed); rand(); printf "%.1f", rand() * (1 + d / 4) }' 2>/dev/null || :
241
+ }
242
+
243
+ fails=0
244
+ delay=1
245
+ if [ "$mode" = helper ]; then
246
+ # Default signal handling: a trap would wait for the foreground ssh pipeline (up to 15 min), and launchd kills a
247
+ # job that sits on TERM for 20 s — a reinstall would find the old job still loaded. Nothing here needs cleanup:
248
+ # launchd (process group) and systemd (control group) take the ssh child along.
249
+ trap - HUP TERM INT
250
+ upload_helper "$target"; exit 0
251
+ fi
252
+ if [ "$mode" = attach ]; then
253
+ while_pid=$$ # ends the helper also when this client is killed without a chance to clean up
254
+ upload_helper "$target" </dev/null >/dev/null 2>&1 &
255
+ helper_pid=$!
256
+ fi
257
+ while :; do
258
+ printf '\033]0;lanes · %s\007' "$target"
259
+ # Said before ssh draws anything: a server under pressure needs many seconds for banner and key exchange, and a
260
+ # window that stays blank meanwhile reads as dead.
261
+ printf '\033[2mlanes: connecting to %s…\033[0m\r\n' "$host"
262
+ log "connecting: $mode $target via $host"
263
+ started=$(date +%s)
264
+ if [ "$mode" = attach ]; then
265
+ run ssh "$host" "$remote attach $target --client $client --default-device $(default_device) $extra" || :
266
+ else
267
+ run ssh "$host" "$remote open $target" || :
268
+ fi
269
+ lasted=$(( $(date +%s) - started ))
270
+ log "ended: exit $rc after ${lasted}s via $host"
271
+ if [ "$lasted" -ge 30 ]; then delay=1; fails=0; host=$primary; fi
272
+ # NOTHING below exits: a client that ends closes its window, and a closed window reads as "my lanes are gone".
273
+ if [ "$rc" = 0 ]; then
274
+ # 0 is not proof of a deliberate detach: zellij also ends a client it found too slow with 0 (a thrashing host
275
+ # did that after 24 s). Say what happened first — the question to the server below can take seconds itself.
276
+ fails=0
277
+ printf '\r\n\033[2mlanes: the view ended (detached, or dropped by a busy server). Your lanes keep running.\033[0m\r\n'
278
+ if [ "$mode" = attach ] && [ "$lasted" -ge 3 ]; then
279
+ # The server just rebuilt this view session (new layout)? That also ends the client with 0: come straight back.
280
+ limit=10; rebuilt=
281
+ run ssh -n -o RequestTTY=no -o BatchMode=yes -o ConnectTimeout=10 "$host" "$remote rebuilt $target --default-device $(default_device)" 2>/dev/null && rebuilt=1
282
+ limit=
283
+ if [ -n "$rebuilt" ]; then
284
+ printf '\033[2mlanes: the view session was rebuilt; reattaching…\033[0m\r\n'
285
+ run sleep 2 || :
286
+ continue
287
+ fi
288
+ fi
289
+ pause=$delay; [ "$pause" -ge 10 ] || pause=10
290
+ what=reattaching
291
+ elif [ "$rc" -ge 64 ] && [ "$rc" -le 78 ]; then
292
+ # 64-78 = the server refused the request (bad name, unknown lane). Retrying soon cannot help; its message
293
+ # stays on screen, and the slow retry picks up a fix made on the server.
294
+ fails=0
295
+ printf '\r\n\033[2mlanes: the server refused this (exit %s, see above).\033[0m\r\n' "$rc"
296
+ pause=60
297
+ what='trying again'
298
+ else
299
+ # Two transport failures in a row on the primary path: try the fallback path once it exists. (25 s: a connect
300
+ # that times out takes ConnectTimeout 20 — it must count, or a dead tailnet never reaches the fallback.)
301
+ if [ "$rc" = 255 ] && [ "$lasted" -lt 25 ]; then fails=$((fails + 1)); else fails=0; fi
302
+ if [ "$fails" -ge 2 ] && [ "$host" = "$primary" ] && fallback_defined; then
303
+ host=$fallback; fails=0; delay=1
304
+ printf '\r\n\033[2mlanes: %s unreachable, using %s\033[0m\r\n' "$primary" "$fallback"
305
+ elif [ "$fails" -ge 2 ] && [ "$host" = "$fallback" ]; then
306
+ host=$primary; fails=0
307
+ fi
308
+ printf '\r\n\033[2mlanes: connection ended (%s).\033[0m\r\n' "$rc"
309
+ pause=$delay
310
+ what=reconnecting
311
+ fi
312
+ printf '\033[2mlanes: %s in about %ss — Enter = now; closing the window (or Ctrl-C) = quit\033[0m\r\n' "$what" "$pause"
313
+ hold "$pause" "$(jitter "$pause")"
314
+ delay=$((delay * 2)); if [ "$delay" -gt 60 ]; then delay=60; fi
315
+ done
@@ -0,0 +1,147 @@
1
+ #!/bin/sh
2
+ # lanes-upload — put local files into a board's uploads/ folder on the workspace server (the same folder Cursor
3
+ # users drop files into), over the direct `lanes` SSH path. macOS and Linux; POSIX sh, no dependencies.
4
+ #
5
+ # lanes-upload [-w <board>] [--paste] <file|dir>... upload; default board = the one this device's view shows
6
+ # lanes-upload --clipboard [-w <board>] [--paste] upload the image (or copied files) on the clipboard
7
+ # lanes-upload --watch-run process the watch folder once (run by launchd / systemd)
8
+ # lanes-upload --allow <dir> | --allowed folders the in-window upload box may read from on THIS machine
9
+ #
10
+ # --paste also type the uploaded path(s) into the lane you are looking at, ready for the agent
11
+ # Afterwards the in-container path(s) are on your clipboard and shown in a notification.
12
+ # Watch folder: ~/Lanes Uploads/ -> the board your view is showing
13
+ # ~/Lanes Uploads/<board>/ -> that board (sent files move to .sent/<date>/)
14
+ set -u
15
+ host=${LANES_HOST:-lanes}
16
+ config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
17
+ watch_root=$(cat "$config/uploads-dir" 2>/dev/null || echo "$HOME/Lanes Uploads")
18
+ device=$(head -n 1 "$config/device" 2>/dev/null || echo mac)
19
+ os=$(uname -s)
20
+ PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:$PATH
21
+
22
+ notify() {
23
+ if [ "$os" = Darwin ]; then
24
+ /usr/bin/osascript -e "display notification \"$2\" with title \"$1\"" >/dev/null 2>&1 || :
25
+ elif command -v notify-send >/dev/null 2>&1; then notify-send -a Lanes "$1" "$2" 2>/dev/null || :; fi
26
+ }
27
+ to_clipboard() {
28
+ if [ "$os" = Darwin ]; then printf '%s' "$1" | /usr/bin/pbcopy
29
+ elif command -v wl-copy >/dev/null 2>&1; then printf '%s' "$1" | wl-copy 2>/dev/null || :; fi
30
+ }
31
+
32
+ # send <board-or-empty> <paste:0|1> <path>... -> prints the remote paths
33
+ send() (
34
+ board=$1; paste=$2; shift 2
35
+ target="--active $device"; [ -n "$board" ] && target="$board --device $device"
36
+ flags=; [ "$paste" = 1 ] && flags=--paste
37
+ [ -n "${request:-}" ] && flags="$flags --request $request"
38
+ [ -n "${LANES_DEFAULT_DEVICE:-}" ] && flags="$flags --default-device $LANES_DEFAULT_DEVICE"
39
+ set -- "$@"
40
+ args=
41
+ for item in "$@"; do
42
+ [ -e "$item" ] || { echo "lanes-upload: no such file: $item" >&2; return 66; }
43
+ dir=$(cd "$(dirname "$item")" && pwd); base=$(basename "$item")
44
+ args="$args -C $(printf '%s' "$dir" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/") $(printf '%s' "./$base" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/")"
45
+ done
46
+ # COPYFILE_DISABLE / --no-mac-metadata: no AppleDouble ._ files in the archive.
47
+ extra=; [ "$os" = Darwin ] && extra=--no-mac-metadata
48
+ # POSIX sh only returns the last pipeline command's status. Keep tar's status separately: a denied file
49
+ # otherwise produces a valid empty/partial archive, and a watch-folder upload can be marked sent anyway.
50
+ transfer=$(mktemp -d) || return 1
51
+ trap 'rm -rf "$transfer"' EXIT
52
+ ( eval "COPYFILE_DISABLE=1 tar $extra -cf - $args"; echo "$?" > "$transfer/tar-status" ) |
53
+ ssh -o RequestTTY=no -o BatchMode=yes "$host" "\$HOME/.local/bin/lanes upload $target $flags"
54
+ ssh_status=$?
55
+ tar_status=$(cat "$transfer/tar-status" 2>/dev/null || echo 1)
56
+ [ "$tar_status" = 0 ] || { echo "lanes-upload: could not read every file (tar exit $tar_status)" >&2; return 1; }
57
+ return "$ssh_status"
58
+ )
59
+
60
+ finish() { # remote paths on stdin
61
+ paths=$(cat)
62
+ [ -n "$paths" ] || return 1
63
+ printf '%s\n' "$paths"
64
+ to_clipboard "$(printf '%s' "$paths" | tr '\n' ' ' | sed 's/ $//')"
65
+ notify 'Uploaded to lanes' "$(printf '%s' "$paths" | head -n 3 | tr '\n' ' ')"
66
+ }
67
+
68
+ clipboard_to_files() { # writes into $1, prints the file paths to upload
69
+ out=$1; stamp=$(date +%Y%m%d-%H%M%S)
70
+ if [ "$os" = Darwin ]; then
71
+ copied=$(/usr/bin/osascript -e 'try' -e 'POSIX path of (the clipboard as «class furl»)' -e 'end try' 2>/dev/null)
72
+ if [ -n "$copied" ] && [ -e "$copied" ]; then printf '%s\n' "$copied"; return 0; fi
73
+ /usr/bin/osascript -e 'try' -e "set png to the clipboard as «class PNGf»" \
74
+ -e "set f to open for access POSIX file \"$out/clip-$stamp.png\" with write permission" \
75
+ -e 'write png to f' -e 'close access f' -e 'end try' >/dev/null 2>&1
76
+ elif command -v wl-paste >/dev/null 2>&1; then
77
+ if wl-paste --list-types 2>/dev/null | grep -q '^image/png'; then wl-paste -t image/png > "$out/clip-$stamp.png"
78
+ elif wl-paste --list-types 2>/dev/null | grep -q '^text/uri-list'; then
79
+ wl-paste -t text/uri-list | sed -n 's|^file://||p' | while IFS= read -r p; do printf '%b\n' "$(printf '%s' "$p" | sed 's/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g')"; done
80
+ return 0
81
+ fi
82
+ fi
83
+ if [ -s "$out/clip-$stamp.png" ]; then printf '%s\n' "$out/clip-$stamp.png"; else return 1; fi
84
+ }
85
+
86
+ watch_run() {
87
+ mkdir -p "$watch_root"
88
+ lock=$watch_root/.lock
89
+ mkdir "$lock" 2>/dev/null || { [ -n "$(find "$lock" -maxdepth 0 -mmin +5 2>/dev/null)" ] && rmdir "$lock" 2>/dev/null; exit 0; }
90
+ trap 'rmdir "$lock" 2>/dev/null' EXIT INT TERM
91
+ sleep 1 # let the writer finish
92
+ for dir in "$watch_root" "$watch_root"/*/; do
93
+ [ -d "$dir" ] || continue
94
+ dir=${dir%/}
95
+ board=; [ "$dir" != "$watch_root" ] && board=$(basename "$dir")
96
+ for item in "$dir"/*; do
97
+ [ -e "$item" ] || continue
98
+ [ "$dir" = "$watch_root" ] && [ -d "$item" ] && continue # board folders themselves
99
+ case "$(basename "$item")" in .*|README.txt|*.crdownload|*.part|*.download) continue ;; esac
100
+ # still being written? wait until its size is stable
101
+ a=$(du -sk "$item" 2>/dev/null | cut -f1); sleep 1; b=$(du -sk "$item" 2>/dev/null | cut -f1)
102
+ [ "$a" = "$b" ] || continue
103
+ if result=$(send "$board" 0 "$item"); then
104
+ sent=$dir/.sent/$(date +%Y-%m-%d); mkdir -p "$sent"; mv "$item" "$sent/" 2>/dev/null
105
+ printf '%s\n' "$result" | finish >/dev/null
106
+ else
107
+ notify 'Lanes upload failed' "$(basename "$item") — it stays in the folder and is retried on the next change"
108
+ fi
109
+ done
110
+ done
111
+ }
112
+
113
+ board=; paste=0; clip=0; request=
114
+ while [ $# -gt 0 ]; do
115
+ case "$1" in
116
+ --request) request=${2:?id}; shift 2 ;;
117
+ --device) device=${2:?device}; shift 2 ;;
118
+ --allow) dir=$(cd "${2:?dir}" && pwd -P) || exit 66; mkdir -p "$config"; grep -qxF "$dir" "$config/upload-allow" 2>/dev/null || printf '%s\n' "$dir" >> "$config/upload-allow"; echo "allowed: $dir"; exit 0 ;;
119
+ --allowed) cat "$config/upload-allow" 2>/dev/null; exit 0 ;;
120
+ -w|--workspace) board=${2:?board id}; shift 2 ;;
121
+ --paste) paste=1; shift ;;
122
+ --clipboard) clip=1; shift ;;
123
+ --watch-run) watch_run; exit 0 ;;
124
+ -h|--help) sed -n '2,13p' "$0"; exit 0 ;;
125
+ --) shift; break ;;
126
+ -*) echo "lanes-upload: unknown option $1" >&2; exit 64 ;;
127
+ *) break ;;
128
+ esac
129
+ done
130
+ case "$board" in *[!A-Za-z0-9_.-]*) echo 'lanes-upload: invalid board id' >&2; exit 64 ;; esac
131
+ case "$request" in *[!0-9-]*) echo 'lanes-upload: invalid request id' >&2; exit 64 ;; esac
132
+
133
+ if [ "$clip" = 1 ]; then
134
+ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
135
+ files=$(clipboard_to_files "$tmp") || { echo 'lanes-upload: the clipboard holds no image or copied file' >&2; notify 'Lanes upload' 'Clipboard holds no image or file'; exit 66; }
136
+ old_ifs=$IFS; IFS='
137
+ '
138
+ # shellcheck disable=SC2086
139
+ set -- $files
140
+ IFS=$old_ifs
141
+ fi
142
+ [ $# -gt 0 ] || { sed -n '2,13p' "$0"; exit 64; }
143
+ if result=$(send "$board" "$paste" "$@"); then
144
+ printf '%s\n' "$result" | finish
145
+ else
146
+ exit "$?"
147
+ fi
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ # Open the lanes window in its own terminal instance. usage: lanes-window [suffix]
3
+ # suffix: an independent view session "<device>-<suffix>" (second monitor / other half of an ultrawide).
4
+ # w2 / w3: a WIDE view session with 2 / 3 viewports side by side per workspace (for a full-width window).
5
+ # macOS: Ghostty. Linux: Ghostty when installed, otherwise Foot with the same window class.
6
+ set -u
7
+ config=${XDG_CONFIG_HOME:-$HOME/.config}/lanes
8
+ launch=$HOME/.local/bin/lanes-launch
9
+ class=org.omarchy.lanes
10
+ device=
11
+ file=$config/ghostty.conf
12
+ if [ -n "${1:-}" ]; then
13
+ case "$1" in *[!A-Za-z0-9_.-]*) echo 'invalid suffix' >&2; exit 64 ;; esac
14
+ device="$(head -n 1 "$config/device")-$1"
15
+ case "$1" in w2) device="$device --viewports 2" ;; w3) device="$device --viewports 3" ;; esac
16
+ file=$config/ghostty.$1.conf
17
+ sed "s|^command = .*|command = $launch $device|" "$config/ghostty.conf" > "$file"
18
+ fi
19
+ case "$(uname -s)" in
20
+ Darwin)
21
+ if ! open -Ra Ghostty.app 2>/dev/null; then
22
+ echo 'lanes-window: Ghostty is not installed (brew install --cask ghostty). Meanwhile: run lanes-launch in any terminal.' >&2
23
+ exit 69
24
+ fi
25
+ exec open -na Ghostty.app --args --config-default-files=false "--config-file=$file" ;;
26
+ esac
27
+ if command -v ghostty >/dev/null 2>&1; then
28
+ exec ghostty --config-default-files=false "--config-file=$file"
29
+ elif command -v foot >/dev/null 2>&1; then
30
+ # Same class, so Omarchy's terminal tag, the launch-or-focus bind and window rules behave identically.
31
+ # --hold = Ghostty's wait-after-command (common.conf): should the client ever end, the window stays, reason on screen.
32
+ # shellcheck disable=SC2086
33
+ exec foot --hold --app-id="$class" --title=lanes -e "$launch" $device
34
+ fi
35
+ echo 'lanes-window: install Ghostty (omarchy-pkg-add ghostty) or Foot' >&2
36
+ exit 69
@@ -0,0 +1,14 @@
1
+ -- A real app identity lets macOS ask for Downloads/Desktop/Documents access for the upload helper.
2
+ -- An anonymous /bin/sh launched directly by launchd is silently denied those folders.
3
+ on run
4
+ set userHome to POSIX path of (path to home folder)
5
+ set configRoot to do shell script "printf '%s' \"${XDG_CONFIG_HOME:-$HOME/.config}\""
6
+ set stateRoot to do shell script "printf '%s' \"${XDG_STATE_HOME:-$HOME/.local/state}\""
7
+ set deviceName to do shell script "/usr/bin/head -n 1 " & quoted form of (configRoot & "/lanes/device")
8
+ set launcher to userHome & ".local/bin/lanes-launch"
9
+ set logPath to stateRoot & "/lanes-client/helper-service.log"
10
+ -- launchd owns this app and its children; closing a terminal does not stop the helper.
11
+ try
12
+ do shell script "exec /bin/sh " & quoted form of launcher & " --helper " & quoted form of deviceName & " --service </dev/null >/dev/null 2>>" & quoted form of logPath
13
+ end try
14
+ end run