gitdone-agent 0.8.7 → 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.
Files changed (2) hide show
  1. package/index.js +127 -3
  2. 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.7'
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
 
@@ -657,6 +666,20 @@ function runDoctor() {
657
666
  console.log(` found repos : ${found.length}`)
658
667
  for (const r of found) console.log(` - ${r.name} (${r.path})`)
659
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
+ }
660
683
  }
661
684
  }
662
685
 
@@ -1068,6 +1091,94 @@ function scanRepos(roots, maxDepth = 5) {
1068
1091
  return found
1069
1092
  }
1070
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
+
1071
1182
  // ─── Server sync + commands ─────────────────────────────────────────────────────
1072
1183
 
1073
1184
  // Keep the deadline below the server-side session stall window. Without a
@@ -3041,6 +3152,7 @@ async function runLoop(cfg) {
3041
3152
  backupConfig(cfg)
3042
3153
  log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
3043
3154
  log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
3155
+ log(` WoW: ${cfg.wowRoots?.join(', ') || '(disabled — add retail path with --wow)'}`)
3044
3156
 
3045
3157
  async function tick() {
3046
3158
  try {
@@ -3049,6 +3161,7 @@ async function runLoop(cfg) {
3049
3161
  // A successful update exits here; the supervisor starts the new stable
3050
3162
  // script before any more repo work is performed.
3051
3163
  await selfUpdate(cfg, state.latestVersion, state.downloadUrl, state.downloadSha256)
3164
+ const wow = await pushWowProgress(cfg)
3052
3165
  const tracked = state.tracked
3053
3166
  let pushed = 0
3054
3167
  let skipped = 0
@@ -3056,7 +3169,10 @@ async function runLoop(cfg) {
3056
3169
  try { (await pushSnapshot(cfg, repo)) ? pushed++ : skipped++ }
3057
3170
  catch (err) { log(`✗ snapshot failed @ ${repo.path}: ${err.message}`) }
3058
3171
  }
3059
- log(`✓ tick discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}, unchanged: ${skipped}`)
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}`)
3060
3176
  } catch (err) {
3061
3177
  log(`✗ sync error: ${err.message}`)
3062
3178
  }
@@ -3073,6 +3189,12 @@ async function runLoop(cfg) {
3073
3189
  async function main() {
3074
3190
  const args = parseArgs()
3075
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
+
3076
3198
  if (args.doctor) {
3077
3199
  runDoctor()
3078
3200
  process.exit(0)
@@ -3084,7 +3206,7 @@ async function main() {
3084
3206
  }
3085
3207
 
3086
3208
  // Setup / install path: merge CLI args into config, optionally (re)install.
3087
- 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) {
3088
3210
  const cfg = buildConfig(args)
3089
3211
  if (!cfg.key) {
3090
3212
  console.error('Error: --key is required on first setup (e.g. --key=gdo_xxx)')
@@ -3100,6 +3222,8 @@ async function main() {
3100
3222
  console.log(` Лог : ${LOG_PATH}`)
3101
3223
  console.log(` Roots :`)
3102
3224
  for (const r of cfg.roots) console.log(` - ${r}`)
3225
+ console.log(` WoW :`)
3226
+ for (const r of cfg.wowRoots ?? []) console.log(` - ${r}`)
3103
3227
  console.log()
3104
3228
 
3105
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.7",
4
- "description": "Local git agent for gitdone watches a local repo and sends snapshots to gitdone.eu",
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
  },