gitdone-agent 0.6.11 → 0.6.13

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 +59 -3
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -9,10 +9,10 @@
9
9
  // roots anytime with --root (repeatable). The running agent reads its config
10
10
  // from ~/.gitdone-agent/config.json, so the autostart entry needs no args.
11
11
 
12
- import { execSync, spawn } from 'node:child_process'
12
+ import { execSync, execFileSync, spawn } from 'node:child_process'
13
13
  import {
14
14
  existsSync, writeFileSync, readFileSync, unlinkSync,
15
- mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync,
15
+ mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
16
16
  } from 'node:fs'
17
17
  import { resolve, join } from 'node:path'
18
18
  import { homedir, hostname, tmpdir } from 'node:os'
@@ -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.11'
30
+ const AGENT_VERSION = '0.6.13'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -237,6 +237,22 @@ function runDoctor() {
237
237
  // return '' (an EMPTY snapshot, worse than the collapsed view). 64 MB is plenty.
238
238
  const GIT_MAX_BUFFER = 64 * 1024 * 1024
239
239
 
240
+ // Cap for synthesising an untracked file's full-content diff (gd-370) — above
241
+ // this the file keeps the "no diff" placeholder so a huge/binary blob can't
242
+ // bloat the snapshot payload.
243
+ const UNTRACKED_DIFF_MAX_BYTES = 1024 * 1024
244
+
245
+ // Synthesising a per-file diff spawns a synchronous `git diff --no-index` per
246
+ // untracked file. Individually cheap, but a repo with a huge untracked/scratch
247
+ // tree (e.g. extracted game assets — thousands of files) would spawn thousands
248
+ // of blocking git processes and freeze the single-threaded agent for MINUTES on
249
+ // every snapshot, starving the 30s sync heartbeat so the machine flips offline.
250
+ // Bound the synthesis two ways — a file-count cap and a wall-clock budget — so
251
+ // no single repo can ever monopolise the event loop. Anything past either keeps
252
+ // the "no diff" placeholder, identical to the size-cap path above.
253
+ const UNTRACKED_DIFF_MAX_FILES = 200
254
+ const UNTRACKED_DIFF_TIME_BUDGET_MS = 3000
255
+
240
256
  function git(cmd, cwd) {
241
257
  try {
242
258
  return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER }).trim()
@@ -257,6 +273,22 @@ function gitRaw(cmd, cwd) {
257
273
  }
258
274
  }
259
275
 
276
+ // Full-content diff of a single UNTRACKED file (against /dev/null), so the
277
+ // console can show a new file's whole content as an all-added diff — like
278
+ // GitHub Desktop (gd-370). Uses execFileSync (argv, no shell) so paths with
279
+ // spaces / Cyrillic reach git intact, unlike a shell command string. `--no-index`
280
+ // exits 1 when the file differs from empty (i.e. always) — that's expected, and
281
+ // its stdout still holds the diff. Returns raw bytes for parseDiffByFile.
282
+ function gitDiffUntracked(file, cwd) {
283
+ try {
284
+ return execFileSync('git', ['diff', '--no-index', '--', '/dev/null', file], {
285
+ cwd, encoding: 'buffer', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: GIT_MAX_BUFFER,
286
+ })
287
+ } catch (err) {
288
+ return err && err.stdout && err.stdout.length ? err.stdout : Buffer.alloc(0)
289
+ }
290
+ }
291
+
260
292
  // Like git(), but surfaces failures (with stderr) instead of swallowing them —
261
293
  // used for push/pull where we need to detect auth rejection.
262
294
  function gitTry(cmd, cwd) {
@@ -494,6 +526,30 @@ function getSnapshot(repoPath) {
494
526
 
495
527
  const diffs = parseDiffByFile(gitRaw('git diff HEAD', repoPath))
496
528
 
529
+ // Untracked files (`??`) aren't in `git diff HEAD`, so they had no content to
530
+ // show ("Нов или untracked файл — няма diff"). Synthesise an all-added diff for
531
+ // each from /dev/null so the console shows the whole new file — like GitHub
532
+ // Desktop (gd-370). Skip anything over the size cap so a stray big/binary blob
533
+ // can't bloat the snapshot; those keep the "no diff" placeholder.
534
+ const untracked = Object.keys(statuses).filter((f) => statuses[f] === '??' && !diffs[f])
535
+ const diffDeadline = Date.now() + UNTRACKED_DIFF_TIME_BUDGET_MS
536
+ let synthesised = 0
537
+ for (const file of untracked) {
538
+ // Stop before either bound so a scratch tree can't starve the heartbeat; the
539
+ // rest keep the "no diff" placeholder. Log once so it's visible why.
540
+ if (synthesised >= UNTRACKED_DIFF_MAX_FILES || Date.now() > diffDeadline) {
541
+ log(`⚠ untracked diff cap reached @ ${repoPath} (${synthesised}/${untracked.length} synthesised) — rest shown without preview`)
542
+ break
543
+ }
544
+ let st
545
+ try { st = statSync(join(repoPath, file)) } catch { continue }
546
+ if (!st.isFile() || st.size > UNTRACKED_DIFF_MAX_BYTES) continue
547
+ synthesised++
548
+ const parsed = parseDiffByFile(gitDiffUntracked(file, repoPath))
549
+ const val = parsed[file] ?? Object.values(parsed)[0]
550
+ if (val) diffs[file] = val
551
+ }
552
+
497
553
  // Origin remote → lets the server auto-detect the GitHub repo for the History tab.
498
554
  const remoteUrl = git('git config --get remote.origin.url', repoPath) || null
499
555
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
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": {