gitdone-agent 0.6.14 → 0.6.16

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 +67 -7
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -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.14'
30
+ const AGENT_VERSION = '0.6.16'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -186,10 +186,52 @@ function findClaude() {
186
186
  return { path: 'claude', shell: true, found: false }
187
187
  }
188
188
 
189
+ // ─── Single instance (gd-407) ─────────────────────────────────────────────────
190
+ // Two agents on one machine (an old one surviving an update, or a manual
191
+ // `npx gitdone-agent` next to the autostarted one) flap the machine's online
192
+ // state and race dispatches. The running agent claims the machine via a pid
193
+ // file; a newcomer that finds a LIVE agent behind it exits with EXIT_DUPLICATE,
194
+ // which the supervisor treats as "do not restart me". First one wins.
195
+ // 86 is outside node's own exit-code range (1-13) — keep in lockstep with the
196
+ // run-agent.cmd template in installStartup().
197
+ const EXIT_DUPLICATE = 86
198
+ const PID_PATH = join(AGENT_DIR, 'agent.pid')
199
+
200
+ // Command line of a live process, '' when unreadable (dead process, no rights).
201
+ function processCmdline(pid) {
202
+ try {
203
+ if (process.platform === 'win32') {
204
+ return execSync(
205
+ `powershell -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine"`,
206
+ { encoding: 'utf8' },
207
+ ).trim()
208
+ }
209
+ return execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim()
210
+ } catch { return '' }
211
+ }
212
+
213
+ function ensureSingleInstance() {
214
+ try {
215
+ const pid = parseInt(readFileSync(PID_PATH, 'utf8'), 10)
216
+ if (pid && pid !== process.pid) {
217
+ process.kill(pid, 0) // throws when that pid is no longer alive
218
+ // PID reuse guard: only defer to a process that really looks like an
219
+ // agent (stable copy agent.mjs, or any gitdone-agent npx/global run).
220
+ if (/gitdone-agent|agent\.mjs/i.test(processCmdline(pid))) {
221
+ log(`✗ друг gitdone-agent вече върви (PID ${pid}) — този процес излиза, за да няма два агента на машината`)
222
+ process.exit(EXIT_DUPLICATE)
223
+ }
224
+ }
225
+ } catch { /* no/stale pid file → the machine is free, we take over */ }
226
+ try { ensureAgentDir(); writeFileSync(PID_PATH, String(process.pid)) } catch { /* best-effort */ }
227
+ }
228
+
189
229
  // Kill any previously-running agent processes so an update applies WITHOUT a
190
- // Windows re-login. Targets node processes launched from the agent dir
191
- // (~/.gitdone-agent), excluding ourselves the npx installer runs from the
192
- // npx cache, so it won't match. Best-effort; failures are non-fatal.
230
+ // Windows re-login. Targets node processes that look like a gitdone agent
231
+ // both the stable copy in ~/.gitdone-agent AND one running straight from the
232
+ // npx cache / global install (its command line contains "gitdone-agent") —
233
+ // excluding ourselves and our parent (the npx wrapper that launched us).
234
+ // Best-effort; failures are non-fatal.
193
235
  function stopRunningAgents() {
194
236
  // Kill the supervisor loops (cmd.exe running run-agent.cmd) FIRST, then the
195
237
  // agents — the other order lets a still-alive supervisor immediately respawn
@@ -200,7 +242,7 @@ function stopRunningAgents() {
200
242
  " Where-Object { $_.CommandLine -like '*run-agent.cmd*' } |",
201
243
  ' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
202
244
  "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" |",
203
- ` Where-Object { $_.CommandLine -like '*.gitdone-agent*' -and $_.ProcessId -ne ${process.pid} } |`,
245
+ ` Where-Object { ($_.CommandLine -like '*gitdone-agent*' -or $_.CommandLine -like '*agent.mjs*') -and $_.ProcessId -ne ${process.pid} -and $_.ProcessId -ne ${process.ppid || 0} } |`,
204
246
  ' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
205
247
  ].join('\n')
206
248
  try {
@@ -236,6 +278,14 @@ function installStartup() {
236
278
  // node-level failures (corrupt file, bad path) finally leave a trace. If the
237
279
  // node path recorded at install time vanished (node upgraded/moved), fall
238
280
  // back to whatever `node` is on PATH.
281
+ //
282
+ // Restart forever on normal crashes, but STOP on process-START failures
283
+ // (0xc0000142 STATUS_DLL_INIT_FAILED, 0xc0000135 missing DLL): those mean the
284
+ // login session itself is broken (e.g. after a hardware crash), every attempt
285
+ // pops a blocking "Application Error" dialog, and a retry can never succeed —
286
+ // the loop turned into an endless dialog storm (gd-405). Same for the
287
+ // ping-as-sleep: if even ping can't start, bail instead of spinning with no
288
+ // delay at all. A Windows re-login restarts the supervisor cleanly.
239
289
  const crashLog = join(AGENT_DIR, 'agent-crash.log')
240
290
  const cmd = [
241
291
  '@echo off',
@@ -244,9 +294,18 @@ function installStartup() {
244
294
  'if not exist "%NODE%" set "NODE=node"',
245
295
  ':loop',
246
296
  `"%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',
297
+ 'set CODE=%errorlevel%',
298
+ `echo [%date% %time%] agent exited (code %CODE%) - restart in 10s >> "${crashLog}"`,
299
+ 'if "%CODE%"=="86" goto duplicate',
300
+ 'if "%CODE%"=="-1073741502" goto dead',
301
+ 'if "%CODE%"=="-1073741515" goto dead',
302
+ 'ping -n 11 127.0.0.1 >nul 2>nul || goto dead',
249
303
  'goto loop',
304
+ ':dead',
305
+ `echo [%date% %time%] node cannot start (code %CODE%) - supervisor giving up until next login >> "${crashLog}"`,
306
+ 'exit /b',
307
+ ':duplicate',
308
+ `echo [%date% %time%] another agent already runs this machine (code 86) - this supervisor exits >> "${crashLog}"`,
250
309
  ].join('\r\n')
251
310
  const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
252
311
  writeFileSync(cmdPath, cmd, 'utf8')
@@ -1590,6 +1649,7 @@ async function streamCommands(cfg) {
1590
1649
  // ─── Main ────────────────────────────────────────────────────────────────────
1591
1650
 
1592
1651
  async function runLoop(cfg) {
1652
+ ensureSingleInstance()
1593
1653
  log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
1594
1654
  log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
1595
1655
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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": {