gitdone-agent 0.6.12 → 0.6.14
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 +124 -18
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { execSync, execFileSync, spawn } from 'node:child_process'
|
|
13
13
|
import {
|
|
14
|
-
existsSync, writeFileSync, readFileSync, unlinkSync,
|
|
14
|
+
existsSync, writeFileSync, readFileSync, unlinkSync, renameSync,
|
|
15
15
|
mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
|
|
16
16
|
} from 'node:fs'
|
|
17
17
|
import { resolve, join } from 'node:path'
|
|
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
27
27
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
28
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
29
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.6.
|
|
30
|
+
const AGENT_VERSION = '0.6.14'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -38,19 +38,60 @@ function ensureAgentDir() {
|
|
|
38
38
|
if (!existsSync(AGENT_DIR)) mkdirSync(AGENT_DIR, { recursive: true })
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Keep agent.log from growing forever: past 5 MB it rolls to agent.log.old
|
|
42
|
+
// (replacing the previous roll). Checked every 500 lines, not per line.
|
|
43
|
+
const LOG_MAX_BYTES = 5 * 1024 * 1024
|
|
44
|
+
let logLinesSinceCheck = 0
|
|
45
|
+
function rotateLogIfNeeded() {
|
|
46
|
+
try {
|
|
47
|
+
if (statSync(LOG_PATH).size > LOG_MAX_BYTES) renameSync(LOG_PATH, LOG_PATH + '.old')
|
|
48
|
+
} catch { /* no log yet / roll failed — keep appending */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
function log(msg) {
|
|
42
52
|
const line = `[${new Date().toISOString()}] ${msg}`
|
|
43
53
|
console.log(line)
|
|
44
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
ensureAgentDir()
|
|
56
|
+
if (logLinesSinceCheck++ % 500 === 0) rotateLogIfNeeded()
|
|
57
|
+
appendFileSync(LOG_PATH, line + '\n')
|
|
58
|
+
} catch { /* best-effort */ }
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
// ─── Crash safety ─────────────────────────────────────────────────────────────
|
|
62
|
+
// Node ≥15 KILLS the process on any unhandled promise rejection, and an uncaught
|
|
63
|
+
// exception always did — and the autostart used to be one-shot, so a single
|
|
64
|
+
// stray error left the machine offline until the next Windows login (gd-403).
|
|
65
|
+
// Log and keep running instead; the loops here are stateless enough that
|
|
66
|
+
// surviving is strictly better than dying. If errors come in a storm (>20/min —
|
|
67
|
+
// something is genuinely broken), exit(1) and let the supervisor loop restart
|
|
68
|
+
// us with a clean slate.
|
|
69
|
+
let crashBurst = { count: 0, since: Date.now() }
|
|
70
|
+
function survive(kind, err) {
|
|
71
|
+
const now = Date.now()
|
|
72
|
+
if (now - crashBurst.since > 60_000) crashBurst = { count: 0, since: now }
|
|
73
|
+
crashBurst.count++
|
|
74
|
+
log(`✗ ${kind}: ${err?.stack || err?.message || err}`)
|
|
75
|
+
if (crashBurst.count > 20) {
|
|
76
|
+
log('✗ over 20 crashes in 60s — exiting so the supervisor restarts us clean')
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
process.on('uncaughtException', (err) => survive('uncaught exception', err))
|
|
81
|
+
process.on('unhandledRejection', (err) => survive('unhandled rejection', err))
|
|
82
|
+
|
|
47
83
|
function readConfig() {
|
|
48
84
|
try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) } catch { return null }
|
|
49
85
|
}
|
|
50
86
|
|
|
51
87
|
function writeConfig(cfg) {
|
|
52
88
|
ensureAgentDir()
|
|
53
|
-
|
|
89
|
+
// Write-then-rename so a crash / reboot mid-write can never leave a truncated
|
|
90
|
+
// config.json — a corrupt config makes every subsequent autostart exit
|
|
91
|
+
// immediately, which reads as "агентът умря и рестартът не помага" (gd-403).
|
|
92
|
+
const tmp = CONFIG_PATH + '.tmp'
|
|
93
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2), 'utf8')
|
|
94
|
+
renameSync(tmp, CONFIG_PATH)
|
|
54
95
|
}
|
|
55
96
|
|
|
56
97
|
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
@@ -150,8 +191,14 @@ function findClaude() {
|
|
|
150
191
|
// (~/.gitdone-agent), excluding ourselves — the npx installer runs from the
|
|
151
192
|
// npx cache, so it won't match. Best-effort; failures are non-fatal.
|
|
152
193
|
function stopRunningAgents() {
|
|
194
|
+
// Kill the supervisor loops (cmd.exe running run-agent.cmd) FIRST, then the
|
|
195
|
+
// agents — the other order lets a still-alive supervisor immediately respawn
|
|
196
|
+
// the agent we just killed, leaving two agents reporting after an update.
|
|
153
197
|
const ps = [
|
|
154
198
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
199
|
+
"Get-CimInstance Win32_Process -Filter \"Name='cmd.exe'\" |",
|
|
200
|
+
" Where-Object { $_.CommandLine -like '*run-agent.cmd*' } |",
|
|
201
|
+
' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
155
202
|
"Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" |",
|
|
156
203
|
` Where-Object { $_.CommandLine -like '*.gitdone-agent*' -and $_.ProcessId -ne ${process.pid} } |`,
|
|
157
204
|
' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
@@ -182,11 +229,32 @@ function installStartup() {
|
|
|
182
229
|
}
|
|
183
230
|
const agentPath = existsSync(STABLE_AGENT) ? STABLE_AGENT : resolve(process.argv[1])
|
|
184
231
|
|
|
185
|
-
// The
|
|
186
|
-
//
|
|
232
|
+
// The autostart used to launch the agent DIRECTLY — one crash and the machine
|
|
233
|
+
// stayed offline until the next Windows login (gd-403). Now the VBS launches a
|
|
234
|
+
// tiny supervisor loop instead: run the agent, and if it ever exits, note it
|
|
235
|
+
// in agent-crash.log and start it again after 10s. stderr is captured too, so
|
|
236
|
+
// node-level failures (corrupt file, bad path) finally leave a trace. If the
|
|
237
|
+
// node path recorded at install time vanished (node upgraded/moved), fall
|
|
238
|
+
// back to whatever `node` is on PATH.
|
|
239
|
+
const crashLog = join(AGENT_DIR, 'agent-crash.log')
|
|
240
|
+
const cmd = [
|
|
241
|
+
'@echo off',
|
|
242
|
+
'rem gitdone-agent supervisor — restarts the agent if it ever dies (gd-403)',
|
|
243
|
+
`set "NODE=${nodePath}"`,
|
|
244
|
+
'if not exist "%NODE%" set "NODE=node"',
|
|
245
|
+
':loop',
|
|
246
|
+
`"%NODE%" "${agentPath}" 2>> "${crashLog}"`,
|
|
247
|
+
`echo [%date% %time%] agent exited (code %errorlevel%) - restart in 10s >> "${crashLog}"`,
|
|
248
|
+
'ping -n 11 127.0.0.1 >nul',
|
|
249
|
+
'goto loop',
|
|
250
|
+
].join('\r\n')
|
|
251
|
+
const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
|
|
252
|
+
writeFileSync(cmdPath, cmd, 'utf8')
|
|
253
|
+
|
|
254
|
+
// VBScript runs the supervisor with window style 0 so nothing pops up on login.
|
|
187
255
|
const vbs = [
|
|
188
256
|
'Set WshShell = CreateObject("WScript.Shell")',
|
|
189
|
-
`WshShell.Run """${
|
|
257
|
+
`WshShell.Run """${cmdPath}""", 0, False`,
|
|
190
258
|
].join('\r\n')
|
|
191
259
|
|
|
192
260
|
writeFileSync(getVbsPath(), vbs, 'utf8')
|
|
@@ -200,6 +268,11 @@ function uninstallStartup() {
|
|
|
200
268
|
} else {
|
|
201
269
|
console.log(`Не е намерен автостарт (${vbsPath})`)
|
|
202
270
|
}
|
|
271
|
+
const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
|
|
272
|
+
if (existsSync(cmdPath)) unlinkSync(cmdPath)
|
|
273
|
+
// Also stop the live supervisor + agent — otherwise they keep running (and
|
|
274
|
+
// the supervisor keeps resurrecting the agent) until the next reboot.
|
|
275
|
+
stopRunningAgents()
|
|
203
276
|
}
|
|
204
277
|
|
|
205
278
|
// ─── Doctor ────────────────────────────────────────────────────────────────────
|
|
@@ -211,8 +284,10 @@ function runDoctor() {
|
|
|
211
284
|
console.log(` agent dir : ${AGENT_DIR} ${existsSync(AGENT_DIR) ? '(ok)' : '(MISSING)'}`)
|
|
212
285
|
console.log(` stable agent : ${STABLE_AGENT} ${existsSync(STABLE_AGENT) ? '(ok)' : '(not installed)'}`)
|
|
213
286
|
console.log(` autostart vbs : ${getVbsPath()} ${existsSync(getVbsPath()) ? '(ok)' : '(not installed)'}`)
|
|
287
|
+
console.log(` supervisor : ${join(AGENT_DIR, 'run-agent.cmd')} ${existsSync(join(AGENT_DIR, 'run-agent.cmd')) ? '(ok)' : '(not installed — пусни --install за авторестарт при crash)'}`)
|
|
214
288
|
console.log(` config : ${CONFIG_PATH} ${existsSync(CONFIG_PATH) ? '(ok)' : '(MISSING)'}`)
|
|
215
289
|
console.log(` log file : ${LOG_PATH} ${existsSync(LOG_PATH) ? '(ok)' : '(none yet)'}`)
|
|
290
|
+
console.log(` crash log : ${join(AGENT_DIR, 'agent-crash.log')} ${existsSync(join(AGENT_DIR, 'agent-crash.log')) ? '(има записи — виж го при проблеми)' : '(празен — няма крашове)'}`)
|
|
216
291
|
const claude = findClaude()
|
|
217
292
|
console.log(` claude cli : ${claude.found ? claude.path : 'NOT FOUND — инсталирай от claude.ai/code и рестартирай агента'}`)
|
|
218
293
|
if (cfg) {
|
|
@@ -242,9 +317,31 @@ const GIT_MAX_BUFFER = 64 * 1024 * 1024
|
|
|
242
317
|
// bloat the snapshot payload.
|
|
243
318
|
const UNTRACKED_DIFF_MAX_BYTES = 1024 * 1024
|
|
244
319
|
|
|
320
|
+
// Synthesising a per-file diff spawns a synchronous `git diff --no-index` per
|
|
321
|
+
// untracked file. Individually cheap, but a repo with a huge untracked/scratch
|
|
322
|
+
// tree (e.g. extracted game assets — thousands of files) would spawn thousands
|
|
323
|
+
// of blocking git processes and freeze the single-threaded agent for MINUTES on
|
|
324
|
+
// every snapshot, starving the 30s sync heartbeat so the machine flips offline.
|
|
325
|
+
// Bound the synthesis two ways — a file-count cap and a wall-clock budget — so
|
|
326
|
+
// no single repo can ever monopolise the event loop. Anything past either keeps
|
|
327
|
+
// the "no diff" placeholder, identical to the size-cap path above.
|
|
328
|
+
const UNTRACKED_DIFF_MAX_FILES = 200
|
|
329
|
+
const UNTRACKED_DIFF_TIME_BUDGET_MS = 3000
|
|
330
|
+
|
|
331
|
+
// Every git call here is execSync — synchronous, on the ONLY thread. A git that
|
|
332
|
+
// stops to ask for credentials (a repo whose remote lost its token → Git
|
|
333
|
+
// Credential Manager pops an invisible dialog) blocks the whole agent forever:
|
|
334
|
+
// no ticks, no heartbeat, machine flips offline until someone restarts it
|
|
335
|
+
// (gd-403). Two guards: never allow interactive prompts, and hard-timeout every
|
|
336
|
+
// call so the worst case is one failed command, not a dead agent. Local
|
|
337
|
+
// commands get 2 min; push/pull (network) get 5.
|
|
338
|
+
const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' }
|
|
339
|
+
const GIT_TIMEOUT_MS = 2 * 60 * 1000
|
|
340
|
+
const GIT_NET_TIMEOUT_MS = 5 * 60 * 1000
|
|
341
|
+
|
|
245
342
|
function git(cmd, cwd) {
|
|
246
343
|
try {
|
|
247
|
-
return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER }).trim()
|
|
344
|
+
return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV }).trim()
|
|
248
345
|
} catch {
|
|
249
346
|
return ''
|
|
250
347
|
}
|
|
@@ -256,7 +353,7 @@ function git(cmd, cwd) {
|
|
|
256
353
|
// (gd-276). Callers decode per-file via decodeDiffText().
|
|
257
354
|
function gitRaw(cmd, cwd) {
|
|
258
355
|
try {
|
|
259
|
-
return execSync(cmd, { cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER })
|
|
356
|
+
return execSync(cmd, { cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV })
|
|
260
357
|
} catch {
|
|
261
358
|
return Buffer.alloc(0)
|
|
262
359
|
}
|
|
@@ -271,7 +368,7 @@ function gitRaw(cmd, cwd) {
|
|
|
271
368
|
function gitDiffUntracked(file, cwd) {
|
|
272
369
|
try {
|
|
273
370
|
return execFileSync('git', ['diff', '--no-index', '--', '/dev/null', file], {
|
|
274
|
-
cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER,
|
|
371
|
+
cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV,
|
|
275
372
|
})
|
|
276
373
|
} catch (err) {
|
|
277
374
|
return err && err.stdout && err.stdout.length ? err.stdout : Buffer.alloc(0)
|
|
@@ -282,10 +379,11 @@ function gitDiffUntracked(file, cwd) {
|
|
|
282
379
|
// used for push/pull where we need to detect auth rejection.
|
|
283
380
|
function gitTry(cmd, cwd) {
|
|
284
381
|
try {
|
|
285
|
-
const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
382
|
+
const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: GIT_NET_TIMEOUT_MS, env: GIT_ENV }).trim()
|
|
286
383
|
return { ok: true, out }
|
|
287
384
|
} catch (err) {
|
|
288
|
-
const
|
|
385
|
+
const timedOut = err.signal === 'SIGTERM' && err.code == null
|
|
386
|
+
const out = (err.stderr || err.stdout || (timedOut ? `git не отговори ${GIT_NET_TIMEOUT_MS / 60000} мин и беше прекратен` : err.message) || '').toString().trim()
|
|
289
387
|
return { ok: false, out }
|
|
290
388
|
}
|
|
291
389
|
}
|
|
@@ -520,12 +618,20 @@ function getSnapshot(repoPath) {
|
|
|
520
618
|
// each from /dev/null so the console shows the whole new file — like GitHub
|
|
521
619
|
// Desktop (gd-370). Skip anything over the size cap so a stray big/binary blob
|
|
522
620
|
// can't bloat the snapshot; those keep the "no diff" placeholder.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
621
|
+
const untracked = Object.keys(statuses).filter((f) => statuses[f] === '??' && !diffs[f])
|
|
622
|
+
const diffDeadline = Date.now() + UNTRACKED_DIFF_TIME_BUDGET_MS
|
|
623
|
+
let synthesised = 0
|
|
624
|
+
for (const file of untracked) {
|
|
625
|
+
// Stop before either bound so a scratch tree can't starve the heartbeat; the
|
|
626
|
+
// rest keep the "no diff" placeholder. Log once so it's visible why.
|
|
627
|
+
if (synthesised >= UNTRACKED_DIFF_MAX_FILES || Date.now() > diffDeadline) {
|
|
628
|
+
log(`⚠ untracked diff cap reached @ ${repoPath} (${synthesised}/${untracked.length} synthesised) — rest shown without preview`)
|
|
629
|
+
break
|
|
630
|
+
}
|
|
631
|
+
let st
|
|
632
|
+
try { st = statSync(join(repoPath, file)) } catch { continue }
|
|
633
|
+
if (!st.isFile() || st.size > UNTRACKED_DIFF_MAX_BYTES) continue
|
|
634
|
+
synthesised++
|
|
529
635
|
const parsed = parseDiffByFile(gitDiffUntracked(file, repoPath))
|
|
530
636
|
const val = parsed[file] ?? Object.values(parsed)[0]
|
|
531
637
|
if (val) diffs[file] = val
|