pi-lxmf 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/CHANGELOG.md +111 -0
- package/README.md +202 -0
- package/SPEC.md +469 -0
- package/package.json +52 -0
- package/src/bin.js +220 -0
- package/src/bridge.js +622 -0
- package/src/bz2.js +46 -0
- package/src/commands.js +288 -0
- package/src/config.js +393 -0
- package/src/identity.js +93 -0
- package/src/lxmf.js +380 -0
- package/src/quota.js +512 -0
- package/src/rpc.js +627 -0
- package/src/text.js +97 -0
package/src/bin.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file bin.js
|
|
5
|
+
*
|
|
6
|
+
* The `pi-lxmf` daemon entry point: loads configuration, starts the mesh
|
|
7
|
+
* side, spawns `pi --mode rpc`, wires the bridge, and stays alive until
|
|
8
|
+
* signalled or told to quit over LXMF.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { basename } from "node:path";
|
|
12
|
+
import { Bridge } from "./bridge.js";
|
|
13
|
+
import {
|
|
14
|
+
loadConfig,
|
|
15
|
+
readSessionPointer,
|
|
16
|
+
writeSessionPointer,
|
|
17
|
+
} from "./config.js";
|
|
18
|
+
import { deriveLxmfDestinationHash } from "./identity.js";
|
|
19
|
+
import { startLxmf } from "./lxmf.js";
|
|
20
|
+
import { GlmQuotaWatcher, readZaiKey } from "./quota.js";
|
|
21
|
+
import { PiRpcClient } from "./rpc.js";
|
|
22
|
+
|
|
23
|
+
const USAGE = `pi-lxmf — drive Pi over LXMF messaging
|
|
24
|
+
|
|
25
|
+
Usage: pi-lxmf [--config <path>] [--version]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--config, -c <path> Configuration file (default: $PI_LXMF_CONFIG or
|
|
29
|
+
~/.config/pi-lxmf/config.json)
|
|
30
|
+
--version, -v Print version and exit
|
|
31
|
+
--help, -h Show this help
|
|
32
|
+
|
|
33
|
+
The daemon announces itself on the Reticulum mesh and relays messages
|
|
34
|
+
between the paired owner and a supervised \`pi --mode rpc\` child process.
|
|
35
|
+
See SPEC.md for details.`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string[]} argv
|
|
39
|
+
* @returns {{config: string|null, version: boolean, help: boolean, error: string|null}}
|
|
40
|
+
*/
|
|
41
|
+
function parseArgv(argv) {
|
|
42
|
+
/** @type {{config: string|null, version: boolean, help: boolean, error: string|null}} */
|
|
43
|
+
const out = { config: null, version: false, help: false, error: null };
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
if (arg === "--config" || arg === "-c") {
|
|
47
|
+
const value = argv[++i];
|
|
48
|
+
if (!value) {
|
|
49
|
+
out.error = `${arg} requires a path`;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
out.config = value;
|
|
53
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
54
|
+
out.version = true;
|
|
55
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
56
|
+
out.help = true;
|
|
57
|
+
} else {
|
|
58
|
+
out.error = `unknown argument: ${arg}`;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} label
|
|
67
|
+
* @param {string} value
|
|
68
|
+
*/
|
|
69
|
+
function bannerLine(label, value) {
|
|
70
|
+
console.log(` ${label.padEnd(10)} ${value}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @type {boolean} */
|
|
74
|
+
let shuttingDown = false;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {unknown} e
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function errText(e) {
|
|
81
|
+
if (e instanceof Error) return e.message;
|
|
82
|
+
return String(e);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} reason
|
|
87
|
+
* @param {object} parts
|
|
88
|
+
* @param {import("./rpc.js").PiRpcClient|null} parts.rpc
|
|
89
|
+
* @param {{stop: () => void}|null} parts.mesh
|
|
90
|
+
* @param {Bridge|null} parts.bridge
|
|
91
|
+
* @param {import("./config.js").PiLxmfConfig|null} [parts.config]
|
|
92
|
+
*/
|
|
93
|
+
function gracefulShutdown(reason, parts) {
|
|
94
|
+
if (shuttingDown) return;
|
|
95
|
+
shuttingDown = true;
|
|
96
|
+
console.log(`pi-lxmf: shutting down (${reason})`);
|
|
97
|
+
const finish = () => {
|
|
98
|
+
parts.rpc?.stop();
|
|
99
|
+
parts.mesh?.stop();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
};
|
|
102
|
+
// Best effort: persist the current session pointer so the next start
|
|
103
|
+
// resumes this session.
|
|
104
|
+
const persist =
|
|
105
|
+
parts.bridge && parts.rpc && parts.config
|
|
106
|
+
? parts.rpc
|
|
107
|
+
.getState()
|
|
108
|
+
.then((/** @type {any} */ state) => {
|
|
109
|
+
if (state?.sessionFile) {
|
|
110
|
+
writeSessionPointer(
|
|
111
|
+
parts.config?.dataDir ?? "",
|
|
112
|
+
parts.config?.workdir ?? "",
|
|
113
|
+
state.sessionFile,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch(() => {})
|
|
118
|
+
: Promise.resolve();
|
|
119
|
+
persist.then(finish, finish);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async function main() {
|
|
126
|
+
const args = parseArgv(process.argv.slice(2));
|
|
127
|
+
if (args.error) {
|
|
128
|
+
console.error(`pi-lxmf: ${args.error}\n\n${USAGE}`);
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
131
|
+
if (args.help) {
|
|
132
|
+
console.log(USAGE);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const config = await loadConfig({ path: args.config ?? undefined });
|
|
137
|
+
if (args.version) {
|
|
138
|
+
console.log(config.name);
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.on("unhandledRejection", (e) => {
|
|
143
|
+
console.error("pi-lxmf: unhandled rejection:", e);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
console.log(`pi-lxmf: ${config.name} starting`);
|
|
147
|
+
bannerLine("workdir", config.workdir);
|
|
148
|
+
bannerLine("config", config.configPath ?? "(defaults)");
|
|
149
|
+
bannerLine("data", config.dataDir);
|
|
150
|
+
|
|
151
|
+
// Mesh first: the identity and delivery destination are needed for the
|
|
152
|
+
// banner, and inbound messages queue in the bridge until Pi is ready.
|
|
153
|
+
const mesh = await startLxmf(config);
|
|
154
|
+
bannerLine("identity", mesh.identityHash);
|
|
155
|
+
bannerLine("lxmf", mesh.deliveryHash);
|
|
156
|
+
bannerLine("interfaces", mesh.interfaceNames.join(", "));
|
|
157
|
+
bannerLine("announce", config.name);
|
|
158
|
+
|
|
159
|
+
bannerLine("owner (identity)", config.owner);
|
|
160
|
+
|
|
161
|
+
const sessionPointer = readSessionPointer(config.dataDir, config.workdir);
|
|
162
|
+
if (sessionPointer) {
|
|
163
|
+
bannerLine("resume", basename(sessionPointer.sessionFile));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @type {import("./rpc.js").PiRpcClient|null} */
|
|
167
|
+
let rpc = null;
|
|
168
|
+
/** @type {Bridge|null} */
|
|
169
|
+
let bridge = null;
|
|
170
|
+
const shutdown = (/** @type {string} */ reason) =>
|
|
171
|
+
gracefulShutdown(reason, { rpc, mesh, bridge, config });
|
|
172
|
+
|
|
173
|
+
rpc = new PiRpcClient({
|
|
174
|
+
piBin: config.piBin,
|
|
175
|
+
model: config.model,
|
|
176
|
+
cwd: config.workdir,
|
|
177
|
+
sessionPath: sessionPointer?.sessionFile ?? null,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
bridge = new Bridge({
|
|
181
|
+
config,
|
|
182
|
+
rpc,
|
|
183
|
+
mesh,
|
|
184
|
+
state: {
|
|
185
|
+
loadSession: () => readSessionPointer(config.dataDir, config.workdir),
|
|
186
|
+
saveSession: (file) =>
|
|
187
|
+
writeSessionPointer(config.dataDir, config.workdir, file),
|
|
188
|
+
},
|
|
189
|
+
quotaWatcher: new GlmQuotaWatcher({
|
|
190
|
+
ownerDestinationHash: deriveLxmfDestinationHash(config.owner),
|
|
191
|
+
sendText: (destHex, text) =>
|
|
192
|
+
mesh.sendText(destHex, text, { title: config.name }),
|
|
193
|
+
log: console,
|
|
194
|
+
apiKey: readZaiKey(),
|
|
195
|
+
}),
|
|
196
|
+
onShutdown: shutdown,
|
|
197
|
+
});
|
|
198
|
+
bridge.start();
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await rpc.start();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error(`pi-lxmf: ${errText(e)}`);
|
|
204
|
+
mesh.stop();
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
bridge.setRpcReady(true);
|
|
208
|
+
console.log("pi-lxmf: ready — listening for LXMF messages");
|
|
209
|
+
|
|
210
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
211
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
212
|
+
|
|
213
|
+
// Run until signalled or shut down over LXMF.
|
|
214
|
+
await new Promise(() => {});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch((e) => {
|
|
218
|
+
console.error("pi-lxmf: fatal:", e instanceof Error ? e.stack : e);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|