alink-cli 0.7.4 → 0.8.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 CHANGED
@@ -6,17 +6,35 @@
6
6
  npx alink-cli
7
7
  ```
8
8
 
9
- 首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可上线;密码不会保存到本机。
9
+ 直接运行会进入基于 Ink 的交互式 TUI。TUI 采用扁平菜单,最多两级:
10
+
11
+ - 顶部状态栏实时显示 daemon 是否运行、PID、Hub、凭证状态。
12
+ - 主菜单:启动 / 停止 / 重启 daemon、查看状态详情、登录 / 配对 / 登出、查看机器、设置、帮助、退出。
13
+ - 启动 daemon 后它在后台运行,退出 TUI 不会停止它。
14
+
15
+ 首次运行输入 AgentLink 账号和密码,这台电脑会自动加入账号并上线。之后直接重复 `npx alink-cli` 即可管理或启动后台服务;密码不会保存到本机。
10
16
 
11
17
  ```bash
18
+ npm install -g alink-cli
19
+ alink-cli help
20
+ alink-cli login
21
+ alink-cli status
22
+ alink-cli machines
23
+ alink-cli logout
24
+
12
25
  npx alink-cli --hub wss://your-hub.example.com
13
26
  npx alink-cli --dir /path/to/project
14
- npx alink-cli logout
15
27
  ```
16
28
 
29
+ - 全局安装后可直接使用 `alink-cli`(兼容旧命令 `agentlink`)。
30
+ - `login`:登录账号并注册这台电脑,但不启动服务。
31
+ - `status`:查看当前登录状态和 Hub。
32
+ - `machines`:验证账号密码后查看账号下的所有机器,不展示内部凭证。
33
+ - `logout`:退出当前账号并清除本机登录记录,下次运行重新输入账号密码。
17
34
  - `--hub`:Hub 地址,默认 `wss://link.harmopath.com`。
18
35
  - `--dir`:默认工作目录,缺省为当前目录。
19
- - `logout`:退出当前账号并清除本机登录记录,下次运行重新输入账号密码。
20
36
  - `--legacy`:临时运行旧 daemon;默认始终是 daemon-t3。
37
+ - `--no-tui` / `AGENTLINK_TUI=0`:跳过交互式 TUI,按旧行为前台启动 daemon(适合脚本或非 TTY 环境)。
38
+ - `CI=true` 或非 TTY 环境会自动跳过 TUI。
21
39
 
22
40
  Node.js 22.16+。连接为出站 WebSocket,无需端口转发;Hub 只转发 E2EE 密文。
@@ -1,6 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { createRequire } from "node:module";
2
3
 
3
- const MACHINE_BUCKET = "agentlink-machines";
4
+ // CloudBase's Node adapter resolves optional dependencies through a runtime
5
+ // `require`; expose this package's resolver so its bundled `ws` dependency is found.
6
+ globalThis.require ??= createRequire(import.meta.url);
7
+
8
+ const MACHINE_TABLE = "agentlink_machine_keys";
4
9
  const REQUEST_TIMEOUT_MS = 10_000;
5
10
 
6
11
  function httpUrl(hub, pathname) {
@@ -14,12 +19,20 @@ async function json(response) {
14
19
  return response.json().catch(() => ({}));
15
20
  }
16
21
 
17
- export async function loadAgentLinkAccountConfig(hub, fetchFn = fetch) {
18
- const response = await fetchFn(httpUrl(hub, "/api/config"), {
19
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
20
- });
21
- if (!response.ok) throw new Error(`Hub 配置读取失败 (${response.status})。`);
22
- return json(response);
22
+ function timestamp(value) {
23
+ const parsed = typeof value === "number" ? value : Number(value);
24
+ return Number.isFinite(parsed) ? parsed : undefined;
25
+ }
26
+
27
+ function normalizeMachineRow(row) {
28
+ const createdAt = timestamp(row?.created_at);
29
+ return {
30
+ machineId: row?.machine_id,
31
+ ...(row?.name ? { name: row.name } : {}),
32
+ ...(row?.hostname ? { hostname: row.hostname } : {}),
33
+ ...(row?.enckey ? { enckey: row.enckey } : {}),
34
+ ...(createdAt !== undefined ? { createdAt } : {}),
35
+ };
23
36
  }
24
37
 
25
38
  function machineIdFromToken(authToken) {
@@ -33,18 +46,12 @@ function machineIdFromToken(authToken) {
33
46
  }
34
47
  }
35
48
 
36
- async function readMachines(bucket, path) {
37
- const { data, error } = await bucket.download(path);
38
- if (error) {
39
- if (/not found|no such|404/i.test(error.message || "")) return [];
40
- throw new Error(`读取账号机器列表失败:${error.message}`);
41
- }
42
- try {
43
- const parsed = JSON.parse(await data.text());
44
- return Array.isArray(parsed.machines) ? parsed.machines : [];
45
- } catch {
46
- throw new Error("账号机器列表格式损坏,请在 Web 端移除异常记录后重试。");
47
- }
49
+ export async function loadAgentLinkAccountConfig(hub, fetchFn = fetch) {
50
+ const response = await fetchFn(httpUrl(hub, "/api/config"), {
51
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
52
+ });
53
+ if (!response.ok) throw new Error(`Hub 配置读取失败 (${response.status})。`);
54
+ return json(response);
48
55
  }
49
56
 
50
57
  export async function registerAgentLinkAccountMachine({
@@ -103,31 +110,56 @@ export async function registerAgentLinkAccountMachine({
103
110
  try {
104
111
  const user = await auth.getCurrentUser();
105
112
  if (!user?.uid) throw new Error("CloudBase 账号登录状态无效。");
106
- const path = `${user.uid}/machines.json`;
107
- const bucket = app.storage.from(MACHINE_BUCKET);
108
- const machines = await readMachines(bucket, path);
109
- const current = machines.find((machine) => machine?.machineId === machineId);
113
+
114
+ const database = app.rdb();
115
+ const { data, error: readError } = await database
116
+ .from(MACHINE_TABLE)
117
+ .select("machine_id,connected_at,created_at")
118
+ .order("updated_at", { ascending: false });
119
+ if (readError) throw new Error(`读取账号机器记录失败:${readError.message}`);
120
+ const current = (Array.isArray(data) ? data : []).find((row) => row?.machine_id === machineId);
121
+
110
122
  const enckey = randomBytesFn(32).toString("base64url");
111
- const machine = {
112
- machineId,
113
- name: machineName,
114
- hostname: machineName,
115
- createdAt: current?.createdAt ?? Date.now(),
116
- enckey,
117
- };
118
- // ponytail: read-modify-write is enough for normal first-time setup; add
119
- // versioned retries if concurrent registrations for one account become common.
120
- const { error } = await bucket.update(
121
- path,
122
- JSON.stringify({
123
- version: 2,
124
- machines: [machine, ...machines.filter((entry) => entry?.machineId !== machineId)],
125
- }),
126
- { contentType: "application/json" },
123
+ const now = Date.now();
124
+ const { error } = await database.from(MACHINE_TABLE).upsert(
125
+ {
126
+ machine_id: machineId,
127
+ name: machineName,
128
+ hostname: machineName,
129
+ enckey,
130
+ connected_at: current?.connected_at ?? null,
131
+ created_at: current?.created_at ?? now,
132
+ updated_at: now,
133
+ },
134
+ { onConflict: "owner_id,machine_id" },
127
135
  );
128
- if (error) throw new Error(`保存账号机器列表失败:${error.message}`);
136
+ if (error) throw new Error(`保存账号机器记录失败:${error.message}`);
129
137
  return { credential: `${authToken}.${enckey}`, machineId };
130
138
  } finally {
131
139
  await auth.signOut().catch(() => undefined);
132
140
  }
133
141
  }
142
+
143
+ export async function listAgentLinkAccountMachines({ config, identifier, password, cloudbaseSdk }) {
144
+ if (!config?.cloudbaseEnvId) throw new Error("当前 Hub 尚未配置账号机器目录。");
145
+ const sdk = cloudbaseSdk ?? (await import("@cloudbase/js-sdk")).default;
146
+ const app = sdk.init({ env: config.cloudbaseEnvId, region: "ap-shanghai" });
147
+ const auth = app.auth();
148
+ const signedIn = await auth.signInWithPassword(
149
+ identifier.includes("@") ? { email: identifier, password } : { username: identifier, password },
150
+ );
151
+ if (signedIn?.error) throw new Error(signedIn.error.message || "账号或密码错误。");
152
+ try {
153
+ const user = await auth.getCurrentUser();
154
+ if (!user?.uid) throw new Error("CloudBase 账号登录状态无效。");
155
+ const database = app.rdb();
156
+ const { data, error } = await database
157
+ .from(MACHINE_TABLE)
158
+ .select("machine_id,name,hostname,enckey,connected_at,created_at")
159
+ .order("updated_at", { ascending: false });
160
+ if (error) throw new Error(`读取账号机器记录失败:${error.message}`);
161
+ return (Array.isArray(data) ? data : []).map(normalizeMachineRow);
162
+ } finally {
163
+ await auth.signOut().catch(() => undefined);
164
+ }
165
+ }
@@ -0,0 +1,55 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ export async function readPassword(label) {
4
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
5
+ throw new Error(
6
+ "当前终端无法安全读取密码,请在交互式终端运行,或设置 AGENTLINK_ACCOUNT 和 AGENTLINK_PASSWORD。",
7
+ );
8
+ }
9
+ process.stdout.write(label);
10
+ return new Promise((resolve, reject) => {
11
+ let password = "";
12
+ const wasRaw = process.stdin.isRaw;
13
+ const finish = () => {
14
+ process.stdin.off("data", onData);
15
+ process.stdin.setRawMode(wasRaw);
16
+ process.stdin.pause();
17
+ process.stdout.write("\n");
18
+ };
19
+ const onData = (chunk) => {
20
+ for (const char of String(chunk)) {
21
+ if (char === "\u0003") {
22
+ finish();
23
+ reject(new Error("已取消登录。"));
24
+ return;
25
+ }
26
+ if (char === "\r" || char === "\n") {
27
+ finish();
28
+ resolve(password);
29
+ return;
30
+ }
31
+ if (char === "\u007f" || char === "\b") password = password.slice(0, -1);
32
+ else if (char >= " ") password += char;
33
+ }
34
+ };
35
+ process.stdin.setRawMode(true);
36
+ process.stdin.setEncoding("utf8");
37
+ process.stdin.resume();
38
+ process.stdin.on("data", onData);
39
+ });
40
+ }
41
+
42
+ export async function accountCredentials() {
43
+ const fromEnv = process.env.AGENTLINK_ACCOUNT?.trim();
44
+ const passwordFromEnv = process.env.AGENTLINK_PASSWORD;
45
+ if (fromEnv && passwordFromEnv) return { identifier: fromEnv, password: passwordFromEnv };
46
+ if (!process.stdin.isTTY) {
47
+ throw new Error("首次运行需要登录账号,请在交互式终端运行。");
48
+ }
49
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
50
+ const identifier = (await readline.question("AgentLink 账号:")).trim();
51
+ readline.close();
52
+ const password = await readPassword("AgentLink 密码:");
53
+ if (!identifier || !password) throw new Error("账号和密码不能为空。");
54
+ return { identifier, password };
55
+ }
package/bin/agentlink.js CHANGED
@@ -1,22 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomBytes } from "node:crypto";
4
+ import { existsSync } from "node:fs";
4
5
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { hostname, homedir } from "node:os";
6
7
  import { join } from "node:path";
7
- import { createInterface } from "node:readline/promises";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import WebSocket from "ws";
10
10
 
11
11
  import {
12
+ listAgentLinkAccountMachines,
12
13
  loadAgentLinkAccountConfig,
13
14
  registerAgentLinkAccountMachine,
14
15
  } from "./agentlink-account.js";
16
+ import { accountCredentials } from "./agentlink-prompts.js";
15
17
 
16
18
  const OFFICIAL_HUB = process.env.AGENTLINK_DEFAULT_HUB || "wss://link.harmopath.com";
17
19
  const STATE_DIR = process.env.AGENTLINK_STATE_DIR || join(homedir(), ".agentlink");
18
20
  const CREDENTIAL_FILE = join(STATE_DIR, "credential");
19
21
  const args = process.argv.slice(2);
22
+ const legacyCredentialCommands = process.env.AGENTLINK_ENABLE_LEGACY_CREDENTIALS === "1";
23
+ const forceTui = process.env.AGENTLINK_TUI === "1";
24
+ const noTui =
25
+ !forceTui &&
26
+ (args.includes("--no-tui") ||
27
+ process.env.AGENTLINK_TUI === "0" ||
28
+ process.env.CI === "true" ||
29
+ !process.stdin.isTTY);
20
30
 
21
31
  function value(name) {
22
32
  const index = args.findIndex((arg) => arg === `--${name}` || arg.startsWith(`--${name}=`));
@@ -87,60 +97,6 @@ async function createSingleCredential(hub) {
87
97
  return credential;
88
98
  }
89
99
 
90
- async function readPassword(label) {
91
- if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
92
- throw new Error(
93
- "当前终端无法安全读取密码,请在交互式终端运行,或设置 AGENTLINK_ACCOUNT 和 AGENTLINK_PASSWORD。",
94
- );
95
- }
96
- process.stdout.write(label);
97
- return new Promise((resolve, reject) => {
98
- let password = "";
99
- const wasRaw = process.stdin.isRaw;
100
- const finish = () => {
101
- process.stdin.off("data", onData);
102
- process.stdin.setRawMode(wasRaw);
103
- process.stdin.pause();
104
- process.stdout.write("\n");
105
- };
106
- const onData = (chunk) => {
107
- for (const char of String(chunk)) {
108
- if (char === "\u0003") {
109
- finish();
110
- reject(new Error("已取消登录。"));
111
- return;
112
- }
113
- if (char === "\r" || char === "\n") {
114
- finish();
115
- resolve(password);
116
- return;
117
- }
118
- if (char === "\u007f" || char === "\b") password = password.slice(0, -1);
119
- else if (char >= " ") password += char;
120
- }
121
- };
122
- process.stdin.setRawMode(true);
123
- process.stdin.setEncoding("utf8");
124
- process.stdin.resume();
125
- process.stdin.on("data", onData);
126
- });
127
- }
128
-
129
- async function accountCredentials() {
130
- const fromEnv = process.env.AGENTLINK_ACCOUNT?.trim();
131
- const passwordFromEnv = process.env.AGENTLINK_PASSWORD;
132
- if (fromEnv && passwordFromEnv) return { identifier: fromEnv, password: passwordFromEnv };
133
- if (!process.stdin.isTTY) {
134
- throw new Error("首次运行需要登录账号,请在交互式终端运行。");
135
- }
136
- const readline = createInterface({ input: process.stdin, output: process.stdout });
137
- const identifier = (await readline.question("AgentLink 账号:")).trim();
138
- readline.close();
139
- const password = await readPassword("AgentLink 密码:");
140
- if (!identifier || !password) throw new Error("账号和密码不能为空。");
141
- return { identifier, password };
142
- }
143
-
144
100
  async function loginAndCreateCredential(hub, config) {
145
101
  console.log("[agentlink] 首次使用,请登录 AgentLink 账号。");
146
102
  const { identifier, password } = await accountCredentials();
@@ -206,43 +162,65 @@ async function pair(hub) {
206
162
  });
207
163
  }
208
164
 
209
- if (args.includes("--help") || args.includes("-h")) {
165
+ // ---------------------------------------------------------------------------
166
+ // Command handlers
167
+ // ---------------------------------------------------------------------------
168
+
169
+ async function showHelp() {
210
170
  console.log(
211
- `用法: npx alink-cli [--hub <wss://…>] [--dir <目录>]\n npx alink-cli logout\n\n首次运行输入 AgentLink 账号密码,这台电脑会自动加入账号;以后直接运行即可上线。\nlogout 退出当前账号;--legacy 临时运行旧 daemon。`,
171
+ `用法:\n alink-cli 进入交互式 TUI(默认)\n alink-cli --no-tui 跳过 TUI,按旧行为启动 daemon\n alink-cli login 登录账号\n alink-cli status 查看登录状态\n alink-cli machines 查看账号下的所有机器\n alink-cli logout 退出账号\n alink-cli --legacy 临时运行旧 daemon\n alink-cli help 查看帮助\n\n选项:\n --hub <wss://…> Hub 地址\n --dir <目录> 默认工作目录\n --no-tui 禁用交互式 TUI`,
212
172
  );
213
- process.exit(0);
214
173
  }
215
174
 
216
- if (args[0] === "logout") {
217
- rmSync(CREDENTIAL_FILE, { force: true });
218
- console.log("[agentlink] 已退出账号;下次启动时需要重新登录。");
219
- process.exit(0);
175
+ async function showStatus() {
176
+ const hub = value("hub") || OFFICIAL_HUB;
177
+ console.log(`[agentlink] 登录状态:${savedCredential(hub) ? "已登录" : "未登录"}`);
178
+ console.log(`[agentlink] Hub:${normalizeHub(hub)}`);
179
+ }
180
+
181
+ async function doLogout() {
182
+ const loggedIn = savedCredential(value("hub") || OFFICIAL_HUB);
183
+ if (loggedIn) rmSync(CREDENTIAL_FILE, { force: true });
184
+ console.log(
185
+ loggedIn ? "[agentlink] 已退出账号;下次启动时需要重新登录。" : "[agentlink] 当前未登录。",
186
+ );
220
187
  }
221
188
 
222
- if (args[0] === "credential") {
189
+ async function doLogin() {
223
190
  const hub = value("hub") || OFFICIAL_HUB;
224
- const command = args[1] || "status";
225
- if (command === "path") console.log(CREDENTIAL_FILE);
226
- else if (command === "status") console.log(savedCredential(hub) ? "已保存凭证" : "未保存凭证");
227
- else if (command === "show") console.log(savedCredential(hub) || "");
228
- else if (command === "set" && args[2] && !/\s/.test(args[2])) saveCredential(args[2], hub);
229
- else if (command === "clear") rmSync(CREDENTIAL_FILE, { force: true });
230
- else throw new Error("用法: alink-cli credential status|path|show|set <credential>|clear");
231
- process.exit(0);
191
+ if (savedCredential(hub)) {
192
+ console.log("[agentlink] 已登录。需要切换或修复机器时,请先运行 alink-cli logout。");
193
+ return;
194
+ }
195
+ const config = await loadAgentLinkAccountConfig(hub);
196
+ if (config?.mode !== "multi") throw new Error("当前 Hub 不使用账号登录。");
197
+ await loginAndCreateCredential(hub, config);
232
198
  }
233
199
 
234
- if (args[0] === "qr") {
235
- const credential =
236
- value("token") || process.env.AGENTLINK_TOKEN || savedCredential(value("hub") || OFFICIAL_HUB);
237
- if (!credential) throw new Error("这台电脑还没有凭证,请先运行 npx alink-cli 完成配对。");
238
- await printQr(credential);
239
- process.exit(0);
200
+ async function doMachines() {
201
+ const hub = value("hub") || OFFICIAL_HUB;
202
+ const config = await loadAgentLinkAccountConfig(hub);
203
+ if (config?.mode !== "multi") throw new Error("当前 Hub 不使用账号机器目录。");
204
+ console.log("[agentlink] 查看账号机器,请验证 AgentLink 账号。");
205
+ const { identifier, password } = await accountCredentials();
206
+ const machines = await listAgentLinkAccountMachines({ config, identifier, password });
207
+ if (machines.length === 0) console.log("[agentlink] 账号下还没有机器。");
208
+ else {
209
+ console.log(`[agentlink] 账号下共 ${machines.length} 台机器:`);
210
+ for (const machine of machines) {
211
+ console.log(
212
+ `- ${machine.name || machine.hostname || machine.machineId} (${machine.machineId})`,
213
+ );
214
+ }
215
+ }
240
216
  }
241
217
 
242
- if (args.includes("--legacy")) {
218
+ async function doLegacy() {
243
219
  process.argv = process.argv.filter((arg) => arg !== "--legacy");
244
220
  await import(new URL("../dist/daemon.js", import.meta.url));
245
- } else {
221
+ }
222
+
223
+ async function doNoTuiStart() {
246
224
  const hub = value("hub") || OFFICIAL_HUB;
247
225
  const explicit = value("token") || process.env.AGENTLINK_TOKEN;
248
226
  const config =
@@ -252,13 +230,13 @@ if (args.includes("--legacy")) {
252
230
  const credential =
253
231
  explicit ||
254
232
  (!args.includes("--pair") && savedCredential(hub)) ||
255
- (config?.mode === "multi"
256
- ? await loginAndCreateCredential(hub, config)
257
- : await createSingleCredential(hub));
233
+ (args.includes("--pair")
234
+ ? await pair(hub)
235
+ : config?.mode === "multi"
236
+ ? await loginAndCreateCredential(hub, config)
237
+ : await createSingleCredential(hub));
258
238
  if (explicit && explicit !== savedCredential(hub)) saveCredential(explicit, hub);
259
- if (!explicit && savedCredential(hub) && !args.includes("--pair")) {
260
- console.log("[agentlink] 已登录,正在让这台电脑上线。");
261
- }
239
+ if (!explicit && !args.includes("--pair")) console.log("[agentlink] 正在让这台电脑上线。");
262
240
  const { machineToken, enckey } = parseCredential(credential);
263
241
  const daemon = fileURLToPath(new URL("../dist/bin.mjs", import.meta.url));
264
242
  const cwd = value("dir") || process.cwd();
@@ -274,3 +252,91 @@ if (args.includes("--legacy")) {
274
252
  for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal));
275
253
  process.exitCode = await new Promise((resolve) => child.on("exit", (code) => resolve(code ?? 1)));
276
254
  }
255
+
256
+ async function doTui() {
257
+ const uiPath = fileURLToPath(new URL("../dist/ui.js", import.meta.url));
258
+ if (!existsSync(uiPath)) {
259
+ console.error(
260
+ `[agentlink] 交互式 UI 包不存在: ${uiPath}\n请先运行 pnpm --filter alink-cli run build。`,
261
+ );
262
+ process.exit(1);
263
+ }
264
+ await import(new URL("../dist/ui.js", import.meta.url));
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Router
269
+ // ---------------------------------------------------------------------------
270
+
271
+ async function main() {
272
+ // Strip internal-only flags before dispatching so downstream handlers see clean args.
273
+ const publicArgs = args.filter((a) => a !== "--no-tui");
274
+
275
+ if (publicArgs[0] === "help" || publicArgs.includes("--help") || publicArgs.includes("-h")) {
276
+ await showHelp();
277
+ return;
278
+ }
279
+
280
+ if (publicArgs[0] === "status") {
281
+ await showStatus();
282
+ return;
283
+ }
284
+
285
+ if (publicArgs[0] === "logout") {
286
+ await doLogout();
287
+ return;
288
+ }
289
+
290
+ if (publicArgs[0] === "login") {
291
+ await doLogin();
292
+ return;
293
+ }
294
+
295
+ if (publicArgs[0] === "machines") {
296
+ await doMachines();
297
+ return;
298
+ }
299
+
300
+ if (publicArgs[0] === "credential" && legacyCredentialCommands) {
301
+ const hub = value("hub") || OFFICIAL_HUB;
302
+ const command = publicArgs[1] || "status";
303
+ if (command === "path") console.log(CREDENTIAL_FILE);
304
+ else if (command === "status") console.log(savedCredential(hub) ? "已保存凭证" : "未保存凭证");
305
+ else if (command === "show") console.log(savedCredential(hub) || "");
306
+ else if (command === "set" && publicArgs[2] && !/\s/.test(publicArgs[2])) saveCredential(publicArgs[2], hub);
307
+ else if (command === "clear") rmSync(CREDENTIAL_FILE, { force: true });
308
+ else throw new Error("用法: alink-cli credential status|path|show|set <credential>|clear");
309
+ return;
310
+ }
311
+
312
+ if (publicArgs[0] === "qr" && legacyCredentialCommands) {
313
+ const credential =
314
+ value("token") || process.env.AGENTLINK_TOKEN || savedCredential(value("hub") || OFFICIAL_HUB);
315
+ if (!credential) throw new Error("这台电脑还没有凭证,请先运行 npx alink-cli 完成配对。");
316
+ await printQr(credential);
317
+ return;
318
+ }
319
+
320
+ if (publicArgs[0] && !publicArgs[0].startsWith("-")) {
321
+ console.error(`[agentlink] 未知命令:${publicArgs[0]}。运行 alink-cli help 查看帮助。`);
322
+ process.exit(1);
323
+ }
324
+
325
+ if (publicArgs.includes("--legacy")) {
326
+ await doLegacy();
327
+ return;
328
+ }
329
+
330
+ // Non-TTY, CI, or explicit --no-tui: preserve the original foreground daemon behavior.
331
+ if (noTui) {
332
+ await doNoTuiStart();
333
+ return;
334
+ }
335
+
336
+ await doTui();
337
+ }
338
+
339
+ main().catch((err) => {
340
+ console.error(err instanceof Error ? err.message : String(err));
341
+ process.exit(1);
342
+ });