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.
Files changed (2) hide show
  1. package/index.js +83 -3
  2. 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.6'
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 data.tracked ?? []
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 tracked = await sync(cfg, discovered)
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {