gitdone-agent 0.8.7 → 0.8.9

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 +138 -5
  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.9'
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
 
@@ -752,11 +775,12 @@ function gitTry(cmd, cwd) {
752
775
  // commit path swallowed that via git() + `|| 'committed'` and reported a fake
753
776
  // success while nothing was committed (gd-445). gitDiffUntracked() already uses
754
777
  // execFileSync for the same reason. Local calls get 2 min; pass net for 5.
755
- function gitArgs(args, cwd, { net = false } = {}) {
778
+ function gitArgs(args, cwd, { net = false, input } = {}) {
756
779
  try {
757
780
  const out = execFileSync('git', args, {
758
781
  cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
759
782
  maxBuffer: GIT_MAX_BUFFER, timeout: net ? GIT_NET_TIMEOUT_MS : GIT_TIMEOUT_MS, env: GIT_ENV,
783
+ ...(input === undefined ? {} : { input }),
760
784
  }).trim()
761
785
  return { ok: true, out }
762
786
  } catch (err) {
@@ -1068,6 +1092,94 @@ function scanRepos(roots, maxDepth = 5) {
1068
1092
  return found
1069
1093
  }
1070
1094
 
1095
+ // ─── World of Warcraft progress bridge ──────────────────────────────────────
1096
+ // WoW addons cannot open network connections. GitDoneProgress therefore writes
1097
+ // a Base64URL-encoded JSON snapshot to its normal SavedVariables file; this
1098
+ // opt-in companion path reads only that explicit file and uploads it with the
1099
+ // same API credential the agent already uses. WoW flushes SavedVariables on
1100
+ // /reload or logout, so there is no process-memory inspection involved.
1101
+
1102
+ function findWowSavedVariableFiles(wowRoots = []) {
1103
+ const files = new Set()
1104
+
1105
+ for (const configuredRoot of wowRoots) {
1106
+ const root = resolve(configuredRoot)
1107
+ if (/GitDoneProgress\.lua$/i.test(root) && existsSync(root)) {
1108
+ files.add(root)
1109
+ continue
1110
+ }
1111
+
1112
+ const normalized = root.replace(/[\\/]+$/, '')
1113
+ const accountRoots = new Set([
1114
+ join(normalized, 'WTF', 'Account'),
1115
+ join(normalized, '_retail_', 'WTF', 'Account'),
1116
+ ])
1117
+ if (/[\\/]WTF$/i.test(normalized)) accountRoots.add(join(normalized, 'Account'))
1118
+ if (/[\\/]Account$/i.test(normalized)) accountRoots.add(normalized)
1119
+
1120
+ for (const accountRoot of accountRoots) {
1121
+ if (!existsSync(accountRoot)) continue
1122
+ let accounts = []
1123
+ try { accounts = readdirSync(accountRoot, { withFileTypes: true }) }
1124
+ catch { continue }
1125
+ for (const account of accounts) {
1126
+ if (!account.isDirectory()) continue
1127
+ const candidate = join(accountRoot, account.name, 'SavedVariables', 'GitDoneProgress.lua')
1128
+ if (existsSync(candidate)) files.add(candidate)
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ return [...files].sort()
1134
+ }
1135
+
1136
+ function decodeWowSavedVariables(path) {
1137
+ const source = readFileSync(path, 'utf8')
1138
+ const match = source.match(/(?:\["payload"\]|payload)\s*=\s*"([A-Za-z0-9_-]+)"/)
1139
+ if (!match) throw new Error('GitDoneProgressDB.payload is missing')
1140
+
1141
+ const json = Buffer.from(match[1], 'base64url').toString('utf8')
1142
+ const snapshot = JSON.parse(json)
1143
+ if (snapshot?.schemaVersion !== 1) {
1144
+ throw new Error(`unsupported snapshot schema ${snapshot?.schemaVersion ?? '?'}`)
1145
+ }
1146
+ if (!snapshot?.character?.guid || !snapshot?.character?.name || !snapshot?.character?.realm) {
1147
+ throw new Error('snapshot character identity is incomplete')
1148
+ }
1149
+ return { snapshot, signature: createHash('sha256').update(match[1]).digest('hex') }
1150
+ }
1151
+
1152
+ const wowSnapshotSigCache = new Map() // SavedVariables path -> last uploaded payload hash
1153
+
1154
+ async function pushWowProgress(cfg) {
1155
+ const files = findWowSavedVariableFiles(cfg.wowRoots)
1156
+ let sent = 0
1157
+ let unchanged = 0
1158
+ let failed = 0
1159
+
1160
+ for (const path of files) {
1161
+ try {
1162
+ const { snapshot, signature } = decodeWowSavedVariables(path)
1163
+ if (wowSnapshotSigCache.get(path) === signature) {
1164
+ unchanged++
1165
+ continue
1166
+ }
1167
+ const result = await api(cfg, '/api/v1/wow/progress', {
1168
+ machineId: cfg.machineId,
1169
+ snapshot,
1170
+ })
1171
+ wowSnapshotSigCache.set(path, signature)
1172
+ sent++
1173
+ log(`✓ WoW progress synced — ${snapshot.character.name}-${snapshot.character.realm}${result.unchanged ? ' (server already current)' : ''}`)
1174
+ } catch (err) {
1175
+ failed++
1176
+ log(`✗ WoW progress sync failed @ ${path}: ${err.message}`)
1177
+ }
1178
+ }
1179
+
1180
+ return { found: files.length, sent, unchanged, failed }
1181
+ }
1182
+
1071
1183
  // ─── Server sync + commands ─────────────────────────────────────────────────────
1072
1184
 
1073
1185
  // Keep the deadline below the server-side session stall window. Without a
@@ -2554,8 +2666,16 @@ async function executeCommand(cfg, cmd, repoPath) {
2554
2666
  const files = Array.isArray(cmd.payload?.files)
2555
2667
  ? cmd.payload.files.filter((f) => typeof f === 'string' && f.trim())
2556
2668
  : []
2669
+ // Do not put every selected path in argv: a large change set can exceed
2670
+ // Windows' ~32K CreateProcess command-line limit before git even starts
2671
+ // (`spawnSync git ENAMETOOLONG`). Git's NUL-delimited stdin pathspec mode
2672
+ // has no such argv limit and also preserves spaces/newlines verbatim.
2557
2673
  const add = files.length
2558
- ? gitArgs(['add', '--', ...files], repoPath)
2674
+ ? gitArgs(
2675
+ ['--literal-pathspecs', 'add', '--pathspec-from-file=-', '--pathspec-file-nul'],
2676
+ repoPath,
2677
+ { input: Buffer.from(`${files.join('\0')}\0`, 'utf8') },
2678
+ )
2559
2679
  : gitArgs(['add', '-A'], repoPath)
2560
2680
  if (!add.ok) throw new Error(`git add се провали: ${add.out}`)
2561
2681
  const commit = gitArgs(['commit', '-m', msg], repoPath)
@@ -3041,6 +3161,7 @@ async function runLoop(cfg) {
3041
3161
  backupConfig(cfg)
3042
3162
  log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
3043
3163
  log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
3164
+ log(` WoW: ${cfg.wowRoots?.join(', ') || '(disabled — add retail path with --wow)'}`)
3044
3165
 
3045
3166
  async function tick() {
3046
3167
  try {
@@ -3049,6 +3170,7 @@ async function runLoop(cfg) {
3049
3170
  // A successful update exits here; the supervisor starts the new stable
3050
3171
  // script before any more repo work is performed.
3051
3172
  await selfUpdate(cfg, state.latestVersion, state.downloadUrl, state.downloadSha256)
3173
+ const wow = await pushWowProgress(cfg)
3052
3174
  const tracked = state.tracked
3053
3175
  let pushed = 0
3054
3176
  let skipped = 0
@@ -3056,7 +3178,10 @@ async function runLoop(cfg) {
3056
3178
  try { (await pushSnapshot(cfg, repo)) ? pushed++ : skipped++ }
3057
3179
  catch (err) { log(`✗ snapshot failed @ ${repo.path}: ${err.message}`) }
3058
3180
  }
3059
- log(`✓ tick discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}, unchanged: ${skipped}`)
3181
+ const wowStatus = cfg.wowRoots?.length
3182
+ ? `, WoW: ${wow.sent} sent/${wow.unchanged} unchanged/${wow.failed} failed`
3183
+ : ''
3184
+ log(`✓ tick — discovered: ${discovered.length}, tracked: ${tracked.length}, pushed: ${pushed}, unchanged: ${skipped}${wowStatus}`)
3060
3185
  } catch (err) {
3061
3186
  log(`✗ sync error: ${err.message}`)
3062
3187
  }
@@ -3073,6 +3198,12 @@ async function runLoop(cfg) {
3073
3198
  async function main() {
3074
3199
  const args = parseArgs()
3075
3200
 
3201
+ if (args.testWow) {
3202
+ const { snapshot, signature } = decodeWowSavedVariables(args.testWow)
3203
+ console.log(JSON.stringify({ signature, snapshot }, null, 2))
3204
+ process.exit(0)
3205
+ }
3206
+
3076
3207
  if (args.doctor) {
3077
3208
  runDoctor()
3078
3209
  process.exit(0)
@@ -3084,7 +3215,7 @@ async function main() {
3084
3215
  }
3085
3216
 
3086
3217
  // Setup / install path: merge CLI args into config, optionally (re)install.
3087
- if (args.install || args.key || args.roots.length || args.url || args.interval) {
3218
+ if (args.install || args.key || args.roots.length || args.wowRoots.length || args.url || args.interval) {
3088
3219
  const cfg = buildConfig(args)
3089
3220
  if (!cfg.key) {
3090
3221
  console.error('Error: --key is required on first setup (e.g. --key=gdo_xxx)')
@@ -3100,6 +3231,8 @@ async function main() {
3100
3231
  console.log(` Лог : ${LOG_PATH}`)
3101
3232
  console.log(` Roots :`)
3102
3233
  for (const r of cfg.roots) console.log(` - ${r}`)
3234
+ console.log(` WoW :`)
3235
+ for (const r of cfg.wowRoots ?? []) console.log(` - ${r}`)
3103
3236
  console.log()
3104
3237
 
3105
3238
  // 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.9",
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
  },