@vibelord/lanes 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/bin/lanes-install.mjs +158 -0
- package/lib/plan.mjs +65 -0
- package/package.json +37 -0
- package/payload/lanes/README.md +152 -0
- package/payload/lanes/VERSION +1 -0
- package/payload/lanes/bin/lanes +1035 -0
- package/payload/lanes/client/ghostty/common.conf +22 -0
- package/payload/lanes/client/ghostty/macos.conf +6 -0
- package/payload/lanes/client/ghostty/omarchy.conf +19 -0
- package/payload/lanes/client/install.sh +16 -0
- package/payload/lanes/client/lanes-launch +315 -0
- package/payload/lanes/client/lanes-upload +147 -0
- package/payload/lanes/client/lanes-window +36 -0
- package/payload/lanes/client/macos-helper.applescript +14 -0
- package/payload/lanes/client/omarchy/discover.sh +56 -0
- package/payload/lanes/client/omarchy/lanes.conf +5 -0
- package/payload/lanes/client/omarchy/lanes.desktop +9 -0
- package/payload/lanes/client/omarchy/lanes.lua +14 -0
- package/payload/lanes/client/ssh.stanza.in +54 -0
- package/payload/lanes/container/env.sh +16 -0
- package/payload/lanes/container/lane +268 -0
- package/payload/lanes/container/lane-run +34 -0
- package/payload/lanes/docs/ARCHITECTURE.md +93 -0
- package/payload/lanes/docs/HOST-GUARD.md +108 -0
- package/payload/lanes/docs/INSTALL.md +192 -0
- package/payload/lanes/docs/RUNBOOK.md +385 -0
- package/payload/lanes/docs/SHARING.md +124 -0
- package/payload/lanes/docs/ZMX-RING0.md +51 -0
- package/payload/lanes/docs/images/desktop.png +0 -0
- package/payload/lanes/docs/images/iphone.png +0 -0
- package/payload/lanes/holders/exec.sh +21 -0
- package/payload/lanes/holders/tmux.sh +111 -0
- package/payload/lanes/holders/zmx.sh +22 -0
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-phone.sh +43 -0
- package/payload/lanes/install/enable-web.sh +16 -0
- package/payload/lanes/install/host-install.sh +72 -0
- package/payload/lanes/install/services.py +61 -0
- package/payload/lanes/install.sh +603 -0
- package/payload/lanes/libexec/cursor_commit.py +159 -0
- package/payload/lanes/libexec/cursor_sync.py +166 -0
- package/payload/lanes/libexec/guard.py +231 -0
- package/payload/lanes/libexec/lanes_lib.py +610 -0
- package/payload/lanes/libexec/legacy_sessions.py +130 -0
- package/payload/lanes/libexec/session_actions.py +276 -0
- package/payload/lanes/libexec/sidebar +8 -0
- package/payload/lanes/libexec/sidebar.py +872 -0
- package/payload/lanes/libexec/statusd.py +764 -0
- package/payload/lanes/libexec/tmpscan.py +921 -0
- package/payload/lanes/libexec/view.sh +179 -0
- package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
- package/payload/lanes/site/README.md +28 -0
- package/payload/lanes/site/beyondwork/known_hosts +1 -0
- package/payload/lanes/site/beyondwork/site.conf +11 -0
- package/payload/lanes/site/site.example.conf +10 -0
- package/payload/lanes/sudo/host-guard.sh +356 -0
- package/payload/lanes/sudo/one-time-root.sh +32 -0
- package/payload/lanes/systemd/lanes-ensure.service +7 -0
- package/payload/lanes/systemd/lanes-ensure.timer +10 -0
- package/payload/lanes/systemd/lanes-gc.service +7 -0
- package/payload/lanes/systemd/lanes-gc.timer +9 -0
- package/payload/lanes/systemd/lanes-guard.service +8 -0
- package/payload/lanes/systemd/lanes-guard.timer +10 -0
- package/payload/lanes/systemd/lanes-mobile.service +23 -0
- package/payload/lanes/systemd/lanes-statusd.service +15 -0
- package/payload/lanes/systemd/lanes-sync.service +10 -0
- package/payload/lanes/systemd/lanes-sync.timer +10 -0
- package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
- package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
- package/payload/lanes/systemd/lanes-web.service +13 -0
- package/payload/lanes/systemd/lanes.slice +11 -0
- package/payload/lanes/web/Caddyfile +37 -0
- package/payload/lanes/web/README.md +35 -0
- package/payload/lanes/web/icon-192.png +0 -0
- package/payload/lanes/web/icon.svg +4 -0
- package/payload/lanes/web/index.html +30 -0
- package/payload/lanes/web/manifest.webmanifest +15 -0
- package/payload/lanes/web/mobile.css +301 -0
- package/payload/lanes/web/mobile.js +367 -0
- package/payload/lanes/workspaces.example/README.md +27 -0
- package/payload/lanes/workspaces.example/catalog.json +7 -0
- package/payload/lanes/workspaces.example/commit-targets.json +8 -0
- package/payload/lanes/workspaces.example/terminals.example.json +19 -0
- package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
- package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
- package/payload/lanes/zellij/config.kdl +360 -0
- package/payload/manifest.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Beyond Work
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @vibelord/lanes
|
|
2
|
+
|
|
3
|
+
npm distribution of [Lanes](../../lanes/README.md): shared agent terminals over SSH with Zellij workspace views,
|
|
4
|
+
registry sync, uploads and a phone interface. The package carries the committed `lanes/` tree and runs its own
|
|
5
|
+
`install.sh`; every option and exit code of that installer applies unchanged.
|
|
6
|
+
|
|
7
|
+
The package is **restricted** to the `@vibelord` npm organization.
|
|
8
|
+
|
|
9
|
+
## Install on a new Mac or Linux machine
|
|
10
|
+
|
|
11
|
+
Requirements: Node.js 18+ (for npm only), OpenSSH, and a route to the server (Tailscale for the Beyond Work profile).
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm login --scope=@vibelord # once per machine: an @vibelord member account
|
|
15
|
+
npx @vibelord/lanes --site beyondwork # install or update the client
|
|
16
|
+
lanes-window # open the desktop view
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Instead of `npm login`, a machine can use a **read-only** npm token (npmjs.com → Access Tokens → Granular,
|
|
20
|
+
read-only, scoped to `@vibelord/lanes`):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
printf '//registry.npmjs.org/:_authToken=%s\n' "$LANES_READ_TOKEN" >> ~/.npmrc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Never put the organization's publish token on an installing machine.
|
|
27
|
+
|
|
28
|
+
Exit status is the installer's: `0` ready · `3` installed, but this machine's SSH key is not yet authorized on
|
|
29
|
+
the server (send the printed **public** key to the admin, then run `npx @vibelord/lanes --check`) · `4` installed,
|
|
30
|
+
but the server is unreachable (Tailscale?) · `1` error.
|
|
31
|
+
|
|
32
|
+
Other sites pass their own profile: `npx @vibelord/lanes --site /path/to/site.conf`.
|
|
33
|
+
|
|
34
|
+
## Server
|
|
35
|
+
|
|
36
|
+
On the Linux x86_64 workspace host, as the workspace account:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx @vibelord/lanes --server --check
|
|
40
|
+
npx @vibelord/lanes --server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What the wrapper does
|
|
44
|
+
|
|
45
|
+
`lanes-install` copies the bundled tree to `~/.local/share/lanes/pkg/<version>/` (`$LANES_PKG_ROOT` overrides the
|
|
46
|
+
parent) and runs `install.sh` from there. The server's systemd user units and `~/.local/share/lanes/current` name
|
|
47
|
+
that stable path, never the npx cache. An identical tree is reused; the newest three versions and any version the
|
|
48
|
+
server still runs from are kept.
|
|
49
|
+
|
|
50
|
+
| Option | Effect |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `--site NAME` | a bundled profile (`beyondwork`); a value with `/` or `.conf` is your own file |
|
|
53
|
+
| `--stage-only` | stage the tree and print its path, run nothing |
|
|
54
|
+
| `--version` | print the package version |
|
|
55
|
+
| `--wrapper-help` | wrapper help only |
|
|
56
|
+
|
|
57
|
+
Update: re-run `npx @vibelord/lanes@latest` with the same options. Remove the client: `npx @vibelord/lanes --uninstall`.
|
|
58
|
+
|
|
59
|
+
## Maintainers
|
|
60
|
+
|
|
61
|
+
The package version equals `lanes/VERSION`; bump both together. `prepack` builds `payload/` from `git ls-files lanes`
|
|
62
|
+
(tests excluded) and refuses uncommitted `lanes/` changes.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
cd npm/lanes && npm test
|
|
66
|
+
scripts/publish.sh --dry-run # then without --dry-run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`publish.sh` reads the publish token from the main checkout's `.env.local` (`NPMJS2` by default; override with
|
|
70
|
+
`LANES_NPM_ENV_FILE` / `LANES_NPM_TOKEN_KEY`) and hands it to npm through the environment only.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// npm transport for the Lanes installer.
|
|
4
|
+
//
|
|
5
|
+
// The package carries the committed `lanes/` tree (payload/lanes) plus a manifest written at pack time. This
|
|
6
|
+
// wrapper copies that tree to a STABLE, versioned directory and runs its own `install.sh` from there:
|
|
7
|
+
//
|
|
8
|
+
// ~/.local/share/lanes/pkg/<version>/ (or $LANES_PKG_ROOT/<version>)
|
|
9
|
+
//
|
|
10
|
+
// A stable path matters for the server: `install/services.py` writes the resolved checkout path into the
|
|
11
|
+
// systemd user units and links ~/.local/share/lanes/current to it, so the tree must outlive the npx cache.
|
|
12
|
+
// Clients copy their commands out of the tree, so for them the location is incidental.
|
|
13
|
+
//
|
|
14
|
+
// Nothing here touches SSH, keys or services itself — install.sh keeps that authority, its options and its
|
|
15
|
+
// exit codes (0 ready · 3 key not yet authorized · 4 server unreachable · 1 error).
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
import { installArgs, platformError, prunable, splitArgs } from '../lib/plan.mjs';
|
|
24
|
+
|
|
25
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const payload = path.join(packageDir, 'payload');
|
|
27
|
+
const STAMP = '.lanes-package.json';
|
|
28
|
+
|
|
29
|
+
function fail(message, code = 1) {
|
|
30
|
+
process.stderr.write(`lanes-install: ${message}\n`);
|
|
31
|
+
process.exit(code);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readManifest() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(path.join(payload, 'manifest.json'), 'utf8'));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fail(`this package has no payload (${error.code || error.message}); reinstall @vibelord/lanes`);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pkgRoot() {
|
|
44
|
+
if (process.env.LANES_PKG_ROOT) return path.resolve(process.env.LANES_PKG_ROOT);
|
|
45
|
+
const data = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
|
46
|
+
return path.join(data, 'lanes', 'pkg');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stampOf(dir) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(path.join(dir, STAMP), 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Copy payload/lanes to <root>/<version> atomically; an identical tree (same digest) is reused as is.
|
|
58
|
+
function stage(manifest) {
|
|
59
|
+
const root = pkgRoot();
|
|
60
|
+
const target = path.join(root, manifest.version);
|
|
61
|
+
if (stampOf(target)?.digest === manifest.digest) return target;
|
|
62
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o755 });
|
|
63
|
+
const staging = fs.mkdtempSync(path.join(root, `.staging-${manifest.version}-`));
|
|
64
|
+
try {
|
|
65
|
+
fs.cpSync(path.join(payload, 'lanes'), staging, { recursive: true });
|
|
66
|
+
// npm does not promise to keep executable bits inside a tarball: the manifest does.
|
|
67
|
+
for (const file of manifest.executables) fs.chmodSync(path.join(staging, file), 0o755);
|
|
68
|
+
fs.chmodSync(staging, 0o755);
|
|
69
|
+
fs.writeFileSync(path.join(staging, STAMP), `${JSON.stringify({
|
|
70
|
+
name: manifest.name, version: manifest.version, digest: manifest.digest, commit: manifest.commit,
|
|
71
|
+
staged_at: new Date().toISOString(),
|
|
72
|
+
}, null, 1)}\n`);
|
|
73
|
+
if (fs.existsSync(target)) {
|
|
74
|
+
// Same version, different bytes (a local rebuild): move the old tree aside, never write into it.
|
|
75
|
+
const aside = `${target}.replaced-${process.pid}`;
|
|
76
|
+
fs.renameSync(target, aside);
|
|
77
|
+
fs.renameSync(staging, target);
|
|
78
|
+
fs.rmSync(aside, { recursive: true, force: true });
|
|
79
|
+
} else {
|
|
80
|
+
fs.renameSync(staging, target);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
prune(root, target);
|
|
87
|
+
return target;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function prune(root, target) {
|
|
91
|
+
const inUse = new Set([target]);
|
|
92
|
+
const data = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
|
93
|
+
try {
|
|
94
|
+
inUse.add(fs.realpathSync(path.join(data, 'lanes', 'current'))); // what the server units point at
|
|
95
|
+
} catch { /* no server install */ }
|
|
96
|
+
const entries = [];
|
|
97
|
+
for (const name of fs.readdirSync(root)) {
|
|
98
|
+
const dir = path.join(root, name);
|
|
99
|
+
if (name.startsWith('.') || !stampOf(dir)) continue; // only trees this wrapper staged
|
|
100
|
+
entries.push({ path: dir, mtimeMs: fs.statSync(dir).mtimeMs });
|
|
101
|
+
}
|
|
102
|
+
for (const dir of prunable(entries, inUse)) fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function usage(manifest) {
|
|
106
|
+
process.stdout.write(`@vibelord/lanes ${manifest.version} — installs Lanes from this package
|
|
107
|
+
|
|
108
|
+
npx @vibelord/lanes --site beyondwork install or update the client (macOS / Linux)
|
|
109
|
+
npx @vibelord/lanes --check verify SSH path, key and server; changes nothing
|
|
110
|
+
npx @vibelord/lanes --server install or update the Linux workspace server
|
|
111
|
+
npx @vibelord/lanes --uninstall remove what the client installer added
|
|
112
|
+
|
|
113
|
+
Wrapper options
|
|
114
|
+
--stage-only copy the bundled tree to ${path.join(pkgRoot(), manifest.version)} and print its path
|
|
115
|
+
--version print the package version
|
|
116
|
+
--wrapper-help this text
|
|
117
|
+
|
|
118
|
+
Bundled site profiles: ${manifest.sites.join(', ') || 'none'} (or --site /path/to/site.conf)
|
|
119
|
+
Every other option goes to install.sh; its own help follows.
|
|
120
|
+
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main() {
|
|
125
|
+
const platform = platformError(process.platform);
|
|
126
|
+
if (platform) fail(platform);
|
|
127
|
+
const manifest = readManifest();
|
|
128
|
+
const { own, passthrough } = splitArgs(process.argv.slice(2));
|
|
129
|
+
if (own.has('--version')) {
|
|
130
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
let staged;
|
|
134
|
+
try {
|
|
135
|
+
staged = stage(manifest);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
fail(`could not stage the Lanes tree under ${pkgRoot()}: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
if (own.has('--stage-only')) {
|
|
140
|
+
process.stdout.write(`${staged}\n`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
const help = own.has('--wrapper-help') || passthrough.includes('--help') || passthrough.includes('-h');
|
|
144
|
+
if (help) usage(manifest);
|
|
145
|
+
if (own.has('--wrapper-help')) return 0;
|
|
146
|
+
let args;
|
|
147
|
+
try {
|
|
148
|
+
args = installArgs(passthrough, staged, manifest.sites);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
fail(error.message);
|
|
151
|
+
}
|
|
152
|
+
// Run through the staged path (not the npx cache), so install.sh resolves its checkout to the stable tree.
|
|
153
|
+
const result = spawnSync('sh', [path.join(staged, 'install.sh'), ...args], { stdio: 'inherit', cwd: staged });
|
|
154
|
+
if (result.error) fail(`could not run install.sh: ${result.error.message}`);
|
|
155
|
+
return result.status ?? 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.exit(main());
|
package/lib/plan.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Pure helpers for the `lanes-install` wrapper, kept apart from the filesystem so they are unit-testable.
|
|
2
|
+
//
|
|
3
|
+
// The wrapper never re-implements the installer: it stages the bundled `lanes/` tree at a stable path and runs
|
|
4
|
+
// that tree's own `install.sh`. These functions decide only the platform gate, the wrapper's own flags and how
|
|
5
|
+
// the passthrough arguments are rewritten.
|
|
6
|
+
|
|
7
|
+
export const SUPPORTED_PLATFORMS = ['darwin', 'linux'];
|
|
8
|
+
|
|
9
|
+
export function platformError(platform) {
|
|
10
|
+
return SUPPORTED_PLATFORMS.includes(platform)
|
|
11
|
+
? null
|
|
12
|
+
: `lanes supports macOS and Linux only (this is ${platform}); on Windows use WSL`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Wrapper-owned flags. Everything else is passed to install.sh unchanged.
|
|
16
|
+
const OWN_FLAGS = new Set(['--stage-only', '--version', '--wrapper-help']);
|
|
17
|
+
|
|
18
|
+
export function splitArgs(argv) {
|
|
19
|
+
const own = new Set();
|
|
20
|
+
const passthrough = [];
|
|
21
|
+
for (const arg of argv) {
|
|
22
|
+
if (OWN_FLAGS.has(arg)) own.add(arg);
|
|
23
|
+
else passthrough.push(arg);
|
|
24
|
+
}
|
|
25
|
+
return { own, passthrough };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Rewrite install.sh arguments for a staged tree:
|
|
29
|
+
// * `--site NAME` names a profile bundled under lanes/site/NAME/ (a value with a `/` or ending in `.conf` stays a
|
|
30
|
+
// path the user owns);
|
|
31
|
+
// * `--server` must be install.sh's FIRST argument (it dispatches on $1 only).
|
|
32
|
+
export function installArgs(passthrough, stagedDir, sites) {
|
|
33
|
+
const out = [];
|
|
34
|
+
let server = false;
|
|
35
|
+
for (let i = 0; i < passthrough.length; i += 1) {
|
|
36
|
+
const arg = passthrough[i];
|
|
37
|
+
if (arg === '--server') {
|
|
38
|
+
server = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === '--site' && i + 1 < passthrough.length) {
|
|
42
|
+
out.push(arg, resolveSite(passthrough[i + 1], stagedDir, sites));
|
|
43
|
+
i += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out.push(arg);
|
|
47
|
+
}
|
|
48
|
+
return server ? ['--server', ...out] : out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSite(value, stagedDir, sites) {
|
|
52
|
+
if (value.includes('/') || value.endsWith('.conf')) return value;
|
|
53
|
+
if (!sites.includes(value)) {
|
|
54
|
+
const known = sites.length ? sites.join(', ') : 'none';
|
|
55
|
+
throw new Error(`unknown site profile '${value}' (bundled: ${known}); or pass --site /path/to/site.conf`);
|
|
56
|
+
}
|
|
57
|
+
return `${stagedDir}/site/${value}/site.conf`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Which staged version directories to delete: keep the newest `keep` (by mtime) and anything still in use.
|
|
61
|
+
// Server user units name the resolved staged path, so a version a running service came from is kept.
|
|
62
|
+
export function prunable(entries, inUse, keep = 3) {
|
|
63
|
+
const sorted = [...entries].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
64
|
+
return sorted.slice(keep).filter((entry) => !inUse.has(entry.path)).map((entry) => entry.path);
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibelord/lanes",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Installer for Lanes: shared agent terminals over SSH with Zellij views, registry sync, uploads and a phone interface.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lanes-install": "bin/lanes-install.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"payload/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"os": [
|
|
18
|
+
"darwin",
|
|
19
|
+
"linux"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "node scripts/build-payload.mjs",
|
|
26
|
+
"prepack": "node scripts/build-payload.mjs",
|
|
27
|
+
"test": "node --test"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "restricted"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/bwllaming/bw-tools.git",
|
|
35
|
+
"directory": "npm/lanes"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Lanes
|
|
2
|
+
|
|
3
|
+
One setup for shared agent terminals: Zellij panes and workspace tabs, live lane status, registry sync,
|
|
4
|
+
Cursor / VS Code integration, uploads, desktop launchers, and a phone interface. The complete implementation
|
|
5
|
+
and its tests live in this folder.
|
|
6
|
+
|
|
7
|
+
Each device gets its own view of the same long-running terminals. Closing a view or losing a connection
|
|
8
|
+
leaves those terminals running in their workspace containers.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
Mac / Linux terminal ── SSH ───────┐
|
|
12
|
+
├─ Linux workspace host ── container tmux sessions
|
|
13
|
+
Safari / Home Screen ── HTTPS ────┘ Zellij views agent lanes
|
|
14
|
+
registry + status
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
**New Mac or Linux machine, from npm** (members of the `@vibelord` npm organization; Node.js 18+ for npm only):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm login # once per machine
|
|
23
|
+
npx @vibelord/lanes --site beyondwork # install or update; no clone needed
|
|
24
|
+
lanes-window
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The package runs the same installer with the same options and exit codes; see [`npm/lanes`](../npm/lanes/README.md).
|
|
28
|
+
|
|
29
|
+
**From a checkout:** clone the repository once on each machine that needs it:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
git clone https://github.com/bwllaming/bw-tools.git
|
|
33
|
+
cd bw-tools/lanes
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Joining an existing server:** get its site file and public host key from the administrator, then run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
./install.sh --site /path/to/site.conf
|
|
40
|
+
lanes-window
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Beyond Work's existing deployment has an explicit profile:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
./install.sh --site site/beyondwork/site.conf
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The client works on macOS and Linux. It needs OpenSSH and a route to the server (Tailscale for the supplied
|
|
50
|
+
profile). Ghostty is recommended for a separate window; `lanes-launch` works in an existing terminal.
|
|
51
|
+
The installer uses an existing SSH key or creates a dedicated one. An administrator authorizes its **public**
|
|
52
|
+
key on the server. Re-run `./install.sh --check` after authorization. Your selected site is remembered.
|
|
53
|
+
|
|
54
|
+
**Setting up a server:** on a Linux x86_64 Docker host, as the workspace account:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
./install.sh --server --check
|
|
58
|
+
./install.sh --server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then configure the workspace catalog and install the container helper. Follow the
|
|
62
|
+
[installation guide](docs/INSTALL.md) for prerequisites, a complete example, phone access, upgrades and removal.
|
|
63
|
+
The installer starts an empty catalog safely; it does not assume anyone's containers, projects or login name.
|
|
64
|
+
|
|
65
|
+
## Daily use
|
|
66
|
+
|
|
67
|
+
| Action | Command or control |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| Open desktop view | `lanes-window` or the Lanes app; `lanes-launch` inside any terminal |
|
|
70
|
+
| Independent second view | `lanes-window 2` |
|
|
71
|
+
| Two or three panes per workspace | `lanes-window w2` / `lanes-window w3` |
|
|
72
|
+
| Switch workspace | Ctrl–Alt–Left / Right |
|
|
73
|
+
| Next or previous lane | Ctrl–Alt–Down / Up, or tap the sidebar |
|
|
74
|
+
| Search lanes | Ctrl–Alt–/ |
|
|
75
|
+
| Select a lane for pane 2 or 3 | `2` / `3` in the sidebar of a wide view |
|
|
76
|
+
| Open one lane directly | `lanes-launch lane <lane-id>`; find IDs with `lanes-launch ls` |
|
|
77
|
+
| Create a persistent workspace shell | **New +** under **Unregistered**, or `+` in the sidebar |
|
|
78
|
+
| End an unregistered session and its jobs | **×** beside the session, or select it and press `x` |
|
|
79
|
+
| Upload | Drop onto the sidebar upload box, use `~/Lanes Uploads/<workspace>/`, or `lanes-upload <files>` |
|
|
80
|
+
| Phone setup from a client | `sh install/enable-phone.sh --host lanes` |
|
|
81
|
+
| Detach | Close the window; the agents keep running |
|
|
82
|
+
|
|
83
|
+
Unregistered sessions also appear when they use shared or previously unlisted tmux servers. Reopening a
|
|
84
|
+
session reuses it; closing a view leaves it running. Registered lanes have no **×** control. Older plain
|
|
85
|
+
Docker shells labelled **Legacy** are tracked and closable, but a lost Docker connection cannot be reopened;
|
|
86
|
+
new shells support reconnection. See the [runbook](docs/RUNBOOK.md) for the transition details.
|
|
87
|
+
|
|
88
|
+
On iPhone, open the printed private HTTPS URL in Safari and choose **Share → Add to Home Screen**.
|
|
89
|
+
The interface provides Workspaces, Lanes and Panes controls, safe areas, portrait/landscape layout and adjustable
|
|
90
|
+
terminal text. The keyboard opens when you tap **Type here** or another input; swipes and updates leave it closed.
|
|
91
|
+
|
|
92
|
+
On macOS, allow **Lanes Helper** to read Downloads/Desktop/Documents when prompted. Upload progress and original
|
|
93
|
+
errors are retained; an active large transfer is not cut off by a total-duration timeout. Files are read only
|
|
94
|
+
from allowed folders and land in the selected workspace's `uploads/` directory.
|
|
95
|
+
|
|
96
|
+
## Share with your team
|
|
97
|
+
|
|
98
|
+
Throttle's **Shared lanes** console provides an SSO-protected, read-only overview
|
|
99
|
+
of selected workspaces. Admins enable publishers by user or team; owners select
|
|
100
|
+
workspaces and recipients. See [sharing setup](docs/SHARING.md) for the required
|
|
101
|
+
platform/Host version, configuration, access rules and output limits.
|
|
102
|
+
|
|
103
|
+
## Screenshots
|
|
104
|
+
|
|
105
|
+
**Desktop:** workspace tabs, registered and unregistered lanes, and uploads alongside the active terminal.
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
**iPhone in Safari:** the lane picker and touch controls for workspaces, panes, and explicit text input.
|
|
110
|
+
|
|
111
|
+
<img src="docs/images/iphone.png" alt="Lanes on iPhone in Safari, showing the lane picker and Workspaces, Lanes, Panes, and Type here controls" width="360">
|
|
112
|
+
|
|
113
|
+
## How sync works
|
|
114
|
+
|
|
115
|
+
The workspace catalog says which repositories belong to each tab. `lanes sync` reads their
|
|
116
|
+
`.vscode/terminals.json` files every two minutes and publishes the host registry and container bindings.
|
|
117
|
+
It retains the previous lanes if a repository's registry is temporarily invalid.
|
|
118
|
+
|
|
119
|
+
`lanes cursor-sync --diff` previews editor integration. `lanes cursor-sync --write` backs up and normalizes
|
|
120
|
+
terminal commands to `lane open <id>`, retaining each original create recipe. Cursor and Lanes then attach
|
|
121
|
+
to the same terminal. Registry sync does not copy repositories between machines or automatically create agents.
|
|
122
|
+
See [workspace configuration](workspaces.example/README.md) and the [architecture](docs/ARCHITECTURE.md).
|
|
123
|
+
|
|
124
|
+
## What is included
|
|
125
|
+
|
|
126
|
+
| Folder | Responsibility |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `install.sh`, `install/` | Client/server installation, service registration and web setup |
|
|
129
|
+
| `client/` | Reconnecting SSH launcher, windows, upload helper, macOS apps and Omarchy integration |
|
|
130
|
+
| `bin/`, `libexec/` | Registry/editor sync, layouts, sidebar, status, uploads, health and housekeeping |
|
|
131
|
+
| `holders/`, `container/` | Guarded attachment and the in-container `lane` contract |
|
|
132
|
+
| `zellij/`, `systemd/` | Keybindings, device views and user-service templates |
|
|
133
|
+
| `web/` | Responsive Safari frontend over the pinned native Zellij client |
|
|
134
|
+
| `site/`, `workspaces.example/` | Deployment profiles and editable configuration examples |
|
|
135
|
+
| `sudo/` | Optional administrator setup and host protection, applied explicitly |
|
|
136
|
+
| `tests/` | Unit tests plus isolated host/container integration suites |
|
|
137
|
+
| `docs/` | [Install](docs/INSTALL.md), [architecture](docs/ARCHITECTURE.md), [operations](docs/RUNBOOK.md), [host guard](docs/HOST-GUARD.md) |
|
|
138
|
+
|
|
139
|
+
## Update and verify
|
|
140
|
+
|
|
141
|
+
After `git pull`, clients re-run `./install.sh`; servers re-run `./install.sh --server` from their installed
|
|
142
|
+
checkout. The server installer resolves the checkout's actual path, including `bw-tools/lanes`; no `~/lanes`
|
|
143
|
+
clone is required. It preserves configured workspaces and existing agent sessions.
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
./install.sh --check # client connectivity
|
|
147
|
+
./install.sh --server --check # server prerequisites
|
|
148
|
+
python3 -m unittest discover -s tests/unit # no Docker or live server needed
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For integration tests and browser verification, see [tests/README.md](tests/README.md).
|
|
152
|
+
For recovery, service updates and troubleshooting, see the [runbook](docs/RUNBOOK.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|