gitdone-agent 0.6.13 → 0.6.15

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 +112 -12
  2. 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.13'
30
+ const AGENT_VERSION = '0.6.15'
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 { ensureAgentDir(); appendFileSync(LOG_PATH, line + '\n') } catch { /* best-effort */ }
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
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8')
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,45 @@ function installStartup() {
182
229
  }
183
230
  const agentPath = existsSync(STABLE_AGENT) ? STABLE_AGENT : resolve(process.argv[1])
184
231
 
185
- // The running agent reads everything from config.jsonno args needed.
186
- // VBScript runs it silently so no console window pops up on login.
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
+ //
240
+ // Restart forever on normal crashes, but STOP on process-START failures
241
+ // (0xc0000142 STATUS_DLL_INIT_FAILED, 0xc0000135 missing DLL): those mean the
242
+ // login session itself is broken (e.g. after a hardware crash), every attempt
243
+ // pops a blocking "Application Error" dialog, and a retry can never succeed —
244
+ // the loop turned into an endless dialog storm (gd-405). Same for the
245
+ // ping-as-sleep: if even ping can't start, bail instead of spinning with no
246
+ // delay at all. A Windows re-login restarts the supervisor cleanly.
247
+ const crashLog = join(AGENT_DIR, 'agent-crash.log')
248
+ const cmd = [
249
+ '@echo off',
250
+ 'rem gitdone-agent supervisor — restarts the agent if it ever dies (gd-403)',
251
+ `set "NODE=${nodePath}"`,
252
+ 'if not exist "%NODE%" set "NODE=node"',
253
+ ':loop',
254
+ `"%NODE%" "${agentPath}" 2>> "${crashLog}"`,
255
+ 'set CODE=%errorlevel%',
256
+ `echo [%date% %time%] agent exited (code %CODE%) - restart in 10s >> "${crashLog}"`,
257
+ 'if "%CODE%"=="-1073741502" goto dead',
258
+ 'if "%CODE%"=="-1073741515" goto dead',
259
+ 'ping -n 11 127.0.0.1 >nul 2>nul || goto dead',
260
+ 'goto loop',
261
+ ':dead',
262
+ `echo [%date% %time%] node cannot start (code %CODE%) - supervisor giving up until next login >> "${crashLog}"`,
263
+ ].join('\r\n')
264
+ const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
265
+ writeFileSync(cmdPath, cmd, 'utf8')
266
+
267
+ // VBScript runs the supervisor with window style 0 so nothing pops up on login.
187
268
  const vbs = [
188
269
  'Set WshShell = CreateObject("WScript.Shell")',
189
- `WshShell.Run """${nodePath}"" ""${agentPath}""", 0, False`,
270
+ `WshShell.Run """${cmdPath}""", 0, False`,
190
271
  ].join('\r\n')
191
272
 
192
273
  writeFileSync(getVbsPath(), vbs, 'utf8')
@@ -200,6 +281,11 @@ function uninstallStartup() {
200
281
  } else {
201
282
  console.log(`Не е намерен автостарт (${vbsPath})`)
202
283
  }
284
+ const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
285
+ if (existsSync(cmdPath)) unlinkSync(cmdPath)
286
+ // Also stop the live supervisor + agent — otherwise they keep running (and
287
+ // the supervisor keeps resurrecting the agent) until the next reboot.
288
+ stopRunningAgents()
203
289
  }
204
290
 
205
291
  // ─── Doctor ────────────────────────────────────────────────────────────────────
