linkshell-cli 0.6.2 → 0.6.3
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 +47 -132
- package/dist/cli/src/auth.js +1 -1
- package/dist/cli/src/auth.js.map +1 -1
- package/dist/cli/src/commands/doctor.d.ts +1 -1
- package/dist/cli/src/commands/doctor.js +76 -36
- package/dist/cli/src/commands/doctor.js.map +1 -1
- package/dist/cli/src/commands/host.d.ts +14 -0
- package/dist/cli/src/commands/host.js +94 -15
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/src/commands/logout.d.ts +1 -1
- package/dist/cli/src/commands/logout.js +10 -2
- package/dist/cli/src/commands/logout.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +11 -0
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/config.js +1 -1
- package/dist/cli/src/config.js.map +1 -1
- package/dist/cli/src/index.js +94 -114
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/carry.d.ts +15 -0
- package/dist/host/src/carry.js +83 -0
- package/dist/host/src/carry.js.map +1 -0
- package/dist/host/src/drivers/acp/driver.d.ts +13 -3
- package/dist/host/src/drivers/acp/driver.js +43 -8
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/mapper.d.ts +5 -1
- package/dist/host/src/drivers/acp/mapper.js +53 -5
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.d.ts +53 -8
- package/dist/host/src/drivers/claude/driver.js +357 -40
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/holders.d.ts +9 -0
- package/dist/host/src/drivers/claude/holders.js +61 -0
- package/dist/host/src/drivers/claude/holders.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +100 -62
- package/dist/host/src/drivers/claude/transcript.js +246 -26
- package/dist/host/src/drivers/claude/transcript.js.map +1 -1
- package/dist/host/src/drivers/codex/commands.d.ts +27 -0
- package/dist/host/src/drivers/codex/commands.js +23 -0
- package/dist/host/src/drivers/codex/commands.js.map +1 -0
- package/dist/host/src/drivers/codex/driver.d.ts +15 -1
- package/dist/host/src/drivers/codex/driver.js +110 -1
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/mapper.d.ts +4 -0
- package/dist/host/src/drivers/codex/mapper.js +29 -5
- package/dist/host/src/drivers/codex/mapper.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +34 -3
- package/dist/host/src/fs.d.ts +26 -4
- package/dist/host/src/fs.js +71 -17
- package/dist/host/src/fs.js.map +1 -1
- package/dist/host/src/gateway.d.ts +3 -0
- package/dist/host/src/gateway.js +9 -0
- package/dist/host/src/gateway.js.map +1 -1
- package/dist/host/src/host.d.ts +7 -3
- package/dist/host/src/host.js +42 -18
- package/dist/host/src/host.js.map +1 -1
- package/dist/host/src/hub.d.ts +91 -15
- package/dist/host/src/hub.js +556 -33
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/rpc/server.d.ts +4 -2
- package/dist/host/src/rpc/server.js +51 -6
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen.d.ts +11 -0
- package/dist/host/src/screen.js +54 -2
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/slim.d.ts +17 -0
- package/dist/host/src/slim.js +66 -0
- package/dist/host/src/slim.js.map +1 -0
- package/dist/host/src/store.d.ts +75 -3
- package/dist/host/src/store.js +216 -16
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/terminals.d.ts +4 -4
- package/dist/host/src/worktrees.d.ts +36 -0
- package/dist/host/src/worktrees.js +96 -0
- package/dist/host/src/worktrees.js.map +1 -0
- package/dist/wire/src/model.d.ts +301 -153
- package/dist/wire/src/model.js +55 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/relay-client.d.ts +2 -0
- package/dist/wire/src/relay-client.js +12 -0
- package/dist/wire/src/relay-client.js.map +1 -1
- package/dist/wire/src/rpc.d.ts +6740 -1022
- package/dist/wire/src/rpc.js +130 -7
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +498 -498
- package/package.json +5 -5
- package/src/auth.ts +1 -1
- package/src/commands/doctor.ts +76 -34
- package/src/commands/host.ts +98 -14
- package/src/commands/logout.ts +10 -2
- package/src/commands/upgrade.ts +11 -0
- package/src/config.ts +1 -1
- package/src/index.ts +93 -131
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkshell-cli",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "Follow, steer and approve the coding agents on your computer (Claude Code, Codex, Gemini, Copilot and more) from your phone",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"qrcode-terminal": "^0.12.0",
|
|
37
37
|
"ws": "^8.18.1",
|
|
38
38
|
"zod": "^3.24.3",
|
|
39
|
-
"@linkshell/gateway": "0.5.
|
|
40
|
-
"@linkshell/host": "0.1.1",
|
|
39
|
+
"@linkshell/gateway": "0.5.3",
|
|
41
40
|
"@linkshell/protocol": "0.4.10",
|
|
42
|
-
"@linkshell/wire": "0.1.
|
|
41
|
+
"@linkshell/wire": "0.1.3",
|
|
42
|
+
"@linkshell/host": "0.1.2"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
45
|
"@anthropic-ai/claude-agent-sdk": "^0.2.128",
|
package/src/auth.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
|
-
const AUTH_DIR = join(homedir(), ".linkshell");
|
|
5
|
+
const AUTH_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
|
|
6
6
|
const AUTH_FILE = join(AUTH_DIR, "auth.json");
|
|
7
7
|
|
|
8
8
|
export interface AuthTokens {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import {
|
|
3
|
+
import { loadAuth } from "../auth.js";
|
|
4
|
+
import { hostRuntimeOk, resolveGateway, withRunningHost } from "./host.js";
|
|
4
5
|
|
|
5
6
|
const requireFromCli = createRequire(import.meta.url);
|
|
6
7
|
|
|
@@ -26,32 +27,32 @@ function which(bin: string): string | undefined {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
async function checkGateway(url: string): Promise<CheckResult> {
|
|
30
|
+
async function checkGateway(name: string, url: string): Promise<CheckResult> {
|
|
30
31
|
try {
|
|
31
|
-
const httpUrl = url.replace(/\/ws\/?$/, "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
32
|
+
const httpUrl = url.replace(/\/(ws|v2\/connect)\/?$/, "").replace(/\/$/, "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
32
33
|
const start = Date.now();
|
|
33
34
|
const res = await fetch(`${httpUrl}/healthz`, { signal: AbortSignal.timeout(5000) });
|
|
34
35
|
const latency = Date.now() - start;
|
|
35
|
-
if (!res.ok) return { name
|
|
36
|
-
return { name
|
|
36
|
+
if (!res.ok) return { name, ok: false, detail: `${url}: HTTP ${res.status}` };
|
|
37
|
+
return { name, ok: true, detail: `${url} answers (${latency}ms)` };
|
|
37
38
|
} catch (e) {
|
|
38
|
-
return { name
|
|
39
|
+
return { name, ok: false, detail: `${url}: ${e instanceof Error ? e.message : "unreachable"}` };
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const AGENT_INSTALL: Record<string, string> = {
|
|
44
|
+
claude: "npm i -g @anthropic-ai/claude-code",
|
|
45
|
+
codex: "npm i -g @openai/codex",
|
|
46
|
+
};
|
|
45
47
|
|
|
48
|
+
export async function runDoctor(version: string, gatewayUrl?: string): Promise<void> {
|
|
46
49
|
process.stdout.write("\n LinkShell Doctor\n\n");
|
|
47
50
|
|
|
48
51
|
const results: CheckResult[] = [];
|
|
49
52
|
|
|
50
53
|
results.push(check("Node.js", () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (major < 18) throw new Error(`v${ver} (need >= 18)`);
|
|
54
|
-
return `v${ver}`;
|
|
54
|
+
if (!hostRuntimeOk()) throw new Error(`v${process.versions.node} (the host needs 22.13 or newer)`);
|
|
55
|
+
return `v${process.versions.node}`;
|
|
55
56
|
}));
|
|
56
57
|
|
|
57
58
|
results.push(check("node-pty", () => {
|
|
@@ -69,32 +70,73 @@ export async function runDoctor(gatewayUrl?: string): Promise<void> {
|
|
|
69
70
|
}
|
|
70
71
|
}));
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
const host = await withRunningHost(async (client) => ({
|
|
74
|
+
info: await client.call("machine.info", {}),
|
|
75
|
+
gateway: await client.call("gateway.status", {}),
|
|
76
|
+
})).catch(() => undefined);
|
|
77
|
+
|
|
78
|
+
if (!host) {
|
|
79
|
+
results.push({ name: "Host", ok: false, detail: "not running — start it with: linkshell host --daemon" });
|
|
80
|
+
// Without the host, at least say whether the two main agents are on the PATH.
|
|
81
|
+
for (const [bin, hint] of Object.entries(AGENT_INSTALL)) {
|
|
82
|
+
const path = which(bin);
|
|
83
|
+
results.push({ name: `${bin} (optional)`, ok: true, detail: path ? `found (${path})` : `not installed — ${hint}` });
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
results.push(
|
|
87
|
+
host.info.hostVersion === version
|
|
88
|
+
? { name: "Host", ok: true, detail: `running, ${version}` }
|
|
89
|
+
: {
|
|
90
|
+
name: "Host",
|
|
91
|
+
ok: false,
|
|
92
|
+
detail: `running ${host.info.hostVersion}, but this CLI is ${version} — update it with: linkshell host stop && linkshell host --daemon`,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
// Agents as the host sees them: what the phone can actually start.
|
|
96
|
+
for (const agent of host.info.agents) {
|
|
97
|
+
if (!agent.installed) {
|
|
98
|
+
const hint = AGENT_INSTALL[agent.id];
|
|
99
|
+
if (hint) results.push({ name: `${agent.label} (optional)`, ok: true, detail: `not installed — ${hint}` });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const signedOut = agent.auth?.state === "missing";
|
|
103
|
+
const problem = agent.problem ?? (signedOut ? (agent.auth?.hint ?? "not logged in") : undefined);
|
|
104
|
+
results.push({ name: agent.label, ok: !problem, detail: `v${agent.version ?? "?"}${problem ? ` — ${problem}` : ""}` });
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}));
|
|
108
|
+
const auth = loadAuth();
|
|
109
|
+
results.push({
|
|
110
|
+
name: "Account",
|
|
111
|
+
ok: true,
|
|
112
|
+
detail: auth?.accessToken ? `logged in as ${auth.email || auth.userId}` : "not logged in (only the official gateway needs it: linkshell login)",
|
|
113
|
+
});
|
|
92
114
|
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
if (host) {
|
|
116
|
+
const gateway = host.gateway;
|
|
117
|
+
if (gateway.status === "online") {
|
|
118
|
+
const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
|
|
119
|
+
const paired = gateway.devices.length ? `, ${gateway.devices.length} paired device(s)` : "";
|
|
120
|
+
results.push({ name: "Gateway", ok: true, detail: `online ${gateway.url}${account}${paired}` });
|
|
121
|
+
} else if (gateway.status === "off") {
|
|
122
|
+
results.push({
|
|
123
|
+
name: "Gateway",
|
|
124
|
+
ok: false,
|
|
125
|
+
detail: "off, so a phone can't reach this computer — linkshell login (official gateway), or linkshell host --gateway <url>",
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
results.push({ name: "Gateway", ok: false, detail: `${gateway.status} ${gateway.url}${gateway.error ? ` — ${gateway.error.message}` : ""}` });
|
|
129
|
+
}
|
|
130
|
+
} else if (!gatewayUrl && hostRuntimeOk()) {
|
|
131
|
+
const { defaultHome } = await import("@linkshell/host");
|
|
132
|
+
const chosen = resolveGateway(defaultHome());
|
|
133
|
+
results.push(
|
|
134
|
+
chosen
|
|
135
|
+
? await checkGateway("Gateway", chosen)
|
|
136
|
+
: { name: "Gateway", ok: false, detail: "none chosen — linkshell login (official gateway), or linkshell host --gateway <url>" },
|
|
137
|
+
);
|
|
97
138
|
}
|
|
139
|
+
if (gatewayUrl) results.push(await checkGateway(host ? "Gateway (--gateway)" : "Gateway", gatewayUrl));
|
|
98
140
|
|
|
99
141
|
for (const r of results) {
|
|
100
142
|
const icon = r.ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
package/src/commands/host.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import type { GatewayStatus } from "@linkshell/wire";
|
|
4
5
|
import { getValidToken, isLoggedIn } from "../auth.js";
|
|
5
6
|
import * as daemon from "../utils/daemon.js";
|
|
6
7
|
|
|
@@ -30,7 +31,8 @@ export const OFFICIAL_GATEWAY = "wss://gateway.itool.tech";
|
|
|
30
31
|
export function resolveGateway(home: string): string | undefined {
|
|
31
32
|
const chosen = process.env.LINKSHELL_GATEWAY || readHostConfig(home).gateway;
|
|
32
33
|
if (chosen === "off") return undefined;
|
|
33
|
-
|
|
34
|
+
// config.json is shared with the v1 bridge, whose gateway URLs end in /ws.
|
|
35
|
+
if (chosen) return chosen.replace(/\/ws\/?$/, "");
|
|
34
36
|
return isLoggedIn() ? OFFICIAL_GATEWAY : undefined;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -52,9 +54,13 @@ export function writeHostConfig(home: string, config: HostConfig): void {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** The host keeps its state in node:sqlite, unflagged from Node 22.13. */
|
|
55
|
-
export function
|
|
57
|
+
export function hostRuntimeOk(): boolean {
|
|
56
58
|
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
57
|
-
|
|
59
|
+
return major > 22 || (major === 22 && minor >= 13);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertHostRuntime(): void {
|
|
63
|
+
if (hostRuntimeOk()) return;
|
|
58
64
|
process.stderr.write(
|
|
59
65
|
`\n LinkShell's host needs Node.js 22.13 or newer (this is ${process.version}).\n` +
|
|
60
66
|
" Upgrade Node (for example: nvm install 22, or brew upgrade node), then try again.\n\n",
|
|
@@ -83,12 +89,13 @@ export async function runHostForeground(version: string): Promise<void> {
|
|
|
83
89
|
const devPort = process.env.LINKSHELL_DEV_PORT ? Number(process.env.LINKSHELL_DEV_PORT) : undefined;
|
|
84
90
|
// Where devices reach this machine from outside the LAN. The account from
|
|
85
91
|
// `linkshell login`, if any, lets that account's devices in without pairing.
|
|
86
|
-
|
|
92
|
+
// Asked again on `gateway.refresh`, so login, logout and `--gateway` apply to a running host.
|
|
93
|
+
const home = host.defaultHome();
|
|
87
94
|
const running = await host.startHost({
|
|
88
95
|
version,
|
|
89
96
|
env,
|
|
90
97
|
log,
|
|
91
|
-
gateway:
|
|
98
|
+
gateway: { url: () => resolveGateway(home), token: async () => (await getValidToken()) ?? undefined },
|
|
92
99
|
tcpPort: devPort,
|
|
93
100
|
claudeCommand: process.env.LINKSHELL_CLAUDE_COMMAND || undefined,
|
|
94
101
|
claudeAdapter: adapter ? { command: adapter, args: [] } : undefined,
|
|
@@ -127,6 +134,89 @@ export async function ensureHostRunning(): Promise<string> {
|
|
|
127
134
|
throw new Error(`LinkShell host did not start; see ${daemon.getLogFile("host")}`);
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
type HostClient = Awaited<ReturnType<Awaited<ReturnType<typeof loadHost>>["connectHost"]>>;
|
|
138
|
+
|
|
139
|
+
/** Runs `use` against the running host; undefined when there is none. */
|
|
140
|
+
export async function withRunningHost<T>(use: (client: HostClient) => Promise<T>): Promise<T | undefined> {
|
|
141
|
+
if (!hostRuntimeOk()) return undefined;
|
|
142
|
+
const host = await loadHost();
|
|
143
|
+
const { hostSocket } = host.hostPaths();
|
|
144
|
+
if (!(await host.isHostRunning(hostSocket))) return undefined;
|
|
145
|
+
const client = await host.connectHost(hostSocket);
|
|
146
|
+
try {
|
|
147
|
+
return await use(client);
|
|
148
|
+
} finally {
|
|
149
|
+
client.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Has the running host apply the current gateway choice and account, then
|
|
155
|
+
* waits (up to `waitMs`) for it to come online or be refused.
|
|
156
|
+
*/
|
|
157
|
+
export async function refreshGateway(waitMs: number): Promise<GatewayStatus | undefined> {
|
|
158
|
+
return withRunningHost(async (client) => {
|
|
159
|
+
let status: GatewayStatus;
|
|
160
|
+
try {
|
|
161
|
+
status = await client.call("gateway.refresh", {});
|
|
162
|
+
} catch {
|
|
163
|
+
// A host from before 0.6.3 (still running after an upgrade) only reads its gateway at start.
|
|
164
|
+
process.stderr.write(" The running host is older than this CLI. Restart it to apply this (stops running agent turns):\n");
|
|
165
|
+
process.stderr.write(" linkshell host stop && linkshell host --daemon\n");
|
|
166
|
+
return client.call("gateway.status", {});
|
|
167
|
+
}
|
|
168
|
+
const deadline = Date.now() + waitMs;
|
|
169
|
+
while (status.status === "connecting" && Date.now() < deadline) {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
171
|
+
status = await client.call("gateway.status", {});
|
|
172
|
+
}
|
|
173
|
+
return status;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function describeGateway(gateway: GatewayStatus): string {
|
|
178
|
+
if (gateway.status === "off") return "off (linkshell login for the official one, or linkshell host --gateway <url>)";
|
|
179
|
+
const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
|
|
180
|
+
const problem = gateway.error ? ` ⚠ ${gateway.error.message}` : "";
|
|
181
|
+
return `${gateway.status} ${gateway.url}${account}${problem}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** After `linkshell login`: this computer joins the gateway its account is for. */
|
|
185
|
+
export async function joinGatewayAfterLogin(plan: "pro" | "free"): Promise<void> {
|
|
186
|
+
const out = (line: string) => process.stderr.write(` ${line}\n`);
|
|
187
|
+
if (!hostRuntimeOk()) {
|
|
188
|
+
out(`The LinkShell host needs Node.js 22.13 or newer (this is ${process.version}); upgrade Node, then: linkshell host --daemon\n`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const host = await loadHost();
|
|
192
|
+
const chosen = process.env.LINKSHELL_GATEWAY || readHostConfig(host.defaultHome()).gateway;
|
|
193
|
+
if (chosen === "off") {
|
|
194
|
+
out("The gateway is turned off on this computer. To use the official one: linkshell host --gateway default\n");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!chosen && plan !== "pro") {
|
|
198
|
+
// A running host now gets the gateway's own answer, instead of an old login's.
|
|
199
|
+
await refreshGateway(0);
|
|
200
|
+
out("The official gateway needs a Pro subscription: https://itool.tech");
|
|
201
|
+
out("Or use your own gateway: linkshell host --gateway <url>, then linkshell pair\n");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await ensureHostRunning();
|
|
205
|
+
const gateway = await refreshGateway(15_000);
|
|
206
|
+
const where = chosen ? (gateway?.url ?? chosen) : "the official gateway";
|
|
207
|
+
if (gateway?.status === "online") {
|
|
208
|
+
out(`\x1b[32m✓\x1b[0m This computer is online on ${where}.`);
|
|
209
|
+
out(
|
|
210
|
+
gateway.account
|
|
211
|
+
? " Open LinkShell on your phone and sign in with the same account: it shows up there, no pairing needed.\n"
|
|
212
|
+
: " Pair your phone with: linkshell pair\n",
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
out(`\x1b[31m✗\x1b[0m Not connected to ${where}${gateway?.error ? `: ${gateway.error.message}` : " yet"}`);
|
|
216
|
+
out(" Check again with: linkshell host status\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
130
220
|
export async function printHostStatus(): Promise<void> {
|
|
131
221
|
const host = await loadHost();
|
|
132
222
|
const { hostSocket } = host.hostPaths();
|
|
@@ -153,15 +243,9 @@ export async function printHostStatus(): Promise<void> {
|
|
|
153
243
|
const active = sessions.filter((s) => s.state === "running" || s.state === "waiting");
|
|
154
244
|
process.stdout.write(` Sessions: ${sessions.length} known, ${active.length} active\n`);
|
|
155
245
|
const gateway = await client.call("gateway.status", {});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
|
|
160
|
-
const problem = gateway.error ? ` ⚠ ${gateway.error.message}` : "";
|
|
161
|
-
process.stdout.write(` Gateway: ${gateway.status} ${gateway.url}${account}${problem}\n`);
|
|
162
|
-
for (const device of gateway.devices) {
|
|
163
|
-
process.stdout.write(` ${device.online ? "●" : "○"} ${device.name} paired ${new Date(device.pairedAt).toLocaleDateString()}\n`);
|
|
164
|
-
}
|
|
246
|
+
process.stdout.write(` Gateway: ${describeGateway(gateway)}\n`);
|
|
247
|
+
for (const device of gateway.devices) {
|
|
248
|
+
process.stdout.write(` ${device.online ? "●" : "○"} ${device.name} paired ${new Date(device.pairedAt).toLocaleDateString()}\n`);
|
|
165
249
|
}
|
|
166
250
|
} finally {
|
|
167
251
|
client.close();
|
package/src/commands/logout.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { clearAuth, loadAuth } from "../auth.js";
|
|
2
|
+
import { refreshGateway, withRunningHost } from "./host.js";
|
|
2
3
|
|
|
3
|
-
export function runLogout(): void {
|
|
4
|
+
export async function runLogout(): Promise<void> {
|
|
4
5
|
const auth = loadAuth();
|
|
5
6
|
if (!auth || !auth.accessToken) {
|
|
6
7
|
process.stderr.write("\n Not currently logged in.\n\n");
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
const before = await withRunningHost((client) => client.call("gateway.status", {}));
|
|
10
12
|
clearAuth();
|
|
11
|
-
process.stderr.write(`\n \x1b[32m✓\x1b[0m Logged out${auth.email ? ` (${auth.email})` : ""}.\n
|
|
13
|
+
process.stderr.write(`\n \x1b[32m✓\x1b[0m Logged out${auth.email ? ` (${auth.email})` : ""}.\n`);
|
|
14
|
+
// A running host leaves the account's gateway now, not at its next reconnect.
|
|
15
|
+
const after = await refreshGateway(0);
|
|
16
|
+
if (before && before.status !== "off" && after?.status === "off") {
|
|
17
|
+
process.stderr.write(" This computer is off the official gateway; the host and its sessions keep running.\n");
|
|
18
|
+
}
|
|
19
|
+
process.stderr.write("\n");
|
|
12
20
|
}
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { withRunningHost } from "./host.js";
|
|
3
4
|
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
5
6
|
const pkg = require("../../../../package.json") as { version: string };
|
|
@@ -31,6 +32,14 @@ async function fetchLatestVersion(): Promise<string | null> {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** The background host keeps running the version it was started with. */
|
|
36
|
+
async function noteRunningHost(latest: string): Promise<void> {
|
|
37
|
+
const running = await withRunningHost((client) => client.call("machine.info", {})).catch(() => undefined);
|
|
38
|
+
if (!running || running.hostVersion === latest) return;
|
|
39
|
+
process.stderr.write(` The running host is still ${running.hostVersion}. Restart it to use ${latest} (stops running agent turns):\n`);
|
|
40
|
+
process.stderr.write(" linkshell host stop && linkshell host --daemon\n\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
export async function runUpgrade(): Promise<void> {
|
|
35
44
|
const current = pkg.version;
|
|
36
45
|
process.stderr.write(`\n Current version: ${current}\n`);
|
|
@@ -55,6 +64,7 @@ export async function runUpgrade(): Promise<void> {
|
|
|
55
64
|
try {
|
|
56
65
|
execSync("brew upgrade linkshell", { stdio: "inherit", timeout: 120_000 });
|
|
57
66
|
process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
|
|
67
|
+
await noteRunningHost(latest);
|
|
58
68
|
} catch {
|
|
59
69
|
process.stderr.write("\n \x1b[31m✗\x1b[0m Homebrew upgrade failed. Try manually:\n");
|
|
60
70
|
process.stderr.write(" brew upgrade linkshell\n\n");
|
|
@@ -65,6 +75,7 @@ export async function runUpgrade(): Promise<void> {
|
|
|
65
75
|
try {
|
|
66
76
|
execSync("npm install -g linkshell-cli@latest", { stdio: "inherit", timeout: 120_000 });
|
|
67
77
|
process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
|
|
78
|
+
await noteRunningHost(latest);
|
|
68
79
|
} catch {
|
|
69
80
|
process.stderr.write("\n \x1b[31m✗\x1b[0m npm upgrade failed. Try manually:\n");
|
|
70
81
|
process.stderr.write(" npm install -g linkshell-cli@latest\n\n");
|
package/src/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface LinkShellConfig {
|
|
|
14
14
|
rows?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const CONFIG_DIR = join(homedir(), ".linkshell");
|
|
17
|
+
const CONFIG_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
|
|
18
18
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
19
19
|
|
|
20
20
|
export function loadConfig(): LinkShellConfig {
|