@vibelord/lanes 1.1.1 → 1.3.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/README.md +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +47 -23
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2753 -202
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +147 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1150 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1318 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +793 -16
- package/payload/lanes/docs/DESIGN.md +127 -46
- package/payload/lanes/docs/INSTALL.md +175 -18
- package/payload/lanes/docs/RUNBOOK.md +968 -76
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +967 -150
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +197 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +21 -2
- package/payload/lanes/libexec/notify.py +169 -29
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2232 -365
- package/payload/lanes/libexec/statusd.py +231 -12
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +148 -35
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +1044 -0
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +165 -28
- package/payload/lanes/web/browser.js +4 -8
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +45 -32
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +6 -3
package/README.md
CHANGED
|
@@ -30,6 +30,12 @@ which redeems it at the server's enrolment service: `invite: redeemed — this m
|
|
|
30
30
|
<login> (device <device>)`. An unknown prefix is refused with the bundled list; `--site` given explicitly wins. The
|
|
31
31
|
code is never printed, stored, or put on a command line. `LANES_INVITE` works like `--invite`.
|
|
32
32
|
|
|
33
|
+
The code is redeemed by the server's own release: when the server (already reachable over the tailnet) runs a newer
|
|
34
|
+
release than this package, the installer says `server runs 1.2.0: fetching that installer first` before it installs
|
|
35
|
+
anything, and the wrapper runs `npx --yes @vibelord/lanes@1.2.0` with the same options (the code through the
|
|
36
|
+
environment). Without npx and npm on the PATH, or when npm does not have that release yet, it says so in one line and
|
|
37
|
+
this package installs as before. `LANES_NO_SELF_UPDATE=1` always installs this package.
|
|
38
|
+
|
|
33
39
|
**Without a code:**
|
|
34
40
|
|
|
35
41
|
```sh
|
|
@@ -50,7 +56,8 @@ the server (a refused code, an unanswered request, or a server without the enrol
|
|
|
50
56
|
the printed command again with `--device NAME` or a key of its own — the invite is still valid) · `4` installed, but
|
|
51
57
|
this machine is not in the
|
|
52
58
|
tailnet (the installer names the Tailscale install command, or opens `tailscale login` and waits up to 120 s) or the
|
|
53
|
-
server is unreachable · `1` error.
|
|
59
|
+
server is unreachable · `1` error. When the installer hands over to the server's release, the exit status is that
|
|
60
|
+
release's installer's.
|
|
54
61
|
|
|
55
62
|
## Server
|
|
56
63
|
|
|
@@ -59,8 +66,17 @@ On the Linux x86_64 workspace host, as the workspace account:
|
|
|
59
66
|
```sh
|
|
60
67
|
npx @vibelord/lanes --server --check
|
|
61
68
|
npx @vibelord/lanes --server
|
|
69
|
+
npx @vibelord/lanes@latest --server --update-only # an update of an installed server: install, restart what runs, record the run
|
|
62
70
|
```
|
|
63
71
|
|
|
72
|
+
The server install enables `lanes-update.timer`: every 30 minutes `lanes update --timer` asks npm for the newest release
|
|
73
|
+
and installs it the same way (`--update-only`), with the npx this install ran with (recorded in
|
|
74
|
+
`~/.local/state/lanes/update/npx`) — publishing a release deploys it. An update with `--update-only` enables nothing,
|
|
75
|
+
the timer included: a server set up before 1.2.0 needs the full `npx @vibelord/lanes@latest --server` once (or
|
|
76
|
+
`systemctl --user enable --now lanes-update.timer`). `lanes update --now` checks by hand, `lanes update --status` prints
|
|
77
|
+
the record, `lanes update --rollback` goes back to the release before; `channel=stable` in `~/.config/lanes/update.conf`
|
|
78
|
+
follows the `lanes-stable` dist-tag instead of `latest`.
|
|
79
|
+
|
|
64
80
|
The server install also enables the enrolment service, `lanes-enrold`, that redeems invites and takes access
|
|
65
81
|
requests. Once its operator check passes, pin request mode with `lanes enrol mode request` (`invite` for invites
|
|
66
82
|
only), so no restart changes it. Members then invite from the server, in their own terminal, with `lanes invite <name>
|
|
@@ -71,10 +87,25 @@ says the server is older.
|
|
|
71
87
|
|
|
72
88
|
## What the wrapper does
|
|
73
89
|
|
|
74
|
-
`lanes-install` copies the bundled tree to `~/.local/share/lanes/pkg/<version>/` (`$
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
server
|
|
90
|
+
`lanes-install` copies the bundled tree to `~/.local/share/lanes/pkg/<version>/` (`$XDG_DATA_HOME/lanes/pkg/` when
|
|
91
|
+
that is set; `$LANES_PKG_ROOT` overrides the parent, under any name — the stamp the wrapper writes into the tree marks
|
|
92
|
+
it as staged) and runs `install.sh` from there. `~/.local/share/lanes/current`
|
|
93
|
+
names that stable path, never the npx cache, and the server's systemd user units, `~/.local/bin/lanes` and view
|
|
94
|
+
layouts name the link (moved last, in one step, by the server install; `current.previous` keeps the tree it named
|
|
95
|
+
before); the link is always under your home directory, whatever `XDG_DATA_HOME` says. An identical tree is reused.
|
|
96
|
+
Where that link does not exist (a client) the newest three versions are kept and older ones deleted; where it does
|
|
97
|
+
(a server, even with a broken link) the wrapper deletes nothing — the server's `lanes gc` (daily) prunes its trees,
|
|
98
|
+
keeping the newest three of each folder and every older one still in use: the tree `current` or `current.previous`
|
|
99
|
+
names, one a view layout or a running daemon's run record names, one a process of the account uses.
|
|
100
|
+
|
|
101
|
+
It runs `install.sh` with `LANES_WRAPPER_REPLY` naming a fresh temporary file. With an invite, `install.sh` asks the
|
|
102
|
+
site's enrolment service for the server's release first; a newer one than this package ends that run before anything
|
|
103
|
+
is installed (`update-to=<release>` in the file, exit 5). The wrapper then asks `npm view
|
|
104
|
+
@vibelord/lanes@<release> version` with the npm beside npx and, when npm has it, runs `npx --yes
|
|
105
|
+
@vibelord/lanes@<release>` with the same arguments, the code moved to `LANES_INVITE` and `LANES_NO_SELF_UPDATE=1`
|
|
106
|
+
(that installer never hands over again), and ends with its status. Otherwise — no npx with its npm, a release npm
|
|
107
|
+
does not have yet, npm not answering — `install.sh` runs once more from this package without the question.
|
|
108
|
+
`LANES_NO_SELF_UPDATE` or `LANES_NO_AUTOUPDATE` (any value) turns the hand-over off.
|
|
78
109
|
|
|
79
110
|
| Option | Effect |
|
|
80
111
|
| --- | --- |
|
package/bin/lanes-install.mjs
CHANGED
|
@@ -5,14 +5,34 @@
|
|
|
5
5
|
// The package carries the committed `lanes/` tree (payload/lanes) plus a manifest written at pack time. This
|
|
6
6
|
// wrapper copies that tree to a STABLE, versioned directory and runs its own `install.sh` from there:
|
|
7
7
|
//
|
|
8
|
-
// ~/.local/share/lanes/pkg/<version>/ (
|
|
8
|
+
// ~/.local/share/lanes/pkg/<version>/ ($XDG_DATA_HOME/lanes/pkg/<version>/ when that is set, or
|
|
9
|
+
// $LANES_PKG_ROOT/<version>)
|
|
9
10
|
//
|
|
10
|
-
// A stable path matters for the server: `install/services.py`
|
|
11
|
-
// systemd user units
|
|
12
|
-
//
|
|
11
|
+
// A stable path matters for the server: `install/services.py` links ~/.local/share/lanes/current to the resolved
|
|
12
|
+
// tree and writes that link into the systemd user units, ~/.local/bin/lanes and (through lanes_lib.CURRENT) the view
|
|
13
|
+
// layouts (W9 U1), so the tree must outlive the npx cache. That link is always under the home directory
|
|
14
|
+
// (lanes_lib.CURRENT and prune() read it there, plan.mjs `currentLink`). The STAMP written into each staged tree is
|
|
15
|
+
// what tells lanes_lib.staged_tree a staged tree from a checkout under a $LANES_PKG_ROOT of any name (DEF-1757).
|
|
16
|
+
// Clients copy their commands out of the tree, so for them the location is incidental — and only there does this
|
|
17
|
+
// wrapper prune old trees: where the link exists, `lanes gc` does (W9 U4, LNS-92).
|
|
13
18
|
//
|
|
14
19
|
// Nothing here touches SSH, keys or services itself — install.sh keeps that authority, its options and its
|
|
15
20
|
// exit codes (0 ready · 3 key not yet authorized · 4 server unreachable · 1 error).
|
|
21
|
+
//
|
|
22
|
+
// The enrolment-first update (W9 K3, LNS-88): an invite is redeemed once, so the installer that redeems it should be
|
|
23
|
+
// the server's own release. install.sh is run with LANES_WRAPPER_REPLY naming a fresh file; with an invite it asks the
|
|
24
|
+
// site's enrolment service which release the server runs before it installs or sends anything, and a newer one than
|
|
25
|
+
// this package ends that run with `update-to=<release>` in the file and exit 5. This wrapper then runs `npx --yes
|
|
26
|
+
// @vibelord/lanes@<release>` with the same arguments (the code moved to LANES_INVITE) and LANES_NO_SELF_UPDATE=1, which
|
|
27
|
+
// never hands over again, and ends with its status. When that cannot run — no npx with its npm on PATH, a release npm
|
|
28
|
+
// does not have (yet), npm not answering, a reply that names no newer release — install.sh runs once more from this
|
|
29
|
+
// package with LANES_NO_SELF_UPDATE=1: the run that handed over installed and redeemed nothing. LANES_NO_SELF_UPDATE or
|
|
30
|
+
// LANES_NO_AUTOUPDATE (any value) keeps the hand-over off; an exit 5 without a reply is install.sh's own status.
|
|
31
|
+
//
|
|
32
|
+
// A server install records the npx it ran with (W9 U3, LNS-91): after `--server` (not --check or --help) ends 0 or 4
|
|
33
|
+
// (installed), the npx on PATH — with its npm beside it, as the hand-over finds them — is written to
|
|
34
|
+
// $LANES_STATE/update/npx (~/.local/state/lanes/update/npx). `lanes update` and lanes-update.timer, whose unit PATH holds
|
|
35
|
+
// no Node.js, install the next release with exactly that npx.
|
|
16
36
|
|
|
17
37
|
import { spawnSync } from 'node:child_process';
|
|
18
38
|
import fs from 'node:fs';
|
|
@@ -20,7 +40,10 @@ import os from 'node:os';
|
|
|
20
40
|
import path from 'node:path';
|
|
21
41
|
import { fileURLToPath } from 'node:url';
|
|
22
42
|
|
|
23
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
currentLink, handOverReply, inferredSite, installArgs, npmAnswer, npxRecord, platformError, prunable, recordsNpx,
|
|
45
|
+
relayArgv, relayEnv, releaseNewer, splitArgs,
|
|
46
|
+
} from '../lib/plan.mjs';
|
|
24
47
|
|
|
25
48
|
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
49
|
const payload = path.join(packageDir, 'payload');
|
|
@@ -31,6 +54,15 @@ function fail(message, code = 1) {
|
|
|
31
54
|
process.exit(code);
|
|
32
55
|
}
|
|
33
56
|
|
|
57
|
+
// Written at once (fs.writeSync): a line said before a blocking spawnSync must reach the terminal before the child's.
|
|
58
|
+
function say(message) {
|
|
59
|
+
fs.writeSync(1, `lanes-install: ${message}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function note(message) {
|
|
63
|
+
fs.writeSync(2, `lanes-install: ${message}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
function readManifest() {
|
|
35
67
|
try {
|
|
36
68
|
return JSON.parse(fs.readFileSync(path.join(payload, 'manifest.json'), 'utf8'));
|
|
@@ -87,19 +119,22 @@ function stage(manifest) {
|
|
|
87
119
|
return target;
|
|
88
120
|
}
|
|
89
121
|
|
|
122
|
+
// A client only (W9 U4, LNS-92; DEF-1768): with the server's `current` link present — even dangling — nothing is
|
|
123
|
+
// deleted here, since what still runs from an old tree is visible only on the server; `lanes gc` prunes it there.
|
|
90
124
|
function prune(root, target) {
|
|
91
|
-
|
|
92
|
-
const data = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
|
125
|
+
let server = false;
|
|
93
126
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
// install/services.py writes the link under $HOME whatever $XDG_DATA_HOME says (W9 U0, DEF-1783).
|
|
128
|
+
fs.lstatSync(currentLink(os.homedir()));
|
|
129
|
+
server = true;
|
|
130
|
+
} catch { /* no link: a client */ }
|
|
96
131
|
const entries = [];
|
|
97
132
|
for (const name of fs.readdirSync(root)) {
|
|
98
133
|
const dir = path.join(root, name);
|
|
99
134
|
if (name.startsWith('.') || !stampOf(dir)) continue; // only trees this wrapper staged
|
|
100
135
|
entries.push({ path: dir, mtimeMs: fs.statSync(dir).mtimeMs });
|
|
101
136
|
}
|
|
102
|
-
for (const dir of prunable(entries,
|
|
137
|
+
for (const dir of prunable(entries, new Set([target]), { server })) fs.rmSync(dir, { recursive: true, force: true });
|
|
103
138
|
}
|
|
104
139
|
|
|
105
140
|
function usage(manifest) {
|
|
@@ -118,12 +153,100 @@ Wrapper options
|
|
|
118
153
|
|
|
119
154
|
Bundled site profiles: ${manifest.sites.join(', ') || 'none'} (or --site /path/to/site.conf)
|
|
120
155
|
Invite prefixes: ${Object.entries(manifest.site_codes || {}).map(([p, site]) => `${p} → ${site}`).join(', ') || 'none'} (an explicit --site wins)
|
|
156
|
+
With an invite, a server that runs a newer release gets that release's installer first (npx @vibelord/lanes@<release>,
|
|
157
|
+
once npm has it); LANES_NO_SELF_UPDATE=1 installs this one.
|
|
121
158
|
Every other option goes to install.sh; its own help follows.
|
|
122
159
|
|
|
123
160
|
`);
|
|
124
161
|
}
|
|
125
162
|
|
|
163
|
+
// An executable regular file named `name` in an absolute PATH directory (never the working directory), or null.
|
|
164
|
+
function onPath(name, searchPath = process.env.PATH || '') {
|
|
165
|
+
for (const dir of searchPath.split(path.delimiter)) {
|
|
166
|
+
if (!path.isAbsolute(dir)) continue;
|
|
167
|
+
const file = path.join(dir, name);
|
|
168
|
+
if (executable(file)) return file;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function executable(file) {
|
|
174
|
+
try {
|
|
175
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
176
|
+
return fs.statSync(file).isFile();
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// install.sh handed over to `release` (W9 K3): run that release's installer through npx and return its status, or null
|
|
183
|
+
// (said why) when it cannot run, so this package's installer runs instead. npx from PATH and the npm beside it (Node.js
|
|
184
|
+
// installs them in one folder), as lanes-launch's self-update finds them; only a release npm has is run (npx would
|
|
185
|
+
// fail on any other only after its own registry round trip: DEF-1786's lesson).
|
|
186
|
+
function handOver(release, manifest, passthrough) {
|
|
187
|
+
const instead = `installing ${manifest.version} from this package instead`;
|
|
188
|
+
if (release === null) {
|
|
189
|
+
note(`install.sh asked for an update that names no release: ${instead}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!releaseNewer(release, manifest.version)) {
|
|
193
|
+
note(`install.sh asked for ${release}, which is not newer than this package: ${instead}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const npx = onPath('npx');
|
|
197
|
+
const npm = npx && path.join(path.dirname(npx), 'npm');
|
|
198
|
+
if (!npx || !executable(npm)) {
|
|
199
|
+
note(`the server runs ${release}, and no npx with its npm is on PATH to fetch that installer: ${instead}`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const { argv, invite } = relayArgv(passthrough, process.env);
|
|
203
|
+
const env = relayEnv(process.env, invite, path.dirname(npx));
|
|
204
|
+
const asked = spawnSync(npm, ['view', `@vibelord/lanes@${release}`, 'version', '--fetch-retries=0'], {
|
|
205
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000, env,
|
|
206
|
+
});
|
|
207
|
+
const answer = npmAnswer(asked, release);
|
|
208
|
+
if (answer.state === 'missing') {
|
|
209
|
+
note(`the server runs ${release}, which npm does not have (yet): ${instead}`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (answer.state === 'failed') {
|
|
213
|
+
note(`the server runs ${release}; asking npm for it failed (${answer.why}): ${instead}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
say(`the server runs ${release}: npx --yes @vibelord/lanes@${release}, with the same options`);
|
|
217
|
+
const relayed = spawnSync(npx, ['--yes', `@vibelord/lanes@${release}`, ...argv], { stdio: 'inherit', env });
|
|
218
|
+
if (relayed.error) {
|
|
219
|
+
note(`could not run npx (${relayed.error.code || relayed.error.message}): ${instead}`);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return relayed.status ?? 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Record the npx of this server install for `lanes update` (W9 U3); a failure to record is a note, never the install's.
|
|
226
|
+
function recordNpx() {
|
|
227
|
+
const npx = onPath('npx');
|
|
228
|
+
const file = npxRecord(process.env, os.homedir());
|
|
229
|
+
if (!npx || !executable(path.join(path.dirname(npx), 'npm'))) {
|
|
230
|
+
note(`no npx with its npm on PATH: ${file} is not written, so lanes update cannot install the next release`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
235
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
236
|
+
fs.writeFileSync(temporary, `${npx}\n`, { mode: 0o600 });
|
|
237
|
+
fs.renameSync(temporary, file);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
note(`could not record npx in ${file} (${error.code || error.message}): lanes update cannot install the next release`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
126
243
|
function main() {
|
|
244
|
+
const status = installMain();
|
|
245
|
+
if (recordsNpx(splitArgs(process.argv.slice(2)), status)) recordNpx();
|
|
246
|
+
return status;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function installMain() {
|
|
127
250
|
const platform = platformError(process.platform);
|
|
128
251
|
if (platform) fail(platform);
|
|
129
252
|
const manifest = readManifest();
|
|
@@ -156,9 +279,39 @@ function main() {
|
|
|
156
279
|
}
|
|
157
280
|
if (inferred) process.stdout.write(`site ${inferred.site}, inferred from the invite code's prefix ${inferred.prefix}\n`);
|
|
158
281
|
// Run through the staged path (not the npx cache), so install.sh resolves its checkout to the stable tree.
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
282
|
+
const install = (env) => {
|
|
283
|
+
const result = spawnSync('sh', [path.join(staged, 'install.sh'), ...args], { stdio: 'inherit', cwd: staged, env });
|
|
284
|
+
if (result.error) {
|
|
285
|
+
note(`could not run install.sh: ${result.error.message}`);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
return result.status ?? 1;
|
|
289
|
+
};
|
|
290
|
+
const plain = { ...process.env };
|
|
291
|
+
delete plain.LANES_WRAPPER_REPLY;
|
|
292
|
+
if (process.env.LANES_NO_SELF_UPDATE || process.env.LANES_NO_AUTOUPDATE) return install(plain);
|
|
293
|
+
let replyDir;
|
|
294
|
+
try {
|
|
295
|
+
replyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lanes-install-'));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
note(`no hand-over file (${error.code || error.message}): the server's release is not asked for`);
|
|
298
|
+
return install(plain);
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const reply = path.join(replyDir, 'reply');
|
|
302
|
+
const status = install({ ...plain, LANES_WRAPPER_REPLY: reply });
|
|
303
|
+
if (status !== 5) return status;
|
|
304
|
+
let text = '';
|
|
305
|
+
try {
|
|
306
|
+
text = fs.readFileSync(reply, 'utf8');
|
|
307
|
+
} catch { /* no hand-over: install.sh's own exit 5 */ }
|
|
308
|
+
const asked = handOverReply(text);
|
|
309
|
+
if (!asked.asked) return status;
|
|
310
|
+
const relayed = handOver(asked.release, manifest, passthrough);
|
|
311
|
+
return relayed ?? install({ ...plain, LANES_NO_SELF_UPDATE: '1' });
|
|
312
|
+
} finally {
|
|
313
|
+
fs.rmSync(replyDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
162
315
|
}
|
|
163
316
|
|
|
164
317
|
process.exit(main());
|
package/lib/plan.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// that tree's own `install.sh`. These functions decide only the platform gate, the wrapper's own flags and how
|
|
5
5
|
// the passthrough arguments are rewritten.
|
|
6
6
|
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
7
9
|
export const SUPPORTED_PLATFORMS = ['darwin', 'linux'];
|
|
8
10
|
|
|
9
11
|
export function platformError(platform) {
|
|
@@ -90,9 +92,117 @@ export function resolveSite(value, stagedDir, sites) {
|
|
|
90
92
|
return `${stagedDir}/site/${value}/site.conf`;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
95
|
+
// The server's `current` link: install/services.py writes it under the HOME directory, never under $XDG_DATA_HOME (the
|
|
96
|
+
// units are rendered from $HOME), and lanes_lib.CURRENT reads the same place — so this link, wherever the staged trees
|
|
97
|
+
// themselves live, is what tells the wrapper it runs on a server (W9 U0, LNS-82; DEF-1783; W9 U4, LNS-92).
|
|
98
|
+
export function currentLink(home) {
|
|
99
|
+
return path.join(home, '.local/share', 'lanes', 'current');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Which staged version directories the wrapper deletes after staging one: on a client, the ones past the newest `keep`
|
|
103
|
+
// (by mtime) that are not in `inUse` (the tree just staged) — a client copies its commands out of the tree, so nothing
|
|
104
|
+
// runs from an old one. On a server (`server`: the `current` link exists, even dangling) none: what still runs from an
|
|
105
|
+
// old tree there — `current.previous` (the rollback target), a view layout built before W9 U1 naming `pkg/<version>`,
|
|
106
|
+
// a process started through `current` before the last flip — is visible only on the server itself, so `lanes gc`
|
|
107
|
+
// prunes it (bin/lanes staged_tree_plan, with the same newest-three rule; W9 U4, LNS-92, DEF-1768).
|
|
108
|
+
export function prunable(entries, inUse, { keep = 3, server = false } = {}) {
|
|
109
|
+
if (server) return [];
|
|
96
110
|
const sorted = [...entries].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
97
111
|
return sorted.slice(keep).filter((entry) => !inUse.has(entry.path)).map((entry) => entry.path);
|
|
98
112
|
}
|
|
113
|
+
|
|
114
|
+
// ---- the enrolment-first update (W9 K3, LNS-88) ---------------------------------------------------------------------
|
|
115
|
+
// With an invite, install.sh asks the site's enrolment service which release the server runs before it installs or
|
|
116
|
+
// sends anything. A newer one than this package ends that run: install.sh writes `update-to=<release>` to the file the
|
|
117
|
+
// wrapper named in LANES_WRAPPER_REPLY and exits 5, and the wrapper runs `npx --yes @vibelord/lanes@<release>` with the
|
|
118
|
+
// same arguments instead — the code moved to LANES_INVITE — and LANES_NO_SELF_UPDATE=1, so it never hands over again.
|
|
119
|
+
|
|
120
|
+
// A release as `lanes hello` (bin/lanes version_key) and install.sh (release_key) read one: major.minor.patch, an
|
|
121
|
+
// optional -/+ suffix that is not compared, at most 32 characters, each number at most 9 digits (install.sh's bound,
|
|
122
|
+
// so the two client halves agree exactly). [major, minor, patch], or null for anything else.
|
|
123
|
+
const RELEASE = /^([0-9]{1,9})\.([0-9]{1,9})\.([0-9]{1,9})(?:[-+][A-Za-z0-9.]+)?$/;
|
|
124
|
+
|
|
125
|
+
export function releaseKey(text) {
|
|
126
|
+
if (typeof text !== 'string' || text.length > 32) return null;
|
|
127
|
+
const match = RELEASE.exec(text);
|
|
128
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Whether `server` is a newer release than `mine`: both are releases, and major.minor.patch is higher as numbers.
|
|
132
|
+
export function releaseNewer(server, mine) {
|
|
133
|
+
const a = releaseKey(server);
|
|
134
|
+
const b = releaseKey(mine);
|
|
135
|
+
if (!a || !b) return false;
|
|
136
|
+
for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] > b[i];
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// install.sh's hand-over file: {asked: false} without an `update-to=` line (an exit 5 that is install.sh's own status),
|
|
141
|
+
// else {asked: true, release} — release null when the value is no release (it is never shown then).
|
|
142
|
+
export function handOverReply(text) {
|
|
143
|
+
const line = String(text ?? '').split('\n').find((each) => each.startsWith('update-to='));
|
|
144
|
+
if (line === undefined) return { asked: false, release: null };
|
|
145
|
+
const release = line.slice('update-to='.length).trim();
|
|
146
|
+
return { asked: true, release: releaseKey(release) ? release : null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The relayed command line: the same arguments without the invite code, which moves to LANES_INVITE — install.sh and
|
|
150
|
+
// this wrapper read it there the same way (the last `--invite CODE`, else LANES_INVITE) — so the code is on no
|
|
151
|
+
// command line of the second installer's processes (a process list shows any user's argv, never another's environment).
|
|
152
|
+
export function relayArgv(passthrough, env = {}) {
|
|
153
|
+
const invite = inviteCode(passthrough, env);
|
|
154
|
+
const argv = [];
|
|
155
|
+
for (let i = 0; i < passthrough.length; i += 1) {
|
|
156
|
+
if (passthrough[i] === '--invite') {
|
|
157
|
+
i += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
argv.push(passthrough[i]);
|
|
161
|
+
}
|
|
162
|
+
return { argv, invite };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The environment of `npm view` and the relayed npx: this one, with LANES_NO_SELF_UPDATE=1 (the newer installer never
|
|
166
|
+
// hands over again), the code in LANES_INVITE, npx's folder first on PATH (node lives beside it), and without this run's
|
|
167
|
+
// hand-over file or the options of the npx that started this wrapper — `npx -p <pkg>` exports npm_config_package and
|
|
168
|
+
// `npx -c` npm_config_call (npm reads either case), with which the nested npx runs `@vibelord/lanes@<v>` as a command
|
|
169
|
+
// (exit 127) or refuses (EUSAGE). MEASURED with npm 11; user configuration such as npm_config_registry stays.
|
|
170
|
+
export function relayEnv(env, invite, npxDir) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [key, value] of Object.entries(env)) {
|
|
173
|
+
if (key === 'LANES_WRAPPER_REPLY' || /^npm_config_(call|package)$/i.test(key)) continue;
|
|
174
|
+
out[key] = value;
|
|
175
|
+
}
|
|
176
|
+
out.LANES_NO_SELF_UPDATE = '1';
|
|
177
|
+
if (invite !== null && invite !== undefined) out.LANES_INVITE = invite;
|
|
178
|
+
if (npxDir) out.PATH = [npxDir, env.PATH].filter(Boolean).join(path.delimiter);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// What `npm view @vibelord/lanes@<release> version --fetch-retries=0` said, read as lanes-launch's self-update reads it
|
|
183
|
+
// (on_npm, W9 K2): it printed exactly the release → 'has'; npm 10's `code E404`, or an earlier npm's empty exit 0 → 'missing'
|
|
184
|
+
// (not published, yet); anything else → 'failed', with why (exit status and npm's error code, or the 15 s bound).
|
|
185
|
+
export function npmAnswer({ status, stdout, stderr, error }, release) {
|
|
186
|
+
const printed = String(stdout ?? '');
|
|
187
|
+
if (printed.split('\n').some((line) => line.trim() === release)) return { state: 'has' };
|
|
188
|
+
const code = /\bcode (E[A-Z0-9_]+)/.exec(String(stderr ?? ''))?.[1] ?? null;
|
|
189
|
+
if (code === 'E404' || (!error && status === 0 && !printed.trim())) return { state: 'missing' };
|
|
190
|
+
if (error) return { state: 'failed', why: error.code === 'ETIMEDOUT' ? 'no answer within 15 s' : (error.code || error.message) };
|
|
191
|
+
return { state: 'failed', why: `exit ${status ?? 'by signal'}${code ? `, ${code}` : ''}` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- lanes update's npx (W9 U3, LNS-91) -----------------------------------------------------------------------------
|
|
195
|
+
// `lanes update` (and lanes-update.timer, whose unit PATH holds no Node.js) installs the next release with the npx a
|
|
196
|
+
// server install ran with, which the wrapper records once that install installed something: `--server` (whatever its
|
|
197
|
+
// position: installArgs moves it first), neither `--check` (changes nothing) nor `--help`, and none of the wrapper's own
|
|
198
|
+
// flags (each ends the run before install.sh), ending 0 (installed, or ok) or 4 (an update that installed and then ended
|
|
199
|
+
// partial). `split` is splitArgs' {own, passthrough}.
|
|
200
|
+
export function recordsNpx({ own, passthrough }, status) {
|
|
201
|
+
return own.size === 0 && passthrough.includes('--server')
|
|
202
|
+
&& !['--check', '--help', '-h'].some((flag) => passthrough.includes(flag)) && (status === 0 || status === 4);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Where: install.sh's state directory ($LANES_STATE, else ~/.local/state/lanes — host-install.sh's `state`), update/npx.
|
|
206
|
+
export function npxRecord(env, home) {
|
|
207
|
+
return path.join(env.LANES_STATE || path.join(home, '.local/state/lanes'), 'update', 'npx');
|
|
208
|
+
}
|
package/package.json
CHANGED
package/payload/lanes/README.md
CHANGED
|
@@ -114,37 +114,49 @@ The installer starts an empty catalog safely; it does not assume anyone's contai
|
|
|
114
114
|
|
|
115
115
|
| Action | Command or control |
|
|
116
116
|
| --- | --- |
|
|
117
|
-
| Join a site from a new machine | `npx @vibelord/lanes --invite <code>` with the code from a member's invite: installs the client, authorizes this machine, then `lanes-launch`, `lanes-launch web phone`, `lanes-launch web laptop`. Without a code the installer asks the site's members for access ([guide](docs/USERGUIDE.md#2-join-a-site)) |
|
|
117
|
+
| Join a site from a new machine | `npx @vibelord/lanes --invite <code>` with the code from a member's invite: installs the client, authorizes this machine, then `lanes-launch`, `lanes-launch web phone`, `lanes-launch web laptop`. When the server runs a newer release (and npm has it), that release's installer runs first and redeems the code (`LANES_NO_SELF_UPDATE=1`: the package you ran). Without a code the installer asks the site's members for access ([guide](docs/USERGUIDE.md#2-join-a-site)) |
|
|
118
118
|
| Open desktop view | `lanes-window` or the Lanes app; `lanes-launch` inside any terminal |
|
|
119
119
|
| Independent second view | `lanes-window 2` |
|
|
120
120
|
| Two or three panes per workspace | `lanes-window w2` / `lanes-window w3` |
|
|
121
121
|
| Switch workspace | Ctrl–Alt–Left / Right |
|
|
122
122
|
| Next or previous lane | Ctrl–Alt–Down / Up (the focused pane moves to the next / previous lane of its workspace; nothing is typed, the focus stays), or tap the sidebar |
|
|
123
|
-
| Plain-ASCII glyphs, light or dark tokens | `lanes-window --ascii`, `lanes-window --light` / `--dark
|
|
124
|
-
| Lock the view (plain keys and Zellij's mode keys reach the lane) | Ctrl–Alt–G, again to unlock; the bar shows LOCKED in red. The Ctrl–Alt keys of `zellij/config.kdl`'s `shared` block (←/→/↓/↑, `/`, U, C, S, PageUp/PageDown/End) stay active while locked. Ctrl–G itself always reaches the lane (Claude Code's editor key) |
|
|
123
|
+
| Plain-ASCII glyphs, light or dark tokens | `lanes-window --ascii`, `lanes-window --light` / `--dark`. On a device with an owner the theme, colours and glyphs go to that person's profile, which every sidebar of all their devices paints within a tick (as a Done lane opened on one leaves Done on all); `lanes-window --light --device-only` keeps the look on this device (`devices/<device>.json`, where every look of a device without an owner goes), and a running sidebar picks it up within one tick. A terminal without 24-bit colour: `lanes attach <device> --color 256` (the fixed xterm-256 set; `--color 16` uses your terminal's own palette, `--color none` glyphs only) ([guide](docs/USERGUIDE.md#9-appearance)) |
|
|
124
|
+
| Lock the view (plain keys and Zellij's mode keys reach the lane) | Ctrl–Alt–G, again to unlock; the bar shows LOCKED in red. The Ctrl–Alt keys of `zellij/config.kdl`'s `shared` block (←/→/↓/↑, `/`, U, L, C, S, PageUp/PageDown/End) stay active while locked. Ctrl–G itself always reaches the lane (Claude Code's editor key) |
|
|
125
125
|
| Zellij modes | Ctrl–Alt–P pane · Ctrl–Alt–T tab · Ctrl–Alt–N resize · Ctrl–Alt–H move · Ctrl–Alt–F scroll · Ctrl–Alt–O session (`d` detaches) · Ctrl–Alt–B tmux-style; Esc returns to normal. Every key: [user guide](docs/USERGUIDE.md#keybindings) |
|
|
126
|
-
| Search lanes | Ctrl–Alt
|
|
126
|
+
| Search lanes | Ctrl–Alt–/ (from anywhere in the tab: the focus moves to the sidebar and its filter starts; nothing is typed into the lane), or `/` in the sidebar: fuzzy (the letters in order, substring and prefix matches first); Enter opens the match under the cursor, Esc clears |
|
|
127
|
+
| Focus the sidebar | Ctrl–Alt–L; while the sidebar has the keyboard its footer says ` ▌ keys go to sidebar · Esc → lane` (Esc, or any action, gives it back to the lane) |
|
|
127
128
|
| Select a lane for pane 2 or 3 | `2` / `3` in the sidebar of a wide view |
|
|
128
129
|
| Open one lane directly | `lanes-launch lane <lane-id>`; find IDs with `lanes-launch ls` |
|
|
130
|
+
| Your settings on every device you own | Once a device has an owner (enrolled through an invite or a request, or `lanes device claim <device> --login <login>` for one from before), your look, the lanes you have opened, your board order and hidden boards and your pinned and hidden lanes live in one profile on the server, `users/<login>.json`, shared by all your devices; `--device-only` keeps a look on one device, and `--notify` / `--dim` always stay per device. `lanes user show <login>` prints it ([guide](docs/USERGUIDE.md#your-profile)) |
|
|
131
|
+
| Pick up where you left off on another device | `lanes-launch here` shows in this machine's view the lane you opened last on any of your devices (its tab comes to the front); asked for, never automatic. On the server `lanes here <device>` |
|
|
132
|
+
| Arrange your boards and lanes on all your devices | `lanes workspaces hide\|show\|order <id…>` and `lanes lane pin\|unpin\|hide\|show <lane-id>` on the server, each `--device <your device>` (or `--login <login>`): kept in your profile, which every device you own reads ([guide](docs/USERGUIDE.md#your-profile)) |
|
|
129
133
|
| Read a markdown file a lane mentioned | Ctrl–Alt–M opens a floating **Preview** pane over the focused lane: the `.md` paths it printed in its last 300 lines that exist in its container (relative ones resolved against its working directory). One is shown at once; several give a numbered menu, newest first (Enter = the newest, `q` closes). Rendered by `glow` in the view's dark/light theme (the server installer adds a pinned glow unless the view server's PATH — `~/.local/share/lanes/bin`, the checkout's `bin`, `/usr/local/bin`, `/usr/bin`, `/bin` — already has one; a glow only in `~/.local/bin` or `~/go/bin` does not count), else `less -R`; `lanes doctor` names which (`preview renderer:`); quitting the pager closes the pane |
|
|
134
|
+
| Edit a lane's AGENTS.md, CLAUDE.md or .env.local | Ctrl–Alt–E opens a floating **Edit** pane over the focused lane: the AGENTS.md and CLAUDE.md of its git checkout (and of the main checkout when it works in a worktree) and the main checkout's .env.local, each a key (`1`–`5`; Enter when only one is there; `q`/Esc closes; a missing file is shown `not present`, none is created). The key starts an editor inside the lane's container as its own user (`lanes attach <device> --editor micro\|hx\|nvim\|vim` picks it per device, or `lanes-launch <device> --editor E` from your machine; else the first of micro, hx, nvim, vim and vi there: Lanes installs none, vi is the floor); quitting the editor closes the pane. One Edit pane per view: pressing it again goes back to the open editor (or names the tab it is in) and never opens a second one on the same file. Off while the view is locked, like Ctrl–Alt–M |
|
|
130
135
|
| Create a persistent workspace shell | The **+ New shell** row under **Unregistered** (desktop sidebar and phone picker), or `+` in the sidebar |
|
|
131
136
|
| Register an existing session | **+** beside an unregistered session, or select it and press `a`; choose a workspace (`lanes register <lane-id> [workspace]` on the server) |
|
|
132
|
-
|
|
|
133
|
-
|
|
|
137
|
+
| Unregister a session you added | `u` in the sidebar (or `lanes unregister <lane-id>` on the server): it moves back to **Unregistered**, still running, same id and name, and the cursor goes with it; ` Unregistered ‘name’ · z undo` — `z` within 10 s adds it back (refused if the session was replaced meanwhile). A lane declared in a repository's `terminals.json` leaves that file (its terminal is removed, the session keeps running and is listed under **Unregistered** after the sync `u` starts; no `z`: the file's `.bak-<UTC>` copy keeps it), refused for the file's last terminal; `x` on any registered lane says ` registered: u to unregister first` |
|
|
138
|
+
| Declare a session in the repository | `P` in the sidebar: the name is asked first (prefilled; Enter accepts, or edit it), then `y` and Enter (any other key cancels). Writes the session's binding into the workspace's one repository `terminals.json` under that name, so it becomes a repository lane `<workspace>--<slug>`; the row stays put with a dim `…` until the sync lists the lane, then moves into the list with the cursor on it |
|
|
139
|
+
| Rename a lane | `R` in the sidebar (Enter saves, Esc cancels; arrows and Tab type nothing in the name, a click on the list cancels it first), or `lanes rename <lane-id> <name>` on the server; the lane id never changes, and your name wins over a `displayName` the repository's `terminals.json` declares |
|
|
134
140
|
| Add a workspace (board) | `lanes workspaces add <id> --container <name> --repo /home/coder/<repo> [--title T]` on the server prints the catalog and manifest diff; add `--write` to apply, then `lanes sync` ([install](docs/INSTALL.md#2-configure-workspaces-and-editor-sync)) |
|
|
135
141
|
| Onboard a user's container | `lanes onboard <login> --container <name> --workspace <id> --repo /home/coder/<repo>` on the server prints every step (board, contract, Claude and Codex hooks, Throttle attachment, canonical recipes, sync, doctor); add `--write` to apply them and keep a receipt ([install](docs/INSTALL.md#onboard-a-users-container)) |
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
142
|
+
| Let an agent see and manage its lanes | The container's agents get the Lanes MCP server, `lane-mcp`, from `lanes install-container <name>` (on by default; `--no-mcp` leaves it out). `lanes_whoami`, `lanes_list`, `lanes_status` and `lanes_capture` read the lanes of that container. `lanes_register`, `lanes_rename`, `lanes_unregister` and `lanes_new` change a lane's declaration, shown on the boards within about 15 s. `lanes_send` types into a sibling lane, `lanes_signal` reports the agent's own status, and `lanes_publish` puts a file on the docs page (`lane publish <file>` in a shell). Ending, interrupting or attaching a lane stays yours ([architecture](docs/ARCHITECTURE.md#agent-api), [guide](docs/USERGUIDE.md#agent-tools-mcp)) |
|
|
143
|
+
| See which agents need you | The sidebar sorts **Needs you** (◆ approval, ✗ error, ◇ waiting for a prompt), **Working** (▸) and **Done** (✓, until you open the lane) above the rest; the header counts them in the same glyphs (`◆1 ◇2 ▸6 ✓1`, zeros left out; `N lanes` when no lane runs an agent). The queue holds still: each section keeps the order its lanes entered it, a lane moves up a section at once but down only after the new state has held 8 s (leaving Needs you) or 5 s (leaving Working or Done), so an agent that asks again within seconds of an answer does not jump; the Needs you and Working headers stay drawn, `(0)` included, and the cursor stays on its lane ([how](docs/ARCHITECTURE.md#attention-queue)) |
|
|
144
|
+
| Read a sidebar row | `▌` cursor · agent glyph · `›` the lane this pane shows (`²`/`³`: shown in pane 2/3; `▲` pinned) · the name (`·` per other viewer, `…` when clipped) · an **age column** on every row (agent turn, else last output: `45s`, ` 3m`, ` 2h`, ` 4d`; amber in **Needs you**) · stream glyph · one tail cell by priority (`?` not observed right now > `⚠` starving: producing output but no request for over 10 min, or a 429 > `↓` downgraded > `$` mostly paid — the economy cells need the opt-in economy poller; a dim `⚠`/`↓` is a frozen last-known figure, not a live alarm) · `×` close. `⋮ N more` ends a list that continues below |
|
|
145
|
+
| End an unregistered session and its jobs | **×** beside the session, or select it and press `x`: ` › close ‘name’ — y⏎ confirm · Esc cancel`; `y` arms (` — ⏎ confirm`), Enter closes; any other key cancels (typed text closes nothing unless it is `x`, `y`, Enter in a row) |
|
|
146
|
+
| Every sidebar key | `?` replaces the list with the key sheet until the next key (that key only closes it): the short form of the [user guide's table](docs/USERGUIDE.md#the-sidebar), first `lanes <installed> · view <running>`, then companion keys on one line (`J K`, `Tab S-Tab`, `+ U d`, `u z`, `p h`, `H o`, `r ?`; `d` shows the cursor lane's docs link), ending with Ctrl–Alt–L, the chord that brings the keyboard to the sidebar from a lane. The footer legend says ` ⏎ open · / find · x close · ? keys`; one prompt at a time, in one shape on the desktop and the phone (` › close ‘name’ — y⏎ confirm · Esc cancel`, ` › Rename: …█`, ` › Register as: …█` then ` › register ‘name’ — y⏎ confirm`) and a filter counts its matches (` /abc · 3 matches`). What an action says in the footer (` Closed sh`, ` Cancelled`, why it failed) stays 3 s or until your next action, not just until the next key. After an action — a close, rename, register, cancel, `Esc`, `r`, closing `?`, a click that opens no lane — the keyboard goes back to the lane, so what you type next reaches the lane, not the sidebar; a prompt, the filter and the armed upload strip keep it |
|
|
147
|
+
| Pin, hide | `p` pins the cursor lane (it leads its section, `▲`, and never falls below `⋮ N more`), `h` hides it, `o` shows the hidden lanes again (faint), `H` then `y` `Enter` hides the whole workspace — its tab closes, its lanes keep running. They are yours, not the device's: every device you own follows your profile on its next tick; a session under **Unregistered** is never hidden ([guide](docs/USERGUIDE.md#the-sidebar)) |
|
|
148
|
+
| Jump to the next lane that needs you | `Tab` / `Shift-Tab` in the sidebar (cycles **Needs you** from the lane the pane shows; the pane title carries the counts and a lane that starts needing you raises one OSC 9 desktop notification on desktop layouts — `lanes-launch <device> --notify 0` (or `lanes attach <device> --notify 0` on the server) silences it for that device, live; `LANES_NOTIFY=0` in the shell that creates a view session does the same for it, see the [user guide](docs/USERGUIDE.md#9-appearance)) |
|
|
141
149
|
| Other workspaces that need you | The row above the footer rule: ` ◆1 Worlds · ◇2 Code` (blank and absent when none; panes of 18 rows or more) |
|
|
142
150
|
| What the fleet spends | With the opt-in economy poller ([RUNBOOK](docs/RUNBOOK.md#economy-projection-opt-in)) the same row is the fleet economy strip instead: ` $8.5k/16k 53% · F2 S1 · spill 76% · free 3/19` — overflow spend against the daily cap (amber at the alert level), new upstream 429s, this workspace's lanes by model tier (`F` fable, `O` opus, `S` sonnet, `H` haiku, `G` gpt), the pool's spill fraction, free accounts and the next 5 h reset, whole parts dropped from the end to fit; a leading `?` means the figures are frozen — the poller could not reach the daemon, or it stopped (no rewrite for over 3 min) |
|
|
143
|
-
| Upload | Click the one-row upload strip above the footer (` ↑ uploads · click, then drop (U)`)
|
|
151
|
+
| Upload | Click the one-row upload strip above the footer (` ↑ uploads · click, then drop (U)`), press `U` in the sidebar or Ctrl–Alt–U from anywhere in the tab, then drop anywhere on the window; or use `~/Lanes Uploads/<workspace>/`, or `lanes-upload <files>` |
|
|
152
|
+
| See what an agent published, or upload from a phone | The Docs page, `https://<server>:8443/lanes-docs/`, open only to your own tailnet login: **More → Docs** on a phone or tablet, **Docs** in the laptop's Lanes bar, a ⌘/Ctrl-click on the sidebar's ` ↑ uploads · ▤ docs N` strip, or `d` on a lane for its own link. An agent publishes with `lanes_publish` (or `~/.lz/bin/lane publish <file>` in a lane's shell): Markdown with Mermaid diagrams rendered, HTML and SVG opened in a sandbox, any other file a download, each with a sha256 receipt; the page's drop area lands files in the workspace's `uploads/` with the same receipt. It answers nobody until the operator pins the Serve header check (`lanes enrol mode request`) ([user guide](docs/USERGUIDE.md#docs)) |
|
|
144
153
|
| Phone view from a client | `lanes-launch web phone` in your own terminal: prepares `<your device>-phone` (phone layout), makes Tailscale Serve forward private HTTPS 8443 when the mapping is missing, prints the private URL with a QR code of it and a login token; scan the code or open the URL in Safari. `sh install/enable-phone.sh --host lanes` is the older one-shot for the device `phone` ([guide](docs/USERGUIDE.md#phone-safari-home-screen)) |
|
|
145
154
|
| Laptop or tablet browser view | `lanes-launch web laptop` (`<your device>-web`, desktop layout) or `lanes-launch web <device> [--desktop\|--mobile]` from a client; on the server `lanes web <device> [--desktop\|--mobile] [--viewports N] [--token \| --print-only]` (prepares the view, enables the loopback web services, adds the Tailscale Serve `https:8443` mapping when missing, prints the private URL) ([guide](docs/INSTALL.md#laptop-and-tablet-browser)) |
|
|
146
155
|
| Web login tokens | `lanes token list` (each token with the device it was created for), `lanes token revoke <device>`, `lanes token revoke-all --yes` ([runbook](docs/RUNBOOK.md#login-tokens)) |
|
|
147
|
-
| Update the client | `lanes-launch update` (the newest client from npm with your remembered install options; flags you add win). A client older than the server says so in one dim line when it attaches |
|
|
156
|
+
| Update the client | Automatic: before a window connects, at most every 6 h, a client older than the server installs exactly the server's release once npm has it (`npx --yes @vibelord/lanes@<version>` with your remembered install options) and starts again, once; a release npm does not have yet is only logged and asked about again 6 h later; a failure is one dim line and the window goes on, and closing the window ends the whole install (`LANES_NO_AUTOUPDATE=1`: off; a checkout's client never does). By hand: `lanes-launch update` (the newest client from npm with your remembered install options; flags you add win). A client older than the server says so in one dim line when it attaches |
|
|
157
|
+
| Update the server | Automatic on an npm-installed server: `lanes-update.timer` (enabled by the full server install; `--update-only` never enables it, so on a server set up before 1.2.0 run `npx @vibelord/lanes@latest --server` once, or `systemctl --user enable --now lanes-update.timer`; `lanes doctor` warns while it is off) checks npm 15 min after a boot and every 30 min and installs a newer release (`npx --yes @vibelord/lanes@<version> --server --update-only` with the npx the install recorded; never a downgrade; `channel=stable` in `~/.config/lanes/update.conf` follows the `lanes-stable` tag). By hand: `lanes update --now`; `lanes update --status` prints the record of the last check and update; `lanes update --rollback` goes back to the release before and marks the one left bad. A checkout server updates with `git pull` ([guide](docs/USERGUIDE.md#update)) |
|
|
158
|
+
| Which release the server runs | `lanes version` on the server: `lanes 1.2.0 (39998ed) · tree pkg/1.2.0 · current ✓ · statusd runs 1.1.0 ✗` — the release, its tree, whether `current` names it, and `✗` for each service whose last start ran an older tree or, on a checkout, came before its `git pull` (`started before its tree changed ✗`): restart it; `--json` for scripts. `lanes doctor` judges the same facts (only processes that run now; a daemon or sidebar with no record started from an older release) and adds the device layouts and the units, each `✗` with its fix ([user guide](docs/USERGUIDE.md#versions)) |
|
|
159
|
+
| Which release this client or phone runs | `lanes-launch --version` (`lanes-launch 1.2.0`); its connecting line says it too (`lanes 1.2.0: connecting to lanes…`). On a phone or tablet, **More** shows the server's release above **Text size** (**Lanes v1.2.0**) |
|
|
148
160
|
| Switch workspace or lane in a laptop browser | The **Lanes** chip (top right) opens the workspace and lane pickers |
|
|
149
161
|
| Invite a teammate, approve a machine, remove one | On the server, in your own terminal: `lanes invite <name> --email <addr>` prints a five-line welcome kit with a one-time code (shown once, on a terminal only); `lanes enrol list` / `lanes enrol approve <id>` decide machines that asked without a code; `lanes enrol mode request` pins request mode once the Serve header check passed; `lanes offboard <device> --yes` revokes a machine's key, signs its browsers and phones out and ends its view ([guide](docs/USERGUIDE.md#invite-a-user-administrator), [install](docs/INSTALL.md#offboard-a-machine)) |
|
|
150
162
|
| Detach | Close the window, or Ctrl–Alt–O then `d`; the agents keep running |
|
|
@@ -156,6 +168,9 @@ session reuses it; closing a view leaves it running. Registered lanes have no **
|
|
|
156
168
|
**Add to workspace** keeps the session running and moves it into the selected workspace's registered list.
|
|
157
169
|
The picker offers workspaces using the same container. Registration keeps its name and open views, persists
|
|
158
170
|
across sync/reconnects, and removes its close control. On a phone, tap the **+** and a workspace; no typing is needed.
|
|
171
|
+
`u` takes it out again (the session keeps running under **Unregistered**); `z` within 10 seconds undoes that.
|
|
172
|
+
Closing from the phone needs no keyboard either: tap **×**, and while the picker asks, a short `⏎ y Esc` key row sits
|
|
173
|
+
above the dock (`y` then `⏎` closes, `Esc` cancels).
|
|
159
174
|
|
|
160
175
|
On iPhone, open the printed private HTTPS URL in Safari and choose **Share → Add to Home Screen**.
|
|
161
176
|
The interface provides Workspaces, Lanes and Panes controls, safe areas, portrait/landscape layout and adjustable
|
|
@@ -174,9 +189,9 @@ long (agent since-time, or last output). Glyph then ASCII twin (`LANES_GLYPHS=as
|
|
|
174
189
|
|
|
175
190
|
| Agent (column 2) | | Terminal (column 33) | | Marks |
|
|
176
191
|
| --- | --- | --- | --- | --- |
|
|
177
|
-
| `◆` `!` needs approval | `◇` `?` needs input | `●` `*` output in the last 20 s | `○` `o` quiet | `▌` `\|` cursor · `›` `=` viewing · `²` `2` / `³` `3` shown in viewport 2/3 |
|
|
178
|
-
| `▸` `>` working | `✓` `v` done (until viewed) | `–` `-` no session right now | `!` `!` pane dead | `·` `.` viewers · `…` `~` clipped · `×` `x` close |
|
|
179
|
-
| `✗` `X` errored | `_` `_` shell · `·` `.` idle | `✗` `x` container down | `?` `?` not observed · `·` `.` helper | `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now |
|
|
192
|
+
| `◆` `!` needs approval | `◇` `?` needs input | `●` `*` output in the last 20 s | `○` `o` quiet | `▌` `\|` cursor · `›` `=` viewing · `²` `2` / `³` `3` shown in viewport 2/3 · `▲` `^` pinned |
|
|
193
|
+
| `▸` `>` working | `✓` `v` done (until viewed) | `–` `-` no session right now | `!` `!` pane dead | `·` `.` viewers · `…` `~` clipped (end of a name) or on its way — closing, registering, a new shell (column 3, dim) · `×` `x` close |
|
|
194
|
+
| `✗` `X` errored | `_` `_` shell · `·` `.` idle | `✗` `x` container down | `?` `?` not observed · `·` `.` helper | `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now · `▤` `#` docs |
|
|
180
195
|
|
|
181
196
|
Column 34 is one tail cell: `?` stale (last known state shown) > `⚠` `!` starving > `↓` `v` downgraded > `$` mostly
|
|
182
197
|
paid. Colour is reinforcement, never the only signal; `LANES_COLOR=none` keeps every glyph. Full tables, tokens and
|
|
@@ -239,13 +254,22 @@ See [workspace configuration](workspaces.example/README.md) and the [architectur
|
|
|
239
254
|
|
|
240
255
|
After `git pull`, clients re-run `./install.sh` (it repeats the options remembered in
|
|
241
256
|
`~/.config/lanes/install-options`); servers re-run `./install.sh --server` from their installed checkout. npm
|
|
242
|
-
client installations update
|
|
243
|
-
as flags);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
client installations update themselves to the server's release when a window opens (at most every 6 h, once npm has it), or by hand
|
|
258
|
+
with `lanes-launch update` (`npx @vibelord/lanes@latest` with those options given again as flags); an npm-installed server updates itself (`lanes-update.timer`, which the full `--server` install enables and `--update-only` never does: `lanes update --timer` every 30 min installs each newer npm release; `lanes update --now` by hand, `lanes update --rollback` back one release) with `npx @vibelord/lanes@<version> --server --update-only`, which you can also run on the host, and which
|
|
259
|
+
then restarts statusd and every optional service that runs, re-installs the container contract, asks the viewports to
|
|
260
|
+
reload, rebuilds each view whose layout changed and that no client is attached to (one you are looking at says
|
|
261
|
+
`update ready · reload (r)` in its sidebar footer) and records the run in `~/.local/state/lanes/update/last.json` — every step runs even when one before it
|
|
262
|
+
failed ([user guide](docs/USERGUIDE.md#update)). After a full `--server` install instead, restart
|
|
263
|
+
`lanes-statusd.service` (and `lanes-web`/`lanes-mobile` when their units or the pinned Zellij changed); existing
|
|
264
|
+
views keep their layout until `lanes rebuild <device>` — one built by 1.2.0 or later runs its
|
|
265
|
+
sidebar and viewports through `~/.local/share/lanes/current`, so each starts the new release when it next starts,
|
|
266
|
+
and `lanes sync` keeps recognising the tabs of either kind. `lanes version` prints the server's
|
|
267
|
+
version and marks `✗` each service whose last start ran an older tree or, on a checkout, came before the pull
|
|
268
|
+
(`lanes hello` prints the version alone; [user guide](docs/USERGUIDE.md#versions)); `lanes doctor` names what still
|
|
269
|
+
runs older code — processes, view layouts, units — with the fix for each. `lanes-launch --version` prints a client's.
|
|
270
|
+
The server installer resolves
|
|
271
|
+
the checkout's actual path, including `bw-tools/lanes`; no `~/lanes` clone is required. It preserves configured
|
|
272
|
+
workspaces and existing agent sessions.
|
|
249
273
|
|
|
250
274
|
```sh
|
|
251
275
|
./install.sh --check # client connectivity
|
package/payload/lanes/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.3.0
|