@@ -211,8 +297,10 @@ function runDoctor() {
211
297
  console.log(` agent dir : ${AGENT_DIR} ${existsSync(AGENT_DIR) ? '(ok)' : '(MISSING)'}`)
212
298
  console.log(` stable agent : ${STABLE_AGENT} ${existsSync(STABLE_AGENT) ? '(ok)' : '(not installed)'}`)
213
299
  console.log(` autostart vbs : ${getVbsPath()} ${existsSync(getVbsPath()) ? '(ok)' : '(not installed)'}`)
300
+ console.log(` supervisor : ${join(AGENT_DIR, 'run-agent.cmd')} ${existsSync(join(AGENT_DIR, 'run-agent.cmd')) ? '(ok)' : '(not installed — пусни --install за авторестарт при crash)'}`)
214
301
  console.log(` config : ${CONFIG_PATH} ${existsSync(CONFIG_PATH) ? '(ok)' : '(MISSING)'}`)
215
302
  console.log(` log file : ${LOG_PATH} ${existsSync(LOG_PATH) ? '(ok)' : '(none yet)'}`)
303
+ console.log(` crash log : ${join(AGENT_DIR, 'agent-crash.log')} ${existsSync(join(AGENT_DIR, 'agent-crash.log')) ? '(има записи — виж го при проблеми)' : '(празен — няма крашове)'}`)
216
304
  const claude = findClaude()
217
305
  console.log(` claude cli : ${claude.found ? claude.path : 'NOT FOUND — инсталирай от claude.ai/code и рестартирай агента'}`)
218
306
  if (cfg) {
@@ -253,9 +341,20 @@ const UNTRACKED_DIFF_MAX_BYTES = 1024 * 1024
253
341
  const UNTRACKED_DIFF_MAX_FILES = 200
254
342
  const UNTRACKED_DIFF_TIME_BUDGET_MS = 3000
255
343
 
344
+ // Every git call here is execSync — synchronous, on the ONLY thread. A git that
345
+ // stops to ask for credentials (a repo whose remote lost its token → Git
346
+ // Credential Manager pops an invisible dialog) blocks the whole agent forever:
347
+ // no ticks, no heartbeat, machine flips offline until someone restarts it
348
+ // (gd-403). Two guards: never allow interactive prompts, and hard-timeout every
349
+ // call so the worst case is one failed command, not a dead agent. Local
350
+ // commands get 2 min; push/pull (network) get 5.
351
+ const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' }
352
+ const GIT_TIMEOUT_MS = 2 * 60 * 1000
353
+ const GIT_NET_TIMEOUT_MS = 5 * 60 * 1000
354
+
256
355
  function git(cmd, cwd) {
257
356
  try {
258
- return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER }).trim()
357
+ return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV }).trim()
259
358
  } catch {
260
359
  return ''
261
360
  }
@@ -267,7 +366,7 @@ function git(cmd, cwd) {
267
366
  // (gd-276). Callers decode per-file via decodeDiffText().
268
367
  function gitRaw(cmd, cwd) {
269
368
  try {
270
- return execSync(cmd, { cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER })
369
+ return execSync(cmd, { cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV })
271
370
  } catch {
272
371
  return Buffer.alloc(0)
273
372
  }
@@ -282,7 +381,7 @@ function gitRaw(cmd, cwd) {
282
381
  function gitDiffUntracked(file, cwd) {
283
382
  try {
284
383
  return execFileSync('git', ['diff', '--no-index', '--', '/dev/null', file], {
285
- cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER,
384
+ cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, env: GIT_ENV,
286
385
  })
287
386
  } catch (err) {
288
387
  return err && err.stdout && err.stdout.length ? err.stdout : Buffer.alloc(0)
@@ -293,10 +392,11 @@ function gitDiffUntracked(file, cwd) {
293
392
  // used for push/pull where we need to detect auth rejection.
294
393
  function gitTry(cmd, cwd) {
295
394
  try {
296
- const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
395
+ const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: GIT_NET_TIMEOUT_MS, env: GIT_ENV }).trim()
297
396
  return { ok: true, out }
298
397
  } catch (err) {
299
- const out = (err.stderr || err.stdout || err.message || '').toString().trim()
398
+ const timedOut = err.signal === 'SIGTERM' && err.code == null
399
+ const out = (err.stderr || err.stdout || (timedOut ? `git не отговори ${GIT_NET_TIMEOUT_MS / 60000} мин и беше прекратен` : err.message) || '').toString().trim()
300
400
  return { ok: false, out }
301
401
  }
302
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {