palmier 0.2.6 → 0.2.8
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/.github/workflows/publish.yml +24 -0
- package/CLAUDE.md +9 -9
- package/README.md +288 -288
- package/dist/agents/shared-prompt.js +16 -16
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +27 -123
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +2 -7
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/run.js +36 -41
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +14 -33
- package/dist/config.js +3 -17
- package/dist/events.d.ts +3 -4
- package/dist/events.js +25 -8
- package/dist/index.js +8 -0
- package/dist/rpc-handler.js +5 -29
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/types.d.ts +1 -3
- package/package.json +44 -36
- package/src/agents/claude.ts +44 -44
- package/src/agents/shared-prompt.ts +28 -28
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +29 -150
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +2 -10
- package/src/commands/pair.ts +50 -63
- package/src/commands/run.ts +619 -633
- package/src/commands/serve.ts +14 -31
- package/src/config.ts +3 -18
- package/src/events.ts +23 -10
- package/src/index.ts +9 -0
- package/src/nats-client.ts +15 -15
- package/src/rpc-handler.ts +388 -414
- package/src/transports/http-transport.ts +123 -19
- package/src/types.ts +62 -66
- package/dist/commands/hook.d.ts +0 -7
- package/dist/commands/hook.js +0 -208
- package/dist/commands/task-cleanup.d.ts +0 -14
- package/dist/commands/task-cleanup.js +0 -84
- package/dist/commands/task-generation.md +0 -28
- package/dist/systemd.d.ts +0 -20
- package/dist/systemd.js +0 -145
package/src/commands/init.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import { randomUUID, randomBytes } from "crypto";
|
|
3
2
|
import { loadConfig, saveConfig } from "../config.js";
|
|
4
3
|
import { detectAgents } from "../agents/agent.js";
|
|
5
|
-
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
4
|
import { getPlatform } from "../platform/index.js";
|
|
7
5
|
import { pairCommand } from "./pair.js";
|
|
8
6
|
import type { HostConfig } from "../types.js";
|
|
@@ -13,7 +11,6 @@ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
|
13
11
|
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
14
12
|
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
15
13
|
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
16
|
-
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
17
14
|
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
18
15
|
|
|
19
16
|
/**
|
|
@@ -33,11 +30,8 @@ export async function initCommand(): Promise<void> {
|
|
|
33
30
|
const agents = await detectAgents();
|
|
34
31
|
|
|
35
32
|
if (agents.length === 0) {
|
|
36
|
-
console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one
|
|
37
|
-
console.log(`
|
|
38
|
-
console.log(` - ${bold("Gemini CLI")} npm install -g @google/gemini-cli`);
|
|
39
|
-
console.log(` - ${bold("Codex CLI")} npm install -g @openai/codex`);
|
|
40
|
-
console.log(` - ${bold("OpenClaw")} See https://github.com/openclaw/openclaw\n`);
|
|
33
|
+
console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one supported agent CLI.\n`);
|
|
34
|
+
console.log(`See supported agents: https://www.palmier.me/agents\n`);
|
|
41
35
|
console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
|
|
42
36
|
rl.close();
|
|
43
37
|
process.exit(1);
|
|
@@ -45,118 +39,44 @@ export async function initCommand(): Promise<void> {
|
|
|
45
39
|
|
|
46
40
|
console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Prepare LAN details for summary
|
|
70
|
-
let lanIp: string | undefined;
|
|
71
|
-
const directPort = 7400;
|
|
72
|
-
if (enableLan) {
|
|
73
|
-
lanIp = detectLanIp();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Summary before committing
|
|
77
|
-
console.log(`\n${bold("--- Setup Summary ---")}\n`);
|
|
78
|
-
console.log(` Mode: ${cyan(mode)}${dim(mode === "auto" ? " (NATS + LAN)" : mode === "nats" ? " (NATS only)" : " (LAN only)")}`);
|
|
79
|
-
if (serverUrl) {
|
|
80
|
-
console.log(` Server: ${cyan(serverUrl)}`);
|
|
81
|
-
}
|
|
82
|
-
if (enableLan && lanIp) {
|
|
83
|
-
console.log(` LAN: ${cyan(`${lanIp}:${directPort}`)}`);
|
|
84
|
-
}
|
|
85
|
-
console.log(` Agents: ${green(agents.map((a) => a.label).join(", "))}`);
|
|
86
|
-
console.log("");
|
|
87
|
-
|
|
88
|
-
const confirm = await ask("Proceed? (Y/n): ");
|
|
89
|
-
if (confirm.trim().toLowerCase() === "n") {
|
|
90
|
-
console.log("\nSetup cancelled.");
|
|
91
|
-
rl.close();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Register with server (after confirmation)
|
|
96
|
-
let registerResponse:
|
|
97
|
-
| { hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string }
|
|
98
|
-
| undefined;
|
|
99
|
-
|
|
100
|
-
if (accessMode === "full" && serverUrl) {
|
|
101
|
-
let existingHostId: string | undefined;
|
|
102
|
-
try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
|
|
103
|
-
|
|
104
|
-
while (true) {
|
|
105
|
-
console.log(`\nRegistering host with ${cyan(serverUrl)}...`);
|
|
106
|
-
try {
|
|
107
|
-
registerResponse = await registerHost(serverUrl, existingHostId);
|
|
108
|
-
console.log(green("Host registered successfully."));
|
|
109
|
-
break;
|
|
110
|
-
} catch (err) {
|
|
111
|
-
console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
|
|
112
|
-
const retry = await ask("\nRetry? (Y/n): ");
|
|
113
|
-
if (retry.trim().toLowerCase() === "n") {
|
|
114
|
-
console.log("\nSetup cancelled.");
|
|
115
|
-
rl.close();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
42
|
+
// Register with server
|
|
43
|
+
let existingHostId: string | undefined;
|
|
44
|
+
try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
|
|
45
|
+
|
|
46
|
+
const serverUrl = "https://app.palmier.me";
|
|
47
|
+
let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string };
|
|
48
|
+
|
|
49
|
+
while (true) {
|
|
50
|
+
console.log(`\nRegistering host...`);
|
|
51
|
+
try {
|
|
52
|
+
registerResponse = await registerHost(serverUrl, existingHostId);
|
|
53
|
+
console.log(green("Host registered successfully."));
|
|
54
|
+
break;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
|
|
57
|
+
const retry = await ask("\nRetry? (Y/n): ");
|
|
58
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
59
|
+
console.log("\nSetup cancelled.");
|
|
60
|
+
rl.close();
|
|
61
|
+
return;
|
|
118
62
|
}
|
|
119
63
|
}
|
|
120
64
|
}
|
|
121
65
|
|
|
122
66
|
// Build config
|
|
123
67
|
const config: HostConfig = {
|
|
124
|
-
hostId: registerResponse
|
|
68
|
+
hostId: registerResponse.hostId,
|
|
125
69
|
projectRoot: process.cwd(),
|
|
126
|
-
|
|
70
|
+
nats: true,
|
|
71
|
+
natsUrl: registerResponse.natsUrl,
|
|
72
|
+
natsWsUrl: registerResponse.natsWsUrl,
|
|
73
|
+
natsToken: registerResponse.natsToken,
|
|
74
|
+
agents,
|
|
127
75
|
};
|
|
128
76
|
|
|
129
|
-
if (registerResponse) {
|
|
130
|
-
config.natsUrl = registerResponse.natsUrl;
|
|
131
|
-
config.natsWsUrl = registerResponse.natsWsUrl;
|
|
132
|
-
config.natsToken = registerResponse.natsToken;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (enableLan) {
|
|
136
|
-
const directToken = randomBytes(32).toString("hex");
|
|
137
|
-
config.directPort = directPort;
|
|
138
|
-
config.directToken = directToken;
|
|
139
|
-
|
|
140
|
-
console.log(`\n LAN IP: ${cyan(lanIp!)}`);
|
|
141
|
-
console.log(` Port: ${cyan(String(directPort))}`);
|
|
142
|
-
|
|
143
|
-
if (process.platform === "win32") {
|
|
144
|
-
console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
|
|
145
|
-
console.log(
|
|
146
|
-
dim(` netsh advfirewall firewall add rule name="Palmier" dir=in action=allow protocol=TCP localport=${directPort}`)
|
|
147
|
-
);
|
|
148
|
-
} else if (process.platform === "darwin") {
|
|
149
|
-
console.log(`\n ${yellow("Firewall:")} macOS will prompt you to allow incoming connections automatically.`);
|
|
150
|
-
} else {
|
|
151
|
-
console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
|
|
152
|
-
console.log(dim(` sudo ufw allow ${directPort}/tcp`));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
config.agents = agents;
|
|
157
77
|
saveConfig(config);
|
|
158
78
|
|
|
159
|
-
console.log(`\n${green("Host provisioned")}
|
|
79
|
+
console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
|
|
160
80
|
console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
|
|
161
81
|
|
|
162
82
|
getPlatform().installDaemon(config);
|
|
@@ -170,36 +90,6 @@ export async function initCommand(): Promise<void> {
|
|
|
170
90
|
}
|
|
171
91
|
}
|
|
172
92
|
|
|
173
|
-
async function promptAccessMode(ask: AskFn, step: number): Promise<"full" | "lan"> {
|
|
174
|
-
console.log(`${bold(`Step ${step}:`)} Choose access mode\n`);
|
|
175
|
-
console.log(` ${bold("1)")} Full access ${dim("— built-in email and push notification support")}`);
|
|
176
|
-
console.log(` ${bold("2)")} LAN only ${dim("— no data relayed by palmier server, requires same network")}`);
|
|
177
|
-
console.log("");
|
|
178
|
-
|
|
179
|
-
const answer = await ask("Select [1]: ");
|
|
180
|
-
const trimmed = answer.trim();
|
|
181
|
-
|
|
182
|
-
if (trimmed === "2") {
|
|
183
|
-
return "lan";
|
|
184
|
-
}
|
|
185
|
-
return "full";
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function promptServerUrl(ask: AskFn, step: number): Promise<string> {
|
|
189
|
-
const defaultUrl = "https://www.palmier.me";
|
|
190
|
-
console.log(`\n${bold(`Step ${step}:`)} Enter your Palmier server URL\n`);
|
|
191
|
-
|
|
192
|
-
while (true) {
|
|
193
|
-
const answer = await ask(`Server URL [${defaultUrl}]: `);
|
|
194
|
-
const trimmed = (answer.trim() || defaultUrl).replace(/\/+$/, "");
|
|
195
|
-
|
|
196
|
-
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
197
|
-
return trimmed;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
console.log(" URL must start with http:// or https://. Please try again.\n");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
93
|
|
|
204
94
|
async function registerHost(
|
|
205
95
|
serverUrl: string,
|
|
@@ -231,14 +121,3 @@ async function registerHost(
|
|
|
231
121
|
throw new Error(`Failed to register host: ${message}`);
|
|
232
122
|
}
|
|
233
123
|
}
|
|
234
|
-
|
|
235
|
-
async function promptEnableLan(ask: AskFn, step: number): Promise<boolean> {
|
|
236
|
-
console.log(`\n${bold(`Step ${step}:`)} Enable LAN access?\n`);
|
|
237
|
-
console.log(` ${dim("When enabled, the app will automatically use a direct LAN connection")}`);
|
|
238
|
-
console.log(` ${dim("when on the same network, and fall back to the server otherwise.")}\n`);
|
|
239
|
-
|
|
240
|
-
const answer = await ask("Enable LAN access? (Y/n): ");
|
|
241
|
-
return answer.trim().toLowerCase() !== "n";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
4
|
+
import { createRpcHandler } from "../rpc-handler.js";
|
|
5
|
+
import { startHttpTransport, detectLanIp } from "../transports/http-transport.js";
|
|
6
|
+
|
|
7
|
+
const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
|
|
8
|
+
|
|
9
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
10
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
11
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
12
|
+
|
|
13
|
+
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
14
|
+
const CODE_LENGTH = 6;
|
|
15
|
+
|
|
16
|
+
function generateCode(): string {
|
|
17
|
+
const bytes = new Uint8Array(CODE_LENGTH);
|
|
18
|
+
crypto.getRandomValues(bytes);
|
|
19
|
+
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeLockfile(port: number): void {
|
|
23
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
fs.writeFileSync(LAN_LOCKFILE, JSON.stringify({ port, pid: process.pid }), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function removeLockfile(): void {
|
|
28
|
+
try { fs.unlinkSync(LAN_LOCKFILE); } catch { /* ignore */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Start an on-demand LAN server for direct HTTP connections.
|
|
33
|
+
* Generates a pairing code and displays it — no separate `palmier pair` needed.
|
|
34
|
+
*/
|
|
35
|
+
export async function lanCommand(opts: { port: number }): Promise<void> {
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
const port = opts.port;
|
|
38
|
+
const ip = detectLanIp();
|
|
39
|
+
const code = generateCode();
|
|
40
|
+
|
|
41
|
+
const handleRpc = createRpcHandler(config);
|
|
42
|
+
|
|
43
|
+
// Write lockfile so other palmier processes can discover us
|
|
44
|
+
writeLockfile(port);
|
|
45
|
+
|
|
46
|
+
// Clean up on exit
|
|
47
|
+
process.on("SIGINT", () => { removeLockfile(); process.exit(0); });
|
|
48
|
+
process.on("SIGTERM", () => { removeLockfile(); process.exit(0); });
|
|
49
|
+
process.on("exit", removeLockfile);
|
|
50
|
+
|
|
51
|
+
// Start the HTTP transport with the pre-generated pairing code
|
|
52
|
+
await startHttpTransport(config, handleRpc, port, code, () => {
|
|
53
|
+
console.log(`\n${bold("Palmier LAN Server")}\n`);
|
|
54
|
+
console.log(` ${cyan("Open the app at:")} ${bold(`http://${ip}:${port}`)}\n`);
|
|
55
|
+
console.log(` ${cyan("Pairing code:")} ${bold(code)}\n`);
|
|
56
|
+
console.log(` ${dim("Press Ctrl+C to stop.")}\n`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -4,17 +4,9 @@ import { z } from "zod";
|
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import type { NatsConnection } from "nats";
|
|
8
|
-
|
|
9
7
|
export async function mcpserverCommand(): Promise<void> {
|
|
10
8
|
const config = loadConfig();
|
|
11
|
-
const
|
|
12
|
-
const useNats = mode === "nats" || mode === "auto";
|
|
13
|
-
|
|
14
|
-
let nc: NatsConnection | undefined;
|
|
15
|
-
if (useNats) {
|
|
16
|
-
nc = await connectNats(config);
|
|
17
|
-
}
|
|
9
|
+
const nc = await connectNats(config);
|
|
18
10
|
|
|
19
11
|
const sc = StringCodec();
|
|
20
12
|
|
|
@@ -23,7 +15,7 @@ export async function mcpserverCommand(): Promise<void> {
|
|
|
23
15
|
{ capabilities: { tools: {} } }
|
|
24
16
|
);
|
|
25
17
|
|
|
26
|
-
// send-push-notification requires NATS — only register
|
|
18
|
+
// send-push-notification requires NATS — only register when server mode is enabled
|
|
27
19
|
if (nc) {
|
|
28
20
|
server.registerTool(
|
|
29
21
|
"send-push-notification",
|
package/src/commands/pair.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import * as http from "node:http";
|
|
2
4
|
import { StringCodec } from "nats";
|
|
3
|
-
import { loadConfig } from "../config.js";
|
|
5
|
+
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
4
6
|
import { connectNats } from "../nats-client.js";
|
|
5
|
-
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
7
|
import { addSession } from "../session-store.js";
|
|
7
8
|
import type { HostConfig } from "../types.js";
|
|
8
9
|
|
|
9
10
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
10
11
|
const CODE_LENGTH = 6;
|
|
11
12
|
const EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
|
+
const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
|
|
12
14
|
|
|
13
15
|
function generateCode(): string {
|
|
14
16
|
const bytes = new Uint8Array(CODE_LENGTH);
|
|
@@ -18,17 +20,14 @@ function generateCode(): string {
|
|
|
18
20
|
|
|
19
21
|
function buildPairResponse(config: HostConfig, label?: string) {
|
|
20
22
|
const session = addSession(label);
|
|
21
|
-
|
|
23
|
+
return {
|
|
22
24
|
hostId: config.hostId,
|
|
23
25
|
sessionToken: session.token,
|
|
24
26
|
};
|
|
25
|
-
|
|
26
|
-
return response;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* POST to the running
|
|
31
|
-
* Returns true if paired, false if expired/failed.
|
|
30
|
+
* POST to the running LAN server and long-poll until paired or expired.
|
|
32
31
|
*/
|
|
33
32
|
function lanPairRegister(port: number, code: string): Promise<boolean> {
|
|
34
33
|
const body = JSON.stringify({ code, expiryMs: EXPIRY_MS });
|
|
@@ -41,16 +40,14 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
41
40
|
path: "/internal/pair-register",
|
|
42
41
|
method: "POST",
|
|
43
42
|
headers: { "Content-Type": "application/json" },
|
|
44
|
-
timeout: EXPIRY_MS + 5000,
|
|
43
|
+
timeout: EXPIRY_MS + 5000,
|
|
45
44
|
},
|
|
46
45
|
(res) => {
|
|
47
46
|
const chunks: Buffer[] = [];
|
|
48
47
|
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
49
48
|
res.on("end", () => {
|
|
50
49
|
try {
|
|
51
|
-
const result = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as {
|
|
52
|
-
paired: boolean;
|
|
53
|
-
};
|
|
50
|
+
const result = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as { paired: boolean };
|
|
54
51
|
resolve(result.paired);
|
|
55
52
|
} catch {
|
|
56
53
|
resolve(false);
|
|
@@ -59,29 +56,29 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
59
56
|
},
|
|
60
57
|
);
|
|
61
58
|
|
|
62
|
-
req.on("error", (
|
|
63
|
-
|
|
64
|
-
console.error("Make sure `palmier serve` is running first.");
|
|
65
|
-
resolve(false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
req.on("timeout", () => {
|
|
69
|
-
req.destroy();
|
|
70
|
-
resolve(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
59
|
+
req.on("error", () => resolve(false));
|
|
60
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
73
61
|
req.end(body);
|
|
74
62
|
});
|
|
75
63
|
}
|
|
76
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Read the LAN lockfile to check if `palmier lan` is running.
|
|
67
|
+
*/
|
|
68
|
+
function getLanPort(): number | null {
|
|
69
|
+
try {
|
|
70
|
+
const raw = fs.readFileSync(LAN_LOCKFILE, "utf-8");
|
|
71
|
+
return (JSON.parse(raw) as { port: number }).port;
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
77
75
|
/**
|
|
78
76
|
* Generate an OTP code and wait for a PWA client to pair.
|
|
79
|
-
* Listens on NATS
|
|
77
|
+
* Listens on NATS always, and also on the LAN server if `palmier lan` is running.
|
|
80
78
|
*/
|
|
81
79
|
export async function pairCommand(): Promise<void> {
|
|
82
80
|
const config = loadConfig();
|
|
83
81
|
const code = generateCode();
|
|
84
|
-
const mode = config.mode ?? "nats";
|
|
85
82
|
|
|
86
83
|
let paired = false;
|
|
87
84
|
|
|
@@ -98,53 +95,43 @@ export async function pairCommand(): Promise<void> {
|
|
|
98
95
|
console.log("");
|
|
99
96
|
console.log(` ${code}`);
|
|
100
97
|
console.log("");
|
|
101
|
-
|
|
102
|
-
if (mode === "lan" || mode === "auto") {
|
|
103
|
-
const ip = detectLanIp();
|
|
104
|
-
const port = config.directPort ?? 7400;
|
|
105
|
-
console.log(` LAN Address: ${ip}:${port}`);
|
|
106
|
-
console.log("");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
98
|
console.log("Code expires in 5 minutes.");
|
|
110
99
|
|
|
111
|
-
// NATS pairing (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const sub = nc.subscribe(subject, { max: 1 });
|
|
117
|
-
|
|
118
|
-
cleanups.push(() => {
|
|
119
|
-
sub.unsubscribe();
|
|
120
|
-
nc.close();
|
|
121
|
-
});
|
|
100
|
+
// NATS pairing (always active)
|
|
101
|
+
const nc = await connectNats(config);
|
|
102
|
+
const sc = StringCodec();
|
|
103
|
+
const subject = `pair.${code}`;
|
|
104
|
+
const sub = nc.subscribe(subject, { max: 1 });
|
|
122
105
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
if (msg.data && msg.data.length > 0) {
|
|
129
|
-
const body = JSON.parse(sc.decode(msg.data)) as { label?: string };
|
|
130
|
-
label = body.label;
|
|
131
|
-
}
|
|
132
|
-
} catch { /* empty body is fine */ }
|
|
106
|
+
cleanups.push(() => {
|
|
107
|
+
sub.unsubscribe();
|
|
108
|
+
nc.close();
|
|
109
|
+
});
|
|
133
110
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
(async () => {
|
|
112
|
+
for await (const msg of sub) {
|
|
113
|
+
if (paired) break;
|
|
114
|
+
let label: string | undefined;
|
|
115
|
+
try {
|
|
116
|
+
if (msg.data && msg.data.length > 0) {
|
|
117
|
+
const body = JSON.parse(sc.decode(msg.data)) as { label?: string };
|
|
118
|
+
label = body.label;
|
|
137
119
|
}
|
|
138
|
-
|
|
120
|
+
} catch { /* empty body is fine */ }
|
|
121
|
+
|
|
122
|
+
const response = buildPairResponse(config, label);
|
|
123
|
+
if (msg.reply) {
|
|
124
|
+
msg.respond(sc.encode(JSON.stringify(response)));
|
|
139
125
|
}
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
onPaired();
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
142
129
|
|
|
143
|
-
// LAN pairing —
|
|
144
|
-
|
|
145
|
-
|
|
130
|
+
// LAN pairing — if `palmier lan` is running, also register with it
|
|
131
|
+
const lanPort = getLanPort();
|
|
132
|
+
if (lanPort) {
|
|
146
133
|
(async () => {
|
|
147
|
-
const result = await lanPairRegister(
|
|
134
|
+
const result = await lanPairRegister(lanPort, code);
|
|
148
135
|
if (result) onPaired();
|
|
149
136
|
})();
|
|
150
137
|
}
|