cofluxd 0.1.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 +33 -0
- package/cofluxd.mjs +270 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# cofluxd
|
|
2
|
+
|
|
3
|
+
coflux daemon 的管理 CLI。daemon 是预编译的 Rust 二进制(supervisor 持 PTY + worker 频繁热升级,**零 node 运行时**);本 CLI 只负责装/起/停/升级——node 仅在你偶尔跑命令时用一下。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i -g cofluxd
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 用法
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
cofluxd # 首次=交互式引导(问服务器/登记密钥/设备名),之后=看状态
|
|
15
|
+
cofluxd onboard # 显式重新走交互式配置
|
|
16
|
+
cofluxd up --enroll-key <KEY> # 非交互(web「添加设备」给的命令)
|
|
17
|
+
cofluxd status # 服务器/登记/服务状态
|
|
18
|
+
cofluxd logs -f # 看 daemon 日志
|
|
19
|
+
cofluxd update # 更新二进制并重启(worker 另可由 server 远程热升级)
|
|
20
|
+
cofluxd reload # 改了 settings.json 后重启生效
|
|
21
|
+
cofluxd down # 停止
|
|
22
|
+
cofluxd uninstall [--purge] # 卸载(--purge 连二进制/配置/凭证一并删)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
默认连公共服务 `wss://api.coflux.dev/daemon`(自托管用 `--server` 改)。登记密钥从 web 控制台「添加设备」获取。
|
|
26
|
+
|
|
27
|
+
## 配置
|
|
28
|
+
|
|
29
|
+
所有配置在 `~/.coflux/settings.json`(`serverUrl` / `enrollKey` / `deviceName` / `shell`,含密钥故权限 600),**daemon 直接读这个文件**。手改后 `cofluxd reload` 生效。
|
|
30
|
+
|
|
31
|
+
支持 macOS(launchd)/ Linux(systemd user service);服务崩溃自启、开机自启。
|
|
32
|
+
|
|
33
|
+
更多见 [coflux 仓库](https://github.com/myWsq/coflux)。
|
package/cofluxd.mjs
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cofluxd —— coflux daemon 管理 CLI。
|
|
3
|
+
// daemon 是两个 Rust 二进制(supervisor 持 PTY + worker 频繁热升级,零 node 运行时);
|
|
4
|
+
// 本 CLI 只负责装/起/停/升级(用一下,不常驻)。systemd(Linux user) / launchd(macOS LaunchAgent)。
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { homedir, hostname, platform, arch } from "node:os";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
// 默认中心服务(公共 SaaS);自托管用 --server 覆盖。
|
|
13
|
+
const DEFAULT_SERVER = "wss://api.coflux.dev/daemon";
|
|
14
|
+
const DEFAULT_WEB = "https://app.coflux.dev";
|
|
15
|
+
|
|
16
|
+
const REPO = "myWsq/coflux";
|
|
17
|
+
const HOME = process.env.COFLUX_HOME || join(homedir(), ".coflux");
|
|
18
|
+
const BIN_DIR = join(HOME, "bin");
|
|
19
|
+
const SETTINGS = join(HOME, "settings.json"); // 用户配置(含一次性登记密钥)→ daemon 直接读;含密钥故 600
|
|
20
|
+
const LOG_FILE = join(HOME, "daemon.log");
|
|
21
|
+
const CRED = join(HOME, "credentials.json");
|
|
22
|
+
const SUP_BIN = join(BIN_DIR, "coflux-supervisor");
|
|
23
|
+
const WRK_BIN = join(BIN_DIR, "coflux-worker");
|
|
24
|
+
const IS_MAC = platform() === "darwin";
|
|
25
|
+
const IS_LINUX = platform() === "linux";
|
|
26
|
+
const PLIST = join(homedir(), "Library", "LaunchAgents", "com.coflux.daemon.plist");
|
|
27
|
+
const UNIT = join(homedir(), ".config", "systemd", "user", "coflux-daemon.service");
|
|
28
|
+
|
|
29
|
+
const die = (m) => { console.error("✗ " + m); process.exit(1); };
|
|
30
|
+
const run = (cmd, args, opts = {}) => spawnSync(cmd, args, { encoding: "utf8", ...opts });
|
|
31
|
+
|
|
32
|
+
function rustTarget() {
|
|
33
|
+
const p = platform(), a = arch();
|
|
34
|
+
if (p === "darwin") return a === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
|
|
35
|
+
if (p === "linux") return a === "arm64" ? "aarch64-unknown-linux-musl" : "x86_64-unknown-linux-musl";
|
|
36
|
+
die(`不支持的平台: ${p}/${a}(仅 macOS / Linux)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readSettings() {
|
|
40
|
+
try { return JSON.parse(fs.readFileSync(SETTINGS, "utf8")); } catch { return {}; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function download(url, dest) {
|
|
44
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
45
|
+
if (!res.ok) die(`下载失败 HTTP ${res.status}: ${url}\n(该版本/平台的 release 资产是否已发布?)`);
|
|
46
|
+
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()), { mode: 0o755 });
|
|
47
|
+
}
|
|
48
|
+
async function ensureBinaries({ version, binDir }) {
|
|
49
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
50
|
+
if (binDir) {
|
|
51
|
+
for (const b of ["coflux-supervisor", "coflux-worker"]) {
|
|
52
|
+
const src = join(binDir, b);
|
|
53
|
+
if (!fs.existsSync(src)) die(`本地产物缺失: ${src}(先 cargo build --release?)`);
|
|
54
|
+
fs.copyFileSync(src, join(BIN_DIR, b));
|
|
55
|
+
fs.chmodSync(join(BIN_DIR, b), 0o755);
|
|
56
|
+
}
|
|
57
|
+
console.log(`✓ 用本地二进制(${binDir})`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const t = rustTarget();
|
|
61
|
+
const base = version === "latest" ? `https://github.com/${REPO}/releases/latest/download` : `https://github.com/${REPO}/releases/download/${version}`;
|
|
62
|
+
for (const b of ["coflux-supervisor", "coflux-worker"]) {
|
|
63
|
+
process.stdout.write(`下载 ${b}-${t} … `);
|
|
64
|
+
await download(`${base}/${b}-${t}`, join(BIN_DIR, b));
|
|
65
|
+
console.log("✓");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 写 settings.json(daemon 直接读;含一次性登记密钥 → 600)。未提供 enrollKey 时保留旧值。
|
|
70
|
+
function applyConfig({ serverUrl, enrollKey, deviceName, shell }) {
|
|
71
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
72
|
+
fs.chmodSync(HOME, 0o700);
|
|
73
|
+
const old = readSettings();
|
|
74
|
+
const settings = { serverUrl, deviceName, enrollKey: enrollKey || old.enrollKey || "" };
|
|
75
|
+
if (shell) settings.shell = shell;
|
|
76
|
+
fs.writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
77
|
+
return settings;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function plistXml() {
|
|
81
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
83
|
+
<plist version="1.0">
|
|
84
|
+
<dict>
|
|
85
|
+
<key>Label</key><string>com.coflux.daemon</string>
|
|
86
|
+
<key>ProgramArguments</key>
|
|
87
|
+
<array><string>${SUP_BIN}</string></array>
|
|
88
|
+
<key>EnvironmentVariables</key>
|
|
89
|
+
<dict><key>COFLUX_HOME</key><string>${HOME}</string></dict>
|
|
90
|
+
<key>RunAtLoad</key><true/>
|
|
91
|
+
<key>KeepAlive</key><true/>
|
|
92
|
+
<key>StandardOutPath</key><string>${LOG_FILE}</string>
|
|
93
|
+
<key>StandardErrorPath</key><string>${LOG_FILE}</string>
|
|
94
|
+
</dict>
|
|
95
|
+
</plist>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
function systemdUnit() {
|
|
99
|
+
return `[Unit]
|
|
100
|
+
Description=coflux daemon (supervisor)
|
|
101
|
+
After=network-online.target
|
|
102
|
+
Wants=network-online.target
|
|
103
|
+
|
|
104
|
+
[Service]
|
|
105
|
+
Environment=COFLUX_HOME=${HOME}
|
|
106
|
+
ExecStart=${SUP_BIN}
|
|
107
|
+
Restart=always
|
|
108
|
+
RestartSec=2
|
|
109
|
+
|
|
110
|
+
[Install]
|
|
111
|
+
WantedBy=default.target
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
function installService(start) {
|
|
115
|
+
if (IS_MAC) {
|
|
116
|
+
fs.mkdirSync(dirname(PLIST), { recursive: true });
|
|
117
|
+
fs.writeFileSync(PLIST, plistXml());
|
|
118
|
+
if (start) { run("launchctl", ["unload", PLIST]); run("launchctl", ["load", PLIST]); }
|
|
119
|
+
console.log(`✓ launchd: ${PLIST}`);
|
|
120
|
+
} else if (IS_LINUX) {
|
|
121
|
+
fs.mkdirSync(dirname(UNIT), { recursive: true });
|
|
122
|
+
fs.writeFileSync(UNIT, systemdUnit());
|
|
123
|
+
if (start) { run("systemctl", ["--user", "daemon-reload"]); run("systemctl", ["--user", "enable", "--now", "coflux-daemon.service"]); }
|
|
124
|
+
console.log(`✓ systemd: ${UNIT}`);
|
|
125
|
+
} else die("仅支持 macOS / Linux");
|
|
126
|
+
}
|
|
127
|
+
function restartService() {
|
|
128
|
+
if (IS_MAC) { run("launchctl", ["unload", PLIST]); run("launchctl", ["load", PLIST]); }
|
|
129
|
+
else if (IS_LINUX) run("systemctl", ["--user", "restart", "coflux-daemon.service"]);
|
|
130
|
+
}
|
|
131
|
+
function stopService() {
|
|
132
|
+
if (IS_MAC) run("launchctl", ["unload", PLIST]);
|
|
133
|
+
else if (IS_LINUX) run("systemctl", ["--user", "stop", "coflux-daemon.service"]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function applyAndStart({ serverUrl, enrollKey, deviceName, shell, version, binDir, noStart }) {
|
|
137
|
+
await ensureBinaries({ version, binDir });
|
|
138
|
+
const settings = applyConfig({ serverUrl, enrollKey, deviceName, shell });
|
|
139
|
+
if (!settings.enrollKey && !fs.existsSync(CRED)) {
|
|
140
|
+
console.warn("⚠ 无登记密钥且未登记:从 web「添加设备」获取后重跑。");
|
|
141
|
+
}
|
|
142
|
+
installService(!noStart);
|
|
143
|
+
console.log(noStart ? "已安装(未启动)。" : `✓ daemon 已启动 → ${serverUrl}`);
|
|
144
|
+
cmdStatus();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ------------------------------ 命令 ------------------------------ */
|
|
148
|
+
|
|
149
|
+
async function cmdUp(v) {
|
|
150
|
+
const s = readSettings();
|
|
151
|
+
await applyAndStart({
|
|
152
|
+
serverUrl: v.server || s.serverUrl || DEFAULT_SERVER,
|
|
153
|
+
enrollKey: v["enroll-key"],
|
|
154
|
+
deviceName: v.name || s.deviceName || hostname(),
|
|
155
|
+
shell: v.shell || s.shell,
|
|
156
|
+
version: v.version, binDir: v["bin-dir"], noStart: v["no-start"],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cmdOnboard(v) {
|
|
161
|
+
const s = readSettings();
|
|
162
|
+
console.log("\n 欢迎使用 coflux —— 配置这台设备\n ──────────────────────────────\n");
|
|
163
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
164
|
+
try {
|
|
165
|
+
const serverUrl = (await rl.question(`服务器地址 [${s.serverUrl || DEFAULT_SERVER}]: `)).trim() || s.serverUrl || DEFAULT_SERVER;
|
|
166
|
+
const web = serverUrl === DEFAULT_SERVER ? DEFAULT_WEB : "你的 coflux web 控制台";
|
|
167
|
+
console.log(`\n → 打开 ${web} 登录 →「添加设备」→ 复制登记密钥\n`);
|
|
168
|
+
const enrollKey = (await rl.question("登记密钥(已登记可留空): ")).trim();
|
|
169
|
+
const deviceName = (await rl.question(`设备名 [${s.deviceName || hostname()}]: `)).trim() || s.deviceName || hostname();
|
|
170
|
+
rl.close();
|
|
171
|
+
console.log("");
|
|
172
|
+
await applyAndStart({ serverUrl, enrollKey, deviceName, shell: s.shell, version: v.version, binDir: v["bin-dir"], noStart: v["no-start"] });
|
|
173
|
+
} finally {
|
|
174
|
+
rl.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function cmdReload() {
|
|
179
|
+
if (!fs.existsSync(SETTINGS)) die("无 settings.json,先 cofluxd up 或 cofluxd onboard");
|
|
180
|
+
restartService(); // daemon 重启时重新读 settings.json
|
|
181
|
+
console.log("✓ 已重启(daemon 重新读取 settings.json)");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cmdDown() { stopService(); console.log("✓ 已停止"); }
|
|
185
|
+
|
|
186
|
+
async function cmdUpdate(v) {
|
|
187
|
+
if (!fs.existsSync(ENV_FILE)) die("尚未安装,先 cofluxd up / onboard");
|
|
188
|
+
await ensureBinaries({ version: v.version, binDir: v["bin-dir"] });
|
|
189
|
+
restartService();
|
|
190
|
+
console.log(`✓ 已更新到 ${v["bin-dir"] ? "本地产物" : v.version} 并重启(supervisor)`);
|
|
191
|
+
console.log(" 注:worker 还可由 server 远程热升级,无需停服。");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cmdStatus() {
|
|
195
|
+
const s = readSettings();
|
|
196
|
+
console.log(`服务器: ${s.serverUrl || "(未配置)"}`);
|
|
197
|
+
console.log(`设备名: ${s.deviceName || "(默认)"}`);
|
|
198
|
+
console.log(`凭证: ${fs.existsSync(CRED) ? "已登记" : "未登记"}`);
|
|
199
|
+
let running = false, active = "未运行";
|
|
200
|
+
if (IS_MAC) {
|
|
201
|
+
running = run("launchctl", ["list", "com.coflux.daemon"]).status === 0;
|
|
202
|
+
active = running ? "运行中" : "未运行";
|
|
203
|
+
} else if (IS_LINUX) {
|
|
204
|
+
active = (run("systemctl", ["--user", "is-active", "coflux-daemon.service"]).stdout || "").trim() || "未运行";
|
|
205
|
+
running = active === "active";
|
|
206
|
+
}
|
|
207
|
+
let pid = "";
|
|
208
|
+
if (running) {
|
|
209
|
+
try { pid = ` (worker pid ${fs.readFileSync(join(HOME, "worker.pid"), "utf8").trim()})`; } catch { /* */ }
|
|
210
|
+
}
|
|
211
|
+
console.log(`服务: ${active}${pid}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cmdLogs(v) {
|
|
215
|
+
if (IS_MAC) {
|
|
216
|
+
if (!fs.existsSync(LOG_FILE)) die(`暂无日志 ${LOG_FILE}`);
|
|
217
|
+
run("tail", v.follow ? ["-f", LOG_FILE] : ["-n", "100", LOG_FILE], { stdio: "inherit" });
|
|
218
|
+
} else if (IS_LINUX) {
|
|
219
|
+
run("journalctl", ["--user", "-u", "coflux-daemon.service", ...(v.follow ? ["-f"] : ["-n", "100"])], { stdio: "inherit" });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cmdUninstall(v) {
|
|
224
|
+
stopService();
|
|
225
|
+
try { fs.rmSync(IS_MAC ? PLIST : UNIT); } catch { /* */ }
|
|
226
|
+
if (IS_LINUX) run("systemctl", ["--user", "daemon-reload"]);
|
|
227
|
+
if (v.purge) { try { fs.rmSync(HOME, { recursive: true, force: true }); } catch { /* */ } console.log("✓ 已卸载并清除 " + HOME); }
|
|
228
|
+
else console.log(`✓ 已卸载服务(保留二进制/配置/凭证于 ${HOME};--purge 可全清)`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const HELP = `cofluxd —— coflux daemon 管理
|
|
232
|
+
|
|
233
|
+
cofluxd 首次=交互式配置(onboard),已配置=status
|
|
234
|
+
cofluxd onboard 交互式配置并启用
|
|
235
|
+
cofluxd up [flags] 非交互装/起(web「添加设备」给的命令用这个)
|
|
236
|
+
cofluxd reload 按 ~/.coflux/settings.json 重载并重启
|
|
237
|
+
cofluxd update 更新二进制并重启(worker 另可远程热升级)
|
|
238
|
+
cofluxd status 服务器/登记/服务状态
|
|
239
|
+
cofluxd logs [-f] 看 daemon 日志
|
|
240
|
+
cofluxd down 停止
|
|
241
|
+
cofluxd uninstall [--purge] 卸载(--purge 连二进制/配置/凭证一并删)
|
|
242
|
+
|
|
243
|
+
up flags: --server <ws://.../daemon> --enroll-key <KEY> --name <名> --shell <路径>
|
|
244
|
+
通用: --version <vX|latest>(默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
|
|
245
|
+
配置都在 ~/.coflux/settings.json(serverUrl/enrollKey/deviceName/shell,含密钥故 600),daemon 直接读;改后 cofluxd reload 生效。`;
|
|
246
|
+
|
|
247
|
+
const { values, positionals } = parseArgs({
|
|
248
|
+
allowPositionals: true,
|
|
249
|
+
options: {
|
|
250
|
+
server: { type: "string" },
|
|
251
|
+
"enroll-key": { type: "string" },
|
|
252
|
+
name: { type: "string" },
|
|
253
|
+
shell: { type: "string" },
|
|
254
|
+
version: { type: "string", default: "latest" },
|
|
255
|
+
"bin-dir": { type: "string" },
|
|
256
|
+
"no-start": { type: "boolean", default: false },
|
|
257
|
+
purge: { type: "boolean", default: false },
|
|
258
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
259
|
+
help: { type: "boolean", short: "h", default: false },
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let cmd = positionals[0];
|
|
264
|
+
if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
|
|
265
|
+
if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "onboard"; // 首次裸跑 → 引导
|
|
266
|
+
|
|
267
|
+
const handlers = { up: cmdUp, onboard: cmdOnboard, reload: cmdReload, update: cmdUpdate, down: cmdDown, status: cmdStatus, logs: cmdLogs, uninstall: cmdUninstall };
|
|
268
|
+
const h = handlers[cmd];
|
|
269
|
+
if (!h) die(`未知命令: ${cmd}\n\n${HELP}`);
|
|
270
|
+
await h(values);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cofluxd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cofluxd": "cofluxd.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cofluxd.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/myWsq/coflux#readme",
|
|
18
|
+
"bugs": "https://github.com/myWsq/coflux/issues",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/myWsq/coflux.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"coflux",
|
|
26
|
+
"daemon",
|
|
27
|
+
"cli",
|
|
28
|
+
"remote-terminal",
|
|
29
|
+
"pty"
|
|
30
|
+
]
|
|
31
|
+
}
|