@clawpilot-app/link 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 +48 -0
- package/package.json +43 -0
- package/scripts/check-node-version.mjs +44 -0
- package/src/cli.js +599 -0
- package/src/constants.js +1 -0
- package/src/daemon.js +1661 -0
- package/src/i18n.js +182 -0
- package/src/network.js +71 -0
- package/src/openclaw.js +297 -0
- package/src/runtime.js +423 -0
- package/src/server-api.js +135 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import qrcode from "qrcode-terminal";
|
|
6
|
+
import { LinkDaemon, isDaemonRunning, sendIpcRequest, spawnBackgroundDaemon, waitForDaemonReady } from "./daemon.js";
|
|
7
|
+
import { createTranslator, resolveLanguage } from "./i18n.js";
|
|
8
|
+
import { ServerApiError, createPairingSession, bootstrapLink } from "./server-api.js";
|
|
9
|
+
import { LINK_DIRECT_PORT, listLanIpv4Addresses } from "./network.js";
|
|
10
|
+
import {
|
|
11
|
+
encodeBase64UrlJson,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadCredentials,
|
|
14
|
+
loadState,
|
|
15
|
+
normalizeHttpsBaseUrl,
|
|
16
|
+
patchState,
|
|
17
|
+
runtimePaths,
|
|
18
|
+
saveCredentials,
|
|
19
|
+
} from "./runtime.js";
|
|
20
|
+
import { detectOpenClawBackend, installSkill } from "./openclaw.js";
|
|
21
|
+
|
|
22
|
+
function parseFlags(argv) {
|
|
23
|
+
const flags = new Set();
|
|
24
|
+
const values = [];
|
|
25
|
+
for (const item of argv) {
|
|
26
|
+
if (item.startsWith("--")) {
|
|
27
|
+
flags.add(item);
|
|
28
|
+
} else {
|
|
29
|
+
values.push(item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
flags,
|
|
34
|
+
values,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printPairingQr(pairing, config) {
|
|
39
|
+
const directAddresses = listLanIpv4Addresses();
|
|
40
|
+
const payload = encodeBase64UrlJson({
|
|
41
|
+
v: 1,
|
|
42
|
+
typ: "join",
|
|
43
|
+
sid: pairing.pairingSessionId,
|
|
44
|
+
tok: pairing.pairingToken,
|
|
45
|
+
displayName: pairing.displayName,
|
|
46
|
+
hostname: pairing.hostname,
|
|
47
|
+
platform: pairing.platform,
|
|
48
|
+
directKey: config.directAccessKey,
|
|
49
|
+
directPort: LINK_DIRECT_PORT,
|
|
50
|
+
directAddresses,
|
|
51
|
+
});
|
|
52
|
+
console.log(`CLAWLINK:${payload}`);
|
|
53
|
+
qrcode.generate(`CLAWLINK:${payload}`, {
|
|
54
|
+
small: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveBootstrapPollDelayMs(attempt) {
|
|
59
|
+
const schedule = [1_500, 2_000, 3_000, 5_000, 8_000, 12_000];
|
|
60
|
+
const baseDelayMs = schedule[Math.min(attempt, schedule.length - 1)];
|
|
61
|
+
const jitterMs = Math.floor(baseDelayMs * 0.15 * Math.random());
|
|
62
|
+
return baseDelayMs + jitterMs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printHumanStatus(translator, snapshot, daemonOnline) {
|
|
66
|
+
const state = snapshot.state ?? snapshot;
|
|
67
|
+
console.log(translator.t("statusTitle"));
|
|
68
|
+
console.log(
|
|
69
|
+
`- ${translator.t("statusLinkIdLabel")}: ${snapshot.credentials?.linkId ?? state.linkId ?? translator.t("statusNotPairedValue")}`,
|
|
70
|
+
);
|
|
71
|
+
console.log(`- ${translator.t("statusStateLabel")}: ${state.connectionStatus}`);
|
|
72
|
+
console.log(
|
|
73
|
+
`- ${translator.t("statusDaemonLabel")}: ${daemonOnline ? translator.t("statusDaemonOnlineValue") : translator.t("statusDaemonOfflineValue")}`,
|
|
74
|
+
);
|
|
75
|
+
console.log(
|
|
76
|
+
`- ${translator.t("statusOpenClawLabel")}: ${resolveBackendStatusMessage(translator, state.backend)}`,
|
|
77
|
+
);
|
|
78
|
+
console.log(
|
|
79
|
+
`- ${translator.t("statusLocalAccessLabel")}: ${resolveDirectAccessStatusMessage(
|
|
80
|
+
translator,
|
|
81
|
+
state.directAccess,
|
|
82
|
+
daemonOnline,
|
|
83
|
+
)}`,
|
|
84
|
+
);
|
|
85
|
+
if (state.lastErrorMessage) {
|
|
86
|
+
console.log(`- ${translator.t("statusLastErrorLabel")}: ${state.lastErrorMessage}`);
|
|
87
|
+
}
|
|
88
|
+
if (state.backend?.httpBaseUrl) {
|
|
89
|
+
console.log(`- ${translator.t("statusBackendUrlLabel")}: ${state.backend.httpBaseUrl}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveBackendStatusMessage(translator, backend) {
|
|
94
|
+
if (!backend || typeof backend !== "object") {
|
|
95
|
+
return translator.t("statusUnknownValue");
|
|
96
|
+
}
|
|
97
|
+
if (!backend.detected) {
|
|
98
|
+
return translator.t("backendMissing");
|
|
99
|
+
}
|
|
100
|
+
if (!backend.supported) {
|
|
101
|
+
return translator.t("backendUnsupported");
|
|
102
|
+
}
|
|
103
|
+
if (typeof backend.port === "number" && Number.isFinite(backend.port)) {
|
|
104
|
+
return translator.t("backendReachableOnPort", { port: backend.port });
|
|
105
|
+
}
|
|
106
|
+
if (typeof backend.message === "string" && backend.message.trim()) {
|
|
107
|
+
return backend.message;
|
|
108
|
+
}
|
|
109
|
+
return translator.t("statusUnknownValue");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveDirectAccessStatusMessage(translator, directAccess, daemonOnline) {
|
|
113
|
+
if (!daemonOnline) {
|
|
114
|
+
return translator.t("localAccessDaemonOffline");
|
|
115
|
+
}
|
|
116
|
+
if (!directAccess || typeof directAccess !== "object") {
|
|
117
|
+
return translator.t("localAccessUnavailable");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const port =
|
|
121
|
+
typeof directAccess.port === "number" && Number.isFinite(directAccess.port)
|
|
122
|
+
? Math.trunc(directAccess.port)
|
|
123
|
+
: LINK_DIRECT_PORT;
|
|
124
|
+
const reason =
|
|
125
|
+
typeof directAccess.reason === "string" && directAccess.reason.trim()
|
|
126
|
+
? directAccess.reason.trim()
|
|
127
|
+
: null;
|
|
128
|
+
const message =
|
|
129
|
+
typeof directAccess.message === "string" && directAccess.message.trim()
|
|
130
|
+
? directAccess.message.trim()
|
|
131
|
+
: null;
|
|
132
|
+
|
|
133
|
+
if (directAccess.status === "listening") {
|
|
134
|
+
return translator.t("localAccessReadyOnPort", { port });
|
|
135
|
+
}
|
|
136
|
+
if (reason === "port_in_use") {
|
|
137
|
+
return translator.t("localAccessPortInUse", { port });
|
|
138
|
+
}
|
|
139
|
+
if (reason === "permission_denied") {
|
|
140
|
+
return translator.t("localAccessPermissionDenied", { port });
|
|
141
|
+
}
|
|
142
|
+
return message ?? translator.t("localAccessUnavailable");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function printDirectAccessWarningIfNeeded(translator) {
|
|
146
|
+
let snapshot = null;
|
|
147
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
148
|
+
try {
|
|
149
|
+
snapshot = await sendIpcRequest("status.get", {});
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (snapshot?.state?.directAccess?.status !== "unknown") {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (snapshot?.state?.directAccess?.status === "listening") {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.warn(translator.t("localAccessNeedsAttention"));
|
|
166
|
+
console.warn(
|
|
167
|
+
resolveDirectAccessStatusMessage(translator, snapshot?.state?.directAccess, true),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function loadRuntimeSnapshot() {
|
|
172
|
+
const [config, state, credentials] = await Promise.all([
|
|
173
|
+
loadConfig(),
|
|
174
|
+
loadState(),
|
|
175
|
+
loadCredentials(),
|
|
176
|
+
]);
|
|
177
|
+
const t = createTranslator(resolveLanguage(config.language));
|
|
178
|
+
return { config, state, credentials, t };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getValidatedRelayBaseUrl(translator, config) {
|
|
182
|
+
const relayBaseUrl = normalizeHttpsBaseUrl(config.relayBaseUrl);
|
|
183
|
+
if (!relayBaseUrl) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
translator.t("relayConfigInsecure", {
|
|
186
|
+
configFile: runtimePaths.configFile,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return relayBaseUrl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function commandPair() {
|
|
194
|
+
const { config, credentials, t } = await loadRuntimeSnapshot();
|
|
195
|
+
if (credentials.linkId) {
|
|
196
|
+
await commandQr();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pairing = await createPairingSession({
|
|
201
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
202
|
+
installId: config.installId,
|
|
203
|
+
displayName: config.displayName,
|
|
204
|
+
hostname: os.hostname(),
|
|
205
|
+
platform: process.platform,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await patchState({
|
|
209
|
+
connectionStatus: "pairing",
|
|
210
|
+
pairingSession: {
|
|
211
|
+
pairingSessionId: pairing.pairingSessionId,
|
|
212
|
+
expiresAt: pairing.expiresAt,
|
|
213
|
+
shortCode: pairing.shortCode,
|
|
214
|
+
claimMode: pairing.claimMode,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
console.log(t.t("pairingCreated"));
|
|
219
|
+
console.log(`${t.t("scanPrompt")}`);
|
|
220
|
+
printPairingQr(pairing, config);
|
|
221
|
+
console.log(`${t.t("manualCode")}: ${pairing.shortCode}`);
|
|
222
|
+
console.log(t.t("waitingForScan"));
|
|
223
|
+
|
|
224
|
+
const expiresAtMs = Date.parse(pairing.expiresAt);
|
|
225
|
+
let pollAttempt = 0;
|
|
226
|
+
while (Date.now() < expiresAtMs) {
|
|
227
|
+
let bootstrap;
|
|
228
|
+
try {
|
|
229
|
+
bootstrap = await bootstrapLink({
|
|
230
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
231
|
+
pairingSessionId: pairing.pairingSessionId,
|
|
232
|
+
pairingToken: pairing.pairingToken,
|
|
233
|
+
installId: config.installId,
|
|
234
|
+
displayName: config.displayName,
|
|
235
|
+
hostname: os.hostname(),
|
|
236
|
+
platform: process.platform,
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (
|
|
240
|
+
error instanceof ServerApiError &&
|
|
241
|
+
(error.status === 401 || error.status === 403 || error.status === 410)
|
|
242
|
+
) {
|
|
243
|
+
console.error(error.message);
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
pollAttempt += 1;
|
|
248
|
+
const remainingMs = expiresAtMs - Date.now();
|
|
249
|
+
if (remainingMs <= 0) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
await new Promise((resolve) =>
|
|
253
|
+
setTimeout(resolve, Math.min(resolveBootstrapPollDelayMs(pollAttempt), remainingMs)),
|
|
254
|
+
);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (bootstrap.ready) {
|
|
259
|
+
await saveCredentials({
|
|
260
|
+
linkId: bootstrap.link.linkId,
|
|
261
|
+
refreshToken: bootstrap.refreshToken,
|
|
262
|
+
refreshTokenExpiresAt: bootstrap.refreshTokenExpiresAt,
|
|
263
|
+
accessToken: bootstrap.accessToken?.token ?? null,
|
|
264
|
+
accessTokenExpiresAt: bootstrap.accessToken?.expiresAt ?? null,
|
|
265
|
+
});
|
|
266
|
+
await patchState({
|
|
267
|
+
linkId: bootstrap.link.linkId,
|
|
268
|
+
pairingSession: null,
|
|
269
|
+
connectionStatus: "paired",
|
|
270
|
+
lastErrorMessage: null,
|
|
271
|
+
});
|
|
272
|
+
await installSkill(resolveLanguage(config.language));
|
|
273
|
+
console.log(t.t("pairingSuccess"));
|
|
274
|
+
if (config.autoStartAfterPair) {
|
|
275
|
+
config.relayBaseUrl = getValidatedRelayBaseUrl(t, config);
|
|
276
|
+
console.log(t.t("startDaemonAfterPair"));
|
|
277
|
+
spawnBackgroundDaemon();
|
|
278
|
+
const ready = await waitForDaemonReady();
|
|
279
|
+
if (!ready) {
|
|
280
|
+
throw new Error(t.t("startDaemonFailed"));
|
|
281
|
+
}
|
|
282
|
+
await printDirectAccessWarningIfNeeded(t);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
pollAttempt += 1;
|
|
288
|
+
const remainingMs = expiresAtMs - Date.now();
|
|
289
|
+
if (remainingMs <= 0) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
await new Promise((resolve) =>
|
|
293
|
+
setTimeout(resolve, Math.min(resolveBootstrapPollDelayMs(pollAttempt), remainingMs)),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.error(t.t("pairingExpired"));
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function commandQr() {
|
|
302
|
+
const { config, credentials, t } = await loadRuntimeSnapshot();
|
|
303
|
+
if (!credentials.linkId) {
|
|
304
|
+
console.error(t.t("qrNeedsPair"));
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const pairing = await createPairingSession({
|
|
310
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
311
|
+
installId: config.installId,
|
|
312
|
+
displayName: config.displayName,
|
|
313
|
+
hostname: os.hostname(),
|
|
314
|
+
platform: process.platform,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await patchState({
|
|
318
|
+
pairingSession: {
|
|
319
|
+
pairingSessionId: pairing.pairingSessionId,
|
|
320
|
+
expiresAt: pairing.expiresAt,
|
|
321
|
+
shortCode: pairing.shortCode,
|
|
322
|
+
claimMode: pairing.claimMode,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
console.log(t.t("qrJoinReady"));
|
|
327
|
+
console.log(`${t.t("scanPrompt")}`);
|
|
328
|
+
printPairingQr(pairing, config);
|
|
329
|
+
console.log(`${t.t("manualCode")}: ${pairing.shortCode}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function commandConnect(flags) {
|
|
333
|
+
const { config, credentials, t } = await loadRuntimeSnapshot();
|
|
334
|
+
if (!credentials.linkId) {
|
|
335
|
+
console.error(t.t("notPaired"));
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (flags.has("--foreground")) {
|
|
341
|
+
config.relayBaseUrl = getValidatedRelayBaseUrl(t, config);
|
|
342
|
+
console.log(t.t("connectForeground"));
|
|
343
|
+
const state = await loadState();
|
|
344
|
+
const daemon = new LinkDaemon(config, state, credentials);
|
|
345
|
+
process.on("SIGINT", () => {
|
|
346
|
+
void daemon.stop();
|
|
347
|
+
});
|
|
348
|
+
process.on("SIGTERM", () => {
|
|
349
|
+
void daemon.stop();
|
|
350
|
+
});
|
|
351
|
+
await daemon.start();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (await isDaemonRunning()) {
|
|
356
|
+
await sendIpcRequest("daemon.reconnect", {});
|
|
357
|
+
console.log(t.t("daemonAlreadyRunning"));
|
|
358
|
+
await printDirectAccessWarningIfNeeded(t);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getValidatedRelayBaseUrl(t, config);
|
|
363
|
+
spawnBackgroundDaemon();
|
|
364
|
+
const ready = await waitForDaemonReady();
|
|
365
|
+
if (!ready) {
|
|
366
|
+
throw new Error(t.t("startDaemonFailed"));
|
|
367
|
+
}
|
|
368
|
+
console.log(t.t("connectBackground"));
|
|
369
|
+
await printDirectAccessWarningIfNeeded(t);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function commandStatus(flags) {
|
|
373
|
+
const { credentials, state, t } = await loadRuntimeSnapshot();
|
|
374
|
+
let daemonOnline = false;
|
|
375
|
+
let snapshot = {
|
|
376
|
+
state,
|
|
377
|
+
credentials,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
snapshot = await sendIpcRequest("status.get", {});
|
|
382
|
+
daemonOnline = true;
|
|
383
|
+
} catch {
|
|
384
|
+
daemonOnline = false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (flags.has("--json")) {
|
|
388
|
+
console.log(JSON.stringify({ ...snapshot, daemonOnline }, null, 2));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
printHumanStatus(t, snapshot, daemonOnline);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function commandDoctor(flags) {
|
|
396
|
+
const { config, state, credentials, t } = await loadRuntimeSnapshot();
|
|
397
|
+
const backend = await detectOpenClawBackend();
|
|
398
|
+
const daemonOnline = await isDaemonRunning();
|
|
399
|
+
let liveState = state;
|
|
400
|
+
if (daemonOnline) {
|
|
401
|
+
try {
|
|
402
|
+
const snapshot = await sendIpcRequest("status.get", {});
|
|
403
|
+
if (snapshot?.state && typeof snapshot.state === "object") {
|
|
404
|
+
liveState = snapshot.state;
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// Ignore IPC failures and fall back to the last persisted state.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const relayBaseUrl = normalizeHttpsBaseUrl(config.relayBaseUrl);
|
|
411
|
+
const relayHealth = relayBaseUrl
|
|
412
|
+
? await fetch(`${relayBaseUrl}/healthz`)
|
|
413
|
+
.then((response) => response.ok)
|
|
414
|
+
.catch(() => false)
|
|
415
|
+
: false;
|
|
416
|
+
const apiHealth = await fetch(`${config.apiBaseUrl.replace(/\/+$/, "")}/healthz`)
|
|
417
|
+
.then((response) => response.ok)
|
|
418
|
+
.catch(() => false);
|
|
419
|
+
const skillPath = path.join(path.dirname(process.env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json")), "skills", "clawpilot-link", "SKILL.md");
|
|
420
|
+
const skillInstalled = await fsp.access(skillPath).then(() => true).catch(() => false);
|
|
421
|
+
|
|
422
|
+
const checks = [
|
|
423
|
+
{
|
|
424
|
+
key: "paired",
|
|
425
|
+
ok: Boolean(credentials.linkId),
|
|
426
|
+
message: Boolean(credentials.linkId) ? t.t("pairedOk") : t.t("notPaired"),
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
key: "daemon",
|
|
430
|
+
ok: daemonOnline,
|
|
431
|
+
message: daemonOnline ? t.t("daemonRunning") : t.t("daemonNotRunning"),
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
key: "relay-config",
|
|
435
|
+
ok: Boolean(relayBaseUrl),
|
|
436
|
+
message: relayBaseUrl
|
|
437
|
+
? t.t("relayConfigSecure")
|
|
438
|
+
: t.t("relayConfigInsecure", {
|
|
439
|
+
configFile: runtimePaths.configFile,
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
key: "backend",
|
|
444
|
+
ok: backend.detected && backend.supported,
|
|
445
|
+
message: resolveBackendStatusMessage(t, backend),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
key: "local-access",
|
|
449
|
+
ok: daemonOnline && liveState.directAccess?.status === "listening",
|
|
450
|
+
message: resolveDirectAccessStatusMessage(t, liveState.directAccess, daemonOnline),
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
key: "relay",
|
|
454
|
+
ok: relayBaseUrl ? relayHealth : false,
|
|
455
|
+
message: relayBaseUrl
|
|
456
|
+
? relayHealth
|
|
457
|
+
? t.t("relayReachable")
|
|
458
|
+
: t.t("relayUnreachable")
|
|
459
|
+
: t.t("relayCheckSkipped"),
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
key: "api",
|
|
463
|
+
ok: apiHealth,
|
|
464
|
+
message: apiHealth ? t.t("apiReachable") : t.t("apiUnreachable"),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
key: "skill",
|
|
468
|
+
ok: skillInstalled,
|
|
469
|
+
message: skillInstalled ? t.t("skillInstalled") : t.t("skillMissing"),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
if (flags.has("--json")) {
|
|
474
|
+
console.log(
|
|
475
|
+
JSON.stringify(
|
|
476
|
+
{
|
|
477
|
+
ok: checks.every((item) => item.ok),
|
|
478
|
+
checks,
|
|
479
|
+
backend,
|
|
480
|
+
daemonOnline,
|
|
481
|
+
state: liveState,
|
|
482
|
+
},
|
|
483
|
+
null,
|
|
484
|
+
2,
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const check of checks) {
|
|
491
|
+
console.log(`- [${check.ok ? "OK" : "NO"}] ${check.message}`);
|
|
492
|
+
}
|
|
493
|
+
console.log(checks.every((item) => item.ok) ? t.t("doctorSummaryOk") : t.t("doctorSummaryFix"));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function commandRestart() {
|
|
497
|
+
const { config, t } = await loadRuntimeSnapshot();
|
|
498
|
+
getValidatedRelayBaseUrl(t, config);
|
|
499
|
+
try {
|
|
500
|
+
await sendIpcRequest("daemon.shutdown", {}, 1_500);
|
|
501
|
+
} catch {
|
|
502
|
+
// Ignore shutdown failures and try a fresh start anyway.
|
|
503
|
+
}
|
|
504
|
+
spawnBackgroundDaemon();
|
|
505
|
+
const ready = await waitForDaemonReady();
|
|
506
|
+
if (!ready) {
|
|
507
|
+
throw new Error(t.t("restartFailed"));
|
|
508
|
+
}
|
|
509
|
+
console.log(t.t("restartDone"));
|
|
510
|
+
await printDirectAccessWarningIfNeeded(t);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function commandUnpair(flags) {
|
|
514
|
+
const { t } = await loadRuntimeSnapshot();
|
|
515
|
+
if (!flags.has("--yes")) {
|
|
516
|
+
console.error(t.t("unpairConfirm"));
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await sendIpcRequest("daemon.shutdown", {}, 1_500);
|
|
522
|
+
} catch {
|
|
523
|
+
// Ignore shutdown failures.
|
|
524
|
+
}
|
|
525
|
+
await saveCredentials({
|
|
526
|
+
linkId: null,
|
|
527
|
+
refreshToken: null,
|
|
528
|
+
refreshTokenExpiresAt: null,
|
|
529
|
+
accessToken: null,
|
|
530
|
+
accessTokenExpiresAt: null,
|
|
531
|
+
});
|
|
532
|
+
await patchState({
|
|
533
|
+
linkId: null,
|
|
534
|
+
connectionStatus: "new",
|
|
535
|
+
pairingSession: null,
|
|
536
|
+
});
|
|
537
|
+
console.log(t.t("daemonStopped"));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function commandDaemon() {
|
|
541
|
+
const [config, state, credentials] = await Promise.all([
|
|
542
|
+
loadConfig(),
|
|
543
|
+
loadState(),
|
|
544
|
+
loadCredentials(),
|
|
545
|
+
]);
|
|
546
|
+
const t = createTranslator(resolveLanguage(config.language));
|
|
547
|
+
config.relayBaseUrl = getValidatedRelayBaseUrl(t, config);
|
|
548
|
+
const daemon = new LinkDaemon(config, state, credentials);
|
|
549
|
+
process.on("SIGINT", () => {
|
|
550
|
+
void daemon.stop();
|
|
551
|
+
});
|
|
552
|
+
process.on("SIGTERM", () => {
|
|
553
|
+
void daemon.stop();
|
|
554
|
+
});
|
|
555
|
+
await daemon.start();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function main() {
|
|
559
|
+
const [, , rawCommand, ...restArgs] = process.argv;
|
|
560
|
+
const command = rawCommand ?? "status";
|
|
561
|
+
const { flags } = parseFlags(restArgs);
|
|
562
|
+
const { t } = await loadRuntimeSnapshot();
|
|
563
|
+
|
|
564
|
+
switch (command) {
|
|
565
|
+
case "pair":
|
|
566
|
+
await commandPair();
|
|
567
|
+
return;
|
|
568
|
+
case "qr":
|
|
569
|
+
await commandQr();
|
|
570
|
+
return;
|
|
571
|
+
case "connect":
|
|
572
|
+
await commandConnect(flags);
|
|
573
|
+
return;
|
|
574
|
+
case "status":
|
|
575
|
+
await commandStatus(flags);
|
|
576
|
+
return;
|
|
577
|
+
case "doctor":
|
|
578
|
+
await commandDoctor(flags);
|
|
579
|
+
return;
|
|
580
|
+
case "restart":
|
|
581
|
+
await commandRestart();
|
|
582
|
+
return;
|
|
583
|
+
case "unpair":
|
|
584
|
+
await commandUnpair(flags);
|
|
585
|
+
return;
|
|
586
|
+
case "daemon":
|
|
587
|
+
await commandDaemon();
|
|
588
|
+
return;
|
|
589
|
+
default:
|
|
590
|
+
console.error(`${t.t("unknownCommand")}: ${command}`);
|
|
591
|
+
console.error(t.t("availableCommands"));
|
|
592
|
+
process.exitCode = 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
main().catch((error) => {
|
|
597
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
598
|
+
process.exitCode = 1;
|
|
599
|
+
});
|
package/src/constants.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LINK_VERSION = "0.1.0";
|