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 +21 -3
- package/bin/agentlink-account.js +72 -40
- package/bin/agentlink-prompts.js +55 -0
- package/bin/agentlink.js +151 -85
- package/bin/migrate-legacy-machines.js +290 -0
- package/dist/bin.mjs +24 -49
- package/dist/bin.mjs.map +1 -1
- package/dist/ui.js +49044 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -6,17 +6,35 @@
|
|
|
6
6
|
npx alink-cli
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
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 密文。
|
package/bin/agentlink-account.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Command handlers
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
async function showHelp() {
|
|
210
170
|
console.log(
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
console.log(
|
|
219
|
-
|
|
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
|
-
|
|
189
|
+
async function doLogin() {
|
|
223
190
|
const hub = value("hub") || OFFICIAL_HUB;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
(
|
|
256
|
-
? await
|
|
257
|
-
:
|
|
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 &&
|
|
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
|
+
});
|