linkshell-cli 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +11 -6
  2. package/dist/cli/src/commands/gateway.d.ts +2 -0
  3. package/dist/cli/src/commands/gateway.js +29 -0
  4. package/dist/cli/src/commands/gateway.js.map +1 -0
  5. package/dist/cli/src/commands/host.d.ts +4 -0
  6. package/dist/cli/src/commands/host.js +12 -2
  7. package/dist/cli/src/commands/host.js.map +1 -1
  8. package/dist/cli/src/commands/screen.d.ts +10 -0
  9. package/dist/cli/src/commands/screen.js +82 -74
  10. package/dist/cli/src/commands/screen.js.map +1 -1
  11. package/dist/cli/src/commands/setup.d.ts +11 -1
  12. package/dist/cli/src/commands/setup.js +102 -38
  13. package/dist/cli/src/commands/setup.js.map +1 -1
  14. package/dist/cli/src/index.js +28 -338
  15. package/dist/cli/src/index.js.map +1 -1
  16. package/dist/cli/src/utils/daemon.d.ts +0 -7
  17. package/dist/cli/src/utils/daemon.js +1 -18
  18. package/dist/cli/src/utils/daemon.js.map +1 -1
  19. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  20. package/dist/gateway/src/accounts.d.ts +6 -0
  21. package/dist/gateway/src/accounts.js +32 -0
  22. package/dist/gateway/src/accounts.js.map +1 -0
  23. package/dist/gateway/src/index.d.ts +4 -0
  24. package/dist/gateway/src/index.js +5 -0
  25. package/dist/gateway/src/index.js.map +1 -0
  26. package/dist/gateway/src/main.d.ts +1 -0
  27. package/dist/gateway/src/main.js +57 -0
  28. package/dist/gateway/src/main.js.map +1 -0
  29. package/dist/gateway/src/rate-limit.d.ts +22 -0
  30. package/dist/gateway/src/rate-limit.js +66 -0
  31. package/dist/gateway/src/rate-limit.js.map +1 -0
  32. package/dist/gateway/src/relay.d.ts +84 -0
  33. package/dist/gateway/src/relay.js +434 -0
  34. package/dist/gateway/src/relay.js.map +1 -0
  35. package/dist/gateway/src/serve.d.ts +34 -0
  36. package/dist/gateway/src/serve.js +102 -0
  37. package/dist/gateway/src/serve.js.map +1 -0
  38. package/dist/gateway/src/store.d.ts +35 -0
  39. package/dist/gateway/src/store.js +109 -0
  40. package/dist/gateway/src/store.js.map +1 -0
  41. package/dist/gateway/src/subscription.d.ts +18 -0
  42. package/dist/gateway/src/subscription.js +46 -0
  43. package/dist/gateway/src/subscription.js.map +1 -0
  44. package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
  45. package/dist/host/src/fs.d.ts +3 -3
  46. package/dist/host/src/input.d.ts +112 -27
  47. package/dist/host/src/input.js +214 -144
  48. package/dist/host/src/input.js.map +1 -1
  49. package/dist/host/src/rpc/server.js +1 -1
  50. package/dist/host/src/rpc/server.js.map +1 -1
  51. package/dist/host/src/screen-pacer.d.ts +25 -11
  52. package/dist/host/src/screen-pacer.js +28 -6
  53. package/dist/host/src/screen-pacer.js.map +1 -1
  54. package/dist/host/src/screen-viewer.d.ts +5 -0
  55. package/dist/host/src/screen-viewer.js +1231 -55
  56. package/dist/host/src/screen-viewer.js.map +1 -1
  57. package/dist/host/src/screen.d.ts +19 -10
  58. package/dist/host/src/screen.js +314 -108
  59. package/dist/host/src/screen.js.map +1 -1
  60. package/dist/host/src/terminals.d.ts +1 -1
  61. package/dist/host/src/terminals.js +1 -0
  62. package/dist/host/src/terminals.js.map +1 -1
  63. package/dist/wire/src/model.d.ts +6 -6
  64. package/dist/wire/src/relay.d.ts +8 -8
  65. package/dist/wire/src/rpc.d.ts +818 -818
  66. package/dist/wire/src/updates.d.ts +18 -18
  67. package/package.json +6 -15
  68. package/src/commands/gateway.ts +31 -0
  69. package/src/commands/host.ts +15 -2
  70. package/src/commands/screen.ts +78 -72
  71. package/src/commands/setup.ts +102 -60
  72. package/src/index.ts +29 -433
  73. package/src/utils/daemon.ts +2 -27
  74. package/dist/cli/src/config.d.ts +0 -14
  75. package/dist/cli/src/config.js +0 -23
  76. package/dist/cli/src/config.js.map +0 -1
  77. package/dist/cli/src/machine-id.d.ts +0 -6
  78. package/dist/cli/src/machine-id.js +0 -50
  79. package/dist/cli/src/machine-id.js.map +0 -1
  80. package/dist/cli/src/providers.d.ts +0 -13
  81. package/dist/cli/src/providers.js +0 -77
  82. package/dist/cli/src/providers.js.map +0 -1
  83. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
  84. package/dist/cli/src/runtime/acp/acp-client.js +0 -430
  85. package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
  86. package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
  87. package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
  88. package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
  89. package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
  90. package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
  91. package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
  92. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
  93. package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
  94. package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
  95. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
  96. package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
  97. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
  98. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
  99. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
  100. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
  101. package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
  102. package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
  103. package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
  104. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
  105. package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
  106. package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
  107. package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
  108. package/dist/cli/src/runtime/bridge-session.js +0 -1364
  109. package/dist/cli/src/runtime/bridge-session.js.map +0 -1
  110. package/dist/cli/src/runtime/hook-command.d.ts +0 -9
  111. package/dist/cli/src/runtime/hook-command.js +0 -80
  112. package/dist/cli/src/runtime/hook-command.js.map +0 -1
  113. package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
  114. package/dist/cli/src/runtime/screen-fallback.js +0 -202
  115. package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
  116. package/dist/cli/src/runtime/screen-share.d.ts +0 -34
  117. package/dist/cli/src/runtime/screen-share.js +0 -376
  118. package/dist/cli/src/runtime/screen-share.js.map +0 -1
  119. package/dist/cli/src/runtime/scrollback.d.ts +0 -11
  120. package/dist/cli/src/runtime/scrollback.js +0 -33
  121. package/dist/cli/src/runtime/scrollback.js.map +0 -1
  122. package/dist/cli/src/runtime/usage-report.d.ts +0 -34
  123. package/dist/cli/src/runtime/usage-report.js +0 -332
  124. package/dist/cli/src/runtime/usage-report.js.map +0 -1
  125. package/dist/cli/src/utils/keep-awake.d.ts +0 -6
  126. package/dist/cli/src/utils/keep-awake.js +0 -48
  127. package/dist/cli/src/utils/keep-awake.js.map +0 -1
  128. package/dist/cli/src/utils/lan-ip.d.ts +0 -9
  129. package/dist/cli/src/utils/lan-ip.js +0 -61
  130. package/dist/cli/src/utils/lan-ip.js.map +0 -1
  131. package/dist/host/src/input-helper.d.ts +0 -1
  132. package/dist/host/src/input-helper.js +0 -564
  133. package/dist/host/src/input-helper.js.map +0 -1
  134. package/dist/shared-protocol/src/index.d.ts +0 -17029
  135. package/dist/shared-protocol/src/index.js +0 -1064
  136. package/dist/shared-protocol/src/index.js.map +0 -1
  137. package/scripts/copy-web.mjs +0 -34
  138. package/src/config.ts +0 -36
  139. package/src/machine-id.ts +0 -61
  140. package/src/providers.ts +0 -97
  141. package/src/runtime/acp/acp-client.ts +0 -521
  142. package/src/runtime/acp/agent-process-discovery.ts +0 -308
  143. package/src/runtime/acp/agent-session.ts +0 -1186
  144. package/src/runtime/acp/agent-workspace.ts +0 -6590
  145. package/src/runtime/acp/claude-sdk-client.ts +0 -1221
  146. package/src/runtime/acp/claude-stream-json-client.ts +0 -633
  147. package/src/runtime/acp/json-rpc.ts +0 -184
  148. package/src/runtime/acp/provider-resolver.ts +0 -154
  149. package/src/runtime/bridge-session.ts +0 -1549
  150. package/src/runtime/hook-command.ts +0 -73
  151. package/src/runtime/screen-fallback.ts +0 -235
  152. package/src/runtime/screen-share.ts +0 -426
  153. package/src/runtime/scrollback.ts +0 -43
  154. package/src/runtime/usage-report.ts +0 -342
  155. package/src/utils/keep-awake.ts +0 -61
  156. package/src/utils/lan-ip.ts +0 -61
  157. package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
  158. package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
  159. package/web/assets/TerminalPanel-CTSklkct.js +0 -1
  160. package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
  161. package/web/assets/index-BsnDaeCt.css +0 -33
  162. package/web/assets/index-q85psYZ5.js +0 -36
  163. package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
  164. package/web/assets/vendor-react-M7xG3qiw.js +0 -49
  165. package/web/assets/vendor-xterm-D15THcI4.js +0 -9
  166. package/web/index.html +0 -37
  167. package/web/logos/claude.png +0 -0
  168. package/web/logos/codex.png +0 -0
  169. package/web/logos/copilot.png +0 -0
  170. package/web/logos/gemini.png +0 -0
  171. package/web/logos/linkshell-1024.png +0 -0
  172. package/web/logos/linkshell-splash.png +0 -0
  173. package/web/logos/linkshell.png +0 -0
