gitdone-agent 0.8.6 → 0.8.7
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/index.js +83 -3
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
29
29
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
30
30
|
// Keep in lockstep with packages/agent/package.json. The server's offline
|
|
31
31
|
// fallback is bumped only after this release has actually reached npm.
|
|
32
|
-
const AGENT_VERSION = '0.8.
|
|
32
|
+
const AGENT_VERSION = '0.8.7'
|
|
33
33
|
|
|
34
34
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
35
35
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -547,6 +547,77 @@ function uninstallStartup() {
|
|
|
547
547
|
|
|
548
548
|
// ─── Doctor ────────────────────────────────────────────────────────────────────
|
|
549
549
|
|
|
550
|
+
// Each sync heartbeat returns the version, URL and SHA-256 of the exact agent
|
|
551
|
+
// script deployed on the server. A supervised stable install downloads it,
|
|
552
|
+
// verifies both checksum and embedded version, atomically replaces itself, and
|
|
553
|
+
// exits; run-agent.cmd then starts the new version within about ten seconds.
|
|
554
|
+
function compareVersions(a, b) {
|
|
555
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0)
|
|
556
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0)
|
|
557
|
+
const len = Math.max(pa.length, pb.length)
|
|
558
|
+
for (let i = 0; i < len; i++) {
|
|
559
|
+
const difference = (pa[i] ?? 0) - (pb[i] ?? 0)
|
|
560
|
+
if (difference !== 0) return difference > 0 ? 1 : -1
|
|
561
|
+
}
|
|
562
|
+
return 0
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const UPDATE_RETRY_MS = 60 * 60 * 1000
|
|
566
|
+
let updateInFlight = false
|
|
567
|
+
let lastUpdateAttempt = { version: null, at: 0 }
|
|
568
|
+
|
|
569
|
+
async function selfUpdate(cfg, latestVersion, downloadUrl, expectedSha256) {
|
|
570
|
+
if (updateInFlight || !latestVersion || !downloadUrl) return
|
|
571
|
+
if (compareVersions(latestVersion, AGENT_VERSION) <= 0) return
|
|
572
|
+
if (lastUpdateAttempt.version === latestVersion && Date.now() - lastUpdateAttempt.at < UPDATE_RETRY_MS) return
|
|
573
|
+
|
|
574
|
+
// A foreground/dev invocation has no supervisor to bring it back after the
|
|
575
|
+
// swap. Announce the update there, but only self-replace a stable install.
|
|
576
|
+
const runningStable = resolve(process.argv[1] || '') === STABLE_AGENT
|
|
577
|
+
const supervised = existsSync(join(AGENT_DIR, 'run-agent.cmd'))
|
|
578
|
+
if (!runningStable || !supervised) {
|
|
579
|
+
log(`↑ налична е нова версия ${latestVersion} (текуща ${AGENT_VERSION}) — пусни „gitdone-agent --install“ еднократно, за да включиш автообновяването`)
|
|
580
|
+
lastUpdateAttempt = { version: latestVersion, at: Date.now() }
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
updateInFlight = true
|
|
585
|
+
lastUpdateAttempt = { version: latestVersion, at: Date.now() }
|
|
586
|
+
const url = downloadUrl.startsWith('http') ? downloadUrl : `${cfg.url}${downloadUrl}`
|
|
587
|
+
const tmp = STABLE_AGENT + '.next'
|
|
588
|
+
log(`↑ автообновяване ${AGENT_VERSION} → ${latestVersion}`)
|
|
589
|
+
try {
|
|
590
|
+
if (!/^[a-f0-9]{64}$/i.test(String(expectedSha256 || ''))) {
|
|
591
|
+
throw new Error('сървърът не върна валиден sha256')
|
|
592
|
+
}
|
|
593
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${cfg.key}` } })
|
|
594
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
595
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
596
|
+
if (buffer.length < 1000) throw new Error(`подозрително малък файл (${buffer.length} B)`)
|
|
597
|
+
|
|
598
|
+
const actualSha256 = createHash('sha256').update(buffer).digest('hex')
|
|
599
|
+
if (actualSha256.toLowerCase() !== String(expectedSha256).toLowerCase()) {
|
|
600
|
+
throw new Error(`sha256 несъвпадение (очаквано ${expectedSha256}, получено ${actualSha256})`)
|
|
601
|
+
}
|
|
602
|
+
const embeddedVersion = buffer
|
|
603
|
+
.subarray(0, 12_000)
|
|
604
|
+
.toString('utf8')
|
|
605
|
+
.match(/const AGENT_VERSION\s*=\s*['"]([^'"]+)['"]/)?.[1]
|
|
606
|
+
if (embeddedVersion !== latestVersion) {
|
|
607
|
+
throw new Error(`файлът е v${embeddedVersion || '?'}, а сървърът обяви v${latestVersion}`)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
writeFileSync(tmp, buffer)
|
|
611
|
+
renameSync(tmp, STABLE_AGENT)
|
|
612
|
+
log(`✓ обновено до ${latestVersion} — supervisor-ът ще рестартира агента`)
|
|
613
|
+
process.exit(0)
|
|
614
|
+
} catch (err) {
|
|
615
|
+
updateInFlight = false
|
|
616
|
+
try { if (existsSync(tmp)) unlinkSync(tmp) } catch { /* best-effort cleanup */ }
|
|
617
|
+
log(`✗ автообновяването се провали: ${err?.message || err} — оставам на ${AGENT_VERSION}, ще опитам пак до 1 час`)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
550
621
|
function runDoctor() {
|
|
551
622
|
const cfg = readConfig()
|
|
552
623
|
console.log('gitdone-agent doctor')
|
|
@@ -2795,7 +2866,12 @@ async function sync(cfg, discovered) {
|
|
|
2795
2866
|
})
|
|
2796
2867
|
if (full) { syncRepoSig = sig; syncFullCountdown = SYNC_FULL_TICKS }
|
|
2797
2868
|
else syncFullCountdown--
|
|
2798
|
-
return
|
|
2869
|
+
return {
|
|
2870
|
+
tracked: data.tracked ?? [],
|
|
2871
|
+
latestVersion: data.latestVersion,
|
|
2872
|
+
downloadUrl: data.downloadUrl,
|
|
2873
|
+
downloadSha256: data.downloadSha256,
|
|
2874
|
+
}
|
|
2799
2875
|
}
|
|
2800
2876
|
|
|
2801
2877
|
// Per-repo cache of the last snapshot we actually SENT, so an idle repo whose
|
|
@@ -2969,7 +3045,11 @@ async function runLoop(cfg) {
|
|
|
2969
3045
|
async function tick() {
|
|
2970
3046
|
try {
|
|
2971
3047
|
const discovered = scanRepos(cfg.roots)
|
|
2972
|
-
const
|
|
3048
|
+
const state = await sync(cfg, discovered)
|
|
3049
|
+
// A successful update exits here; the supervisor starts the new stable
|
|
3050
|
+
// script before any more repo work is performed.
|
|
3051
|
+
await selfUpdate(cfg, state.latestVersion, state.downloadUrl, state.downloadSha256)
|
|
3052
|
+
const tracked = state.tracked
|
|
2973
3053
|
let pushed = 0
|
|
2974
3054
|
let skipped = 0
|
|
2975
3055
|
for (const repo of tracked) {
|