cofluxd 0.1.2 → 0.3.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 +7 -4
- package/cofluxd.mjs +104 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@ npm i -g cofluxd
|
|
|
11
11
|
## 用法
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
cofluxd #
|
|
14
|
+
cofluxd # 首次=交互式引导(问登记密钥/设备名,不问服务器地址),之后=看状态
|
|
15
15
|
cofluxd onboard # 显式重新走交互式配置
|
|
16
|
-
cofluxd up
|
|
17
|
-
cofluxd
|
|
16
|
+
cofluxd up # 零参数即可:起服务后打印浏览器授权链接,登录确认即完成登记
|
|
17
|
+
cofluxd up --enroll-key <KEY> # 走存量登记密钥流程(web「添加设备」给的命令)
|
|
18
|
+
cofluxd status # 服务器/登记(含"等待授权")/服务状态
|
|
18
19
|
cofluxd logs -f # 看 daemon 日志
|
|
19
20
|
cofluxd update # 更新二进制并重启(worker 另可由 server 远程热升级)
|
|
20
21
|
cofluxd reload # 改了 settings.json 后重启生效
|
|
@@ -22,7 +23,9 @@ cofluxd down # 停止
|
|
|
22
23
|
cofluxd uninstall [--purge] # 卸载(--purge 连二进制/配置/凭证一并删)
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
默认连公共服务 `wss://api.coflux.dev/daemon`(自托管用 `--server`
|
|
26
|
+
默认连公共服务 `wss://api.coflux.dev/daemon`(自托管用 `--server` 改;已保存的地址继续生效,非默认时会有醒目提示)。
|
|
27
|
+
|
|
28
|
+
不带 `--enroll-key` 时(推荐、默认):`cofluxd up` 起服务后会打印一个一次性授权链接,在浏览器用已登录的账号打开确认即可,无需先去 web 控制台生成密钥。仍可用 `--enroll-key`(从 web 控制台「添加设备」获取)走无头/脚本化登记。
|
|
26
29
|
|
|
27
30
|
## 配置
|
|
28
31
|
|
package/cofluxd.mjs
CHANGED
|
@@ -11,7 +11,6 @@ import { spawnSync } from "node:child_process";
|
|
|
11
11
|
|
|
12
12
|
// 默认中心服务(公共 SaaS);自托管用 --server 覆盖。
|
|
13
13
|
const DEFAULT_SERVER = "wss://api.coflux.dev/daemon";
|
|
14
|
-
const DEFAULT_WEB = "https://app.coflux.dev";
|
|
15
14
|
|
|
16
15
|
const REPO = "myWsq/coflux";
|
|
17
16
|
const HOME = process.env.COFLUX_HOME || join(homedir(), ".coflux");
|
|
@@ -19,6 +18,8 @@ const BIN_DIR = join(HOME, "bin");
|
|
|
19
18
|
const SETTINGS = join(HOME, "settings.json"); // 用户配置(含一次性登记密钥)→ daemon 直接读;含密钥故 600
|
|
20
19
|
const LOG_FILE = join(HOME, "daemon.log");
|
|
21
20
|
const CRED = join(HOME, "credentials.json");
|
|
21
|
+
const PENDING_AUTH = join(HOME, "pending-auth.json"); // worker 落盘的待授权链接(daemon.authorizePending)
|
|
22
|
+
const FDA_STATUS = join(HOME, "fda-status"); // supervisor 启动时探测落盘(仅 macOS,见 crates/supervisor/src/fda.rs)
|
|
22
23
|
const SUP_BIN = join(BIN_DIR, "coflux-supervisor");
|
|
23
24
|
const WRK_BIN = join(BIN_DIR, "coflux-worker");
|
|
24
25
|
const IS_MAC = platform() === "darwin";
|
|
@@ -40,6 +41,20 @@ function readSettings() {
|
|
|
40
41
|
try { return JSON.parse(fs.readFileSync(SETTINGS, "utf8")); } catch { return {}; }
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function readPendingAuth() {
|
|
45
|
+
try { return JSON.parse(fs.readFileSync(PENDING_AUTH, "utf8")); } catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// "granted" | "denied" | "unknown" | null(文件不存在——supervisor 还没起过/还没写)
|
|
49
|
+
function readFdaStatus() {
|
|
50
|
+
try { return fs.readFileSync(FDA_STATUS, "utf8").trim(); } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
function fdaLabel(status) {
|
|
53
|
+
return status === "granted" ? "已授予" : status === "denied" ? "未授予" : "未知";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
57
|
+
|
|
43
58
|
async function download(url, dest) {
|
|
44
59
|
const res = await fetch(url, { redirect: "follow" });
|
|
45
60
|
if (!res.ok) die(`下载失败 HTTP ${res.status}: ${url}\n(该版本/平台的 release 资产是否已发布?)`);
|
|
@@ -157,12 +172,48 @@ function stopService() {
|
|
|
157
172
|
async function applyAndStart({ serverUrl, enrollKey, deviceName, shell, version, binDir, noStart }) {
|
|
158
173
|
await ensureBinaries({ version, binDir });
|
|
159
174
|
const settings = applyConfig({ serverUrl, enrollKey, deviceName, shell });
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
// 非默认服务器醒目提示:保存值/--server 仍生效(不强制覆盖),但防止 staging 之类残留值静默错连。
|
|
176
|
+
if (serverUrl !== DEFAULT_SERVER) {
|
|
177
|
+
console.log(`⚠ 使用非默认服务器: ${serverUrl}`);
|
|
162
178
|
}
|
|
163
179
|
installService(!noStart);
|
|
164
180
|
console.log(noStart ? "已安装(未启动)。" : `✓ daemon 已启动 → ${serverUrl}`);
|
|
165
|
-
|
|
181
|
+
if (!noStart && !settings.enrollKey && !fs.existsSync(CRED)) {
|
|
182
|
+
// 默认流程(零参数 up):无登记密钥、未登记 → 走浏览器授权,轮询 daemon 落盘的链接/凭证文件。
|
|
183
|
+
await waitForAuthorization();
|
|
184
|
+
} else {
|
|
185
|
+
cmdStatus();
|
|
186
|
+
}
|
|
187
|
+
if (IS_MAC && !noStart && readFdaStatus() !== "granted") {
|
|
188
|
+
console.log("\n⚠ 完全磁盘访问权限尚未授予:PTY 里访问桌面/文稿/下载等目录时,系统弹窗可能因无人点击而卡住。");
|
|
189
|
+
console.log(" 运行 `cofluxd fda` 完成一次性授权(授予后需重启服务生效)。");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 轮询 ~/.coflux/pending-auth.json(授权链接)与 credentials.json(登记成功)给出全程反馈。
|
|
194
|
+
// CLI 保持零协议:只读文件,不连 WS;daemon 断线重连会自动重发 enrollRequest、换发新链接。
|
|
195
|
+
async function waitForAuthorization() {
|
|
196
|
+
console.log("\n 等待设备授权 …\n");
|
|
197
|
+
const maxWaitMs = 11 * 60 * 1000; // 只是前台等待的上限;daemon 会持续自动续期授权链接,超时后用 status 看最新链接即可
|
|
198
|
+
const start = Date.now();
|
|
199
|
+
let printedUrl = null;
|
|
200
|
+
while (Date.now() - start < maxWaitMs) {
|
|
201
|
+
if (fs.existsSync(CRED)) {
|
|
202
|
+
console.log("✓ 设备已登记\n");
|
|
203
|
+
cmdStatus();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const pending = readPendingAuth();
|
|
207
|
+
if (pending?.url && pending.url !== printedUrl) {
|
|
208
|
+
printedUrl = pending.url;
|
|
209
|
+
const mins = Number.isFinite(pending.expiresAt) ? Math.max(1, Math.round((pending.expiresAt - Date.now()) / 60000)) : null;
|
|
210
|
+
console.log(` 在浏览器打开以下链接,用已登录账号授权此设备${mins ? `(约 ${mins} 分钟内有效)` : ""}:\n`);
|
|
211
|
+
console.log(` ${pending.url}\n`);
|
|
212
|
+
}
|
|
213
|
+
await sleep(1000);
|
|
214
|
+
}
|
|
215
|
+
// 链接会过期换新(daemon 自动续期),别引导用户用"上面的链接"——过期后那是死链
|
|
216
|
+
console.log(" 仍未完成授权;daemon 已在后台运行并会自动更换过期的授权链接,用 `cofluxd status` 查看最新链接。\n");
|
|
166
217
|
}
|
|
167
218
|
|
|
168
219
|
/* ------------------------------ 命令 ------------------------------ */
|
|
@@ -180,13 +231,15 @@ async function cmdUp(v) {
|
|
|
180
231
|
|
|
181
232
|
async function cmdOnboard(v) {
|
|
182
233
|
const s = readSettings();
|
|
234
|
+
// 服务器地址不再交互询问(用户拍板 2026-07-04):--server > 已保存值 > 默认公共服务;
|
|
235
|
+
// 优先级与 cmdUp 一致(见 packages/cli/cofluxd.mjs 里 applyAndStart 的非默认提示)。
|
|
236
|
+
const serverUrl = v.server || s.serverUrl || DEFAULT_SERVER;
|
|
183
237
|
console.log("\n 欢迎使用 coflux —— 配置这台设备\n ──────────────────────────────\n");
|
|
238
|
+
console.log(` 服务器: ${serverUrl}\n`);
|
|
239
|
+
console.log(" 登记方式:留空走浏览器授权(推荐,起服务后打印链接,登录确认即可);\n 已有登记密钥可直接粘贴。\n");
|
|
184
240
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
185
241
|
try {
|
|
186
|
-
const
|
|
187
|
-
const web = serverUrl === DEFAULT_SERVER ? DEFAULT_WEB : "你的 coflux web 控制台";
|
|
188
|
-
console.log(`\n → 打开 ${web} 登录 →「添加设备」→ 复制登记密钥\n`);
|
|
189
|
-
const enrollKey = (await rl.question("登记密钥(已登记可留空): ")).trim();
|
|
242
|
+
const enrollKey = (v["enroll-key"] ?? (await rl.question("登记密钥(可留空): "))).trim();
|
|
190
243
|
const deviceName = (await rl.question(`设备名 [${s.deviceName || hostname()}]: `)).trim() || s.deviceName || hostname();
|
|
191
244
|
rl.close();
|
|
192
245
|
console.log("");
|
|
@@ -216,7 +269,17 @@ function cmdStatus() {
|
|
|
216
269
|
const s = readSettings();
|
|
217
270
|
console.log(`服务器: ${s.serverUrl || "(未配置)"}`);
|
|
218
271
|
console.log(`设备名: ${s.deviceName || "(默认)"}`);
|
|
219
|
-
|
|
272
|
+
const registered = fs.existsSync(CRED);
|
|
273
|
+
const pending = !registered ? readPendingAuth() : null;
|
|
274
|
+
if (registered) {
|
|
275
|
+
console.log("凭证: 已登记");
|
|
276
|
+
} else if (pending?.url) {
|
|
277
|
+
const mins = Number.isFinite(pending.expiresAt) ? Math.max(0, Math.round((pending.expiresAt - Date.now()) / 60000)) : null;
|
|
278
|
+
console.log(`凭证: 等待授权${mins !== null ? `(约 ${mins} 分钟内有效)` : ""}`);
|
|
279
|
+
console.log(` ${pending.url}`);
|
|
280
|
+
} else {
|
|
281
|
+
console.log("凭证: 未登记");
|
|
282
|
+
}
|
|
220
283
|
let running = false, active = "未运行";
|
|
221
284
|
if (IS_MAC) {
|
|
222
285
|
running = run("launchctl", ["list", "com.coflux.daemon"]).status === 0;
|
|
@@ -230,6 +293,11 @@ function cmdStatus() {
|
|
|
230
293
|
try { pid = ` (worker pid ${fs.readFileSync(join(HOME, "worker.pid"), "utf8").trim()})`; } catch { /* */ }
|
|
231
294
|
}
|
|
232
295
|
console.log(`服务: ${active}${pid}`);
|
|
296
|
+
if (IS_MAC) {
|
|
297
|
+
const fda = readFdaStatus();
|
|
298
|
+
const hint = fda === "granted" ? "" : "(`cofluxd fda` 引导授权,避免弹窗卡住 PTY 会话)";
|
|
299
|
+
console.log(`FDA: ${fdaLabel(fda)}${hint}`);
|
|
300
|
+
}
|
|
233
301
|
}
|
|
234
302
|
|
|
235
303
|
function cmdLogs(v) {
|
|
@@ -241,6 +309,28 @@ function cmdLogs(v) {
|
|
|
241
309
|
}
|
|
242
310
|
}
|
|
243
311
|
|
|
312
|
+
// 完全磁盘访问权限(FDA)引导:macOS 不允许程序自动弹出 FDA 授权窗(Apple 刻意设计),
|
|
313
|
+
// 上限就是检测 + 跳转系统设置 + 引导手动添加。授权对象是 supervisor 二进制本身——worker/PTY/agent
|
|
314
|
+
// 都是它的子进程,TCC 按 launchd 服务的 responsible process 归属,一次授权覆盖全树。
|
|
315
|
+
async function cmdFda() {
|
|
316
|
+
if (!IS_MAC) die("此命令仅 macOS 可用(Linux 无 TCC/FDA 概念,无需授权)");
|
|
317
|
+
console.log("\n 完全磁盘访问权限(FDA)引导\n ──────────────────────────\n");
|
|
318
|
+
console.log(" macOS 不支持程序自动弹出 FDA 授权窗,需手动在「系统设置」里添加。");
|
|
319
|
+
console.log(" 请把下面这个二进制拖进「隐私与安全性 → 完全磁盘访问权限」列表并勾选:\n");
|
|
320
|
+
console.log(` ${SUP_BIN}\n`);
|
|
321
|
+
console.log(" 即将打开系统设置面板,并在 Finder 中定位该二进制……\n");
|
|
322
|
+
run("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"]);
|
|
323
|
+
run("open", ["-R", SUP_BIN]);
|
|
324
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
325
|
+
try {
|
|
326
|
+
await rl.question(" 添加并勾选后按回车,重启服务使授权生效… ");
|
|
327
|
+
} finally {
|
|
328
|
+
rl.close();
|
|
329
|
+
}
|
|
330
|
+
restartService();
|
|
331
|
+
console.log("\n✓ 已重启服务(FDA 对已运行进程不生效,必须重启才能生效)。稍候用 `cofluxd status` 确认已授予。");
|
|
332
|
+
}
|
|
333
|
+
|
|
244
334
|
function cmdUninstall(v) {
|
|
245
335
|
stopService();
|
|
246
336
|
try { fs.rmSync(IS_MAC ? PLIST : UNIT); } catch { /* */ }
|
|
@@ -253,15 +343,16 @@ const HELP = `cofluxd —— coflux daemon 管理
|
|
|
253
343
|
|
|
254
344
|
cofluxd 首次=交互式配置(onboard),已配置=status
|
|
255
345
|
cofluxd onboard 交互式配置并启用
|
|
256
|
-
cofluxd up [flags]
|
|
346
|
+
cofluxd up [flags] 非交互装/起,零参数即可(不带 --enroll-key 时打印浏览器授权链接)
|
|
257
347
|
cofluxd reload 按 ~/.coflux/settings.json 重载并重启
|
|
258
348
|
cofluxd update 更新二进制并重启(worker 另可远程热升级)
|
|
259
|
-
cofluxd status
|
|
349
|
+
cofluxd status 服务器/登记(含"等待授权")/服务状态
|
|
350
|
+
cofluxd fda [仅 macOS] 引导授予完全磁盘访问权限(避免 PTY 因 TCC 弹窗卡住)
|
|
260
351
|
cofluxd logs [-f] 看 daemon 日志
|
|
261
352
|
cofluxd down 停止
|
|
262
353
|
cofluxd uninstall [--purge] 卸载(--purge 连二进制/配置/凭证一并删)
|
|
263
354
|
|
|
264
|
-
up flags: --server <ws://.../daemon> --enroll-key <KEY
|
|
355
|
+
up flags: --server <ws://.../daemon> --enroll-key <KEY>(留空则走浏览器授权) --name <名> --shell <路径>
|
|
265
356
|
通用: --version <vX|latest>(默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
|
|
266
357
|
配置都在 ~/.coflux/settings.json(serverUrl/enrollKey/deviceName/shell,含密钥故 600),daemon 直接读;改后 cofluxd reload 生效。`;
|
|
267
358
|
|
|
@@ -285,7 +376,7 @@ let cmd = positionals[0];
|
|
|
285
376
|
if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
|
|
286
377
|
if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "onboard"; // 首次裸跑 → 引导
|
|
287
378
|
|
|
288
|
-
const handlers = { up: cmdUp, onboard: cmdOnboard, reload: cmdReload, update: cmdUpdate, down: cmdDown, status: cmdStatus, logs: cmdLogs, uninstall: cmdUninstall };
|
|
379
|
+
const handlers = { up: cmdUp, onboard: cmdOnboard, reload: cmdReload, update: cmdUpdate, down: cmdDown, status: cmdStatus, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall };
|
|
289
380
|
const h = handlers[cmd];
|
|
290
381
|
if (!h) die(`未知命令: ${cmd}\n\n${HELP}`);
|
|
291
382
|
await h(values);
|