gitdone-agent 0.8.6 → 0.8.8
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 +209 -5
- package/package.json +5 -2
package/index.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Setup (once):
|
|
6
6
|
// npx gitdone-agent --key=gdo_xxx --root=C:\path\to\projects --install
|
|
7
|
+
// Optional WoW progress sync:
|
|
8
|
+
// npx gitdone-agent --wow="C:\Program Files (x86)\World of Warcraft\_retail_" --install
|
|
7
9
|
//
|
|
8
10
|
// Then pick which discovered repos to track from gitdone.eu/github. Add more
|
|
9
11
|
// roots anytime with --root (repeatable). The running agent reads its config
|
|
@@ -29,7 +31,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
29
31
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
30
32
|
// Keep in lockstep with packages/agent/package.json. The server's offline
|
|
31
33
|
// fallback is bumped only after this release has actually reached npm.
|
|
32
|
-
const AGENT_VERSION = '0.8.
|
|
34
|
+
const AGENT_VERSION = '0.8.8'
|
|
33
35
|
|
|
34
36
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
35
37
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -182,21 +184,25 @@ function parseArgs() {
|
|
|
182
184
|
const argv = process.argv.slice(2)
|
|
183
185
|
const flags = {}
|
|
184
186
|
const roots = []
|
|
187
|
+
const wowRoots = []
|
|
185
188
|
for (const a of argv) {
|
|
186
189
|
if (!a.startsWith('--')) continue
|
|
187
190
|
const [k, ...rest] = a.slice(2).split('=')
|
|
188
191
|
const v = rest.join('=')
|
|
189
192
|
if (k === 'root') { if (v) roots.push(resolve(v)) }
|
|
193
|
+
else if (k === 'wow') { if (v) wowRoots.push(resolve(v)) }
|
|
190
194
|
else flags[k] = v
|
|
191
195
|
}
|
|
192
196
|
return {
|
|
193
197
|
key: flags.key,
|
|
194
198
|
roots,
|
|
199
|
+
wowRoots,
|
|
195
200
|
interval: flags.interval ? Number(flags.interval) : undefined,
|
|
196
201
|
url: flags.url ? flags.url.replace(/\/$/, '') : undefined,
|
|
197
202
|
install: 'install' in flags,
|
|
198
203
|
uninstall: 'uninstall' in flags,
|
|
199
204
|
doctor: 'doctor' in flags,
|
|
205
|
+
testWow: flags['test-wow'] ? resolve(flags['test-wow']) : undefined,
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -204,6 +210,7 @@ function parseArgs() {
|
|
|
204
210
|
function buildConfig(args) {
|
|
205
211
|
const existing = readConfig() ?? {}
|
|
206
212
|
const mergedRoots = Array.from(new Set([...(existing.roots ?? []), ...args.roots]))
|
|
213
|
+
const mergedWowRoots = Array.from(new Set([...(existing.wowRoots ?? []), ...args.wowRoots]))
|
|
207
214
|
return {
|
|
208
215
|
key: args.key ?? existing.key,
|
|
209
216
|
url: args.url ?? existing.url ?? 'https://gitdone.eu',
|
|
@@ -211,6 +218,8 @@ function buildConfig(args) {
|
|
|
211
218
|
machineId: existing.machineId ?? randomUUID(),
|
|
212
219
|
hostname: existing.hostname ?? hostname(),
|
|
213
220
|
roots: mergedRoots,
|
|
221
|
+
wowRoots: mergedWowRoots,
|
|
222
|
+
...(existing.auth ? { auth: existing.auth } : {}),
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
|
|
@@ -547,6 +556,77 @@ function uninstallStartup() {
|
|
|
547
556
|
|
|
548
557
|
// ─── Doctor ────────────────────────────────────────────────────────────────────
|
|
549
558
|
|
|
559
|
+
// Each sync heartbeat returns the version, URL and SHA-256 of the exact agent
|
|
560
|
+
// script deployed on the server. A supervised stable install downloads it,
|
|
561
|
+
// verifies both checksum and embedded version, atomically replaces itself, and
|
|
562
|
+
// exits; run-agent.cmd then starts the new version within about ten seconds.
|
|
563
|
+
function compareVersions(a, b) {
|
|
564
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0)
|
|
565
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0)
|
|
566
|
+
const len = Math.max(pa.length, pb.length)
|
|
567
|
+
for (let i = 0; i < len; i++) {
|
|
568
|
+
const difference = (pa[i] ?? 0) - (pb[i] ?? 0)
|
|
569
|
+
if (difference !== 0) return difference > 0 ? 1 : -1
|
|
570
|
+
}
|
|
571
|
+
return 0
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const UPDATE_RETRY_MS = 60 * 60 * 1000
|
|
575
|
+
let updateInFlight = false
|
|
576
|
+
let lastUpdateAttempt = { version: null, at: 0 }
|
|
577
|
+
|
|
578
|
+
async function selfUpdate(cfg, latestVersion, downloadUrl, expectedSha256) {
|
|
579
|
+
if (updateInFlight || !latestVersion || !downloadUrl) return
|
|
580
|
+
if (compareVersions(latestVersion, AGENT_VERSION) <= 0) return
|
|
581
|
+
if (lastUpdateAttempt.version === latestVersion && Date.now() - lastUpdateAttempt.at < UPDATE_RETRY_MS) return
|
|
582
|
+
|
|
583
|
+
// A foreground/dev invocation has no supervisor to bring it back after the
|
|
584
|
+
// swap. Announce the update there, but only self-replace a stable install.
|
|
585
|
+
const runningStable = resolve(process.argv[1] || '') === STABLE_AGENT
|
|
586
|
+
const supervised = existsSync(join(AGENT_DIR, 'run-agent.cmd'))
|
|
587
|
+
if (!runningStable || !supervised) {
|
|
588
|
+
log(`↑ налична е нова версия ${latestVersion} (текуща ${AGENT_VERSION}) — пусни „gitdone-agent --install“ еднократно, за да включиш автообновяването`)
|
|
589
|
+
lastUpdateAttempt = { version: latestVersion, at: Date.now() }
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
updateInFlight = true
|
|
594
|
+
lastUpdateAttempt = { version: latestVersion, at: Date.now() }
|
|
595
|
+
const url = downloadUrl.startsWith('http') ? downloadUrl : `${cfg.url}${downloadUrl}`
|
|
596
|
+
const tmp = STABLE_AGENT + '.next'
|
|
597
|
+
log(`↑ автообновяване ${AGENT_VERSION} → ${latestVersion}`)
|
|
598
|
+
try {
|
|
599
|
+
if (!/^[a-f0-9]{64}$/i.test(String(expectedSha256 || ''))) {
|
|
600
|
+
throw new Error('сървърът не върна валиден sha256')
|
|
601
|
+
}
|
|
602
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${cfg.key}` } })
|
|
603
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
604
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
605
|
+
if (buffer.length < 1000) throw new Error(`подозрително малък файл (${buffer.length} B)`)
|
|
606
|
+
|
|
607
|
+
const actualSha256 = createHash('sha256').update(buffer).digest('hex')
|
|
608
|
+
if (actualSha256.toLowerCase() !== String(expectedSha256).toLowerCase()) {
|
|
609
|
+
throw new Error(`sha256 несъвпадение (очаквано ${expectedSha256}, получено ${actualSha256})`)
|
|
610
|
+
}
|
|
611
|
+
const embeddedVersion = buffer
|
|
612
|
+
.subarray(0, 12_000)
|
|
613
|
+
.toString('utf8')
|
|
614
|
+
.match(/const AGENT_VERSION\s*=\s*['"]([^'"]+)['"]/)?.[1]
|
|
615
|
+
if (embeddedVersion !== latestVersion) {
|
|
616
|
+
throw new Error(`файлът е v${embeddedVersion || '?'}, а сървърът обяви v${latestVersion}`)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
writeFileSync(tmp, buffer)
|
|
620
|
+
renameSync(tmp, STABLE_AGENT)
|
|
621
|
+
log(`✓ обновено до ${latestVersion} — supervisor-ът ще рестартира агента`)
|
|
622
|
+
process.exit(0)
|
|
623
|
+
} catch (err) {
|
|
624
|
+
updateInFlight = false
|
|
625
|
+
try { if (existsSync(tmp)) unlinkSync(tmp) } catch { /* best-effort cleanup */ }
|
|
626
|
+
log(`✗ автообновяването се провали: ${err?.message || err} — оставам на ${AGENT_VERSION}, ще опитам пак до 1 час`)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
550
630
|
function runDoctor() {
|
|
551
631
|
const cfg = readConfig()
|
|
552
632
|
console.log('gitdone-agent doctor')
|
|
@@ -586,6 +666,20 @@ function runDoctor() {
|
|
|
586
666
|
console.log(` found repos : ${found.length}`)
|
|
587
667
|
for (const r of found) console.log(` - ${r.name} (${r.path})`)
|
|
588
668
|
}
|
|
669
|
+
console.log(` WoW roots : ${cfg.wowRoots?.length ? '' : '(not configured)'}`)
|
|
670
|
+
for (const root of cfg.wowRoots ?? []) console.log(` - ${root} ${existsSync(root) ? '' : '(MISSING)'}`)
|
|
671
|
+
if (cfg.wowRoots?.length) {
|
|
672
|
+
const files = findWowSavedVariableFiles(cfg.wowRoots)
|
|
673
|
+
console.log(` WoW snapshots : ${files.length}`)
|
|
674
|
+
for (const file of files) {
|
|
675
|
+
try {
|
|
676
|
+
const { snapshot } = decodeWowSavedVariables(file)
|
|
677
|
+
console.log(` - ${snapshot.character.name}-${snapshot.character.realm} (${file})`)
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.log(` - INVALID (${file}): ${err.message}`)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
589
683
|
}
|
|
590
684
|
}
|
|
591
685
|
|
|
@@ -997,6 +1091,94 @@ function scanRepos(roots, maxDepth = 5) {
|
|
|
997
1091
|
return found
|
|
998
1092
|
}
|
|
999
1093
|
|
|
1094
|
+
// ─── World of Warcraft progress bridge ──────────────────────────────────────
|
|
1095
|
+
// WoW addons cannot open network connections. GitDoneProgress therefore writes
|
|
1096
|
+
// a Base64URL-encoded JSON snapshot to its normal SavedVariables file; this
|
|
1097
|
+
// opt-in companion path reads only that explicit file and uploads it with the
|
|
1098
|
+
// same API credential the agent already uses. WoW flushes SavedVariables on
|
|
1099
|
+
// /reload or logout, so there is no process-memory inspection involved.
|
|
1100
|
+
|
|
1101
|
+
function findWowSavedVariableFiles(wowRoots = []) {
|
|
1102
|
+
const files = new Set()
|
|
1103
|
+
|
|
1104
|
+
for (const configuredRoot of wowRoots) {
|
|
1105
|
+
const root = resolve(configuredRoot)
|
|
1106
|
+
if (/GitDoneProgress\.lua$/i.test(root) && existsSync(root)) {
|
|
1107
|
+
files.add(root)
|
|
1108
|
+
continue
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const normalized = root.replace(/[\\/]+$/, '')
|
|
1112
|
+
const accountRoots = new Set([
|
|
1113
|
+
join(normalized, 'WTF', 'Account'),
|
|
1114
|
+
join(normalized, '_retail_', 'WTF', 'Account'),
|
|
1115
|
+
])
|
|
1116
|
+
if (/[\\/]WTF$/i.test(normalized)) accountRoots.add(join(normalized, 'Account'))
|
|
1117
|
+
if (/[\\/]Account$/i.test(normalized)) accountRoots.add(normalized)
|
|
1118
|
+
|
|
1119
|
+
for (const accountRoot of accountRoots) {
|
|
1120
|
+
if (!existsSync(accountRoot)) continue
|
|
1121
|
+
let accounts = []
|
|
1122
|
+
try { accounts = readdirSync(accountRoot, { withFileTypes: true }) }
|
|
1123
|
+
catch { continue }
|
|
1124
|
+
for (const account of accounts) {
|
|
1125
|
+
if (!account.isDirectory()) continue
|
|
1126
|
+
const candidate = join(accountRoot, account.name, 'SavedVariables', 'GitDoneProgress.lua')
|
|
1127
|
+
if (existsSync(candidate)) files.add(candidate)
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return [...files].sort()
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function decodeWowSavedVariables(path) {
|
|
1136
|
+
const source = readFileSync(path, 'utf8')
|
|
1137
|
+
const match = source.match(/(?:\["payload"\]|payload)\s*=\s*"([A-Za-z0-9_-]+)"/)
|
|
1138
|
+
if (!match) throw new Error('GitDoneProgressDB.payload is missing')
|
|
1139
|
+
|
|
1140
|
+
const json = Buffer.from(match[1], 'base64url').toString('utf8')
|
|
1141
|
+
const snapshot = JSON.parse(json)
|
|
1142
|
+
if (snapshot?.schemaVersion !== 1) {
|
|
1143
|
+
throw new Error(`unsupported snapshot schema ${snapshot?.schemaVersion ?? '?'}`)
|
|
1144
|
+
}
|
|
1145
|
+
if (!snapshot?.character?.guid || !snapshot?.character?.name || !snapshot?.character?.realm) {
|
|
1146
|
+
throw new Error('snapshot character identity is incomplete')
|
|
1147
|
+
}
|
|
1148
|
+
return { snapshot, signature: createHash('sha256').update(match[1]).digest('hex') }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const wowSnapshotSigCache = new Map() // SavedVariables path -> last uploaded payload hash
|
|
1152
|
+
|
|
1153
|
+
async function pushWowProgress(cfg) {
|
|
1154
|
+
const files = findWowSavedVariableFiles(cfg.wowRoots)
|
|
1155
|
+
let sent = 0
|
|
1156
|
+
let unchanged = 0
|
|
1157
|
+
let failed = 0
|
|
1158
|
+
|
|
1159
|
+
for (const path of files) {
|
|
1160
|
+
try {
|
|
1161
|
+
const { snapshot, signature } = decodeWowSavedVariables(path)
|
|
1162
|
+
if (wowSnapshotSigCache.get(path) === signature) {
|
|
1163
|
+
unchanged++
|
|
1164
|
+
continue
|
|
1165
|
+
}
|
|
1166
|
+
const result = await api(cfg, '/api/v1/wow/progress', {
|
|
1167
|
+
machineId: cfg.machineId,
|
|
1168
|
+
snapshot,
|
|
1169
|
+
})
|
|
1170
|
+
wowSnapshotSigCache.set(path, signature)
|
|
1171
|
+
sent++
|
|
1172
|
+
log(`✓ WoW progress synced — ${snapshot.character.name}-${snapshot.character.realm}${result.unchanged ? ' (server already current)' : ''}`)
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
failed++
|
|
1175
|
+
log(`✗ WoW progress sync failed @ ${path}: ${err.message}`)
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return { found: files.length, sent, unchanged, failed }
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1000
1182
|
// ─── Server sync + commands ─────────────────────────────────────────────────────
|
|
1001
1183
|
|
|
1002
1184
|
// Keep the deadline below the server-side session stall window. Without a
|
|
@@ -2795,7 +2977,12 @@ async function sync(cfg, discovered) {
|
|
|
2795
2977
|
})
|
|
2796
2978
|
if (full) { syncRepoSig = sig; syncFullCountdown = SYNC_FULL_TICKS }
|
|
2797
2979
|
else syncFullCountdown--
|
|
2798
|
-
return
|
|
2980
|
+
return {
|
|
2981
|
+
tracked: data.tracked ?? [],
|
|
2982
|
+
latestVersion: data.latestVersion,
|
|
2983
|
+
downloadUrl: data.downloadUrl,
|
|
2984
|
+
downloadSha256: data.downloadSha256,
|
|
2985
|
+
}
|
|
2799
2986
|
}
|
|
2800
2987
|
|
|
2801
2988
|
// Per-repo cache of the last snapshot we actually SENT, so an idle repo whose
|
|
@@ -2965,18 +3152,27 @@ async function runLoop(cfg) {
|
|
|
2965
3152
|
backupConfig(cfg)
|
|
2966
3153
|
log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
|
|
2967
3154
|
log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
|
|
3155
|
+
log(` WoW: ${cfg.wowRoots?.join(', ') || '(disabled — add retail path with --wow)'}`)
|
|
2968
3156
|
|
|
2969
3157
|
async function tick() {
|
|
2970
3158
|
try {
|
|
2971
3159
|
const discovered = scanRepos(cfg.roots)
|
|
2972
|
-
const
|
|
3160
|
+
const state = await sync(cfg, discovered)
|
|
3161
|
+
// A successful update exits here; the supervisor starts the new stable
|
|
3162
|
+
// script before any more repo work is performed.
|
|
3163
|
+
await selfUpdate(cfg, state.latestVersion, state.downloadUrl, state.downloadSha256)
|
|
3164
|
+
const wow = await pushWowProgress(cfg)
|
|
3165
|
+
const tracked = state.tracked
|
|
2973
3166
|
let pushed = 0
|
|
2974
3167
|
let skipped = 0
|
|
2975
3168
|
for (const repo of tracked) {
|
|
2976
3169
|
try { (await pushSnapshot(cfg, repo)) ? pushed++ : skipped++ }
|
|
2977
3170
|
catch (err) { log(`✗ snapshot failed @ ${repo.path}: ${err.message}`) }
|
|
2978
3171
|
}
|
|
2979
|
-
|
|
3172
|
+
const wowStatus = cfg.wowRoots?.length
|
|
3173
|
+
? `, WoW: ${wow.sent} sent/${wow.unchanged} unchanged/${wow.failed} failed`
|
|
3174
|
+
: ''
|
|
3175
|
+
log(`✓ tick — discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}, unchanged: ${skipped}${wowStatus}`)
|
|
2980
3176
|
} catch (err) {
|
|
2981
3177
|
log(`✗ sync error: ${err.message}`)
|
|
2982
3178
|
}
|
|
@@ -2993,6 +3189,12 @@ async function runLoop(cfg) {
|
|
|
2993
3189
|
async function main() {
|
|
2994
3190
|
const args = parseArgs()
|
|
2995
3191
|
|
|
3192
|
+
if (args.testWow) {
|
|
3193
|
+
const { snapshot, signature } = decodeWowSavedVariables(args.testWow)
|
|
3194
|
+
console.log(JSON.stringify({ signature, snapshot }, null, 2))
|
|
3195
|
+
process.exit(0)
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2996
3198
|
if (args.doctor) {
|
|
2997
3199
|
runDoctor()
|
|
2998
3200
|
process.exit(0)
|
|
@@ -3004,7 +3206,7 @@ async function main() {
|
|
|
3004
3206
|
}
|
|
3005
3207
|
|
|
3006
3208
|
// Setup / install path: merge CLI args into config, optionally (re)install.
|
|
3007
|
-
if (args.install || args.key || args.roots.length || args.url || args.interval) {
|
|
3209
|
+
if (args.install || args.key || args.roots.length || args.wowRoots.length || args.url || args.interval) {
|
|
3008
3210
|
const cfg = buildConfig(args)
|
|
3009
3211
|
if (!cfg.key) {
|
|
3010
3212
|
console.error('Error: --key is required on first setup (e.g. --key=gdo_xxx)')
|
|
@@ -3020,6 +3222,8 @@ async function main() {
|
|
|
3020
3222
|
console.log(` Лог : ${LOG_PATH}`)
|
|
3021
3223
|
console.log(` Roots :`)
|
|
3022
3224
|
for (const r of cfg.roots) console.log(` - ${r}`)
|
|
3225
|
+
console.log(` WoW :`)
|
|
3226
|
+
for (const r of cfg.wowRoots ?? []) console.log(` - ${r}`)
|
|
3023
3227
|
console.log()
|
|
3024
3228
|
|
|
3025
3229
|
// Launch silently in the background right now.
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitdone-agent",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Local
|
|
3
|
+
"version": "0.8.8",
|
|
4
|
+
"description": "Local gitDone companion for repository and optional World of Warcraft progress sync",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js"
|
|
8
|
+
],
|
|
6
9
|
"bin": {
|
|
7
10
|
"gitdone-agent": "index.js"
|
|
8
11
|
},
|