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.
- package/index.js +138 -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.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(
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Local
|
|
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
|
},
|