package/src/index.ts CHANGED
@@ -1,68 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { BridgeSession } from "./runtime/bridge-session.js";
4
- import { resolveProviderConfig } from "./providers.js";
5
- import { loadConfig } from "./config.js";
6
3
  import { runDoctor } from "./commands/doctor.js";
7
4
  import { runSetup } from "./commands/setup.js";
8
5
  import { runUpgrade } from "./commands/upgrade.js";
9
6
  import { runLogin } from "./commands/login.js";
10
7
  import { runLogout } from "./commands/logout.js";
11
- import { getLanIp } from "./utils/lan-ip.js";
12
- import { shouldKeepAwake } from "./utils/keep-awake.js";
13
-
14
8
  import { createRequire } from "node:module";
15
- import { fileURLToPath } from "node:url";
16
- import { dirname, resolve } from "node:path";
17
- import { existsSync, statSync } from "node:fs";
18
9
 
19
10
  const require = createRequire(import.meta.url);
20
11
  const pkg = require("../../../package.json") as { version: string };
21
12
 
22
- // Locate the bundled web-dashboard SPA and point the (embedded) gateway's
23
- // static server at it via WEB_DIST — BEFORE any gateway module is imported,
24
- // because static-web.ts freezes WEB_DIST at module-load time. This is what lets
25
- // the in-app WebView render for LAN / self-hosted users whose CLI runs its own
26
- // embedded gateway (the cloud image bundles web separately). A user-set
27
- // WEB_DIST always wins. Probed across published-package, monorepo-compiled, and
28
- // dev (tsx) layouts; the first candidate containing index.html is used.
29
- if (!process.env.WEB_DIST) {
30
- const here = dirname(fileURLToPath(import.meta.url));
31
- const candidates = [
32
- resolve(here, "../../../web"), // published npm package root, and packages/cli/web (compiled)
33
- resolve(here, "../../../apps/web-dashboard/dist"), // dev: tsx src/index.ts in the monorepo
34
- ];
35
- for (const dir of candidates) {
36
- try {
37
- if (existsSync(resolve(dir, "index.html")) && statSync(dir).isDirectory()) {
38
- process.env.WEB_DIST = dir;
39
- break;
40
- }
41
- } catch {
42
- // ignore and try the next candidate
43
- }
44
- }
45
- }
46
-
47
-
48
- const config = loadConfig();
49
13
  const program = new Command();
