backeer-opencode 0.1.4 → 0.1.5
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 +10 -0
- package/README.md +15 -0
- package/dist/backeer-permission.js +166 -7
- package/package.json +1 -1
- package/src/management.js +46 -5
- package/src/plugin.js +9 -0
- package/src/updates.js +136 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `backeer-opencode` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.5 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Check the NPM registry for a newer Backeer plugin version once per day.
|
|
8
|
+
- Notify the OpenCode agent once per session when an update is available.
|
|
9
|
+
- Add `/backeer update-check` for an explicit registry check.
|
|
10
|
+
- Add `/backeer update` to delegate an exact-version installation and
|
|
11
|
+
verification workflow to the OpenCode agent.
|
|
12
|
+
- Require the agent workflow to preserve the token and avoid `@latest` cache
|
|
13
|
+
ambiguity.
|
|
14
|
+
|
|
5
15
|
## 0.1.4 - 2026-09-01
|
|
6
16
|
|
|
7
17
|
- Add `/backeer` and `/backeer status` for an in-session integration report.
|
package/README.md
CHANGED
|
@@ -156,6 +156,8 @@ The plugin registers a `/backeer` command inside OpenCode:
|
|
|
156
156
|
```text
|
|
157
157
|
/backeer
|
|
158
158
|
/backeer status
|
|
159
|
+
/backeer update-check
|
|
160
|
+
/backeer update
|
|
159
161
|
/backeer token
|
|
160
162
|
/backeer token-file /absolute/path/to/a/protected-token-file
|
|
161
163
|
```
|
|
@@ -173,6 +175,19 @@ atomically copies it to the configured token location, applies mode `0600`, and
|
|
|
173
175
|
activates it immediately without restarting OpenCode. The source file is not
|
|
174
176
|
removed.
|
|
175
177
|
|
|
178
|
+
The plugin checks the NPM registry at most once every 24 hours. When a newer
|
|
179
|
+
version exists, it adds one synthetic update notice to each OpenCode session so
|
|
180
|
+
the agent can inform the user. The notice never authorizes installation by
|
|
181
|
+
itself.
|
|
182
|
+
|
|
183
|
+
`/backeer update-check` performs an explicit check. `/backeer update` is the
|
|
184
|
+
user authorization point: it gives the OpenCode agent an exact-version install
|
|
185
|
+
command, requires verification of both the global version pin and package
|
|
186
|
+
cache, and forbids reading or changing the Backeer token. The agent does not
|
|
187
|
+
terminate the active OpenCode process; restart OpenCode after a successful
|
|
188
|
+
update. Exact versions are used instead of `@latest` to avoid stale OpenCode
|
|
189
|
+
package caches.
|
|
190
|
+
|
|
176
191
|
## Events and decisions
|
|
177
192
|
|
|
178
193
|
The plugin publishes:
|
|
@@ -357,8 +357,130 @@ import {
|
|
|
357
357
|
writeFileSync
|
|
358
358
|
} from "node:fs";
|
|
359
359
|
import { dirname, resolve } from "node:path";
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
|
|
361
|
+
// src/updates.js
|
|
362
|
+
var PLUGIN_VERSION = "0.1.5";
|
|
363
|
+
var PACKAGE_NAME = "backeer-opencode";
|
|
364
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
365
|
+
var CHECK_TIMEOUT_MS = 3e3;
|
|
366
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
367
|
+
function parseVersion(value) {
|
|
368
|
+
const match = String(value).trim().match(
|
|
369
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/
|
|
370
|
+
);
|
|
371
|
+
if (!match) return;
|
|
372
|
+
return {
|
|
373
|
+
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
374
|
+
prerelease: match[4]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function isVersionNewer(latestValue, currentValue) {
|
|
378
|
+
const latest = parseVersion(latestValue);
|
|
379
|
+
const current = parseVersion(currentValue);
|
|
380
|
+
if (!latest || !current) return false;
|
|
381
|
+
for (let index = 0; index < 3; index++) {
|
|
382
|
+
if (latest.parts[index] > current.parts[index]) return true;
|
|
383
|
+
if (latest.parts[index] < current.parts[index]) return false;
|
|
384
|
+
}
|
|
385
|
+
if (!latest.prerelease && current.prerelease) return true;
|
|
386
|
+
if (latest.prerelease && !current.prerelease) return false;
|
|
387
|
+
return Boolean(
|
|
388
|
+
latest.prerelease && current.prerelease && latest.prerelease !== current.prerelease
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function createUpdateChecker({
|
|
392
|
+
fetchImplementation = globalThis.fetch,
|
|
393
|
+
now = () => Date.now()
|
|
394
|
+
} = {}) {
|
|
395
|
+
let checkedAt = 0;
|
|
396
|
+
let cachedResult;
|
|
397
|
+
let pendingCheck;
|
|
398
|
+
async function fetchLatest() {
|
|
399
|
+
const controller = new AbortController();
|
|
400
|
+
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
|
401
|
+
try {
|
|
402
|
+
const response = await fetchImplementation(
|
|
403
|
+
`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`,
|
|
404
|
+
{ signal: controller.signal }
|
|
405
|
+
);
|
|
406
|
+
if (!response.ok) return { state: "unavailable", currentVersion: PLUGIN_VERSION };
|
|
407
|
+
const data = await response.json();
|
|
408
|
+
const latestVersion = typeof data?.version === "string" ? data.version.trim() : "";
|
|
409
|
+
if (!parseVersion(latestVersion)) {
|
|
410
|
+
return { state: "unavailable", currentVersion: PLUGIN_VERSION };
|
|
411
|
+
}
|
|
412
|
+
if (!isVersionNewer(latestVersion, PLUGIN_VERSION)) {
|
|
413
|
+
return {
|
|
414
|
+
state: "current",
|
|
415
|
+
currentVersion: PLUGIN_VERSION,
|
|
416
|
+
latestVersion
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
state: "available",
|
|
421
|
+
currentVersion: PLUGIN_VERSION,
|
|
422
|
+
latestVersion,
|
|
423
|
+
updateCommand: `opencode plugin ${PACKAGE_NAME}@${latestVersion} -g --force`
|
|
424
|
+
};
|
|
425
|
+
} catch {
|
|
426
|
+
return { state: "unavailable", currentVersion: PLUGIN_VERSION };
|
|
427
|
+
} finally {
|
|
428
|
+
clearTimeout(timeout);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
async check({ force = false } = {}) {
|
|
433
|
+
if (!force && checkedAt && now() - checkedAt < CHECK_INTERVAL_MS) {
|
|
434
|
+
return cachedResult;
|
|
435
|
+
}
|
|
436
|
+
if (pendingCheck) return pendingCheck;
|
|
437
|
+
pendingCheck = fetchLatest().then((result) => {
|
|
438
|
+
cachedResult = result;
|
|
439
|
+
checkedAt = now();
|
|
440
|
+
return result;
|
|
441
|
+
}).finally(() => {
|
|
442
|
+
pendingCheck = void 0;
|
|
443
|
+
});
|
|
444
|
+
return pendingCheck;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function formatAgentUpdateInstruction(info) {
|
|
449
|
+
return [
|
|
450
|
+
"[BACKEER UPDATE REQUESTED]",
|
|
451
|
+
`The user explicitly requested the Backeer OpenCode plugin update from v${info.currentVersion} to v${info.latestVersion}.`,
|
|
452
|
+
`Run exactly: ${info.updateCommand}`,
|
|
453
|
+
"Then verify that the global OpenCode config is pinned to the exact new version and that the matching package exists in the OpenCode cache.",
|
|
454
|
+
"Do not read, print, replace, or otherwise expose the Backeer token.",
|
|
455
|
+
"Do not use @latest and do not claim success until both installation and verification pass.",
|
|
456
|
+
"Do not terminate the active OpenCode process; tell the user to restart OpenCode after success."
|
|
457
|
+
].join("\n");
|
|
458
|
+
}
|
|
459
|
+
function createUpdateNoticeHook(updateChecker) {
|
|
460
|
+
const notifiedSessions = /* @__PURE__ */ new Set();
|
|
461
|
+
return async (input, output) => {
|
|
462
|
+
if (notifiedSessions.has(input.sessionID)) return;
|
|
463
|
+
notifiedSessions.add(input.sessionID);
|
|
464
|
+
const info = await updateChecker.check();
|
|
465
|
+
if (info.state !== "available") return;
|
|
466
|
+
output.parts.push({
|
|
467
|
+
id: `prt_backeer-update-${Date.now()}`,
|
|
468
|
+
sessionID: input.sessionID,
|
|
469
|
+
messageID: output.message.id,
|
|
470
|
+
type: "text",
|
|
471
|
+
synthetic: true,
|
|
472
|
+
text: [
|
|
473
|
+
"[BACKEER UPDATE AVAILABLE]",
|
|
474
|
+
`Backeer OpenCode plugin v${info.latestVersion} is available; current version is v${info.currentVersion}.`,
|
|
475
|
+
"Inform the user that an update is available and suggest `/backeer update`.",
|
|
476
|
+
"Do not install it solely because of this notice."
|
|
477
|
+
].join("\n")
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/management.js
|
|
483
|
+
var COMMAND_DESCRIPTION = "Show Backeer status, update it, or manage its token: /backeer [status|update-check|update|token|token-file PATH]";
|
|
362
484
|
function tokenFileState(path) {
|
|
363
485
|
try {
|
|
364
486
|
const stats = statSync(path);
|
|
@@ -418,13 +540,24 @@ async function probeGateway(config) {
|
|
|
418
540
|
clearTimeout(timeout);
|
|
419
541
|
}
|
|
420
542
|
}
|
|
421
|
-
|
|
422
|
-
|
|
543
|
+
function updateStatus(info) {
|
|
544
|
+
if (info.state === "available") {
|
|
545
|
+
return `v${info.latestVersion} available \u2014 use /backeer update`;
|
|
546
|
+
}
|
|
547
|
+
if (info.state === "current") return `current (v${info.currentVersion})`;
|
|
548
|
+
return "check unavailable";
|
|
549
|
+
}
|
|
550
|
+
async function buildStatus(config, permissionController, updateChecker) {
|
|
551
|
+
const [gateway, update] = await Promise.all([
|
|
552
|
+
probeGateway(config),
|
|
553
|
+
updateChecker.check()
|
|
554
|
+
]);
|
|
423
555
|
const permissionStatus = permissionController.status();
|
|
424
556
|
return [
|
|
425
557
|
"## Backeer status",
|
|
426
558
|
"",
|
|
427
559
|
`- Plugin version: ${PLUGIN_VERSION}`,
|
|
560
|
+
`- Update: ${updateStatus(update)}`,
|
|
428
561
|
`- Gateway: ${config.origin.origin} \u2014 ${gateway}`,
|
|
429
562
|
`- Token: ${config.token ? `configured via ${config.tokenSource}` : "missing"}`,
|
|
430
563
|
`- Token file: ${config.tokenFile} \u2014 ${tokenFileState(config.tokenFile)}`,
|
|
@@ -443,6 +576,8 @@ function helpMessage() {
|
|
|
443
576
|
"## Backeer commands",
|
|
444
577
|
"",
|
|
445
578
|
"- `/backeer` or `/backeer status` \u2014 show integration status",
|
|
579
|
+
"- `/backeer update-check` \u2014 check NPM for a newer version",
|
|
580
|
+
"- `/backeer update` \u2014 ask the OpenCode agent to install and verify it",
|
|
446
581
|
"- `/backeer token` \u2014 show secure token-change instructions",
|
|
447
582
|
"- `/backeer token-file /absolute/protected/path` \u2014 import and activate a token",
|
|
448
583
|
"",
|
|
@@ -486,7 +621,8 @@ function createBackeerCommandHandler({
|
|
|
486
621
|
client,
|
|
487
622
|
config,
|
|
488
623
|
permissionController,
|
|
489
|
-
reporter
|
|
624
|
+
reporter,
|
|
625
|
+
updateChecker
|
|
490
626
|
}) {
|
|
491
627
|
return async (input, output) => {
|
|
492
628
|
if (input.command !== "backeer") return;
|
|
@@ -498,7 +634,26 @@ function createBackeerCommandHandler({
|
|
|
498
634
|
let message;
|
|
499
635
|
try {
|
|
500
636
|
if (!subcommand || subcommand === "status") {
|
|
501
|
-
message = await buildStatus(config, permissionController);
|
|
637
|
+
message = await buildStatus(config, permissionController, updateChecker);
|
|
638
|
+
} else if (subcommand === "update-check") {
|
|
639
|
+
const update = await updateChecker.check({ force: true });
|
|
640
|
+
message = [
|
|
641
|
+
"## Backeer update check",
|
|
642
|
+
"",
|
|
643
|
+
update.state === "available" ? `Version ${update.latestVersion} is available; current version is ${update.currentVersion}. Use \`/backeer update\` to let the OpenCode agent install and verify it.` : update.state === "current" ? `Backeer is current at version ${update.currentVersion}.` : "The NPM registry could not be reached. No update was attempted."
|
|
644
|
+
].join("\n");
|
|
645
|
+
} else if (subcommand === "update") {
|
|
646
|
+
const update = await updateChecker.check({ force: true });
|
|
647
|
+
if (update.state === "available") {
|
|
648
|
+
output.parts.push({
|
|
649
|
+
type: "text",
|
|
650
|
+
text: formatAgentUpdateInstruction(update)
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
message = update.state === "current" ? `## Backeer update
|
|
655
|
+
|
|
656
|
+
Backeer is already current at version ${update.currentVersion}.` : "## Backeer update\n\nThe NPM registry could not be reached. No update was attempted.";
|
|
502
657
|
} else if (subcommand === "token") {
|
|
503
658
|
message = tokenInstructions();
|
|
504
659
|
} else if (subcommand === "token-file") {
|
|
@@ -835,12 +990,15 @@ async function BackeerPlugin({ client, directory }, options = {}) {
|
|
|
835
990
|
});
|
|
836
991
|
const todoCompletionBySessionID = /* @__PURE__ */ new Map();
|
|
837
992
|
const publishedQuestions = /* @__PURE__ */ new Map();
|
|
993
|
+
const updateChecker = createUpdateChecker();
|
|
838
994
|
const handleBackeerCommand = createBackeerCommandHandler({
|
|
839
995
|
client,
|
|
840
996
|
config,
|
|
841
997
|
permissionController,
|
|
842
|
-
reporter
|
|
998
|
+
reporter,
|
|
999
|
+
updateChecker
|
|
843
1000
|
});
|
|
1001
|
+
const handleUpdateNotice = createUpdateNoticeHook(updateChecker);
|
|
844
1002
|
if (!config.token) {
|
|
845
1003
|
reporter.error(
|
|
846
1004
|
"Notifications disabled \u2014 use /backeer token",
|
|
@@ -903,6 +1061,7 @@ async function BackeerPlugin({ client, directory }, options = {}) {
|
|
|
903
1061
|
registerBackeerCommand(opencodeConfig);
|
|
904
1062
|
},
|
|
905
1063
|
"command.execute.before": handleBackeerCommand,
|
|
1064
|
+
"chat.message": handleUpdateNotice,
|
|
906
1065
|
event: async ({ event }) => {
|
|
907
1066
|
if (!config.token) return;
|
|
908
1067
|
if (config.permissions && event.type === "permission.asked") {
|
package/package.json
CHANGED
package/src/management.js
CHANGED
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
} from "node:fs"
|
|
9
9
|
import { dirname, resolve } from "node:path"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import {
|
|
12
|
+
formatAgentUpdateInstruction,
|
|
13
|
+
PLUGIN_VERSION,
|
|
14
|
+
} from "./updates.js"
|
|
12
15
|
|
|
13
16
|
const COMMAND_DESCRIPTION =
|
|
14
|
-
"Show Backeer status or manage its token: /backeer [status|token|token-file PATH]"
|
|
17
|
+
"Show Backeer status, update it, or manage its token: /backeer [status|update-check|update|token|token-file PATH]"
|
|
15
18
|
|
|
16
19
|
function tokenFileState(path) {
|
|
17
20
|
try {
|
|
@@ -83,13 +86,25 @@ async function probeGateway(config) {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
function updateStatus(info) {
|
|
90
|
+
if (info.state === "available") {
|
|
91
|
+
return `v${info.latestVersion} available — use /backeer update`
|
|
92
|
+
}
|
|
93
|
+
if (info.state === "current") return `current (v${info.currentVersion})`
|
|
94
|
+
return "check unavailable"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function buildStatus(config, permissionController, updateChecker) {
|
|
98
|
+
const [gateway, update] = await Promise.all([
|
|
99
|
+
probeGateway(config),
|
|
100
|
+
updateChecker.check(),
|
|
101
|
+
])
|
|
88
102
|
const permissionStatus = permissionController.status()
|
|
89
103
|
return [
|
|
90
104
|
"## Backeer status",
|
|
91
105
|
"",
|
|
92
106
|
`- Plugin version: ${PLUGIN_VERSION}`,
|
|
107
|
+
`- Update: ${updateStatus(update)}`,
|
|
93
108
|
`- Gateway: ${config.origin.origin} — ${gateway}`,
|
|
94
109
|
`- Token: ${config.token ? `configured via ${config.tokenSource}` : "missing"}`,
|
|
95
110
|
`- Token file: ${config.tokenFile} — ${tokenFileState(config.tokenFile)}`,
|
|
@@ -109,6 +124,8 @@ function helpMessage() {
|
|
|
109
124
|
"## Backeer commands",
|
|
110
125
|
"",
|
|
111
126
|
"- `/backeer` or `/backeer status` — show integration status",
|
|
127
|
+
"- `/backeer update-check` — check NPM for a newer version",
|
|
128
|
+
"- `/backeer update` — ask the OpenCode agent to install and verify it",
|
|
112
129
|
"- `/backeer token` — show secure token-change instructions",
|
|
113
130
|
"- `/backeer token-file /absolute/protected/path` — import and activate a token",
|
|
114
131
|
"",
|
|
@@ -157,6 +174,7 @@ export function createBackeerCommandHandler({
|
|
|
157
174
|
config,
|
|
158
175
|
permissionController,
|
|
159
176
|
reporter,
|
|
177
|
+
updateChecker,
|
|
160
178
|
}) {
|
|
161
179
|
return async (input, output) => {
|
|
162
180
|
if (input.command !== "backeer") return
|
|
@@ -174,7 +192,30 @@ export function createBackeerCommandHandler({
|
|
|
174
192
|
let message
|
|
175
193
|
try {
|
|
176
194
|
if (!subcommand || subcommand === "status") {
|
|
177
|
-
message = await buildStatus(config, permissionController)
|
|
195
|
+
message = await buildStatus(config, permissionController, updateChecker)
|
|
196
|
+
} else if (subcommand === "update-check") {
|
|
197
|
+
const update = await updateChecker.check({ force: true })
|
|
198
|
+
message = [
|
|
199
|
+
"## Backeer update check",
|
|
200
|
+
"",
|
|
201
|
+
update.state === "available"
|
|
202
|
+
? `Version ${update.latestVersion} is available; current version is ${update.currentVersion}. Use \`/backeer update\` to let the OpenCode agent install and verify it.`
|
|
203
|
+
: update.state === "current"
|
|
204
|
+
? `Backeer is current at version ${update.currentVersion}.`
|
|
205
|
+
: "The NPM registry could not be reached. No update was attempted.",
|
|
206
|
+
].join("\n")
|
|
207
|
+
} else if (subcommand === "update") {
|
|
208
|
+
const update = await updateChecker.check({ force: true })
|
|
209
|
+
if (update.state === "available") {
|
|
210
|
+
output.parts.push({
|
|
211
|
+
type: "text",
|
|
212
|
+
text: formatAgentUpdateInstruction(update),
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
message = update.state === "current"
|
|
217
|
+
? `## Backeer update\n\nBackeer is already current at version ${update.currentVersion}.`
|
|
218
|
+
: "## Backeer update\n\nThe NPM registry could not be reached. No update was attempted."
|
|
178
219
|
} else if (subcommand === "token") {
|
|
179
220
|
message = tokenInstructions()
|
|
180
221
|
} else if (subcommand === "token-file") {
|
package/src/plugin.js
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
} from "./management.js"
|
|
13
13
|
import { createPermissionController } from "./permission-controller.js"
|
|
14
14
|
import { createConsoleReporter } from "./reporter.js"
|
|
15
|
+
import {
|
|
16
|
+
createUpdateChecker,
|
|
17
|
+
createUpdateNoticeHook,
|
|
18
|
+
} from "./updates.js"
|
|
15
19
|
import { rememberBounded } from "./utilities.js"
|
|
16
20
|
|
|
17
21
|
/**
|
|
@@ -43,12 +47,15 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
43
47
|
})
|
|
44
48
|
const todoCompletionBySessionID = new Map()
|
|
45
49
|
const publishedQuestions = new Map()
|
|
50
|
+
const updateChecker = createUpdateChecker()
|
|
46
51
|
const handleBackeerCommand = createBackeerCommandHandler({
|
|
47
52
|
client,
|
|
48
53
|
config,
|
|
49
54
|
permissionController,
|
|
50
55
|
reporter,
|
|
56
|
+
updateChecker,
|
|
51
57
|
})
|
|
58
|
+
const handleUpdateNotice = createUpdateNoticeHook(updateChecker)
|
|
52
59
|
|
|
53
60
|
if (!config.token) {
|
|
54
61
|
reporter.error(
|
|
@@ -123,6 +130,8 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
123
130
|
|
|
124
131
|
"command.execute.before": handleBackeerCommand,
|
|
125
132
|
|
|
133
|
+
"chat.message": handleUpdateNotice,
|
|
134
|
+
|
|
126
135
|
event: async ({ event }) => {
|
|
127
136
|
if (!config.token) return
|
|
128
137
|
|
package/src/updates.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const PLUGIN_VERSION = "0.1.5"
|
|
2
|
+
|
|
3
|
+
const PACKAGE_NAME = "backeer-opencode"
|
|
4
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org"
|
|
5
|
+
const CHECK_TIMEOUT_MS = 3000
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
7
|
+
|
|
8
|
+
export function parseVersion(value) {
|
|
9
|
+
const match = String(value).trim().match(
|
|
10
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/,
|
|
11
|
+
)
|
|
12
|
+
if (!match) return
|
|
13
|
+
return {
|
|
14
|
+
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
15
|
+
prerelease: match[4],
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isVersionNewer(latestValue, currentValue) {
|
|
20
|
+
const latest = parseVersion(latestValue)
|
|
21
|
+
const current = parseVersion(currentValue)
|
|
22
|
+
if (!latest || !current) return false
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < 3; index++) {
|
|
25
|
+
if (latest.parts[index] > current.parts[index]) return true
|
|
26
|
+
if (latest.parts[index] < current.parts[index]) return false
|
|
27
|
+
}
|
|
28
|
+
if (!latest.prerelease && current.prerelease) return true
|
|
29
|
+
if (latest.prerelease && !current.prerelease) return false
|
|
30
|
+
return Boolean(
|
|
31
|
+
latest.prerelease &&
|
|
32
|
+
current.prerelease &&
|
|
33
|
+
latest.prerelease !== current.prerelease,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createUpdateChecker({
|
|
38
|
+
fetchImplementation = globalThis.fetch,
|
|
39
|
+
now = () => Date.now(),
|
|
40
|
+
} = {}) {
|
|
41
|
+
let checkedAt = 0
|
|
42
|
+
let cachedResult
|
|
43
|
+
let pendingCheck
|
|
44
|
+
|
|
45
|
+
async function fetchLatest() {
|
|
46
|
+
const controller = new AbortController()
|
|
47
|
+
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS)
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetchImplementation(
|
|
50
|
+
`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`,
|
|
51
|
+
{ signal: controller.signal },
|
|
52
|
+
)
|
|
53
|
+
if (!response.ok) return { state: "unavailable", currentVersion: PLUGIN_VERSION }
|
|
54
|
+
const data = await response.json()
|
|
55
|
+
const latestVersion = typeof data?.version === "string"
|
|
56
|
+
? data.version.trim()
|
|
57
|
+
: ""
|
|
58
|
+
if (!parseVersion(latestVersion)) {
|
|
59
|
+
return { state: "unavailable", currentVersion: PLUGIN_VERSION }
|
|
60
|
+
}
|
|
61
|
+
if (!isVersionNewer(latestVersion, PLUGIN_VERSION)) {
|
|
62
|
+
return {
|
|
63
|
+
state: "current",
|
|
64
|
+
currentVersion: PLUGIN_VERSION,
|
|
65
|
+
latestVersion,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
state: "available",
|
|
70
|
+
currentVersion: PLUGIN_VERSION,
|
|
71
|
+
latestVersion,
|
|
72
|
+
updateCommand:
|
|
73
|
+
`opencode plugin ${PACKAGE_NAME}@${latestVersion} -g --force`,
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return { state: "unavailable", currentVersion: PLUGIN_VERSION }
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
async check({ force = false } = {}) {
|
|
84
|
+
if (!force && checkedAt && now() - checkedAt < CHECK_INTERVAL_MS) {
|
|
85
|
+
return cachedResult
|
|
86
|
+
}
|
|
87
|
+
if (pendingCheck) return pendingCheck
|
|
88
|
+
|
|
89
|
+
pendingCheck = fetchLatest()
|
|
90
|
+
.then((result) => {
|
|
91
|
+
cachedResult = result
|
|
92
|
+
checkedAt = now()
|
|
93
|
+
return result
|
|
94
|
+
})
|
|
95
|
+
.finally(() => { pendingCheck = undefined })
|
|
96
|
+
return pendingCheck
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function formatAgentUpdateInstruction(info) {
|
|
102
|
+
return [
|
|
103
|
+
"[BACKEER UPDATE REQUESTED]",
|
|
104
|
+
`The user explicitly requested the Backeer OpenCode plugin update from v${info.currentVersion} to v${info.latestVersion}.`,
|
|
105
|
+
`Run exactly: ${info.updateCommand}`,
|
|
106
|
+
"Then verify that the global OpenCode config is pinned to the exact new version and that the matching package exists in the OpenCode cache.",
|
|
107
|
+
"Do not read, print, replace, or otherwise expose the Backeer token.",
|
|
108
|
+
"Do not use @latest and do not claim success until both installation and verification pass.",
|
|
109
|
+
"Do not terminate the active OpenCode process; tell the user to restart OpenCode after success.",
|
|
110
|
+
].join("\n")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createUpdateNoticeHook(updateChecker) {
|
|
114
|
+
const notifiedSessions = new Set()
|
|
115
|
+
|
|
116
|
+
return async (input, output) => {
|
|
117
|
+
if (notifiedSessions.has(input.sessionID)) return
|
|
118
|
+
notifiedSessions.add(input.sessionID)
|
|
119
|
+
|
|
120
|
+
const info = await updateChecker.check()
|
|
121
|
+
if (info.state !== "available") return
|
|
122
|
+
output.parts.push({
|
|
123
|
+
id: `prt_backeer-update-${Date.now()}`,
|
|
124
|
+
sessionID: input.sessionID,
|
|
125
|
+
messageID: output.message.id,
|
|
126
|
+
type: "text",
|
|
127
|
+
synthetic: true,
|
|
128
|
+
text: [
|
|
129
|
+
"[BACKEER UPDATE AVAILABLE]",
|
|
130
|
+
`Backeer OpenCode plugin v${info.latestVersion} is available; current version is v${info.currentVersion}.`,
|
|
131
|
+
"Inform the user that an update is available and suggest `/backeer update`.",
|
|
132
|
+
"Do not install it solely because of this notice.",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|