@yep-ai/cli 0.0.0-stage → 1.0.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/bin/yep.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { runMarketplace } from "./market.js";
3
+ import { runDevice } from "../lib/device/command.js";
4
+ const argv = process.argv.slice(2);
5
+ try {
6
+ if (argv[0] === "device") await runDevice(argv.slice(1));
7
+ else await runMarketplace(argv);
8
+ } catch (error) {
9
+ console.error(error.message);
10
+ process.exitCode = 1;
11
+ }
@@ -0,0 +1,411 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { spawn } from "node:child_process";
6
+ import WebSocket from "ws";
7
+ import { rootDescriptor, resolveSafe, list, execute } from "./tools.js";
8
+ const PROTOCOL = 2;
9
+ const DEFAULT_SERVER = "https://yep.nihao.cool";
10
+ function serverOrigin(value) {
11
+ const u = new URL(value);
12
+ if (u.username || u.password || u.search || u.hash || u.pathname !== "/") throw new Error("--server must be an origin");
13
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
14
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) throw new Error("HTTPS required (HTTP only for loopback development)");
15
+ return u.origin;
16
+ }
17
+ function configDirectory() {
18
+ return process.env.YEP_DEVICE_CONFIG_DIR || path.join(process.env.YEP_CONFIG_DIR || path.join(process.env.YEP_HOME || os.homedir(), ".config", "yep"), "devices");
19
+ }
20
+ async function protect(location, directory = false) {
21
+ if (process.platform === "win32") {
22
+ await new Promise((resolve, reject) => {
23
+ const identity = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}` : process.env.USERNAME;
24
+ if (!identity) return reject(new Error("Cannot determine Windows account for private credentials"));
25
+ const p = spawn("icacls", [location, "/inheritance:r", "/grant:r", `${identity}:${directory ? "(OI)(CI)" : ""}F`], { stdio: "ignore", windowsHide: true });
26
+ p.on("error", reject);
27
+ p.on("exit", (code) => code === 0 ? resolve() : reject(new Error("Failed to restrict Windows credential ACL")));
28
+ });
29
+ } else await fs.chmod(location, directory ? 448 : 384);
30
+ }
31
+ async function checkPrivate(location, directory = false) {
32
+ const st = await fs.lstat(location);
33
+ if (st.isSymbolicLink() || directory && !st.isDirectory() || !directory && !st.isFile()) throw new Error("Unsafe CLI credential path");
34
+ if (process.platform !== "win32" && ((st.mode & 63) !== 0 || st.uid !== process.getuid())) throw new Error("CLI credential permissions are unsafe; correct ownership and permissions");
35
+ if (process.platform === "win32") {
36
+ const script = "$ErrorActionPreference='Stop'; $acl=Get-Acl -LiteralPath $env:YEP_DEVICE_ACL_PATH; if(-not $acl.AreAccessRulesProtected){exit 2}; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value; foreach($rule in $acl.Access){ if($rule.AccessControlType -eq 'Allow'){ $who=$rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value; if($who -ne $sid -and $who -ne 'S-1-5-18' -and $who -ne 'S-1-5-32-544'){exit 3} } }; exit 0";
37
+ await new Promise((resolve, reject) => {
38
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { env: { ...process.env, YEP_DEVICE_ACL_PATH: location }, stdio: "ignore", windowsHide: true });
39
+ child.on("error", () => reject(new Error("Cannot verify Windows private credential ACL")));
40
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error("Windows credential ACL is unsafe; restrict access to the current account")));
41
+ });
42
+ }
43
+ return st;
44
+ }
45
+ async function initializeConfig(directory = configDirectory()) {
46
+ try {
47
+ const created = await fs.mkdir(directory, { recursive: true, mode: 448 });
48
+ if (created) await protect(directory, true);
49
+ await checkPrivate(directory, true);
50
+ } catch (e) {
51
+ if (e.code !== "ENOENT") throw e;
52
+ }
53
+ ;
54
+ const uuidPath = path.join(directory, "installation-id");
55
+ let installationId;
56
+ try {
57
+ await checkPrivate(uuidPath);
58
+ installationId = (await fs.readFile(uuidPath, "utf8")).trim();
59
+ if (!/^[0-9a-f-]{36}$/u.test(installationId)) throw new Error("Corrupt installation identity");
60
+ } catch (e) {
61
+ if (e.code !== "ENOENT") throw e;
62
+ installationId = randomUUID();
63
+ await fs.writeFile(uuidPath, installationId, { mode: 384, flag: "wx" });
64
+ await protect(uuidPath);
65
+ }
66
+ ;
67
+ return { directory, installationId };
68
+ }
69
+ function credentialPath(directory, server) {
70
+ return path.join(directory, createHash("sha256").update(server).digest("hex") + ".json");
71
+ }
72
+ async function loadCredential(directory, server) {
73
+ const location = credentialPath(directory, server);
74
+ try {
75
+ await checkPrivate(location);
76
+ const v = JSON.parse(await fs.readFile(location, "utf8"));
77
+ if (v.server !== server || typeof v.token !== "string" || !v.deviceId || !Number.isFinite(Date.parse(v.expiresAt))) throw new Error("Corrupt or wrong-origin device credential");
78
+ return v;
79
+ } catch (e) {
80
+ if (e.code === "ENOENT") return null;
81
+ throw e;
82
+ }
83
+ }
84
+ async function saveCredential(directory, server, value) {
85
+ const location = credentialPath(directory, server);
86
+ const temporary = location + "." + randomUUID();
87
+ await fs.writeFile(temporary, JSON.stringify({ ...value, server }), { mode: 384, flag: "wx" });
88
+ await protect(temporary);
89
+ await fs.rename(temporary, location);
90
+ }
91
+ async function loadDefaultServer(directory) {
92
+ const location = path.join(directory, "default-server.json");
93
+ try {
94
+ await checkPrivate(location);
95
+ return serverOrigin(JSON.parse(await fs.readFile(location, "utf8")).server);
96
+ } catch (error) {
97
+ if (error.code === "ENOENT") return DEFAULT_SERVER;
98
+ throw error;
99
+ }
100
+ }
101
+ async function rememberServer(directory, server) {
102
+ const location = path.join(directory, "default-server.json");
103
+ try { await checkPrivate(location); } catch (error) { if (error.code !== "ENOENT") throw error; }
104
+ const temporary = location + "." + randomUUID();
105
+ await fs.writeFile(temporary, JSON.stringify({ server: serverOrigin(server) }), { mode: 0o600, flag: "wx" });
106
+ await protect(temporary);
107
+ await fs.rename(temporary, location);
108
+ }
109
+ async function api(server, route, { token, body, method = "GET" } = {}) {
110
+ const response = await fetch(server + route, { method, headers: { ...token ? { Authorization: `Bearer ${token}` } : {}, ...body ? { "Content-Type": "application/json" } : {} }, body: body ? JSON.stringify(body) : void 0, signal: AbortSignal.timeout(15e3), redirect: "error" });
111
+ let result;
112
+ try {
113
+ result = await response.json();
114
+ } catch {
115
+ throw new Error("Server returned an invalid response");
116
+ }
117
+ ;
118
+ if (!response.ok) {
119
+ const e = new Error(result.error || `HTTP ${response.status}`);
120
+ e.status = response.status;
121
+ throw e;
122
+ }
123
+ return result;
124
+ }
125
+ function openBrowser(url) {
126
+ const exe = process.platform === "win32" ? "rundll32" : process.platform === "darwin" ? "open" : "xdg-open";
127
+ const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
128
+ const child = spawn(exe, args, { stdio: "ignore", detached: true, windowsHide: true });
129
+ child.on("error", () => {
130
+ });
131
+ child.unref();
132
+ }
133
+ async function login(server, config, { name = os.hostname(), noOpen = false, signal } = {}) {
134
+ const result = await api(server, "/api/local-cli/login/start", { method: "POST", body: { installationId: config.installationId, name, platform: process.platform } });
135
+ const verify = new URL(result.verificationUri);
136
+ if (verify.origin !== server) throw new Error("Server returned a pairing URL for another origin");
137
+ console.log(`在网页核对设备「${name}」及确认码 ${result.userCode}:
138
+ ${verify.href}`);
139
+ if (!noOpen) openBrowser(verify.href);
140
+ while (Date.now() < Date.parse(result.expiresAt)) {
141
+ await new Promise((resolve, reject) => {
142
+ const t = setTimeout(resolve, Math.max(2, result.interval) * 1e3);
143
+ signal?.addEventListener("abort", () => {
144
+ clearTimeout(t);
145
+ reject(new Error("Pairing canceled"));
146
+ }, { once: true });
147
+ });
148
+ if (signal?.aborted) throw new Error("Pairing canceled");
149
+ let response;
150
+ try {
151
+ response = await api(server, "/api/local-cli/login/poll", { method: "POST", body: { deviceCode: result.deviceCode } });
152
+ } catch (e) {
153
+ if (e.message === "slow_down") continue;
154
+ throw e;
155
+ }
156
+ ;
157
+ if (response.status === "denied") throw new Error("Device authorization was denied");
158
+ if (response.status === "authorized") {
159
+ await saveCredential(config.directory, server, response);
160
+ console.log("设备授权已保存(最多24小时)。");
161
+ return response;
162
+ }
163
+ if (response.status === "claimed") throw new Error("Pairing credential was already claimed");
164
+ }
165
+ ;
166
+ throw new Error("Pairing expired");
167
+ }
168
+ async function systemRoots(privateDir) {
169
+ let directories;
170
+ if (process.platform === "win32") {
171
+ directories = [];
172
+ for (const letter of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
173
+ const drive = letter + ":\\";
174
+ try {
175
+ await fs.access(drive);
176
+ directories.push(drive);
177
+ } catch {
178
+ }
179
+ }
180
+ } else directories = ["/"];
181
+ return Promise.all(directories.map(async (p, i) => {
182
+ const root = await rootDescriptor("root-" + (i + 1), p, privateDir);
183
+ if (process.platform === "win32") root.name = `${p[0]} 盘`;
184
+ return root;
185
+ }));
186
+ }
187
+ class DeviceAgent {
188
+ constructor(roots, { restricted = true, privateDir, token, connectionId, expiresAt } = {}) {
189
+ this.roots = roots;
190
+ this.restricted = restricted;
191
+ this.privateDir = privateDir;
192
+ this.token = token;
193
+ this.connectionId = connectionId;
194
+ this.expiresAt = expiresAt;
195
+ this.closed = false;
196
+ this.bindings = /* @__PURE__ */ new Map();
197
+ this.active = /* @__PURE__ */ new Map();
198
+ }
199
+ publicRoots() {
200
+ return this.roots.map(({ id, name }) => ({ id, name }));
201
+ }
202
+ cancel(frame = {}) {
203
+ const waiting = [];
204
+ for (const [id, op] of this.active) {
205
+ if (!frame.id && !frame.roundId && !frame.bindingId || frame.id === id || frame.roundId === op.roundId || frame.bindingId === op.bindingId) {
206
+ op.abort.abort();
207
+ waiting.push(op.done);
208
+ }
209
+ }
210
+ return Promise.allSettled(waiting);
211
+ }
212
+ close() {
213
+ this.closed = true;
214
+ this.cancel();
215
+ this.bindings.clear();
216
+ }
217
+ async frame(frame) {
218
+ if (this.closed) throw new Error("Stale device connection");
219
+ if (this.expiresAt && Date.now() >= Date.parse(this.expiresAt)) {
220
+ this.close();
221
+ throw new Error("Device authorization expired");
222
+ }
223
+ const base = { id: frame.id, consultationId: frame.consultationId, bindingId: frame.bindingId, roundId: frame.roundId };
224
+ if (frame.type === "cancel") {
225
+ await this.cancel(frame);
226
+ return null;
227
+ }
228
+ if (frame.type === "unbound") {
229
+ const pending = this.cancel(frame);
230
+ this.bindings.delete(frame.bindingId);
231
+ await pending;
232
+ return null;
233
+ }
234
+ if (frame.type === "browse") {
235
+ const root2 = this.roots.find((x) => x.id === frame.folderId);
236
+ if (!root2) throw new Error("Unknown root");
237
+ return { ...base, type: "browse_result", browse: await list(root2, frame.path ?? "", frame.offset ?? 0, frame.includeFiles !== false) };
238
+ }
239
+ if (frame.type === "bind") {
240
+ if (!Array.isArray(frame.folders) || frame.folders.length < 1 || frame.folders.length > 5 || !["full", "confirm", "read-only"].includes(frame.permissionMode)) throw new Error("Invalid binding");
241
+ const folders = [];
242
+ for (const item of frame.folders) {
243
+ const source = this.roots.find((x) => x.id === item.id);
244
+ if (!source) throw new Error("Unknown root");
245
+ const resolved = await resolveSafe(source, item.path ?? "", item.kind === "file" ? "file" : "directory");
246
+ const targetFile = item.kind === "file" ? path.basename(resolved.absolute) : void 0;
247
+ const absolute = targetFile ? path.dirname(resolved.absolute) : resolved.absolute;
248
+ const root2 = await rootDescriptor("folder-" + randomUUID(), absolute, this.privateDir);
249
+ if (targetFile) root2.targetFile = targetFile;
250
+ folders.push(root2);
251
+ }
252
+ const id = frame.bindingId || frame.id;
253
+ if (this.closed) throw new Error("Stale device connection");
254
+ this.bindings.set(id, { id, consultationId: frame.consultationId, permissionMode: frame.permissionMode, permissionVersion: frame.permissionVersion ?? 1, folders });
255
+ return { ...base, type: "bind_result", approved: true, folders: folders.map(({ id: id2, name, targetFile }) => ({ id: id2, name, targetFile })) };
256
+ }
257
+ if (frame.type === "permissions") {
258
+ const binding2 = this.bindings.get(frame.bindingId);
259
+ if (!binding2 || binding2.changing || binding2.consultationId !== frame.consultationId || !["full", "confirm", "read-only"].includes(frame.permissionMode) || frame.permissionVersion !== binding2.permissionVersion + 1) throw new Error("Invalid permission change");
260
+ binding2.changing = true;
261
+ await this.cancel({ bindingId: frame.bindingId });
262
+ binding2.permissionMode = frame.permissionMode;
263
+ binding2.permissionVersion = frame.permissionVersion;
264
+ binding2.changing = false;
265
+ return { ...base, type: "permissions_result", approved: true, permissionVersion: binding2.permissionVersion };
266
+ }
267
+ if (frame.type !== "request") return null;
268
+ if (this.active.size >= 8 || this.active.has(frame.id)) throw new Error("Too many or duplicate local operations");
269
+ const binding = this.bindings.get(frame.bindingId);
270
+ if (!binding || binding.changing || binding.consultationId !== frame.consultationId || frame.connectionId !== this.connectionId || frame.permissionVersion !== binding.permissionVersion || frame.permissionMode !== binding.permissionMode) throw new Error("Stale connection, binding or permission");
271
+ const request = frame.request;
272
+ if (!request || typeof request !== "object") throw new Error("Invalid request");
273
+ if (request.operation === "exec" && binding.permissionMode === "read-only" || ["exec", "read"].includes(request.operation) && binding.permissionMode === "confirm" && frame.approved !== true) throw new Error("Operation requires current approval");
274
+ const root = binding.folders.find((x) => x.id === request.folderId);
275
+ if (!root) throw new Error("Folder outside binding");
276
+ const abort = new AbortController();
277
+ let complete;
278
+ const done = new Promise((resolve) => {
279
+ complete = resolve;
280
+ });
281
+ this.active.set(frame.id, { abort, bindingId: frame.bindingId, roundId: frame.roundId, done });
282
+ try {
283
+ const result = await execute(root, request, { restricted: this.restricted, signal: abort.signal, secrets: [this.token] });
284
+ if (abort.signal.aborted || this.bindings.get(frame.bindingId) !== binding || frame.permissionVersion !== binding.permissionVersion) throw new Error("Authorization changed during operation");
285
+ return { ...base, type: "result", result };
286
+ } finally {
287
+ this.active.delete(frame.id);
288
+ complete();
289
+ }
290
+ }
291
+ }
292
+ async function start(server, config, credential, directories, { name, noOpen, signal } = {}) {
293
+ const restricted = directories.length > 0;
294
+ if (directories.length > 5) throw new Error("At most five startup directories");
295
+ const roots = restricted ? await Promise.all(directories.map((p, i) => rootDescriptor("root-" + (i + 1), p, config.directory))) : await systemRoots(config.directory);
296
+ if (!roots.length) throw new Error("No accessible filesystem roots");
297
+ console.log(restricted ? "受限目录模式:仅内置命令,不能启动 shell 或外部程序。" : "完整系统模式:网页授权执行的命令拥有当前系统用户权限,可访问其他文件和网络;这不是沙箱。");
298
+ let delay = 1e3;
299
+ while (!signal?.aborted) {
300
+ if (Date.now() >= Date.parse(credential.expiresAt)) throw new Error("Device authorization expired; run login again");
301
+ let fatal;
302
+ await new Promise((resolve, reject) => {
303
+ const url = new URL("/api/local-cli/connect", server);
304
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
305
+ const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${credential.token}` }, maxPayload: 15 * 1024 * 1024, handshakeTimeout: 15e3, followRedirects: false });
306
+ const agent = new DeviceAgent(roots, { restricted, privateDir: config.directory, token: credential.token, expiresAt: credential.expiresAt });
307
+ let ready = false;
308
+ let heartbeat;
309
+ const cleanup = () => {
310
+ clearInterval(heartbeat);
311
+ agent.close();
312
+ signal?.removeEventListener("abort", abort);
313
+ };
314
+ const abort = () => {
315
+ ready = false;
316
+ agent.close();
317
+ ws.close();
318
+ };
319
+ signal?.addEventListener("abort", abort, { once: true });
320
+ ws.on("open", () => {
321
+ ws.send(JSON.stringify({ type: "hello", protocol: PROTOCOL, consentVersion: "yep-device-v2", directoryMode: restricted ? "restricted" : "system", folders: agent.publicRoots(), commandsEnabled: true, fileSelection: true }));
322
+ });
323
+ ws.on("unexpected-response", (_req, res) => {
324
+ if ([401, 403].includes(res.statusCode)) {
325
+ fatal = new Error("Device authorization expired or revoked");
326
+ ws.terminate();
327
+ }
328
+ });
329
+ ws.on("message", async (bytes) => {
330
+ if (fatal || signal?.aborted) return;
331
+ let frame;
332
+ try {
333
+ frame = JSON.parse(bytes.toString());
334
+ if (frame.type === "ready") {
335
+ if (frame.protocol !== PROTOCOL) throw new Error("Unsupported server protocol");
336
+ agent.connectionId = frame.connectionId;
337
+ ready = true;
338
+ delay = 1e3;
339
+ console.log("设备已上线。请在网页选择目录;退出此终端将断开。");
340
+ heartbeat = setInterval(() => {
341
+ if (Date.now() >= Date.parse(credential.expiresAt)) {
342
+ fatal = new Error("Device authorization expired");
343
+ agent.close();
344
+ ws.close();
345
+ return;
346
+ }
347
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
348
+ }, 1e4);
349
+ return;
350
+ }
351
+ if (["revoked", "expired", "replaced"].includes(frame.type)) {
352
+ fatal = new Error(`Device ${frame.type}; local operations stopped`);
353
+ ready = false;
354
+ agent.close();
355
+ ws.close();
356
+ return;
357
+ }
358
+ if (frame.type === "ping") {
359
+ ws.send(JSON.stringify({ type: "pong" }));
360
+ return;
361
+ }
362
+ if (!ready) return;
363
+ const result = await agent.frame(frame);
364
+ if (result && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(result));
365
+ } catch (e) {
366
+ if (fatal || signal?.aborted) return;
367
+ if (frame?.id && ws.readyState === WebSocket.OPEN) {
368
+ let error = String(e.message).split(credential.token).join("[REDACTED]").split(config.directory).join("[私有目录]");
369
+ for (const root of [...agent.roots].sort((a, b) => b.absolute.length - a.absolute.length)) error = error.split(root.absolute).join("[授权目录]");
370
+ ws.send(JSON.stringify({ type: "result", id: frame.id, bindingId: frame.bindingId, consultationId: frame.consultationId, error: error.slice(0, 8192) }));
371
+ } else {
372
+ fatal = e;
373
+ ws.close();
374
+ }
375
+ }
376
+ });
377
+ ws.on("error", () => {
378
+ });
379
+ ws.on("close", () => {
380
+ cleanup();
381
+ resolve();
382
+ });
383
+ });
384
+ if (fatal) throw fatal;
385
+ if (signal?.aborted) break;
386
+ console.log(`连接已断开;${Math.ceil(delay / 1e3)}秒后重连。原咨询绑定已失效,重连后需重新选择。`);
387
+ await new Promise((resolve) => {
388
+ const t = setTimeout(resolve, delay);
389
+ signal?.addEventListener("abort", () => {
390
+ clearTimeout(t);
391
+ resolve();
392
+ }, { once: true });
393
+ });
394
+ delay = Math.min(delay * 2, 3e4);
395
+ }
396
+ }
397
+ export {
398
+ DeviceAgent,
399
+ PROTOCOL,
400
+ api,
401
+ configDirectory,
402
+ credentialPath,
403
+ initializeConfig,
404
+ loadCredential,
405
+ loadDefaultServer,
406
+ login,
407
+ rememberServer,
408
+ saveCredential,
409
+ serverOrigin,
410
+ start
411
+ };
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import { readFileSync } from "node:fs";
4
+ const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
5
+ import { serverOrigin, initializeConfig, loadCredential, loadDefaultServer, rememberServer, credentialPath, login, start, api } from "./client.js";
6
+ const HELP = `Yep 本地设备节点 ${version} (协议2,Node.js 22+)
7
+
8
+ yep device start [目录1 ... 目录5] [--server https://your-site.example]
9
+ yep device login [--server https://your-site.example] [--no-open]
10
+ yep device status [--server https://your-site.example]
11
+ yep device logout [--server https://your-site.example]
12
+
13
+ 选项:--name 名称、--no-open、--help、--version
14
+ 不带目录的 start 使用当前系统用户 shell(非沙箱);带目录只支持受限内置命令。
15
+ 授权最长24小时;start不安装后台服务。
16
+ 默认连接 https://yep.nihao.cool;显式 --server 会记住源站,令牌仍按源站隔离。`;
17
+ export async function runDevice(args = process.argv.slice(2)) {
18
+ args = [...args];
19
+ if (args.includes("--help") || args.length === 0) {
20
+ console.log(HELP);
21
+ return;
22
+ }
23
+ if (args.includes("--version")) {
24
+ console.log(version);
25
+ return;
26
+ }
27
+ const command = args.shift();
28
+ let server, name, noOpen = false;
29
+ const directories = [];
30
+ while (args.length) {
31
+ const a = args.shift();
32
+ if (a === "--server") {
33
+ server = args.shift();
34
+ if (!server) throw new Error("--server requires a URL");
35
+ } else if (a === "--name") {
36
+ name = args.shift();
37
+ if (!name) throw new Error("--name requires a value");
38
+ } else if (a === "--no-open") noOpen = true;
39
+ else if (a.startsWith("-")) throw new Error(`Unknown option ${a}`);
40
+ else directories.push(a);
41
+ }
42
+ if (!["start", "login", "status", "logout"].includes(command)) throw new Error("Unknown command; use --help");
43
+ if (server) server = serverOrigin(server);
44
+ const config = await initializeConfig();
45
+ if (server) await rememberServer(config.directory, server);
46
+ else server = await loadDefaultServer(config.directory);
47
+ let credential = await loadCredential(config.directory, server);
48
+ const controller = new AbortController();
49
+ const stop = () => controller.abort();
50
+ process.once("SIGINT", stop);
51
+ process.once("SIGTERM", stop);
52
+ try {
53
+ if (command === "logout") {
54
+ if (credential) {
55
+ try {
56
+ await api(server, "/api/local-cli/logout", { method: "POST", token: credential.token });
57
+ } catch (error) {
58
+ if (error.status !== 401 && error.status !== 403) throw error;
59
+ }
60
+ await fs.unlink(credentialPath(config.directory, server));
61
+ }
62
+ console.log("设备授权已撤销并删除。");
63
+ return;
64
+ }
65
+ if (command === "status") {
66
+ if (!credential) {
67
+ console.log("尚未授权。");
68
+ return;
69
+ }
70
+ if (Date.now() >= Date.parse(credential.expiresAt)) throw new Error("Device authorization expired");
71
+ const me = await api(server, "/api/local-cli/me", { token: credential.token });
72
+ console.log(JSON.stringify({ deviceId: me.deviceId, name: me.name, expiresAt: me.expiresAt, protocol: me.protocol }, null, 2));
73
+ return;
74
+ }
75
+ if (command === "login") {
76
+ await login(server, config, { name, noOpen, signal: controller.signal });
77
+ return;
78
+ }
79
+ if (credential && Date.now() < Date.parse(credential.expiresAt)) {
80
+ try {
81
+ await api(server, "/api/local-cli/me", { token: credential.token });
82
+ } catch (e) {
83
+ if (e.status === 401 || e.status === 403) credential = null;
84
+ else throw e;
85
+ }
86
+ } else credential = null;
87
+ if (!credential) credential = await login(server, config, { name, noOpen, signal: controller.signal });
88
+ await start(server, config, credential, directories, { name, noOpen, signal: controller.signal });
89
+ } finally {
90
+ process.removeListener("SIGINT", stop);
91
+ process.removeListener("SIGTERM", stop);
92
+ }
93
+ }