linkshell-cli 0.10.65 → 0.10.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkshell-cli",
3
- "version": "0.10.65",
3
+ "version": "0.10.66",
4
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/cli/src/index.js",
@@ -34,9 +34,9 @@
34
34
  "node-pty": "^1.1.0",
35
35
  "qrcode-terminal": "^0.12.0",
36
36
  "ws": "^8.21.0",
37
- "@linkshell/gateway": "0.6.37",
37
+ "@linkshell/host": "0.4.63",
38
38
  "@linkshell/wire": "0.2.20",
39
- "@linkshell/host": "0.4.62"
39
+ "@linkshell/gateway": "0.6.37"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/ws": "^8.5.14",
@@ -12,6 +12,7 @@ import { sep, join } from "node:path";
12
12
  import { ensureHostRunning, silenceSqliteWarning, withRunningHost } from "./host.js";
13
13
  import { linkshellDir, readHostLaunch, readHostOwner, readPid, stopDaemonAndWait } from "../utils/daemon.js";
14
14
  import { cachedMembership, redeemDesktopPro, refreshMembership } from "../pro-membership.js";
15
+ import { accessPasswordEnabled, applyAccessPassword } from "./password.js";
15
16
 
16
17
  export function installationSource(path: string): "homebrew" | "npm" {
17
18
  return path.includes(`${sep}Cellar${sep}linkshell${sep}`) ? "homebrew" : "npm";
@@ -27,7 +28,8 @@ export async function desktopSnapshot(version: string) {
27
28
  let executable = process.argv[1]!;
28
29
  try { executable = realpathSync(executable); } catch { /* Retain the launch path for diagnosis. */ }
29
30
  const account = desktopAccount();
30
- const base = { account, schema: 1, cliVersion: version, home: linkshellDir(), source: standaloneBundle() ? "standalone" : installationSource(executable), executable, hostOwner: readPid("host") ? readHostOwner(readPid("host")!) ?? "external" : "none", appBundle: standaloneBundle() };
31
+ const access = { password: await accessPasswordEnabled().catch(() => false) };
32
+ const base = { account, access, schema: 1, cliVersion: version, home: linkshellDir(), source: standaloneBundle() ? "standalone" : installationSource(executable), executable, hostOwner: readPid("host") ? readHostOwner(readPid("host")!) ?? "external" : "none", appBundle: standaloneBundle() };
31
33
  const result = await withRunningHost(async (client) => {
32
34
  const machine = await client.call("machine.info", {}, 5000);
33
35
  const [gateway, sessions] = await Promise.all([
@@ -40,6 +42,22 @@ export async function desktopSnapshot(version: string) {
40
42
  return result ? { ...base, status: "running", ...result } : { ...base, status: readPid("host") ? "unreachable" : "stopped" };
41
43
  }
42
44
 
45
+ /** The new password, as JSON on stdin (never on the command line, where `ps` shows it). */
46
+ async function passwordInput(input: AsyncIterable<string | Buffer>): Promise<string> {
47
+ const chunks: Buffer[] = [];
48
+ let length = 0;
49
+ for await (const chunk of input) {
50
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
51
+ length += buffer.length;
52
+ if (length > 4096) throw new Error("密码过长,请输入 4 到 128 位");
53
+ chunks.push(buffer);
54
+ }
55
+ let password: unknown;
56
+ try { password = JSON.parse(Buffer.concat(chunks).toString("utf8"))?.password; } catch { /* Never echo the input. */ }
57
+ if (typeof password !== "string" || password.length < 4 || password.length > 128) throw new Error("密码需要 4 到 128 位");
58
+ return password;
59
+ }
60
+
43
61
  async function redemptionInput(input: AsyncIterable<string | Buffer>): Promise<{ code: string; userId: string }> {
44
62
  const chunks: Buffer[] = [];
45
63
  let length = 0;
@@ -122,6 +140,8 @@ export async function runDesktop(version: string, action: string, input: AsyncIt
122
140
  await ensureHostRunning();
123
141
  await refreshGateway(15_000);
124
142
  }
143
+ else if (action === "set-password") await applyAccessPassword(await passwordInput(input));
144
+ else if (action === "remove-password") await applyAccessPassword(undefined);
125
145
  else if (action === "prepare-update") { await prepareDesktopUpdate(); }
126
146
  else if (action === "recover-update") { await recoverDesktopUpdate(version); }
127
147
  else if (action === "stop-update-host") { await stopDesktopUpdateHost(); }
@@ -1,6 +1,6 @@
1
1
  import { RpcError } from "@linkshell/wire";
2
2
  import { linkshellDir } from "../utils/daemon.js";
3
- import { assertHostRuntime, withRunningHost } from "./host.js";
3
+ import { assertHostRuntime, silenceSqliteWarning, withRunningHost } from "./host.js";
4
4
 
5
5
  /** Reads a line without echoing it; from a pipe, just the first line. */
6
6
  function readHidden(prompt: string): Promise<string> {
@@ -44,7 +44,7 @@ function unknownMethod(error: unknown): boolean {
44
44
  }
45
45
 
46
46
  /** Applies the change through the running host, or writes it for the next start. */
47
- async function apply(password: string | undefined): Promise<"live" | "restart" | "file"> {
47
+ export async function applyAccessPassword(password: string | undefined): Promise<"live" | "restart" | "file"> {
48
48
  try {
49
49
  const result = await withRunningHost((client) => client.call("access.set", password ? { password } : {}));
50
50
  if (result) return "live";
@@ -60,8 +60,17 @@ async function apply(password: string | undefined): Promise<"live" | "restart" |
60
60
  return "file";
61
61
  }
62
62
 
63
+ /** Whether this computer has an access password: the running host's answer, else the file. */
64
+ export async function accessPasswordEnabled(): Promise<boolean> {
65
+ const live = await withRunningHost((client) => client.call("access.status", {})).catch(() => undefined);
66
+ if (live) return live.password;
67
+ const host = await import("@linkshell/host");
68
+ return new host.AccessControl(linkshellDir()).enabled;
69
+ }
70
+
63
71
  export async function runPassword(action: "status" | "set" | "remove"): Promise<void> {
64
72
  assertHostRuntime();
73
+ silenceSqliteWarning();
65
74
  const host = await import("@linkshell/host");
66
75
  if (action === "status") {
67
76
  const live = await withRunningHost((client) => client.call("access.status", {})).catch(() => undefined);
@@ -84,7 +93,7 @@ export async function runPassword(action: "status" | "set" | "remove"): Promise<
84
93
  throw error;
85
94
  }
86
95
  }
87
- const how = await apply(password);
96
+ const how = await applyAccessPassword(password);
88
97
  const what = password ? "Access password set." : "Access password removed.";
89
98
  const when =
90
99
  how === "live"