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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/cofluxd.mjs +104 -13
  3. 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 --enroll-key <KEY> # 非交互(web「添加设备」给的命令)
17
- cofluxd status # 服务器/登记/服务状态
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` 改)。登记密钥从 web 控制台「添加设备」获取。
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
- if (!settings.enrollKey && !fs.existsSync(CRED)) {
161
- console.warn("⚠ 无登记密钥且未登记:从 web「添加设备」获取后重跑。");
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
- cmdStatus();
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 serverUrl = (await rl.question(`服务器地址 [${s.serverUrl || DEFAULT_SERVER}]: `)).trim() || s.serverUrl || DEFAULT_SERVER;
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
- console.log(`凭证: ${fs.existsSync(CRED) ? "已登记" : "未登记"}`);
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] 非交互装/起(web「添加设备」给的命令用这个)
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> --name <名> --shell <路径>
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cofluxd",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
5
5
  "type": "module",
6
6
  "bin": {