50
14
 
51
15
  program
52
16
  .name("linkshell")
53
17
  .description(
54
- "Your coding agents and terminals, on your phone. Run `linkshell host --daemon`, then `linkshell pair` (or `linkshell login` for the official gateway).",
18
+ "Your coding agents and terminals, on your phone. Start with `linkshell setup`.",
55
19
  )
56
20
  .version(pkg.version);
57
21
 
58
- // ── host (v2 session daemon) ────────────────────────────────────────
22
+ // ── host ────────────────────────────────────────────────────────────
59
23
 
60
24
  const hostCmd = program
61
25
  .command("host")
62
- .description("Run the LinkShell host that owns agent sessions (v2)")
26
+ .description("Run the LinkShell host that owns agent sessions")
63
27
  .option("--daemon", "Run in background (detached)")
64
28
  .option("--dev-port <port>", "Also serve the host API on 127.0.0.1:<port> (local development clients only)")
65
- .option("--gateway <url>", "Reach this computer through a v2 gateway (saved; 'off' to disable, 'default' for the official one when logged in)")
29
+ .option("--gateway <url>", "Reach this computer through a gateway (saved; 'off' to disable, 'default' for the official one when logged in)")
66
30
  .option("--_foreground-host", undefined) // internal
67
31
  .action(async (options) => {
68
32
  const { runHostForeground, ensureHostRunning, readHostConfig, writeHostConfig, assertHostRuntime, silenceSqliteWarning, withRunningHost, refreshGateway, describeGateway } =
@@ -93,7 +57,9 @@ const hostCmd = program
93
57
  process.stderr.write(` Log: ${daemon.getLogFile("host")}\n`);
94
58
  process.stderr.write(` Status: linkshell host status\n`);
95
59
  process.stderr.write(` Stop: linkshell host stop\n`);
96
- // The screen needs setting up once, at the computer: said here, where someone is.
60
+ // The first start here: the rest of the setup happens now, while someone is at the computer.
61
+ const { offerSetup } = await import("./commands/setup.js");
62
+ if (await offerSetup(pkg.version)) process.exit(process.exitCode ?? 0);
97
63
  const { screenAccess, describeScreen, screenReady } = await import("./commands/screen.js");
98
64
  const screen = await screenAccess().catch(() => undefined);
99
65
  if (screen && (screen === "old" || !screenReady(screen))) process.stderr.write(` Screen: ${describeScreen(screen)}\n`);
@@ -202,322 +168,35 @@ for (const [agent, description] of [
202
168
  });
203
169
  }
204
170
 
205
- // ── start ───────────────────────────────────────────────────────────
206
-
207
- program
208
- .command("start")
209
- .description("Start a v1 bridge session (for the v1 app; with built-in or remote gateway)")
210
- .option(
211
- "--gateway <url>",
212
- "Gateway websocket URL (omit to start built-in gateway)",
213
- config.gateway ?? undefined,
214
- )
215
- .option(
216
- "--pairing-gateway <url-or-host>",
217
- "Public HTTP gateway used in QR/deep link output",
218
- config.pairingGateway,
219
- )
220
- .option("--port <port>", "Port for built-in gateway", "8787")
221
- .option("--session-id <id>", "Session identifier (auto-created if omitted)")
222
- .option(
223
- "--provider <provider>",
224
- "(deprecated — always custom; launch your CLI manually inside the shell)",
225
- "custom",
226
- )
227
- .option(
228
- "--command <command>",
229
- "Shell or CLI to spawn in the PTY (default: $SHELL)",
230
- config.command,
231
- )
232
- .option(
233
- "--client-name <name>",
234
- "Display name for this CLI",
235
- config.clientName ?? "local-cli",
236
- )
237
- .option(
238
- "--hostname <name>",
239
- "Override hostname sent to gateway",
240
- config.hostname,
241
- )
242
- .option(
243
- "--cols <cols>",
244
- "Initial terminal columns",
245
- String(config.cols ?? 120),
246
- )
247
- .option("--rows <rows>", "Initial terminal rows", String(config.rows ?? 36))
248
- .option("--screen", "Enable screen sharing capability")
249
- .option("--no-keep-awake", "Disable macOS keep-awake while bridge is running")
250
- .option("--agent-ui", "Enable ACP Agent Workspace channel", true)
251
- .option("--no-agent-ui", "Disable ACP Agent Workspace channel")
252
- .option(
253
- "--agent-provider <provider>",
254
- "Agent GUI provider: codex | claude | custom",
255
- )
256
- .option(
257
- "--agent-command <command>",
258
- "ACP agent command (required for claude/custom, optional for codex)",
259
- )
260
- .option("--daemon", "Run in background (detached)")
261
- .option("--verbose", "Enable verbose logging")
262
- .option("--_foreground-bridge", undefined) // internal
263
- .allowUnknownOption(true)
264
- .allowExcessArguments(true)
265
- .action(async (options, command) => {
266
- const daemon = await import("./utils/daemon.js");
267
- const keepAwake = shouldKeepAwake(options.keepAwake);
268
-
269
- // Daemon mode: spawn detached child and exit
270
- if (options.daemon && !options._foregroundBridge) {
271
- const existingPid = daemon.readPid("bridge");
272
- if (existingPid) {
273
- process.stderr.write(` Bridge already running (PID ${existingPid})\n`);
274
- process.stderr.write(` Run: linkshell stop\n\n`);
275
- return;
276
- }
277
-
278
- // Rebuild args for the child, replacing --daemon with --_foreground-bridge
279
- const childArgs = ["start", "--_foreground-bridge"];
280
- if (options.gateway) childArgs.push("--gateway", options.gateway);
281
- if (options.pairingGateway)
282
- childArgs.push("--pairing-gateway", options.pairingGateway);
283
- childArgs.push("--port", String(options.port));
284
- childArgs.push("--provider", options.provider);
285
- if (options.command) childArgs.push("--command", options.command);
286
- childArgs.push("--client-name", options.clientName);
287
- if (options.hostname) childArgs.push("--hostname", options.hostname);
288
- childArgs.push("--cols", String(options.cols));
289
- childArgs.push("--rows", String(options.rows));
290
- if (options.verbose) childArgs.push("--verbose");
291
- if (options.screen) childArgs.push("--screen");
292
- if (!keepAwake) childArgs.push("--no-keep-awake");
293
- if (options.agentUi) childArgs.push("--agent-ui");
294
- if (options.agentProvider)
295
- childArgs.push("--agent-provider", options.agentProvider);
296
- if (options.agentCommand)
297
- childArgs.push("--agent-command", options.agentCommand);
298
- if (options.sessionId) childArgs.push("--session-id", options.sessionId);
299
- // Pass through extra args
300
- const extra = command.args.filter((v: string) => v !== "--");
301
- if (extra.length) childArgs.push("--", ...extra);
302
-
303
- const pid = daemon.spawnDaemon("bridge", childArgs);
304
- daemon.saveMetadata("bridge", { keepAwake, startedAt: Date.now() });
305
- process.stderr.write(`\n LinkShell bridge started in background\n`);
306
- process.stderr.write(` PID: ${pid}\n`);
307
- process.stderr.write(` Provider: ${options.provider}\n`);
308
- process.stderr.write(
309
- ` Keep awake: ${keepAwake ? "enabled (use --no-keep-awake to disable)" : "disabled"}\n`,
310
- );
311
- process.stderr.write(` Log: ${daemon.getLogFile("bridge")}\n\n`);
312
- process.stderr.write(` Stop: linkshell stop\n`);
313
- process.stderr.write(` Status: linkshell status\n`);
314
- process.stderr.write(
315
- ` Logs: tail -f ${daemon.getLogFile("bridge")}\n\n`,
316
- );
317
- return;
318
- }
319
-
320
- // Foreground mode
321
- const passthroughArgs = command.args.filter(
322
- (value: string) => value !== "--",
323
- );
324
- const providerConfig = resolveProviderConfig({
325
- provider: options.provider,
326
- command: options.command,
327
- args: passthroughArgs,
328
- });
329
-
330
- let gatewayUrl = options.gateway as string | undefined;
331
- let gatewayHttpUrl: string;
332
- let pairingGateway = options.pairingGateway as string | undefined;
333
- let embeddedGatewayHandle: { close: () => Promise<void> } | undefined;
334
-
335
- if (!gatewayUrl) {
336
- const { startEmbeddedGateway } =
337
- await import("@linkshell/gateway/embedded");
338
- const port = Number(options.port);
339
- const gw = await startEmbeddedGateway({
340
- port,
341
- logLevel: options.verbose ? "debug" : "warn",
342
- silent: false,
343
- });
344
- embeddedGatewayHandle = gw;
345
- gatewayUrl = gw.wsUrl;
346
- gatewayHttpUrl = gw.httpUrl;
347
-
348
- const lanIp = getLanIp();
349
- if (!pairingGateway && lanIp !== "127.0.0.1") {
350
- pairingGateway = `http://${lanIp}:${gw.port}`;
351
- }
352
-
353
- process.stderr.write(`\n Built-in gateway started on port ${gw.port}\n`);
354
- if (pairingGateway) {
355
- process.stderr.write(` LAN address: ${pairingGateway}\n`);
356
- }
357
- process.stderr.write("\n");
358
- } else {
359
- gatewayHttpUrl = gatewayUrl
360
- .replace(/\/ws\/?$/, "")
361
- .replace(/^wss:/, "https:")
362
- .replace(/^ws:/, "http:");
363
- }
364
-
365
- // Save PID for status/stop
366
- daemon.savePid("bridge", process.pid);
367
- daemon.saveMetadata("bridge", { keepAwake, startedAt: Date.now() });
368
-
369
- // Load auth token if logged in
370
- let authToken: string | undefined;
371
- try {
372
- const { getValidToken } = await import("./auth.js");
373
- authToken = (await getValidToken()) ?? undefined;
374
- } catch {}
375
-
376
- const session = new BridgeSession({
377
- gatewayUrl,
378
- gatewayHttpUrl,
379
- pairingGateway,
380
- sessionId: options.sessionId,
381
- cols: Number(options.cols),
382
- rows: Number(options.rows),
383
- clientName: options.clientName,
384
- hostname: options.hostname,
385
- verbose: Boolean(options.verbose),
386
- screen: Boolean(options.screen),
387
- providerConfig,
388
- authToken,
389
- keepAwake,
390
- agentUi: Boolean(options.agentUi),
391
- agentProvider: options.agentProvider,
392
- agentCommand: options.agentCommand,
393
- });
394
-
395
- const cleanup = async () => {
396
- session.stop(0);
397
- daemon.removePid("bridge");
398
- if (embeddedGatewayHandle) await embeddedGatewayHandle.close();
399
- };
400
-
401
- // When all PTYs exit naturally (not via a signal), the session closes its
402
- // own socket but the embedded gateway loop and stale PID file would linger.
403
- // Run the same process-level cleanup and then exit.
404
- session.setOnAllTerminalsExited(async () => {
405
- daemon.removePid("bridge");
406
- if (embeddedGatewayHandle) await embeddedGatewayHandle.close();
407
- process.exit(process.exitCode ?? 0);
408
- });
409
-
410
- process.on("SIGINT", () => {
411
- cleanup().then(() => process.exit(0));
412
- });
413
- process.on("SIGTERM", () => {
414
- cleanup().then(() => process.exit(0));
415
- });
416
-
417
- // Global crash handlers: never die silently. In daemon mode keep the bridge
418
- // alive (it has its own gateway reconnect logic); stderr is redirected to the
419
- // daemon log file so the trace is recoverable.
420
- const isDaemonChild = Boolean(options._foregroundBridge);
421
- process.on("uncaughtException", (err) => {
422
- process.stderr.write(
423
- `[bridge] uncaught exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`,
424
- );
425
- if (!isDaemonChild) {
426
- cleanup().then(() => process.exit(1));
427
- }
428
- });
429
- process.on("unhandledRejection", (reason) => {
430
- process.stderr.write(
431
- `[bridge] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}\n`,
432
- );
433
- });
434
-
435
- await session.start();
436
- });
437
-
438
171
  // ── gateway ─────────────────────────────────────────────────────────
439
172
 
440
173
  const gatewayCmd = program
441
174
  .command("gateway")
442
- .description("Manage the standalone gateway server")
175
+ .description("Run a gateway of your own: it relays encrypted frames between your computers and phones, and brokers pairing")
443
176
  .option("--port <port>", "Listen port", "8787")
444
- .option(
445
- "--log-level <level>",
446
- "Log level: debug | info | warn | error",
447
- "info",
448
- )
449
177
  .option("--daemon", "Run in background (detached)")
450
178
  .option("--_foreground-gw", undefined) // internal
451
179
  .action(async (options) => {
452
- const daemon = await import("./utils/daemon.js");
453
-
454
180
  if (options.daemon && !options._foregroundGw) {
181
+ const daemon = await import("./utils/daemon.js");
455
182
  const existingPid = daemon.readPid("gateway");
456
183
  if (existingPid) {
457
- process.stderr.write(
458
- ` Gateway already running (PID ${existingPid})\n`,
459
- );
184
+ process.stderr.write(` Gateway already running (PID ${existingPid})\n`);
460
185
  process.stderr.write(` Run: linkshell gateway stop\n\n`);
461
186
  return;
462
187
  }
463
- const pid = daemon.spawnDaemon("gateway", [
464
- "gateway",
465
- "--_foreground-gw",
466
- "--port",
467
- String(options.port),
468
- "--log-level",
469
- options.logLevel,
470
- ]);
188
+ const pid = daemon.spawnDaemon("gateway", ["gateway", "--_foreground-gw", "--port", String(options.port)]);
471
189
  process.stderr.write(`\n LinkShell Gateway started in background\n`);
472
190
  process.stderr.write(` PID: ${pid}\n`);
473
191
  process.stderr.write(` Port: ${options.port}\n`);
474
192
  process.stderr.write(` Log: ${daemon.getLogFile("gateway")}\n\n`);
475
193
  process.stderr.write(` Stop: linkshell gateway stop\n`);
476
194
  process.stderr.write(` Status: linkshell gateway status\n`);
477
- process.stderr.write(
478
- ` Logs: tail -f ${daemon.getLogFile("gateway")}\n\n`,
479
- );
195
+ process.stderr.write(` Logs: tail -f ${daemon.getLogFile("gateway")}\n\n`);
480
196
  return;
481
197
  }
482
-
483
- // Foreground mode
484
- const { startEmbeddedGateway } =
485
- await import("@linkshell/gateway/embedded");
486
- const port = Number(options.port);
487
- const { join } = await import("node:path");
488
- (await import("./commands/host.js")).silenceSqliteWarning();
489
- const gw = await startEmbeddedGateway({
490
- port,
491
- logLevel: options.logLevel,
492
- silent: false,
493
- // v2 pairings live here; keep this file to keep phones paired.
494
- relayDataPath: join(daemon.linkshellDir(), "relay.db"),
495
- });
496
-
497
- daemon.savePid("gateway", process.pid);
498
-
499
- process.stderr.write(`\n LinkShell Gateway ${pkg.version}\n`);
500
- process.stderr.write(` Listening on http://0.0.0.0:${gw.port}\n`);
501
- process.stderr.write(` PID: ${process.pid}\n`);
502
- process.stderr.write(` Log level: ${options.logLevel}\n\n`);
503
- process.stderr.write(` Computers (v2): linkshell host --gateway ws://this-server:${gw.port}\n`);
504
- process.stderr.write(
505
- ` v1 clients: ws://this-server:${gw.port}/ws\n`,
506
- );
507
- process.stderr.write(
508
- ` Health check: curl http://your-server:${gw.port}/healthz\n\n`,
509
- );
510
-
511
- const shutdown = async () => {
512
- process.stderr.write("[gateway] shutting down...\n");
513
- daemon.removePid("gateway");
514
- await gw.close();
515
- process.exit(0);
516
- };
517
- process.on("SIGINT", shutdown);
518
- process.on("SIGTERM", shutdown);
519
-
520
- await new Promise(() => {});
198
+ const { runGatewayForeground } = await import("./commands/gateway.js");
199
+ await runGatewayForeground(pkg.version, Number(options.port));
521
200
  });
522
201
 
523
202
  gatewayCmd
@@ -551,7 +230,7 @@ gatewayCmd
551
230
 
552
231
  program
553
232
  .command("screen")
554
- .description("Set this computer up for watching and controlling its screen from the phone (permissions, ffmpeg)")
233
+ .description("Set this computer up for watching and controlling its screen from the phone (the two macOS permissions; on Linux, ffmpeg)")
555
234
  .option("--check", "Only say what is and isn't ready")
556
235
  .action(async (options) => {
557
236
  const { runScreenSetup } = await import("./commands/screen.js");
@@ -560,7 +239,7 @@ program
560
239
  await runScreenSetup({ check: options.check === true });
561
240
  });
562
241
 
563
- // ── stop (stops both bridge and gateway) ────────────────────────────
242
+ // ── stop (the host and the gateway server) ──────────────────────────
564
243
 
565
244
  program
566
245
  .command("stop")
@@ -568,10 +247,11 @@ program
568
247
  .action(async () => {
569
248
  const { stopDaemon } = await import("./utils/daemon.js");
570
249
  const hostStopped = stopDaemon("host");
250
+ // A bridge left running by a 1.x install: nothing else here can end it.
571
251
  const bridgeStopped = stopDaemon("bridge");
572
252
  const gatewayStopped = stopDaemon("gateway");
573
253
  if (hostStopped) process.stderr.write(" Host stopped.\n");
574
- if (bridgeStopped) process.stderr.write(" v1 bridge stopped.\n");
254
+ if (bridgeStopped) process.stderr.write(" 1.x bridge stopped.\n");
575
255
  if (gatewayStopped) process.stderr.write(" Gateway server stopped.\n");
576
256
  if (!hostStopped && !bridgeStopped && !gatewayStopped) {
577
257
  process.stderr.write(" No running processes found.\n");
@@ -584,18 +264,13 @@ program
584
264
  .command("status")
585
265
  .description("Show the host, its gateway connection, and anything else LinkShell runs here")
586
266
  .action(async () => {
587
- const { readPid, getLogFile, readMetadata } = await import("./utils/daemon.js");
267
+ const { readPid, getLogFile } = await import("./utils/daemon.js");
588
268
  const { printHostStatus, assertHostRuntime } = await import("./commands/host.js");
589
269
  assertHostRuntime();
590
270
  process.stdout.write("\n");
591
271
  await printHostStatus();
592
272
  const gatewayPid = readPid("gateway");
593
273
  if (gatewayPid) process.stdout.write(` Gateway server: running (PID ${gatewayPid}), log ${getLogFile("gateway")}\n`);
594
- const bridgePid = readPid("bridge");
595
- if (bridgePid) {
596
- const keepAwake = readMetadata("bridge")?.keepAwake ? ", keeps this computer awake" : "";
597
- process.stdout.write(` v1 bridge: running (PID ${bridgePid}${keepAwake}), log ${getLogFile("bridge")}\n`);
598
- }
599
274
  process.stdout.write("\n");
600
275
  });
601
276
 
@@ -611,9 +286,10 @@ program
611
286
 
612
287
  program
613
288
  .command("setup")
614
- .description("Configure the v1 bridge (`linkshell start`)")
289
+ .description("Set this computer up, once: the host, the screen's permissions, and your phone")
615
290
  .action(async () => {
616
- await runSetup();
291
+ await runSetup(pkg.version);
292
+ process.exit(process.exitCode ?? 0);
617
293
  });
618
294
 
619
295
  program
@@ -642,93 +318,13 @@ program
642
318
  await runLogout();
643
319
  });
644
320
 
645
- program
646
- .command("list")
647
- .description("List your v1 sessions on official gateways")
648
- .action(async () => {
649
- const { getValidToken, loadAuth, SUPABASE_URL, SUPABASE_ANON_KEY } =
650
- await import("./auth.js");
651
- const token = await getValidToken();
652
- if (!token) {
653
- process.stderr.write(
654
- "\n Not logged in. Run: linkshell login\n\n",
655
- );
656
- return;
657
- }
658
-
659
- const auth = loadAuth();
660
- process.stderr.write(`\n Logged in as ${auth?.email || auth?.userId || "unknown"}\n\n`);
661
-
662
- // Fetch official gateways
663
- let gateways: { url: string; name: string; region: string | null }[] = [];
664
- try {
665
- const res = await fetch(
666
- `${SUPABASE_URL}/rest/v1/linkshell_official_gateways?enabled=eq.true&select=url,name,region`,
667
- {
668
- headers: {
669
- Authorization: `Bearer ${token}`,
670
- apikey: SUPABASE_ANON_KEY,
671
- },
672
- signal: AbortSignal.timeout(10_000),
673
- },
674
- );
675
- if (res.ok) {
676
- gateways = (await res.json()) as typeof gateways;
677
- }
678
- } catch {}
679
-
680
- if (gateways.length === 0) {
681
- process.stderr.write(" No official gateways available.\n\n");
682
- return;
683
- }
684
-
685
- process.stderr.write(" Official Gateways:\n\n");
686
-
687
- for (const gw of gateways) {
688
- const label = gw.region ? `${gw.name} (${gw.region})` : gw.name;
689
- // Fetch user's sessions on this gateway
690
- let sessions: {
691
- id: string;
692
- provider: string | null;
693
- projectName: string | null;
694
- hasHost: boolean;
695
- lastActivity: number;
696
- }[] = [];
697
- try {
698
- const res = await fetch(`${gw.url}/sessions/mine`, {
699
- headers: { Authorization: `Bearer ${token}` },
700
- signal: AbortSignal.timeout(5_000),
701
- });
702
- if (res.ok) {
703
- const body = (await res.json()) as { sessions: typeof sessions };
704
- sessions = body.sessions;
705
- }
706
- } catch {}
707
-
708
- process.stderr.write(` \x1b[32m✓\x1b[0m ${label}\n`);
709
- const wsUrl = gw.url.replace(/\/$/, "").replace(/^https:/, "wss:").replace(/^http:/, "ws:") + "/ws";
710
- process.stderr.write(` ${wsUrl}\n`);
711
-
712
- if (sessions.length === 0) {
713
- process.stderr.write(" (no active sessions)\n\n");
714
- } else {
715
- for (const s of sessions) {
716
- const ago = Math.round((Date.now() - s.lastActivity) / 60_000);
717
- const agoStr = ago < 1 ? "just now" : `${ago}m ago`;
718
- const info = [s.provider, s.projectName].filter(Boolean).join(" · ");
719
- const hostIcon = s.hasHost ? "\x1b[32m●\x1b[0m" : "\x1b[31m●\x1b[0m";
720
- process.stderr.write(
721
- ` └ ${hostIcon} ${s.id.slice(0, 8)} — ${info || "unknown"} · ${agoStr}\n`,
722
- );
723
- }
724
- process.stderr.write("\n");
725
- }
726
- }
727
-
728
- process.stderr.write(
729
- " Connect: linkshell start --gateway <url>\n\n",
730
- );
731
- });
321
+ // `linkshell` by itself, on a computer where nothing has been set up yet: the setup, rather than a page of help.
322
+ program.action(async () => {
323
+ const { setupDone } = await import("./commands/setup.js");
324
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true || (await setupDone())) return program.help();
325
+ await runSetup(pkg.version);
326
+ process.exit(process.exitCode ?? 0);
327
+ });
732
328
 
733
329
  program.parseAsync(process.argv).catch((error: unknown) => {
734
330
  const message = error instanceof Error ? error.message : String(error);
@@ -5,13 +5,9 @@ import { spawn } from "node:child_process";
5
5
 
6
6
  const LINKSHELL_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
7
7
 
8
+ // "bridge" is the 1.x daemon: nothing starts one any more, but `linkshell stop` still ends one left running.
8
9
  type ServiceName = "gateway" | "bridge" | "host";
9
10
 
10
- export interface ServiceMetadata {
11
- keepAwake?: boolean;
12
- startedAt?: number;
13
- }
14
-
15
11
  function pidFile(service: ServiceName): string {
16
12
  return join(LINKSHELL_DIR, `${service}.pid`);
17
13
  }
@@ -20,6 +16,7 @@ function logFile(service: ServiceName): string {
20
16
  return join(LINKSHELL_DIR, `${service}.log`);
21
17
  }
22
18
 
19
+ /** 1.x kept a `<service>.json` beside its pid file; removed with it. */
23
20
  function metadataFile(service: ServiceName): string {
24
21
  return join(LINKSHELL_DIR, `${service}.json`);
25
22
  }
@@ -61,28 +58,6 @@ export function getLogFile(service: ServiceName): string {
61
58
  return logFile(service);
62
59
  }
63
60
 
64
- export function getPidFile(service: ServiceName): string {
65
- return pidFile(service);
66
- }
67
-
68
- export function saveMetadata(
69
- service: ServiceName,
70
- metadata: ServiceMetadata,
71
- ): void {
72
- mkdirSync(LINKSHELL_DIR, { recursive: true });
73
- writeFileSync(metadataFile(service), JSON.stringify(metadata), "utf8");
74
- }
75
-
76
- export function readMetadata(service: ServiceName): ServiceMetadata | null {
77
- try {
78
- const file = metadataFile(service);
79
- if (!existsSync(file)) return null;
80
- return JSON.parse(readFileSync(file, "utf8")) as ServiceMetadata;
81
- } catch {
82
- return null;
83
- }
84
- }
85
-
86
61
  export function spawnDaemon(service: ServiceName, args: string[]): number {
87
62
  mkdirSync(LINKSHELL_DIR, { recursive: true });
88
63
  const log = logFile(service);
@@ -1,14 +0,0 @@
1
- import type { ProviderName } from "./providers.js";
2
- export interface LinkShellConfig {
3
- gateway?: string;
4
- pairingGateway?: string;
5
- provider?: ProviderName;
6
- command?: string;
7
- clientName?: string;
8
- hostname?: string;
9
- cols?: number;
10
- rows?: number;
11
- }
12
- export declare function loadConfig(): LinkShellConfig;
13
- export declare function saveConfig(config: LinkShellConfig): void;
14
- export declare function getConfigPath(): string;
@@ -1,23 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- const CONFIG_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
5
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
- export function loadConfig() {
7
- try {
8
- if (!existsSync(CONFIG_FILE))
9
- return {};
10
- return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
11
- }
12
- catch {
13
- return {};
14
- }
15
- }
16
- export function saveConfig(config) {
17
- mkdirSync(CONFIG_DIR, { recursive: true });
18
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
19
- }
20
- export function getConfigPath() {
21
- return CONFIG_FILE;
22
- }
23
- //# sourceMappingURL=config.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAclC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AAC/E,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEpD,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAoB,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAuB;IAChD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -1,6 +0,0 @@
1
- export interface MachineIdentity {
2
- machineId: string;
3
- hostname: string;
4
- createdAt: string;
5
- }
6
- export declare function loadOrCreateMachineIdentity(homeDir?: string): MachineIdentity;