backeer-opencode 0.2.0 → 0.2.1
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 +8 -0
- package/README.md +12 -0
- package/dist/backeer-permission.js +142 -6
- package/package.json +1 -1
- package/src/config.js +1 -0
- package/src/diagnostics.js +148 -0
- package/src/management.js +16 -2
- package/src/updates.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `backeer-opencode` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Add `/backeer doctor` for read-only local configuration, protected token,
|
|
8
|
+
source authentication, gateway capability, and APNs diagnostics.
|
|
9
|
+
- Add `/backeer test` to send a uniquely correlated end-to-end mobile test
|
|
10
|
+
notification.
|
|
11
|
+
- Keep doctor reports and command output free of bearer token values.
|
|
12
|
+
|
|
5
13
|
## 0.2.0 - 2026-09-01
|
|
6
14
|
|
|
7
15
|
- Notify when an OpenCode assistant run completes successfully.
|
package/README.md
CHANGED
|
@@ -177,6 +177,8 @@ The plugin registers a `/backeer` command inside OpenCode:
|
|
|
177
177
|
```text
|
|
178
178
|
/backeer
|
|
179
179
|
/backeer status
|
|
180
|
+
/backeer doctor
|
|
181
|
+
/backeer test
|
|
180
182
|
/backeer update-check
|
|
181
183
|
/backeer update
|
|
182
184
|
/backeer token
|
|
@@ -188,6 +190,16 @@ notification switches and lifecycle thresholds, command-stream state, pending
|
|
|
188
190
|
permissions, and log level. It never displays the token. Gateway reachability
|
|
189
191
|
does not validate the source credential.
|
|
190
192
|
|
|
193
|
+
`/backeer doctor` performs a read-only diagnostic pass. It verifies the local
|
|
194
|
+
configuration, protected token-file permissions, source authentication, event
|
|
195
|
+
and command capabilities, and gateway APNs readiness. It does not create an
|
|
196
|
+
event, open a command stream, or display the bearer token.
|
|
197
|
+
|
|
198
|
+
`/backeer test` publishes one uniquely correlated, non-actionable application
|
|
199
|
+
event and asks the gateway to deliver a real push notification. Use it after
|
|
200
|
+
installation, token rotation, or network changes to verify the complete path
|
|
201
|
+
from OpenCode to the phone.
|
|
202
|
+
|
|
191
203
|
`/backeer token` displays safe rotation instructions. Never paste a raw token
|
|
192
204
|
into slash-command arguments because OpenCode may retain command text in session
|
|
193
205
|
history. To rotate from inside OpenCode, first place the new token in an absolute
|
|
@@ -140,6 +140,7 @@ function loadConfig(options = {}) {
|
|
|
140
140
|
return {
|
|
141
141
|
origin,
|
|
142
142
|
eventsEndpoint: new URL("/v1/events", origin),
|
|
143
|
+
sourceHealthEndpoint: new URL("/v1/source/health", origin),
|
|
143
144
|
commandStreamEndpoint: new URL("/v1/commands/stream", origin),
|
|
144
145
|
token: loadedToken.token,
|
|
145
146
|
tokenSource: loadedToken.source,
|
|
@@ -391,13 +392,17 @@ import {
|
|
|
391
392
|
mkdirSync,
|
|
392
393
|
readFileSync as readFileSync2,
|
|
393
394
|
renameSync,
|
|
394
|
-
statSync,
|
|
395
|
+
statSync as statSync2,
|
|
395
396
|
writeFileSync
|
|
396
397
|
} from "node:fs";
|
|
397
398
|
import { dirname, resolve } from "node:path";
|
|
398
399
|
|
|
400
|
+
// src/diagnostics.js
|
|
401
|
+
import { randomUUID } from "node:crypto";
|
|
402
|
+
import { statSync } from "node:fs";
|
|
403
|
+
|
|
399
404
|
// src/updates.js
|
|
400
|
-
var PLUGIN_VERSION = "0.2.
|
|
405
|
+
var PLUGIN_VERSION = "0.2.1";
|
|
401
406
|
var PACKAGE_NAME = "backeer-opencode";
|
|
402
407
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
403
408
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
@@ -517,11 +522,129 @@ function createUpdateNoticeHook(updateChecker) {
|
|
|
517
522
|
};
|
|
518
523
|
}
|
|
519
524
|
|
|
525
|
+
// src/diagnostics.js
|
|
526
|
+
var PASS = "\u2713";
|
|
527
|
+
var FAIL = "\xD7";
|
|
528
|
+
var INFO = "\u2022";
|
|
529
|
+
function tokenFileCheck(config) {
|
|
530
|
+
if (config.tokenSource === "environment") {
|
|
531
|
+
return { ok: true, text: "Token loaded from the environment; token file not required" };
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const stats = statSync(config.tokenFile);
|
|
535
|
+
const mode = stats.mode & 511;
|
|
536
|
+
if (!stats.isFile()) return { ok: false, text: "Token path is not a regular file" };
|
|
537
|
+
if ((mode & 63) !== 0) {
|
|
538
|
+
return {
|
|
539
|
+
ok: false,
|
|
540
|
+
text: `Token file mode ${mode.toString(8).padStart(3, "0")} is too permissive; expected 600`
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
ok: true,
|
|
545
|
+
text: `Token file is protected (mode ${mode.toString(8).padStart(3, "0")})`
|
|
546
|
+
};
|
|
547
|
+
} catch {
|
|
548
|
+
return { ok: false, text: "Token file is missing or unreadable" };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function probeSource(config, fetchImplementation = globalThis.fetch) {
|
|
552
|
+
const controller = new AbortController();
|
|
553
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
554
|
+
try {
|
|
555
|
+
const response = await fetchImplementation(config.sourceHealthEndpoint, {
|
|
556
|
+
method: "GET",
|
|
557
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
558
|
+
signal: controller.signal
|
|
559
|
+
});
|
|
560
|
+
const responseBody = limit(await response.text(), 2e3);
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
throw new HttpError(
|
|
563
|
+
`Gateway source check failed with status ${response.status}`,
|
|
564
|
+
response.status,
|
|
565
|
+
responseBody
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return JSON.parse(responseBody);
|
|
570
|
+
} catch {
|
|
571
|
+
throw new Error("Gateway source check returned malformed JSON");
|
|
572
|
+
}
|
|
573
|
+
} finally {
|
|
574
|
+
clearTimeout(timeout);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function buildDoctorReport(config, { fetchImplementation = globalThis.fetch } = {}) {
|
|
578
|
+
const checks = [];
|
|
579
|
+
checks.push({ ok: true, text: `Plugin version ${PLUGIN_VERSION}` });
|
|
580
|
+
checks.push({
|
|
581
|
+
ok: config.origin.protocol === "https:",
|
|
582
|
+
text: `Gateway uses ${config.origin.protocol === "https:" ? "HTTPS" : "an insecure protocol"}: ${config.origin.origin}`
|
|
583
|
+
});
|
|
584
|
+
checks.push({
|
|
585
|
+
ok: Boolean(config.token),
|
|
586
|
+
text: config.token ? `Source token loaded from ${config.tokenSource}` : "Source token is missing"
|
|
587
|
+
});
|
|
588
|
+
checks.push(tokenFileCheck(config));
|
|
589
|
+
let source;
|
|
590
|
+
if (config.token) {
|
|
591
|
+
try {
|
|
592
|
+
const result = await probeSource(config, fetchImplementation);
|
|
593
|
+
source = result?.source;
|
|
594
|
+
checks.push({
|
|
595
|
+
ok: result?.ok === true && typeof source?.id === "string",
|
|
596
|
+
text: source?.id ? `Source authentication accepted: ${source.name || source.id} (${source.id})` : "Gateway did not return a source identity"
|
|
597
|
+
});
|
|
598
|
+
checks.push({
|
|
599
|
+
ok: result?.capabilities?.events === true && result?.capabilities?.commands === true,
|
|
600
|
+
text: result?.capabilities?.events === true && result?.capabilities?.commands === true ? "Event ingest and command stream are available" : "Event ingest or command stream is unavailable"
|
|
601
|
+
});
|
|
602
|
+
checks.push({
|
|
603
|
+
ok: result?.capabilities?.apns === true,
|
|
604
|
+
text: result?.capabilities?.apns === true ? "Gateway APNs delivery is enabled" : "Gateway APNs delivery is disabled"
|
|
605
|
+
});
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const detail = error instanceof HttpError && error.status === 401 ? "Source authentication rejected (HTTP 401)" : error?.name === "AbortError" ? "Gateway source check timed out" : `Gateway source check failed: ${limit(error?.message || error, 300)}`;
|
|
608
|
+
checks.push({ ok: false, text: detail });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const healthy = checks.every((check) => check.ok);
|
|
612
|
+
return [
|
|
613
|
+
"## Backeer doctor",
|
|
614
|
+
"",
|
|
615
|
+
...checks.map((check) => `${check.ok ? PASS : FAIL} ${check.text}`),
|
|
616
|
+
"",
|
|
617
|
+
healthy ? `${PASS} Result: healthy. Use \`/backeer test\` to verify phone delivery.` : `${FAIL} Result: issues found. Resolve failed checks and run \`/backeer doctor\` again.`,
|
|
618
|
+
"",
|
|
619
|
+
`${INFO} No event was created and the token value was not displayed.`
|
|
620
|
+
].join("\n");
|
|
621
|
+
}
|
|
622
|
+
async function publishTestNotification(config, reporter) {
|
|
623
|
+
if (!config.token) throw new Error("Backeer source token is missing");
|
|
624
|
+
const testID = randomUUID();
|
|
625
|
+
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
626
|
+
type: "application",
|
|
627
|
+
severity: "info",
|
|
628
|
+
title: "Backeer test notification",
|
|
629
|
+
body: "OpenCode integration is connected and push delivery is working.",
|
|
630
|
+
payload: [
|
|
631
|
+
{ key: "Plugin version", value: PLUGIN_VERSION },
|
|
632
|
+
{ key: "Test ID", value: testID }
|
|
633
|
+
],
|
|
634
|
+
externalId: `opencode:test:${testID}`,
|
|
635
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
636
|
+
notify: true,
|
|
637
|
+
direction: "in"
|
|
638
|
+
});
|
|
639
|
+
reporter.success("Test notification sent", `HTTP ${result.status}`);
|
|
640
|
+
return testID;
|
|
641
|
+
}
|
|
642
|
+
|
|
520
643
|
// src/management.js
|
|
521
|
-
var COMMAND_DESCRIPTION = "
|
|
644
|
+
var COMMAND_DESCRIPTION = "Check, test, update, or manage Backeer: /backeer [status|doctor|test|update-check|update|token|token-file PATH]";
|
|
522
645
|
function tokenFileState(path) {
|
|
523
646
|
try {
|
|
524
|
-
const stats =
|
|
647
|
+
const stats = statSync2(path);
|
|
525
648
|
const mode = (stats.mode & 511).toString(8).padStart(3, "0");
|
|
526
649
|
return stats.isFile() ? `present (mode ${mode})` : "not a regular file";
|
|
527
650
|
} catch {
|
|
@@ -546,7 +669,7 @@ function writeTokenAtomically(path, token) {
|
|
|
546
669
|
function rotateTokenFromFile(config, sourcePathValue) {
|
|
547
670
|
if (!sourcePathValue.trim()) throw new Error("A token file path is required");
|
|
548
671
|
const sourcePath = resolve(cleanPathArgument(sourcePathValue));
|
|
549
|
-
const sourceStats =
|
|
672
|
+
const sourceStats = statSync2(sourcePath);
|
|
550
673
|
if (!sourceStats.isFile()) throw new Error("The token source is not a regular file");
|
|
551
674
|
if ((sourceStats.mode & 63) !== 0) {
|
|
552
675
|
throw new Error("The token source must not be readable by group or other users");
|
|
@@ -619,6 +742,8 @@ function helpMessage() {
|
|
|
619
742
|
"## Backeer commands",
|
|
620
743
|
"",
|
|
621
744
|
"- `/backeer` or `/backeer status` \u2014 show integration status",
|
|
745
|
+
"- `/backeer doctor` \u2014 diagnose config, credentials, gateway, and APNs without creating an event",
|
|
746
|
+
"- `/backeer test` \u2014 send a real test notification to the Backeer application",
|
|
622
747
|
"- `/backeer update-check` \u2014 check NPM for a newer version",
|
|
623
748
|
"- `/backeer update` \u2014 ask the OpenCode agent to install and verify it",
|
|
624
749
|
"- `/backeer token` \u2014 show secure token-change instructions",
|
|
@@ -678,6 +803,17 @@ function createBackeerCommandHandler({
|
|
|
678
803
|
try {
|
|
679
804
|
if (!subcommand || subcommand === "status") {
|
|
680
805
|
message = await buildStatus(config, permissionController, updateChecker);
|
|
806
|
+
} else if (subcommand === "doctor") {
|
|
807
|
+
message = await buildDoctorReport(config);
|
|
808
|
+
} else if (subcommand === "test") {
|
|
809
|
+
const testID = await publishTestNotification(config, reporter);
|
|
810
|
+
message = [
|
|
811
|
+
"## Backeer test notification",
|
|
812
|
+
"",
|
|
813
|
+
"\u2713 The gateway accepted the test event.",
|
|
814
|
+
`- Test ID: ${testID}`,
|
|
815
|
+
"- Check the Backeer application for the notification."
|
|
816
|
+
].join("\n");
|
|
681
817
|
} else if (subcommand === "update-check") {
|
|
682
818
|
const update = await updateChecker.check({ force: true });
|
|
683
819
|
message = [
|
|
@@ -714,7 +850,7 @@ Backeer is already current at version ${update.currentVersion}.` : "## Backeer u
|
|
|
714
850
|
message = helpMessage();
|
|
715
851
|
}
|
|
716
852
|
} catch (error) {
|
|
717
|
-
reporter.error("
|
|
853
|
+
reporter.error("Backeer command failed", error);
|
|
718
854
|
message = [
|
|
719
855
|
"## Backeer command failed",
|
|
720
856
|
"",
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -85,6 +85,7 @@ export function loadConfig(options = {}) {
|
|
|
85
85
|
return {
|
|
86
86
|
origin,
|
|
87
87
|
eventsEndpoint: new URL("/v1/events", origin),
|
|
88
|
+
sourceHealthEndpoint: new URL("/v1/source/health", origin),
|
|
88
89
|
commandStreamEndpoint: new URL("/v1/commands/stream", origin),
|
|
89
90
|
token: loadedToken.token,
|
|
90
91
|
tokenSource: loadedToken.source,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { statSync } from "node:fs"
|
|
3
|
+
|
|
4
|
+
import { HttpError, postJsonWithRetry } from "./http.js"
|
|
5
|
+
import { PLUGIN_VERSION } from "./updates.js"
|
|
6
|
+
import { limit } from "./utilities.js"
|
|
7
|
+
|
|
8
|
+
const PASS = "✓"
|
|
9
|
+
const FAIL = "×"
|
|
10
|
+
const INFO = "•"
|
|
11
|
+
|
|
12
|
+
function tokenFileCheck(config) {
|
|
13
|
+
if (config.tokenSource === "environment") {
|
|
14
|
+
return { ok: true, text: "Token loaded from the environment; token file not required" }
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const stats = statSync(config.tokenFile)
|
|
18
|
+
const mode = stats.mode & 0o777
|
|
19
|
+
if (!stats.isFile()) return { ok: false, text: "Token path is not a regular file" }
|
|
20
|
+
if ((mode & 0o077) !== 0) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
text: `Token file mode ${(mode).toString(8).padStart(3, "0")} is too permissive; expected 600`,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
text: `Token file is protected (mode ${mode.toString(8).padStart(3, "0")})`,
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
return { ok: false, text: "Token file is missing or unreadable" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function probeSource(config, fetchImplementation = globalThis.fetch) {
|
|
36
|
+
const controller = new AbortController()
|
|
37
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetchImplementation(config.sourceHealthEndpoint, {
|
|
40
|
+
method: "GET",
|
|
41
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
})
|
|
44
|
+
const responseBody = limit(await response.text(), 2000)
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new HttpError(
|
|
47
|
+
`Gateway source check failed with status ${response.status}`,
|
|
48
|
+
response.status,
|
|
49
|
+
responseBody,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(responseBody)
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error("Gateway source check returned malformed JSON")
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timeout)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function buildDoctorReport(
|
|
63
|
+
config,
|
|
64
|
+
{ fetchImplementation = globalThis.fetch } = {},
|
|
65
|
+
) {
|
|
66
|
+
const checks = []
|
|
67
|
+
checks.push({ ok: true, text: `Plugin version ${PLUGIN_VERSION}` })
|
|
68
|
+
checks.push({
|
|
69
|
+
ok: config.origin.protocol === "https:",
|
|
70
|
+
text: `Gateway uses ${config.origin.protocol === "https:" ? "HTTPS" : "an insecure protocol"}: ${config.origin.origin}`,
|
|
71
|
+
})
|
|
72
|
+
checks.push({
|
|
73
|
+
ok: Boolean(config.token),
|
|
74
|
+
text: config.token
|
|
75
|
+
? `Source token loaded from ${config.tokenSource}`
|
|
76
|
+
: "Source token is missing",
|
|
77
|
+
})
|
|
78
|
+
checks.push(tokenFileCheck(config))
|
|
79
|
+
|
|
80
|
+
let source
|
|
81
|
+
if (config.token) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await probeSource(config, fetchImplementation)
|
|
84
|
+
source = result?.source
|
|
85
|
+
checks.push({
|
|
86
|
+
ok: result?.ok === true && typeof source?.id === "string",
|
|
87
|
+
text: source?.id
|
|
88
|
+
? `Source authentication accepted: ${source.name || source.id} (${source.id})`
|
|
89
|
+
: "Gateway did not return a source identity",
|
|
90
|
+
})
|
|
91
|
+
checks.push({
|
|
92
|
+
ok: result?.capabilities?.events === true &&
|
|
93
|
+
result?.capabilities?.commands === true,
|
|
94
|
+
text: result?.capabilities?.events === true &&
|
|
95
|
+
result?.capabilities?.commands === true
|
|
96
|
+
? "Event ingest and command stream are available"
|
|
97
|
+
: "Event ingest or command stream is unavailable",
|
|
98
|
+
})
|
|
99
|
+
checks.push({
|
|
100
|
+
ok: result?.capabilities?.apns === true,
|
|
101
|
+
text: result?.capabilities?.apns === true
|
|
102
|
+
? "Gateway APNs delivery is enabled"
|
|
103
|
+
: "Gateway APNs delivery is disabled",
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const detail = error instanceof HttpError && error.status === 401
|
|
107
|
+
? "Source authentication rejected (HTTP 401)"
|
|
108
|
+
: error?.name === "AbortError"
|
|
109
|
+
? "Gateway source check timed out"
|
|
110
|
+
: `Gateway source check failed: ${limit(error?.message || error, 300)}`
|
|
111
|
+
checks.push({ ok: false, text: detail })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const healthy = checks.every((check) => check.ok)
|
|
116
|
+
return [
|
|
117
|
+
"## Backeer doctor",
|
|
118
|
+
"",
|
|
119
|
+
...checks.map((check) => `${check.ok ? PASS : FAIL} ${check.text}`),
|
|
120
|
+
"",
|
|
121
|
+
healthy
|
|
122
|
+
? `${PASS} Result: healthy. Use \`/backeer test\` to verify phone delivery.`
|
|
123
|
+
: `${FAIL} Result: issues found. Resolve failed checks and run \`/backeer doctor\` again.`,
|
|
124
|
+
"",
|
|
125
|
+
`${INFO} No event was created and the token value was not displayed.`,
|
|
126
|
+
].join("\n")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function publishTestNotification(config, reporter) {
|
|
130
|
+
if (!config.token) throw new Error("Backeer source token is missing")
|
|
131
|
+
const testID = randomUUID()
|
|
132
|
+
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
133
|
+
type: "application",
|
|
134
|
+
severity: "info",
|
|
135
|
+
title: "Backeer test notification",
|
|
136
|
+
body: "OpenCode integration is connected and push delivery is working.",
|
|
137
|
+
payload: [
|
|
138
|
+
{ key: "Plugin version", value: PLUGIN_VERSION },
|
|
139
|
+
{ key: "Test ID", value: testID },
|
|
140
|
+
],
|
|
141
|
+
externalId: `opencode:test:${testID}`,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
notify: true,
|
|
144
|
+
direction: "in",
|
|
145
|
+
})
|
|
146
|
+
reporter.success("Test notification sent", `HTTP ${result.status}`)
|
|
147
|
+
return testID
|
|
148
|
+
}
|
package/src/management.js
CHANGED
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
} from "node:fs"
|
|
9
9
|
import { dirname, resolve } from "node:path"
|
|
10
10
|
|
|
11
|
+
import { buildDoctorReport, publishTestNotification } from "./diagnostics.js"
|
|
11
12
|
import {
|
|
12
13
|
formatAgentUpdateInstruction,
|
|
13
14
|
PLUGIN_VERSION,
|
|
14
15
|
} from "./updates.js"
|
|
15
16
|
|
|
16
17
|
const COMMAND_DESCRIPTION =
|
|
17
|
-
"
|
|
18
|
+
"Check, test, update, or manage Backeer: /backeer [status|doctor|test|update-check|update|token|token-file PATH]"
|
|
18
19
|
|
|
19
20
|
function tokenFileState(path) {
|
|
20
21
|
try {
|
|
@@ -129,6 +130,8 @@ function helpMessage() {
|
|
|
129
130
|
"## Backeer commands",
|
|
130
131
|
"",
|
|
131
132
|
"- `/backeer` or `/backeer status` — show integration status",
|
|
133
|
+
"- `/backeer doctor` — diagnose config, credentials, gateway, and APNs without creating an event",
|
|
134
|
+
"- `/backeer test` — send a real test notification to the Backeer application",
|
|
132
135
|
"- `/backeer update-check` — check NPM for a newer version",
|
|
133
136
|
"- `/backeer update` — ask the OpenCode agent to install and verify it",
|
|
134
137
|
"- `/backeer token` — show secure token-change instructions",
|
|
@@ -198,6 +201,17 @@ export function createBackeerCommandHandler({
|
|
|
198
201
|
try {
|
|
199
202
|
if (!subcommand || subcommand === "status") {
|
|
200
203
|
message = await buildStatus(config, permissionController, updateChecker)
|
|
204
|
+
} else if (subcommand === "doctor") {
|
|
205
|
+
message = await buildDoctorReport(config)
|
|
206
|
+
} else if (subcommand === "test") {
|
|
207
|
+
const testID = await publishTestNotification(config, reporter)
|
|
208
|
+
message = [
|
|
209
|
+
"## Backeer test notification",
|
|
210
|
+
"",
|
|
211
|
+
"✓ The gateway accepted the test event.",
|
|
212
|
+
`- Test ID: ${testID}`,
|
|
213
|
+
"- Check the Backeer application for the notification.",
|
|
214
|
+
].join("\n")
|
|
201
215
|
} else if (subcommand === "update-check") {
|
|
202
216
|
const update = await updateChecker.check({ force: true })
|
|
203
217
|
message = [
|
|
@@ -238,7 +252,7 @@ export function createBackeerCommandHandler({
|
|
|
238
252
|
message = helpMessage()
|
|
239
253
|
}
|
|
240
254
|
} catch (error) {
|
|
241
|
-
reporter.error("
|
|
255
|
+
reporter.error("Backeer command failed", error)
|
|
242
256
|
message = [
|
|
243
257
|
"## Backeer command failed",
|
|
244
258
|
"",
|
package/src/updates.js
CHANGED