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.
- package/README.md +11 -6
- package/dist/cli/src/commands/gateway.d.ts +2 -0
- package/dist/cli/src/commands/gateway.js +29 -0
- package/dist/cli/src/commands/gateway.js.map +1 -0
- package/dist/cli/src/commands/host.d.ts +4 -0
- package/dist/cli/src/commands/host.js +12 -2
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/src/commands/screen.d.ts +10 -0
- package/dist/cli/src/commands/screen.js +82 -74
- package/dist/cli/src/commands/screen.js.map +1 -1
- package/dist/cli/src/commands/setup.d.ts +11 -1
- package/dist/cli/src/commands/setup.js +102 -38
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/index.js +28 -338
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +0 -7
- package/dist/cli/src/utils/daemon.js +1 -18
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/gateway/src/accounts.d.ts +6 -0
- package/dist/gateway/src/accounts.js +32 -0
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -0
- package/dist/gateway/src/index.js +5 -0
- package/dist/gateway/src/index.js.map +1 -0
- package/dist/gateway/src/main.d.ts +1 -0
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -0
- package/dist/gateway/src/relay.js +434 -0
- package/dist/gateway/src/relay.js.map +1 -0
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/gateway/src/store.d.ts +35 -0
- package/dist/gateway/src/store.js +109 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/input.d.ts +112 -27
- package/dist/host/src/input.js +214 -144
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/rpc/server.js +1 -1
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen-pacer.d.ts +25 -11
- package/dist/host/src/screen-pacer.js +28 -6
- package/dist/host/src/screen-pacer.js.map +1 -1
- package/dist/host/src/screen-viewer.d.ts +5 -0
- package/dist/host/src/screen-viewer.js +1231 -55
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/screen.d.ts +19 -10
- package/dist/host/src/screen.js +314 -108
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/terminals.d.ts +1 -1
- package/dist/host/src/terminals.js +1 -0
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/wire/src/model.d.ts +6 -6
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +818 -818
- package/dist/wire/src/updates.d.ts +18 -18
- package/package.json +6 -15
- package/src/commands/gateway.ts +31 -0
- package/src/commands/host.ts +15 -2
- package/src/commands/screen.ts +78 -72
- package/src/commands/setup.ts +102 -60
- package/src/index.ts +29 -433
- package/src/utils/daemon.ts +2 -27
- package/dist/cli/src/config.d.ts +0 -14
- package/dist/cli/src/config.js +0 -23
- package/dist/cli/src/config.js.map +0 -1
- package/dist/cli/src/machine-id.d.ts +0 -6
- package/dist/cli/src/machine-id.js +0 -50
- package/dist/cli/src/machine-id.js.map +0 -1
- package/dist/cli/src/providers.d.ts +0 -13
- package/dist/cli/src/providers.js +0 -77
- package/dist/cli/src/providers.js.map +0 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
- package/dist/cli/src/runtime/acp/acp-client.js +0 -430
- package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
- package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
- package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
- package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
- package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
- package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
- package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
- package/dist/cli/src/runtime/bridge-session.js +0 -1364
- package/dist/cli/src/runtime/bridge-session.js.map +0 -1
- package/dist/cli/src/runtime/hook-command.d.ts +0 -9
- package/dist/cli/src/runtime/hook-command.js +0 -80
- package/dist/cli/src/runtime/hook-command.js.map +0 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
- package/dist/cli/src/runtime/screen-fallback.js +0 -202
- package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
- package/dist/cli/src/runtime/screen-share.d.ts +0 -34
- package/dist/cli/src/runtime/screen-share.js +0 -376
- package/dist/cli/src/runtime/screen-share.js.map +0 -1
- package/dist/cli/src/runtime/scrollback.d.ts +0 -11
- package/dist/cli/src/runtime/scrollback.js +0 -33
- package/dist/cli/src/runtime/scrollback.js.map +0 -1
- package/dist/cli/src/runtime/usage-report.d.ts +0 -34
- package/dist/cli/src/runtime/usage-report.js +0 -332
- package/dist/cli/src/runtime/usage-report.js.map +0 -1
- package/dist/cli/src/utils/keep-awake.d.ts +0 -6
- package/dist/cli/src/utils/keep-awake.js +0 -48
- package/dist/cli/src/utils/keep-awake.js.map +0 -1
- package/dist/cli/src/utils/lan-ip.d.ts +0 -9
- package/dist/cli/src/utils/lan-ip.js +0 -61
- package/dist/cli/src/utils/lan-ip.js.map +0 -1
- package/dist/host/src/input-helper.d.ts +0 -1
- package/dist/host/src/input-helper.js +0 -564
- package/dist/host/src/input-helper.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/scripts/copy-web.mjs +0 -34
- package/src/config.ts +0 -36
- package/src/machine-id.ts +0 -61
- package/src/providers.ts +0 -97
- package/src/runtime/acp/acp-client.ts +0 -521
- package/src/runtime/acp/agent-process-discovery.ts +0 -308
- package/src/runtime/acp/agent-session.ts +0 -1186
- package/src/runtime/acp/agent-workspace.ts +0 -6590
- package/src/runtime/acp/claude-sdk-client.ts +0 -1221
- package/src/runtime/acp/claude-stream-json-client.ts +0 -633
- package/src/runtime/acp/json-rpc.ts +0 -184
- package/src/runtime/acp/provider-resolver.ts +0 -154
- package/src/runtime/bridge-session.ts +0 -1549
- package/src/runtime/hook-command.ts +0 -73
- package/src/runtime/screen-fallback.ts +0 -235
- package/src/runtime/screen-share.ts +0 -426
- package/src/runtime/scrollback.ts +0 -43
- package/src/runtime/usage-report.ts +0 -342
- package/src/utils/keep-awake.ts +0 -61
- package/src/utils/lan-ip.ts +0 -61
- package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
- package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
- package/web/assets/TerminalPanel-CTSklkct.js +0 -1
- package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
- package/web/assets/index-BsnDaeCt.css +0 -33
- package/web/assets/index-q85psYZ5.js +0 -36
- package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
- package/web/assets/vendor-react-M7xG3qiw.js +0 -49
- package/web/assets/vendor-xterm-D15THcI4.js +0 -9
- package/web/index.html +0 -37
- package/web/logos/claude.png +0 -0
- package/web/logos/codex.png +0 -0
- package/web/logos/copilot.png +0 -0
- package/web/logos/gemini.png +0 -0
- package/web/logos/linkshell-1024.png +0 -0
- package/web/logos/linkshell-splash.png +0 -0
- package/web/logos/linkshell.png +0 -0
|
@@ -1,44 +1,108 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import { isLoggedIn, loadAuth } from "../auth.js";
|
|
3
|
+
import { assertHostRuntime, ensureHostRunning, joinGatewayAfterLogin, OFFICIAL_GATEWAY, readHostConfig, sameGateway, withRunningHost, writeHostConfig } from "./host.js";
|
|
4
|
+
import { setUpScreen } from "./screen.js";
|
|
5
|
+
// `linkshell setup`: everything a new installation needs, once, in order —
|
|
6
|
+
// the host running, the screen's permissions, a phone that can reach this
|
|
7
|
+
// computer. Someone who installs and never comes back to the terminal should
|
|
8
|
+
// leave with all of it done: the first `linkshell host --daemon` runs this by
|
|
9
|
+
// itself (see `offerSetup`).
|
|
10
|
+
const green = (text) => `\x1b[32m${text}\x1b[0m`;
|
|
11
|
+
const dim = (text) => `\x1b[2m${text}\x1b[0m`;
|
|
12
|
+
const bold = (text) => `\x1b[1m${text}\x1b[0m`;
|
|
13
|
+
function ask(question) {
|
|
14
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
resolve(answer.trim().toLowerCase());
|
|
18
|
+
}));
|
|
11
19
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
async function home() {
|
|
21
|
+
const { defaultHome } = await import("@linkshell/host");
|
|
22
|
+
return defaultHome();
|
|
23
|
+
}
|
|
24
|
+
/** Whether this installation has been through setup (or said no to it). */
|
|
25
|
+
export async function setupDone() {
|
|
26
|
+
return readHostConfig(await home()).setup !== undefined;
|
|
27
|
+
}
|
|
28
|
+
async function remember(outcome) {
|
|
29
|
+
const at = await home();
|
|
30
|
+
writeHostConfig(at, { ...readHostConfig(at), setup: outcome });
|
|
31
|
+
}
|
|
32
|
+
/** The phone's way to this computer: an account both are signed in to, or a pairing through a gateway. */
|
|
33
|
+
async function setUpPhone(out, interactive) {
|
|
34
|
+
const gateway = await withRunningHost((client) => client.call("gateway.status", {}));
|
|
35
|
+
// The account counts on LinkShell's own gateway only; with a gateway of one's own, phones are paired.
|
|
36
|
+
const official = !gateway || gateway.status === "off" || !gateway.url || sameGateway(gateway.url, OFFICIAL_GATEWAY);
|
|
37
|
+
if (isLoggedIn() && official) {
|
|
38
|
+
const email = loadAuth()?.email;
|
|
39
|
+
if (gateway?.status === "online") {
|
|
40
|
+
out(` ${green("✓")} Signed in${email ? ` as ${email}` : ""}: this computer is online.`);
|
|
41
|
+
out(dim(" Open LinkShell on your phone and sign in with the same account — it shows up there, no pairing needed."));
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Signed in, not online yet (a host started before the login, or no subscription): say which.
|
|
45
|
+
await joinGatewayAfterLogin("pro");
|
|
46
|
+
return (await withRunningHost((client) => client.call("gateway.status", {})))?.status === "online";
|
|
47
|
+
}
|
|
48
|
+
if (gateway && gateway.status !== "off" && gateway.devices.length > 0) {
|
|
49
|
+
out(` ${green("✓")} Paired with ${gateway.devices.map((device) => device.name).join(", ")}.`);
|
|
50
|
+
return true;
|
|
15
51
|
}
|
|
16
|
-
|
|
52
|
+
if (gateway && gateway.status !== "off") {
|
|
53
|
+
out(" This computer uses your own gateway. Pair your phone with: linkshell pair");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (!interactive) {
|
|
57
|
+
out(" Not connected to a phone yet: linkshell login (LinkShell account), or your own gateway and linkshell pair.");
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
out(" Your phone reaches this computer through your LinkShell account (Pro), from anywhere.");
|
|
61
|
+
const answer = await ask(" Sign in now? It opens your browser. [Y/n] ");
|
|
62
|
+
if (answer === "n" || answer === "no") {
|
|
63
|
+
out(dim(" Later: linkshell login — or with your own gateway: linkshell host --gateway <url>, then linkshell pair"));
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const { runLogin } = await import("./login.js");
|
|
67
|
+
const result = await runLogin();
|
|
68
|
+
if (!result)
|
|
69
|
+
return false;
|
|
70
|
+
await joinGatewayAfterLogin(result.plan);
|
|
71
|
+
return (await withRunningHost((client) => client.call("gateway.status", {})))?.status === "online";
|
|
72
|
+
}
|
|
73
|
+
/** The whole of it. `from`: who is running it — said aloud only when it wasn't asked for. */
|
|
74
|
+
export async function runSetup(version, from = "command") {
|
|
75
|
+
const out = (line = "") => process.stdout.write(`${line}\n`);
|
|
76
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
77
|
+
assertHostRuntime();
|
|
78
|
+
out(`\n ${bold("LinkShell setup")}${from === "first-start" ? dim(" (first start on this computer; again any time with: linkshell setup)") : ""}\n`);
|
|
79
|
+
out(` ${bold("1")} The host`);
|
|
80
|
+
await ensureHostRunning();
|
|
81
|
+
out(` ${green("✓")} Running (${version}); it keeps running in the background.\n`);
|
|
82
|
+
out(` ${bold("2")} The screen ${dim("— watch and control this computer from the phone")}`);
|
|
83
|
+
const screen = await setUpScreen(out, interactive);
|
|
84
|
+
out();
|
|
85
|
+
out(` ${bold("3")} Your phone`);
|
|
86
|
+
const phone = await setUpPhone(out, interactive);
|
|
87
|
+
out();
|
|
88
|
+
// Run where nobody could answer, it has set nothing up: the first start at a terminal still offers it.
|
|
89
|
+
if (interactive)
|
|
90
|
+
await remember("done");
|
|
91
|
+
if (screen && phone)
|
|
92
|
+
out(` ${green("All set.")} Open LinkShell on your phone.\n`);
|
|
93
|
+
else
|
|
94
|
+
out(` Done for now. ${[!screen && "The screen: linkshell screen", !phone && "The phone: linkshell login, or linkshell pair"].filter(Boolean).join(" · ")}\n`);
|
|
17
95
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
process.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const hostnameName = await ask(rl, "Hostname (display name for your machine)", existing.hostname ?? hostname());
|
|
30
|
-
rl.close();
|
|
31
|
-
const config = {
|
|
32
|
-
gateway: gateway || undefined,
|
|
33
|
-
provider: "custom",
|
|
34
|
-
command: command || undefined,
|
|
35
|
-
clientName,
|
|
36
|
-
hostname: hostnameName || undefined,
|
|
37
|
-
};
|
|
38
|
-
saveConfig(config);
|
|
39
|
-
process.stdout.write(`\n \x1b[32mConfig saved to ${getConfigPath()}\x1b[0m\n\n`);
|
|
40
|
-
process.stdout.write(" Next steps:\n");
|
|
41
|
-
process.stdout.write(" linkshell doctor — verify your setup\n");
|
|
42
|
-
process.stdout.write(" linkshell start — start a bridge session\n\n");
|
|
96
|
+
/**
|
|
97
|
+
* The first start on a computer: the host has just been started by someone at
|
|
98
|
+
* a terminal, and nothing has ever been set up here. Rather than leave the
|
|
99
|
+
* rest for the day it is needed (by then nobody is at the computer), it is
|
|
100
|
+
* done now. Once: a second start says nothing.
|
|
101
|
+
*/
|
|
102
|
+
export async function offerSetup(version) {
|
|
103
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true || (await setupDone()))
|
|
104
|
+
return false;
|
|
105
|
+
await runSetup(version, "first-start");
|
|
106
|
+
return true;
|
|
43
107
|
}
|
|
44
108
|
//# sourceMappingURL=setup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../src/commands/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../src/commands/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACzK,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,6BAA6B;AAE7B,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,WAAW,IAAI,SAAS,CAAC;AACzD,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,IAAI,SAAS,CAAC;AACtD,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,IAAI,SAAS,CAAC;AAEvD,SAAS,GAAG,CAAC,QAAgB;IAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAC7B,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;QAC/B,EAAE,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACxD,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,OAAO,cAAc,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC;AAC1D,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,OAAe;IACrC,MAAM,EAAE,GAAG,MAAM,IAAI,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,EAAE,EAAE,GAAG,cAAc,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,0GAA0G;AAC1G,KAAK,UAAU,UAAU,CAAC,GAA4B,EAAE,WAAoB;IAC1E,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC;IACrF,sGAAsG;IACtG,MAAM,QAAQ,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IACpH,IAAI,UAAU,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,QAAQ,EAAE,EAAE,KAAK,CAAC;QAChC,IAAI,OAAO,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAC;YACzF,GAAG,CAAC,GAAG,CAAC,4GAA4G,CAAC,CAAC,CAAC;YACvH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,8FAA8F;QAC9F,MAAM,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,CAAC,MAAM,eAAe,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,QAAQ,CAAC;IACrG,CAAC;IACD,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,gBAAgB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/F,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QACxC,GAAG,CAAC,8EAA8E,CAAC,CAAC;QACpF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,GAAG,CAAC,8GAA8G,CAAC,CAAC;QACpH,OAAO,KAAK,CAAC;IACf,CAAC;IACD,GAAG,CAAC,yFAAyF,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,8CAA8C,CAAC,CAAC;IACzE,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACtC,GAAG,CAAC,GAAG,CAAC,4GAA4G,CAAC,CAAC,CAAC;QACvH,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,QAAQ,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,MAAM,eAAe,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,QAAQ,CAAC;AACrG,CAAC;AAED,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAe,EAAE,OAAkC,SAAS;IACzF,MAAM,GAAG,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;IAC7D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;IAClF,iBAAiB,EAAE,CAAC;IACpB,GAAG,CAAC,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAEtJ,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAChC,MAAM,iBAAiB,EAAE,CAAC;IAC1B,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,aAAa,OAAO,0CAA0C,CAAC,CAAC;IAEnF,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,kDAAkD,CAAC,EAAE,CAAC,CAAC;IAC7F,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACnD,GAAG,EAAE,CAAC;IAEN,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACjD,GAAG,EAAE,CAAC;IAEN,uGAAuG;IACvG,IAAI,WAAW;QAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,MAAM,IAAI,KAAK;QAAE,GAAG,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC;;QAC9E,GAAG,CAAC,mBAAmB,CAAC,CAAC,MAAM,IAAI,8BAA8B,EAAE,CAAC,KAAK,IAAI,+CAA+C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtK,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAe;IAC9C,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,SAAS,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IACvG,MAAM,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -1,59 +1,25 @@
|
|
|
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
8
|
import { createRequire } from "node:module";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { dirname, resolve } from "node:path";
|
|
16
|
-
import { existsSync, statSync } from "node:fs";
|
|
17
9
|
const require = createRequire(import.meta.url);
|
|
18
10
|
const pkg = require("../../../package.json");
|
|
19
|
-
// Locate the bundled web-dashboard SPA and point the (embedded) gateway's
|
|
20
|
-
// static server at it via WEB_DIST — BEFORE any gateway module is imported,
|
|
21
|
-
// because static-web.ts freezes WEB_DIST at module-load time. This is what lets
|
|
22
|
-
// the in-app WebView render for LAN / self-hosted users whose CLI runs its own
|
|
23
|
-
// embedded gateway (the cloud image bundles web separately). A user-set
|
|
24
|
-
// WEB_DIST always wins. Probed across published-package, monorepo-compiled, and
|
|
25
|
-
// dev (tsx) layouts; the first candidate containing index.html is used.
|
|
26
|
-
if (!process.env.WEB_DIST) {
|
|
27
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const candidates = [
|
|
29
|
-
resolve(here, "../../../web"), // published npm package root, and packages/cli/web (compiled)
|
|
30
|
-
resolve(here, "../../../apps/web-dashboard/dist"), // dev: tsx src/index.ts in the monorepo
|
|
31
|
-
];
|
|
32
|
-
for (const dir of candidates) {
|
|
33
|
-
try {
|
|
34
|
-
if (existsSync(resolve(dir, "index.html")) && statSync(dir).isDirectory()) {
|
|
35
|
-
process.env.WEB_DIST = dir;
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// ignore and try the next candidate
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const config = loadConfig();
|
|
45
11
|
const program = new Command();
|
|
46
12
|
program
|
|
47
13
|
.name("linkshell")
|
|
48
|
-
.description("Your coding agents and terminals, on your phone.
|
|
14
|
+
.description("Your coding agents and terminals, on your phone. Start with `linkshell setup`.")
|
|
49
15
|
.version(pkg.version);
|
|
50
|
-
// ── host
|
|
16
|
+
// ── host ────────────────────────────────────────────────────────────
|
|
51
17
|
const hostCmd = program
|
|
52
18
|
.command("host")
|
|
53
|
-
.description("Run the LinkShell host that owns agent sessions
|
|
19
|
+
.description("Run the LinkShell host that owns agent sessions")
|
|
54
20
|
.option("--daemon", "Run in background (detached)")
|
|
55
21
|
.option("--dev-port <port>", "Also serve the host API on 127.0.0.1:<port> (local development clients only)")
|
|
56
|
-
.option("--gateway <url>", "Reach this computer through a
|
|
22
|
+
.option("--gateway <url>", "Reach this computer through a gateway (saved; 'off' to disable, 'default' for the official one when logged in)")
|
|
57
23
|
.option("--_foreground-host", undefined) // internal
|
|
58
24
|
.action(async (options) => {
|
|
59
25
|
const { runHostForeground, ensureHostRunning, readHostConfig, writeHostConfig, assertHostRuntime, silenceSqliteWarning, withRunningHost, refreshGateway, describeGateway } = await import("./commands/host.js");
|
|
@@ -85,7 +51,10 @@ const hostCmd = program
|
|
|
85
51
|
process.stderr.write(` Log: ${daemon.getLogFile("host")}\n`);
|
|
86
52
|
process.stderr.write(` Status: linkshell host status\n`);
|
|
87
53
|
process.stderr.write(` Stop: linkshell host stop\n`);
|
|
88
|
-
// The
|
|
54
|
+
// The first start here: the rest of the setup happens now, while someone is at the computer.
|
|
55
|
+
const { offerSetup } = await import("./commands/setup.js");
|
|
56
|
+
if (await offerSetup(pkg.version))
|
|
57
|
+
process.exit(process.exitCode ?? 0);
|
|
89
58
|
const { screenAccess, describeScreen, screenReady } = await import("./commands/screen.js");
|
|
90
59
|
const screen = await screenAccess().catch(() => undefined);
|
|
91
60
|
if (screen && (screen === "old" || !screenReady(screen)))
|
|
@@ -190,214 +159,23 @@ for (const [agent, description] of [
|
|
|
190
159
|
await runAgentShim(agent, args ?? []);
|
|
191
160
|
});
|
|
192
161
|
}
|
|
193
|
-
// ── start ───────────────────────────────────────────────────────────
|
|
194
|
-
program
|
|
195
|
-
.command("start")
|
|
196
|
-
.description("Start a v1 bridge session (for the v1 app; with built-in or remote gateway)")
|
|
197
|
-
.option("--gateway <url>", "Gateway websocket URL (omit to start built-in gateway)", config.gateway ?? undefined)
|
|
198
|
-
.option("--pairing-gateway <url-or-host>", "Public HTTP gateway used in QR/deep link output", config.pairingGateway)
|
|
199
|
-
.option("--port <port>", "Port for built-in gateway", "8787")
|
|
200
|
-
.option("--session-id <id>", "Session identifier (auto-created if omitted)")
|
|
201
|
-
.option("--provider <provider>", "(deprecated — always custom; launch your CLI manually inside the shell)", "custom")
|
|
202
|
-
.option("--command <command>", "Shell or CLI to spawn in the PTY (default: $SHELL)", config.command)
|
|
203
|
-
.option("--client-name <name>", "Display name for this CLI", config.clientName ?? "local-cli")
|
|
204
|
-
.option("--hostname <name>", "Override hostname sent to gateway", config.hostname)
|
|
205
|
-
.option("--cols <cols>", "Initial terminal columns", String(config.cols ?? 120))
|
|
206
|
-
.option("--rows <rows>", "Initial terminal rows", String(config.rows ?? 36))
|
|
207
|
-
.option("--screen", "Enable screen sharing capability")
|
|
208
|
-
.option("--no-keep-awake", "Disable macOS keep-awake while bridge is running")
|
|
209
|
-
.option("--agent-ui", "Enable ACP Agent Workspace channel", true)
|
|
210
|
-
.option("--no-agent-ui", "Disable ACP Agent Workspace channel")
|
|
211
|
-
.option("--agent-provider <provider>", "Agent GUI provider: codex | claude | custom")
|
|
212
|
-
.option("--agent-command <command>", "ACP agent command (required for claude/custom, optional for codex)")
|
|
213
|
-
.option("--daemon", "Run in background (detached)")
|
|
214
|
-
.option("--verbose", "Enable verbose logging")
|
|
215
|
-
.option("--_foreground-bridge", undefined) // internal
|
|
216
|
-
.allowUnknownOption(true)
|
|
217
|
-
.allowExcessArguments(true)
|
|
218
|
-
.action(async (options, command) => {
|
|
219
|
-
const daemon = await import("./utils/daemon.js");
|
|
220
|
-
const keepAwake = shouldKeepAwake(options.keepAwake);
|
|
221
|
-
// Daemon mode: spawn detached child and exit
|
|
222
|
-
if (options.daemon && !options._foregroundBridge) {
|
|
223
|
-
const existingPid = daemon.readPid("bridge");
|
|
224
|
-
if (existingPid) {
|
|
225
|
-
process.stderr.write(` Bridge already running (PID ${existingPid})\n`);
|
|
226
|
-
process.stderr.write(` Run: linkshell stop\n\n`);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Rebuild args for the child, replacing --daemon with --_foreground-bridge
|
|
230
|
-
const childArgs = ["start", "--_foreground-bridge"];
|
|
231
|
-
if (options.gateway)
|
|
232
|
-
childArgs.push("--gateway", options.gateway);
|
|
233
|
-
if (options.pairingGateway)
|
|
234
|
-
childArgs.push("--pairing-gateway", options.pairingGateway);
|
|
235
|
-
childArgs.push("--port", String(options.port));
|
|
236
|
-
childArgs.push("--provider", options.provider);
|
|
237
|
-
if (options.command)
|
|
238
|
-
childArgs.push("--command", options.command);
|
|
239
|
-
childArgs.push("--client-name", options.clientName);
|
|
240
|
-
if (options.hostname)
|
|
241
|
-
childArgs.push("--hostname", options.hostname);
|
|
242
|
-
childArgs.push("--cols", String(options.cols));
|
|
243
|
-
childArgs.push("--rows", String(options.rows));
|
|
244
|
-
if (options.verbose)
|
|
245
|
-
childArgs.push("--verbose");
|
|
246
|
-
if (options.screen)
|
|
247
|
-
childArgs.push("--screen");
|
|
248
|
-
if (!keepAwake)
|
|
249
|
-
childArgs.push("--no-keep-awake");
|
|
250
|
-
if (options.agentUi)
|
|
251
|
-
childArgs.push("--agent-ui");
|
|
252
|
-
if (options.agentProvider)
|
|
253
|
-
childArgs.push("--agent-provider", options.agentProvider);
|
|
254
|
-
if (options.agentCommand)
|
|
255
|
-
childArgs.push("--agent-command", options.agentCommand);
|
|
256
|
-
if (options.sessionId)
|
|
257
|
-
childArgs.push("--session-id", options.sessionId);
|
|
258
|
-
// Pass through extra args
|
|
259
|
-
const extra = command.args.filter((v) => v !== "--");
|
|
260
|
-
if (extra.length)
|
|
261
|
-
childArgs.push("--", ...extra);
|
|
262
|
-
const pid = daemon.spawnDaemon("bridge", childArgs);
|
|
263
|
-
daemon.saveMetadata("bridge", { keepAwake, startedAt: Date.now() });
|
|
264
|
-
process.stderr.write(`\n LinkShell bridge started in background\n`);
|
|
265
|
-
process.stderr.write(` PID: ${pid}\n`);
|
|
266
|
-
process.stderr.write(` Provider: ${options.provider}\n`);
|
|
267
|
-
process.stderr.write(` Keep awake: ${keepAwake ? "enabled (use --no-keep-awake to disable)" : "disabled"}\n`);
|
|
268
|
-
process.stderr.write(` Log: ${daemon.getLogFile("bridge")}\n\n`);
|
|
269
|
-
process.stderr.write(` Stop: linkshell stop\n`);
|
|
270
|
-
process.stderr.write(` Status: linkshell status\n`);
|
|
271
|
-
process.stderr.write(` Logs: tail -f ${daemon.getLogFile("bridge")}\n\n`);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// Foreground mode
|
|
275
|
-
const passthroughArgs = command.args.filter((value) => value !== "--");
|
|
276
|
-
const providerConfig = resolveProviderConfig({
|
|
277
|
-
provider: options.provider,
|
|
278
|
-
command: options.command,
|
|
279
|
-
args: passthroughArgs,
|
|
280
|
-
});
|
|
281
|
-
let gatewayUrl = options.gateway;
|
|
282
|
-
let gatewayHttpUrl;
|
|
283
|
-
let pairingGateway = options.pairingGateway;
|
|
284
|
-
let embeddedGatewayHandle;
|
|
285
|
-
if (!gatewayUrl) {
|
|
286
|
-
const { startEmbeddedGateway } = await import("@linkshell/gateway/embedded");
|
|
287
|
-
const port = Number(options.port);
|
|
288
|
-
const gw = await startEmbeddedGateway({
|
|
289
|
-
port,
|
|
290
|
-
logLevel: options.verbose ? "debug" : "warn",
|
|
291
|
-
silent: false,
|
|
292
|
-
});
|
|
293
|
-
embeddedGatewayHandle = gw;
|
|
294
|
-
gatewayUrl = gw.wsUrl;
|
|
295
|
-
gatewayHttpUrl = gw.httpUrl;
|
|
296
|
-
const lanIp = getLanIp();
|
|
297
|
-
if (!pairingGateway && lanIp !== "127.0.0.1") {
|
|
298
|
-
pairingGateway = `http://${lanIp}:${gw.port}`;
|
|
299
|
-
}
|
|
300
|
-
process.stderr.write(`\n Built-in gateway started on port ${gw.port}\n`);
|
|
301
|
-
if (pairingGateway) {
|
|
302
|
-
process.stderr.write(` LAN address: ${pairingGateway}\n`);
|
|
303
|
-
}
|
|
304
|
-
process.stderr.write("\n");
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
gatewayHttpUrl = gatewayUrl
|
|
308
|
-
.replace(/\/ws\/?$/, "")
|
|
309
|
-
.replace(/^wss:/, "https:")
|
|
310
|
-
.replace(/^ws:/, "http:");
|
|
311
|
-
}
|
|
312
|
-
// Save PID for status/stop
|
|
313
|
-
daemon.savePid("bridge", process.pid);
|
|
314
|
-
daemon.saveMetadata("bridge", { keepAwake, startedAt: Date.now() });
|
|
315
|
-
// Load auth token if logged in
|
|
316
|
-
let authToken;
|
|
317
|
-
try {
|
|
318
|
-
const { getValidToken } = await import("./auth.js");
|
|
319
|
-
authToken = (await getValidToken()) ?? undefined;
|
|
320
|
-
}
|
|
321
|
-
catch { }
|
|
322
|
-
const session = new BridgeSession({
|
|
323
|
-
gatewayUrl,
|
|
324
|
-
gatewayHttpUrl,
|
|
325
|
-
pairingGateway,
|
|
326
|
-
sessionId: options.sessionId,
|
|
327
|
-
cols: Number(options.cols),
|
|
328
|
-
rows: Number(options.rows),
|
|
329
|
-
clientName: options.clientName,
|
|
330
|
-
hostname: options.hostname,
|
|
331
|
-
verbose: Boolean(options.verbose),
|
|
332
|
-
screen: Boolean(options.screen),
|
|
333
|
-
providerConfig,
|
|
334
|
-
authToken,
|
|
335
|
-
keepAwake,
|
|
336
|
-
agentUi: Boolean(options.agentUi),
|
|
337
|
-
agentProvider: options.agentProvider,
|
|
338
|
-
agentCommand: options.agentCommand,
|
|
339
|
-
});
|
|
340
|
-
const cleanup = async () => {
|
|
341
|
-
session.stop(0);
|
|
342
|
-
daemon.removePid("bridge");
|
|
343
|
-
if (embeddedGatewayHandle)
|
|
344
|
-
await embeddedGatewayHandle.close();
|
|
345
|
-
};
|
|
346
|
-
// When all PTYs exit naturally (not via a signal), the session closes its
|
|
347
|
-
// own socket but the embedded gateway loop and stale PID file would linger.
|
|
348
|
-
// Run the same process-level cleanup and then exit.
|
|
349
|
-
session.setOnAllTerminalsExited(async () => {
|
|
350
|
-
daemon.removePid("bridge");
|
|
351
|
-
if (embeddedGatewayHandle)
|
|
352
|
-
await embeddedGatewayHandle.close();
|
|
353
|
-
process.exit(process.exitCode ?? 0);
|
|
354
|
-
});
|
|
355
|
-
process.on("SIGINT", () => {
|
|
356
|
-
cleanup().then(() => process.exit(0));
|
|
357
|
-
});
|
|
358
|
-
process.on("SIGTERM", () => {
|
|
359
|
-
cleanup().then(() => process.exit(0));
|
|
360
|
-
});
|
|
361
|
-
// Global crash handlers: never die silently. In daemon mode keep the bridge
|
|
362
|
-
// alive (it has its own gateway reconnect logic); stderr is redirected to the
|
|
363
|
-
// daemon log file so the trace is recoverable.
|
|
364
|
-
const isDaemonChild = Boolean(options._foregroundBridge);
|
|
365
|
-
process.on("uncaughtException", (err) => {
|
|
366
|
-
process.stderr.write(`[bridge] uncaught exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
367
|
-
if (!isDaemonChild) {
|
|
368
|
-
cleanup().then(() => process.exit(1));
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
process.on("unhandledRejection", (reason) => {
|
|
372
|
-
process.stderr.write(`[bridge] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}\n`);
|
|
373
|
-
});
|
|
374
|
-
await session.start();
|
|
375
|
-
});
|
|
376
162
|
// ── gateway ─────────────────────────────────────────────────────────
|
|
377
163
|
const gatewayCmd = program
|
|
378
164
|
.command("gateway")
|
|
379
|
-
.description("
|
|
165
|
+
.description("Run a gateway of your own: it relays encrypted frames between your computers and phones, and brokers pairing")
|
|
380
166
|
.option("--port <port>", "Listen port", "8787")
|
|
381
|
-
.option("--log-level <level>", "Log level: debug | info | warn | error", "info")
|
|
382
167
|
.option("--daemon", "Run in background (detached)")
|
|
383
168
|
.option("--_foreground-gw", undefined) // internal
|
|
384
169
|
.action(async (options) => {
|
|
385
|
-
const daemon = await import("./utils/daemon.js");
|
|
386
170
|
if (options.daemon && !options._foregroundGw) {
|
|
171
|
+
const daemon = await import("./utils/daemon.js");
|
|
387
172
|
const existingPid = daemon.readPid("gateway");
|
|
388
173
|
if (existingPid) {
|
|
389
174
|
process.stderr.write(` Gateway already running (PID ${existingPid})\n`);
|
|
390
175
|
process.stderr.write(` Run: linkshell gateway stop\n\n`);
|
|
391
176
|
return;
|
|
392
177
|
}
|
|
393
|
-
const pid = daemon.spawnDaemon("gateway", [
|
|
394
|
-
"gateway",
|
|
395
|
-
"--_foreground-gw",
|
|
396
|
-
"--port",
|
|
397
|
-
String(options.port),
|
|
398
|
-
"--log-level",
|
|
399
|
-
options.logLevel,
|
|
400
|
-
]);
|
|
178
|
+
const pid = daemon.spawnDaemon("gateway", ["gateway", "--_foreground-gw", "--port", String(options.port)]);
|
|
401
179
|
process.stderr.write(`\n LinkShell Gateway started in background\n`);
|
|
402
180
|
process.stderr.write(` PID: ${pid}\n`);
|
|
403
181
|
process.stderr.write(` Port: ${options.port}\n`);
|
|
@@ -407,35 +185,8 @@ const gatewayCmd = program
|
|
|
407
185
|
process.stderr.write(` Logs: tail -f ${daemon.getLogFile("gateway")}\n\n`);
|
|
408
186
|
return;
|
|
409
187
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const port = Number(options.port);
|
|
413
|
-
const { join } = await import("node:path");
|
|
414
|
-
(await import("./commands/host.js")).silenceSqliteWarning();
|
|
415
|
-
const gw = await startEmbeddedGateway({
|
|
416
|
-
port,
|
|
417
|
-
logLevel: options.logLevel,
|
|
418
|
-
silent: false,
|
|
419
|
-
// v2 pairings live here; keep this file to keep phones paired.
|
|
420
|
-
relayDataPath: join(daemon.linkshellDir(), "relay.db"),
|
|
421
|
-
});
|
|
422
|
-
daemon.savePid("gateway", process.pid);
|
|
423
|
-
process.stderr.write(`\n LinkShell Gateway ${pkg.version}\n`);
|
|
424
|
-
process.stderr.write(` Listening on http://0.0.0.0:${gw.port}\n`);
|
|
425
|
-
process.stderr.write(` PID: ${process.pid}\n`);
|
|
426
|
-
process.stderr.write(` Log level: ${options.logLevel}\n\n`);
|
|
427
|
-
process.stderr.write(` Computers (v2): linkshell host --gateway ws://this-server:${gw.port}\n`);
|
|
428
|
-
process.stderr.write(` v1 clients: ws://this-server:${gw.port}/ws\n`);
|
|
429
|
-
process.stderr.write(` Health check: curl http://your-server:${gw.port}/healthz\n\n`);
|
|
430
|
-
const shutdown = async () => {
|
|
431
|
-
process.stderr.write("[gateway] shutting down...\n");
|
|
432
|
-
daemon.removePid("gateway");
|
|
433
|
-
await gw.close();
|
|
434
|
-
process.exit(0);
|
|
435
|
-
};
|
|
436
|
-
process.on("SIGINT", shutdown);
|
|
437
|
-
process.on("SIGTERM", shutdown);
|
|
438
|
-
await new Promise(() => { });
|
|
188
|
+
const { runGatewayForeground } = await import("./commands/gateway.js");
|
|
189
|
+
await runGatewayForeground(pkg.version, Number(options.port));
|
|
439
190
|
});
|
|
440
191
|
gatewayCmd
|
|
441
192
|
.command("stop")
|
|
@@ -467,7 +218,7 @@ gatewayCmd
|
|
|
467
218
|
// ── screen ──────────────────────────────────────────────────────────
|
|
468
219
|
program
|
|
469
220
|
.command("screen")
|
|
470
|
-
.description("Set this computer up for watching and controlling its screen from the phone (permissions, ffmpeg)")
|
|
221
|
+
.description("Set this computer up for watching and controlling its screen from the phone (the two macOS permissions; on Linux, ffmpeg)")
|
|
471
222
|
.option("--check", "Only say what is and isn't ready")
|
|
472
223
|
.action(async (options) => {
|
|
473
224
|
const { runScreenSetup } = await import("./commands/screen.js");
|
|
@@ -475,19 +226,20 @@ program
|
|
|
475
226
|
silenceSqliteWarning();
|
|
476
227
|
await runScreenSetup({ check: options.check === true });
|
|
477
228
|
});
|
|
478
|
-
// ── stop (
|
|
229
|
+
// ── stop (the host and the gateway server) ──────────────────────────
|
|
479
230
|
program
|
|
480
231
|
.command("stop")
|
|
481
232
|
.description("Stop all running LinkShell processes")
|
|
482
233
|
.action(async () => {
|
|
483
234
|
const { stopDaemon } = await import("./utils/daemon.js");
|
|
484
235
|
const hostStopped = stopDaemon("host");
|
|
236
|
+
// A bridge left running by a 1.x install: nothing else here can end it.
|
|
485
237
|
const bridgeStopped = stopDaemon("bridge");
|
|
486
238
|
const gatewayStopped = stopDaemon("gateway");
|
|
487
239
|
if (hostStopped)
|
|
488
240
|
process.stderr.write(" Host stopped.\n");
|
|
489
241
|
if (bridgeStopped)
|
|
490
|
-
process.stderr.write("
|
|
242
|
+
process.stderr.write(" 1.x bridge stopped.\n");
|
|
491
243
|
if (gatewayStopped)
|
|
492
244
|
process.stderr.write(" Gateway server stopped.\n");
|
|
493
245
|
if (!hostStopped && !bridgeStopped && !gatewayStopped) {
|
|
@@ -499,7 +251,7 @@ program
|
|
|
499
251
|
.command("status")
|
|
500
252
|
.description("Show the host, its gateway connection, and anything else LinkShell runs here")
|
|
501
253
|
.action(async () => {
|
|
502
|
-
const { readPid, getLogFile
|
|
254
|
+
const { readPid, getLogFile } = await import("./utils/daemon.js");
|
|
503
255
|
const { printHostStatus, assertHostRuntime } = await import("./commands/host.js");
|
|
504
256
|
assertHostRuntime();
|
|
505
257
|
process.stdout.write("\n");
|
|
@@ -507,11 +259,6 @@ program
|
|
|
507
259
|
const gatewayPid = readPid("gateway");
|
|
508
260
|
if (gatewayPid)
|
|
509
261
|
process.stdout.write(` Gateway server: running (PID ${gatewayPid}), log ${getLogFile("gateway")}\n`);
|
|
510
|
-
const bridgePid = readPid("bridge");
|
|
511
|
-
if (bridgePid) {
|
|
512
|
-
const keepAwake = readMetadata("bridge")?.keepAwake ? ", keeps this computer awake" : "";
|
|
513
|
-
process.stdout.write(` v1 bridge: running (PID ${bridgePid}${keepAwake}), log ${getLogFile("bridge")}\n`);
|
|
514
|
-
}
|
|
515
262
|
process.stdout.write("\n");
|
|
516
263
|
});
|
|
517
264
|
// ── doctor / setup ──────────────────────────────────────────────────
|
|
@@ -524,9 +271,10 @@ program
|
|
|
524
271
|
});
|
|
525
272
|
program
|
|
526
273
|
.command("setup")
|
|
527
|
-
.description("
|
|
274
|
+
.description("Set this computer up, once: the host, the screen's permissions, and your phone")
|
|
528
275
|
.action(async () => {
|
|
529
|
-
await runSetup();
|
|
276
|
+
await runSetup(pkg.version);
|
|
277
|
+
process.exit(process.exitCode ?? 0);
|
|
530
278
|
});
|
|
531
279
|
program
|
|
532
280
|
.command("upgrade")
|
|
@@ -551,71 +299,13 @@ program
|
|
|
551
299
|
.action(async () => {
|
|
552
300
|
await runLogout();
|
|
553
301
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
process.stderr.write("\n Not logged in. Run: linkshell login\n\n");
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const auth = loadAuth();
|
|
565
|
-
process.stderr.write(`\n Logged in as ${auth?.email || auth?.userId || "unknown"}\n\n`);
|
|
566
|
-
// Fetch official gateways
|
|
567
|
-
let gateways = [];
|
|
568
|
-
try {
|
|
569
|
-
const res = await fetch(`${SUPABASE_URL}/rest/v1/linkshell_official_gateways?enabled=eq.true&select=url,name,region`, {
|
|
570
|
-
headers: {
|
|
571
|
-
Authorization: `Bearer ${token}`,
|
|
572
|
-
apikey: SUPABASE_ANON_KEY,
|
|
573
|
-
},
|
|
574
|
-
signal: AbortSignal.timeout(10_000),
|
|
575
|
-
});
|
|
576
|
-
if (res.ok) {
|
|
577
|
-
gateways = (await res.json());
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
catch { }
|
|
581
|
-
if (gateways.length === 0) {
|
|
582
|
-
process.stderr.write(" No official gateways available.\n\n");
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
process.stderr.write(" Official Gateways:\n\n");
|
|
586
|
-
for (const gw of gateways) {
|
|
587
|
-
const label = gw.region ? `${gw.name} (${gw.region})` : gw.name;
|
|
588
|
-
// Fetch user's sessions on this gateway
|
|
589
|
-
let sessions = [];
|
|
590
|
-
try {
|
|
591
|
-
const res = await fetch(`${gw.url}/sessions/mine`, {
|
|
592
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
593
|
-
signal: AbortSignal.timeout(5_000),
|
|
594
|
-
});
|
|
595
|
-
if (res.ok) {
|
|
596
|
-
const body = (await res.json());
|
|
597
|
-
sessions = body.sessions;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
catch { }
|
|
601
|
-
process.stderr.write(` \x1b[32m✓\x1b[0m ${label}\n`);
|
|
602
|
-
const wsUrl = gw.url.replace(/\/$/, "").replace(/^https:/, "wss:").replace(/^http:/, "ws:") + "/ws";
|
|
603
|
-
process.stderr.write(` ${wsUrl}\n`);
|
|
604
|
-
if (sessions.length === 0) {
|
|
605
|
-
process.stderr.write(" (no active sessions)\n\n");
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
for (const s of sessions) {
|
|
609
|
-
const ago = Math.round((Date.now() - s.lastActivity) / 60_000);
|
|
610
|
-
const agoStr = ago < 1 ? "just now" : `${ago}m ago`;
|
|
611
|
-
const info = [s.provider, s.projectName].filter(Boolean).join(" · ");
|
|
612
|
-
const hostIcon = s.hasHost ? "\x1b[32m●\x1b[0m" : "\x1b[31m●\x1b[0m";
|
|
613
|
-
process.stderr.write(` └ ${hostIcon} ${s.id.slice(0, 8)} — ${info || "unknown"} · ${agoStr}\n`);
|
|
614
|
-
}
|
|
615
|
-
process.stderr.write("\n");
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
process.stderr.write(" Connect: linkshell start --gateway <url>\n\n");
|
|
302
|
+
// `linkshell` by itself, on a computer where nothing has been set up yet: the setup, rather than a page of help.
|
|
303
|
+
program.action(async () => {
|
|
304
|
+
const { setupDone } = await import("./commands/setup.js");
|
|
305
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true || (await setupDone()))
|
|
306
|
+
return program.help();
|
|
307
|
+
await runSetup(pkg.version);
|
|
308
|
+
process.exit(process.exitCode ?? 0);
|
|
619
309
|
});
|
|
620
310
|
program.parseAsync(process.argv).catch((error) => {
|
|
621
311
|
const message = error instanceof Error ? error.message : String(error);
|