@tsconfigdotjson/vibe-os 0.0.0-stage → 0.1.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 +928 -2
- package/THIRD_PARTY_NOTICES.md +2467 -0
- package/bin/vibe-os.mjs +13 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-B77Qk8B5.js +35 -0
- package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
- package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css +1 -0
- package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-app.svg +19 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/icon-maskable.svg +13 -0
- package/dist/web/icon.svg +15 -0
- package/dist/web/index.html +36 -0
- package/dist/web/ssh.wasm +0 -0
- package/dist/web/ssh.wasm.br +0 -0
- package/dist/web/ssh.wasm.gz +0 -0
- package/dist/web/sw.js +29 -0
- package/dist/web/wasm_exec.js +575 -0
- package/dist/web/wasm_exec.js.br +0 -0
- package/dist/web/wasm_exec.js.gz +0 -0
- package/package.json +82 -4
- package/scripts/compile.ts +62 -0
- package/scripts/compress.mjs +54 -0
- package/scripts/fetch-wasm.mjs +242 -0
- package/scripts/gen-assets.ts +76 -0
- package/scripts/lib/walk.ts +18 -0
- package/scripts/notices.ts +189 -0
- package/server/acme.ts +182 -0
- package/server/api.ts +515 -0
- package/server/assets.generated.ts +53 -0
- package/server/attach.ts +524 -0
- package/server/auth.ts +194 -0
- package/server/bridge.ts +145 -0
- package/server/browser.ts +519 -0
- package/server/cli.ts +843 -0
- package/server/config.ts +362 -0
- package/server/db.ts +262 -0
- package/server/doctor.ts +966 -0
- package/server/fsx.ts +88 -0
- package/server/harness.ts +654 -0
- package/server/http.ts +53 -0
- package/server/index.ts +469 -0
- package/server/log.ts +40 -0
- package/server/main.ts +7 -0
- package/server/mcp.ts +291 -0
- package/server/preflight.ts +87 -0
- package/server/profiles.ts +238 -0
- package/server/projects.ts +721 -0
- package/server/reaper.ts +303 -0
- package/server/runtime.ts +64 -0
- package/server/session.ts +330 -0
- package/server/ssh-ca.ts +368 -0
- package/server/static.ts +147 -0
- package/server/wallpapers.ts +248 -0
- package/server/windows.ts +237 -0
- package/shared/args.ts +91 -0
- package/shared/grid.ts +32 -0
- package/shared/wire.ts +226 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// What a window actually runs when something logs in as it.
|
|
2
|
+
//
|
|
3
|
+
// This is the single source of truth for the tmux invocation behind a window,
|
|
4
|
+
// and it has two callers that must never disagree: the certificate signer,
|
|
5
|
+
// which bakes it into a `force-command` for the browser, and `vibe-os attach`,
|
|
6
|
+
// which runs it directly for someone arriving over SSH. Both resolve the same
|
|
7
|
+
// window id to the same command, so a terminal and a browser tab land in the
|
|
8
|
+
// same session, in the same worktree, with the same options set.
|
|
9
|
+
//
|
|
10
|
+
// It lives in its own module rather than in api.ts because attach.ts needs it
|
|
11
|
+
// too, and api.ts needs attach.ts — importing in a circle to share three pure
|
|
12
|
+
// functions is not a trade worth making.
|
|
13
|
+
|
|
14
|
+
import type { Config } from "./config.ts";
|
|
15
|
+
import type { Profile } from "./profiles.ts";
|
|
16
|
+
import { SOCK_VAR } from "./reaper.ts";
|
|
17
|
+
|
|
18
|
+
/** Single-quote for the login shell that runs a certificate's force-command. */
|
|
19
|
+
export function shellQuote(value: string): string {
|
|
20
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cursor's name for a directory, as it spells it under `~/.cursor/projects`.
|
|
25
|
+
*
|
|
26
|
+
* Every run of anything that is not a letter or a digit becomes one dash, and
|
|
27
|
+
* the leading one that a path always starts with is dropped. Case survives.
|
|
28
|
+
* Read off a real install rather than from documentation, which does not
|
|
29
|
+
* mention the directory at all: `/home/ubuntu` is `home-ubuntu`, and
|
|
30
|
+
* `/tmp/Cursor.Test_1 space/sub` is `tmp-Cursor-Test-1-space-sub`.
|
|
31
|
+
*/
|
|
32
|
+
export function cursorProjectSlug(dir: string): string {
|
|
33
|
+
return dir.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shell that points this worktree's Cursor project at the box's MCP credentials.
|
|
38
|
+
*
|
|
39
|
+
* Cursor keeps MCP OAuth tokens per working directory, in
|
|
40
|
+
* `~/.cursor/projects/<slug of the directory>/mcp-auth.json`. The server list is
|
|
41
|
+
* global — `~/.cursor/mcp.json` — so a new window shows Linear as configured and
|
|
42
|
+
* then says `requires_authentication`, because the tokens are somewhere else.
|
|
43
|
+
* Every workspace here is a freshly cut worktree at a path Cursor has never
|
|
44
|
+
* seen, which makes "log in once on this box" mean "log in once per window"
|
|
45
|
+
* unless something intervenes.
|
|
46
|
+
*
|
|
47
|
+
* A symlink is what intervenes, and it is a symlink rather than a copy because
|
|
48
|
+
* Cursor writes through it: an access token lasts a day, so the refresh that
|
|
49
|
+
* happens in whichever window is open has to be the refresh every other window
|
|
50
|
+
* sees. Copies would go stale a day apart and each one would race the others
|
|
51
|
+
* for a refresh token the provider rotates.
|
|
52
|
+
*
|
|
53
|
+
* The link is only ever created where there is nothing at all. An existing file
|
|
54
|
+
* is somebody's real per-directory login and is left alone; an existing link is
|
|
55
|
+
* this, already done. Neither is worth overwriting to save a stat.
|
|
56
|
+
*
|
|
57
|
+
* The shared store is deliberately allowed not to exist yet. Writing through a
|
|
58
|
+
* dangling symlink creates the target, so `cursor-agent mcp login linear` in any
|
|
59
|
+
* window is the one login the box needs, wherever it happens — which is why the
|
|
60
|
+
* store's directory is created here even though the file is not.
|
|
61
|
+
*
|
|
62
|
+
* Failure is silent on purpose, and inert rather than fatal. If Cursor ever
|
|
63
|
+
* changes how it names these directories, or `CURSOR_DATA_DIR` moves them, the
|
|
64
|
+
* link lands somewhere Cursor does not read and windows behave exactly as they
|
|
65
|
+
* did before: a login prompt, not a broken window.
|
|
66
|
+
*/
|
|
67
|
+
function cursorMcpLink(cwd: string, stateDir: string): string {
|
|
68
|
+
const store = `${stateDir}/cursor/mcp-auth.json`;
|
|
69
|
+
const storeDir = shellQuote(`${stateDir}/cursor`);
|
|
70
|
+
// The slug is letters, digits and dashes by construction, so it needs no
|
|
71
|
+
// quoting of its own; `$HOME` is left to the shell because the session runs as
|
|
72
|
+
// the user whose credentials these are.
|
|
73
|
+
const dir = `"$HOME/.cursor/projects/${cursorProjectSlug(cwd)}"`;
|
|
74
|
+
const link = `"$HOME/.cursor/projects/${cursorProjectSlug(cwd)}/mcp-auth.json"`;
|
|
75
|
+
return [
|
|
76
|
+
`mkdir -p ${dir} ${storeDir} 2>/dev/null`,
|
|
77
|
+
// Refresh tokens, so no wider than the state dir this sits in.
|
|
78
|
+
`chmod 700 ${storeDir} 2>/dev/null`,
|
|
79
|
+
`{ [ -e ${link} ] || [ -L ${link} ] || ln -s ${shellQuote(store)} ${link} 2>/dev/null; }`,
|
|
80
|
+
].join("; ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The command a profile window starts its pane with.
|
|
85
|
+
*
|
|
86
|
+
* **Quitting the harness ends the window.** tmux ends a session when its last
|
|
87
|
+
* pane exits, and nothing is appended here to keep the pane alive, so leaving
|
|
88
|
+
* Claude closes the window everywhere at once — the browser tile and any
|
|
89
|
+
* terminal attached to the same session. That is deliberate: a window opened as
|
|
90
|
+
* a role exists to run that role, and being dropped into a shell in the
|
|
91
|
+
* worktree instead means every finished conversation leaves a window behind to
|
|
92
|
+
* be tidied up by hand.
|
|
93
|
+
*
|
|
94
|
+
* It cuts both ways and the other edge is worth knowing. A harness that fails
|
|
95
|
+
* to launch at all — the wrong command, or a PATH that does not reach it — also
|
|
96
|
+
* exits immediately, so the window closes before anyone can read the error.
|
|
97
|
+
* `vibe-os doctor` and the server log are where that shows up.
|
|
98
|
+
*
|
|
99
|
+
* Quoting happens at two levels and both are handled: every token is quoted
|
|
100
|
+
* individually so a flag can contain spaces and cannot contain a second
|
|
101
|
+
* command, and the whole string is quoted again by the caller.
|
|
102
|
+
*/
|
|
103
|
+
export function harnessCommand(
|
|
104
|
+
profile: Profile,
|
|
105
|
+
/** The worktree it runs in and the state dir, for the Cursor MCP link. */
|
|
106
|
+
where: { cwd: string; stateDir: string },
|
|
107
|
+
): string | undefined {
|
|
108
|
+
const executable =
|
|
109
|
+
profile.harness === "claude"
|
|
110
|
+
? "claude"
|
|
111
|
+
: profile.harness === "hermes"
|
|
112
|
+
? "hermes"
|
|
113
|
+
: // The unambiguous of the two names its installer symlinks — `agent`
|
|
114
|
+
// is the other, and too generic to be the one a PATH lookup bets on.
|
|
115
|
+
profile.harness === "cursor"
|
|
116
|
+
? "cursor-agent"
|
|
117
|
+
: profile.harness === "custom"
|
|
118
|
+
? profile.command
|
|
119
|
+
: null;
|
|
120
|
+
if (!executable) return undefined;
|
|
121
|
+
/*
|
|
122
|
+
* `hermes` on its own opens a chat, but `--model` and `--provider` belong to
|
|
123
|
+
* the `chat` subcommand rather than to the top-level command, so a profile
|
|
124
|
+
* that picks a model has to name it. It is prepended here rather than stored
|
|
125
|
+
* in the profile's argv because it is not a flag anyone should be able to
|
|
126
|
+
* delete in the editor: without it every model selection is silently ignored.
|
|
127
|
+
*/
|
|
128
|
+
const leading = profile.harness === "hermes" ? ["chat"] : [];
|
|
129
|
+
const argv = [executable, ...leading, ...profile.args]
|
|
130
|
+
.map(shellQuote)
|
|
131
|
+
.join(" ");
|
|
132
|
+
|
|
133
|
+
// Only Cursor keeps credentials per directory, so only Cursor pays for this.
|
|
134
|
+
const setup =
|
|
135
|
+
profile.harness === "cursor"
|
|
136
|
+
? `${cursorMcpLink(where.cwd, where.stateDir)}; `
|
|
137
|
+
: "";
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* The PATH a forced command gets is not the PATH you get when you log in.
|
|
141
|
+
*
|
|
142
|
+
* sshd runs a certificate's force-command as `$SHELL -c`, which for bash is
|
|
143
|
+
* neither a login shell nor an interactive one — so `.profile` never runs,
|
|
144
|
+
* and `~/.local/bin` is missing. That is exactly where Claude's native
|
|
145
|
+
* installer puts its binary, and where most `curl | sh` installers put
|
|
146
|
+
* theirs, so the harness fails with "command not found" while typing the
|
|
147
|
+
* same name by hand in the same window works perfectly.
|
|
148
|
+
*
|
|
149
|
+
* Restored here rather than by asking for a login shell: `$SHELL -lc` would
|
|
150
|
+
* mean another layer of quoting around an already twice-quoted string, and
|
|
151
|
+
* a `.profile` is entitled to do surprising things like change directory,
|
|
152
|
+
* which would undo the `-c` this window was started with.
|
|
153
|
+
*/
|
|
154
|
+
return `export PATH="$HOME/.local/bin:$PATH"; ${setup}exec ${argv}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Where a window's dtach socket lives. */
|
|
158
|
+
export function socketPath(config: Config, session: string): string {
|
|
159
|
+
return `${config.stateDir}/sessions/${session}.sock`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds the command a window runs on login.
|
|
164
|
+
*
|
|
165
|
+
* The working directory is the point of the projects feature: the session
|
|
166
|
+
* starts in the worktree of the workspace the window belongs to. That path is
|
|
167
|
+
* looked up server-side from the window id — the browser never sends a
|
|
168
|
+
* directory, so there is nothing to smuggle a path through. The same is true of
|
|
169
|
+
* the profile: the window row says which one it was opened as, so the browser
|
|
170
|
+
* never sends a command either.
|
|
171
|
+
*
|
|
172
|
+
* ── Why dtach and not tmux ───────────────────────────────────────────────────
|
|
173
|
+
* A window needs exactly two things from a session manager: survive a reload,
|
|
174
|
+
* and let a real terminal take over. tmux does both, and brings a full terminal
|
|
175
|
+
* emulator with it — a second screen model that it keeps in sync with the
|
|
176
|
+
* client by sending only the cells it believes have changed. Measured on one
|
|
177
|
+
* window: 48,998 cursor hops and 2,124 erase-character sequences in a single
|
|
178
|
+
* capture, in place of simply forwarding the program's output.
|
|
179
|
+
*
|
|
180
|
+
* That optimisation is the bug. If the client's grid and tmux's model ever
|
|
181
|
+
* disagree about one cell, tmux will not send that cell again — it is certain
|
|
182
|
+
* the client already has it right — so a momentary disagreement becomes
|
|
183
|
+
* permanent, and text arrives with fragments of older frames wedged into it.
|
|
184
|
+
* The pane itself stays perfect, which is why `capture-pane` always read
|
|
185
|
+
* correctly while the browser did not, and why resizing a window cleaned it up:
|
|
186
|
+
* a resize is the one thing that makes tmux throw the model away and repaint.
|
|
187
|
+
*
|
|
188
|
+
* dtach keeps no model. It holds the pty and moves bytes, so the program talks
|
|
189
|
+
* to the browser's terminal directly and there is nothing to fall out of sync.
|
|
190
|
+
* The scrollback that tmux would have kept was never worth anything here
|
|
191
|
+
* anyway: a full-screen program like Claude runs on the alternate screen, where
|
|
192
|
+
* nothing scrolls into history — measured `history_size=0` on every window.
|
|
193
|
+
*
|
|
194
|
+
* ── The shape of the command ─────────────────────────────────────────────────
|
|
195
|
+
* `-n` creates the session detached and `-a` attaches to it, deliberately kept
|
|
196
|
+
* as two invocations rather than one `-A`. It costs a liveness probe and buys
|
|
197
|
+
* the ability to tell a client from the process that owns the pty: they differ
|
|
198
|
+
* in argv, so "detach whoever is attached" is something the server can do
|
|
199
|
+
* without risking the session itself.
|
|
200
|
+
*
|
|
201
|
+
* `-E` disables the detach character, because every key dtach reserves is a key
|
|
202
|
+
* the program underneath cannot have — the same argument that ruled out
|
|
203
|
+
* tmux's prefix. `-z` lets the suspend key through for the same reason. `-r
|
|
204
|
+
* winch` asks the program to repaint on attach, which is how a reattached
|
|
205
|
+
* window fills itself in without a screen model to replay.
|
|
206
|
+
*/
|
|
207
|
+
export function windowCommand(
|
|
208
|
+
session: string,
|
|
209
|
+
cwd: string,
|
|
210
|
+
config: Config,
|
|
211
|
+
profile?: Profile | null,
|
|
212
|
+
/** `forDisplay` drops the locking wrapper, leaving the readable essentials. */
|
|
213
|
+
opts?: { forDisplay?: boolean },
|
|
214
|
+
): string | undefined {
|
|
215
|
+
if (!config.sessions) return undefined;
|
|
216
|
+
|
|
217
|
+
const sock = socketPath(config, session);
|
|
218
|
+
const harness = profile
|
|
219
|
+
? harnessCommand(profile, { cwd, stateDir: config.stateDir })
|
|
220
|
+
: undefined;
|
|
221
|
+
// Falls back to the login shell, so a window with no profile is still a
|
|
222
|
+
// session that survives a reload rather than a bare ssh command.
|
|
223
|
+
/*
|
|
224
|
+
* Say what this terminal can actually do.
|
|
225
|
+
*
|
|
226
|
+
* sshterm asks sshd for a pty as `xterm`, and terminfo's `xterm` promises
|
|
227
|
+
* eight colours. Programs believe it: Claude picks a duller palette, and
|
|
228
|
+
* anything consulting `tput colors` gets 8. The browser's terminal is not that
|
|
229
|
+
* — xterm.js has done 256 colours and 24-bit for years — so the honest value
|
|
230
|
+
* is the wider one, and `COLORTERM` is how the true-colour half of that is
|
|
231
|
+
* conventionally advertised.
|
|
232
|
+
*
|
|
233
|
+
* Set on the session rather than the attachment because a program reads this
|
|
234
|
+
* once, at startup. Reattaching later inherits nothing, which is the same
|
|
235
|
+
* reason the working directory belongs here too.
|
|
236
|
+
*/
|
|
237
|
+
const env = "export TERM=xterm-256color COLORTERM=truecolor;";
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* Give the pty a baud rate Linux will accept back.
|
|
241
|
+
*
|
|
242
|
+
* SSH carries terminal modes in its pty-req, `TTY_OP_ISPEED` and
|
|
243
|
+
* `TTY_OP_OSPEED` among them, and sshd writes whatever it is sent straight
|
|
244
|
+
* into the pty. The browser's client is Go, and the Go SSH example every
|
|
245
|
+
* project copies from hardcodes 14400 — a rate Linux has no encoding for, and
|
|
246
|
+
* one `stty 14400` will not even set by hand. sshd sets it anyway, because it
|
|
247
|
+
* writes the raw termios fields rather than going through cfsetspeed.
|
|
248
|
+
*
|
|
249
|
+
* A pty then exists whose speed cannot be written back. Reading it works, so
|
|
250
|
+
* nothing looks wrong until a program does the ordinary raw-mode dance —
|
|
251
|
+
* tcgetattr, flip some flags, tcsetattr — at which point the unchanged speed
|
|
252
|
+
* field comes back as EINVAL. Hermes dies exactly there, inside
|
|
253
|
+
* prompt_toolkit, before it paints anything. Claude survives the same pty
|
|
254
|
+
* because Node's setRawMode swallows the error, which is why this went
|
|
255
|
+
* unnoticed until a second harness arrived.
|
|
256
|
+
*
|
|
257
|
+
* Set on the session rather than the attachment: the harness reads it once at
|
|
258
|
+
* startup, and this is the pty the harness actually runs on. dtach copies
|
|
259
|
+
* termios from the sshd pty when it creates its own, so the bad value is
|
|
260
|
+
* inherited here and has to be corrected here.
|
|
261
|
+
*
|
|
262
|
+
* 38400 is the conventional pty speed and what every other path already
|
|
263
|
+
* reports. Failure is ignored on purpose — a box where `stty` is missing
|
|
264
|
+
* should still open a window.
|
|
265
|
+
*/
|
|
266
|
+
const baud = "stty 38400 2>/dev/null;";
|
|
267
|
+
const inner = `cd ${shellQuote(cwd)} && ${baud} ${env} ${harness ?? 'exec "$SHELL"'}`;
|
|
268
|
+
|
|
269
|
+
// `dtach -p` writes to a live socket and fails on a dead one, which makes it
|
|
270
|
+
// a liveness probe. A socket left behind by a crashed session would otherwise
|
|
271
|
+
// make every later attach fail with no way back except deleting it by hand.
|
|
272
|
+
const probe = `dtach -p ${shellQuote(sock)} < /dev/null > /dev/null 2>&1`;
|
|
273
|
+
/*
|
|
274
|
+
* `VIBE_OS_SOCK` is the session's name tag, and inheritance is the feature.
|
|
275
|
+
*
|
|
276
|
+
* Set on the create and nowhere else, it rides the environment into dtach,
|
|
277
|
+
* into the shell on the pty, into the harness, and into everything the
|
|
278
|
+
* harness ever starts — across fork, exec, daemonisation, even a second
|
|
279
|
+
* setsid. A process group cannot make that promise: dtach creates the pty
|
|
280
|
+
* with forkpty, which makes its child a session leader, so no pgid of ours
|
|
281
|
+
* ever covers the grandchildren. The environment does.
|
|
282
|
+
*
|
|
283
|
+
* That is what lets killSession end a session *tree* instead of one pid, and
|
|
284
|
+
* what lets the reaper name the owner of a process found running in a
|
|
285
|
+
* deleted worktree a week later. reaper.ts is the only reader.
|
|
286
|
+
*/
|
|
287
|
+
const create = `${SOCK_VAR}=${shellQuote(sock)} dtach -n ${shellQuote(sock)} -E -z /bin/sh -c ${shellQuote(inner)}`;
|
|
288
|
+
const attach = `exec dtach -a ${shellQuote(sock)} -E -z -r winch`;
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* Probe-then-create has to be one critical section.
|
|
292
|
+
*
|
|
293
|
+
* This whole string is the certificate's force-command, so it runs once per
|
|
294
|
+
* login — and two logins for the same window are ordinary: two tabs, a reload
|
|
295
|
+
* racing a reconnect, two machines. Unserialised, both probe a missing socket
|
|
296
|
+
* and both take the create branch, at which point the second one's `rm -f`
|
|
297
|
+
* unlinks the socket the first is already living on. That session survives
|
|
298
|
+
* with no name: unreachable, still holding a pty, invisible to `clientsOn`
|
|
299
|
+
* and `killSession`, which can only ever find the socket that replaced it.
|
|
300
|
+
*
|
|
301
|
+
* `-o` is not optional, and leaving it off is a trap worth naming. flock
|
|
302
|
+
* holds the lock on an open file descriptor, and a descriptor is inherited
|
|
303
|
+
* across fork and exec — so `dtach -n`, which daemonises and outlives the
|
|
304
|
+
* command, kept that descriptor and held the lock for the entire life of the
|
|
305
|
+
* session. Every later login then blocked on flock forever: the terminal got
|
|
306
|
+
* as far as verifying the host key and hung there, because sshd was still
|
|
307
|
+
* waiting for the forced command to produce anything. `-o` closes the
|
|
308
|
+
* descriptor in the child before the command runs, so the lock lasts exactly
|
|
309
|
+
* as long as the critical section and nothing spawned inside it inherits it.
|
|
310
|
+
*
|
|
311
|
+
* flock is in util-linux and present on every distro this runs on, but the
|
|
312
|
+
* fallback keeps a box without it working exactly as it did before rather
|
|
313
|
+
* than failing to open a terminal at all.
|
|
314
|
+
*/
|
|
315
|
+
const critical = `${probe} || { rm -f ${shellQuote(sock)}; ${create}; }`;
|
|
316
|
+
|
|
317
|
+
// The banner wants to show what a window actually runs, which is the create
|
|
318
|
+
// and the attach. Printing the locking wrapper as well — twice, once per
|
|
319
|
+
// branch, with three levels of nested quoting — buries that in a wall of
|
|
320
|
+
// backslashes and tells the reader nothing they can act on.
|
|
321
|
+
if (opts?.forDisplay) return [create, attach].join("; ");
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
`mkdir -p ${shellQuote(`${config.stateDir}/sessions`)}`,
|
|
325
|
+
`if command -v flock > /dev/null 2>&1; then ` +
|
|
326
|
+
`flock -o ${shellQuote(`${sock}.lock`)} -c ${shellQuote(critical)}; ` +
|
|
327
|
+
`else { ${critical}; }; fi`,
|
|
328
|
+
attach,
|
|
329
|
+
].join("; ");
|
|
330
|
+
}
|