gitdone-agent 0.2.1 → 0.3.1

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 +93 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -24,6 +24,11 @@ import { randomUUID } from 'node:crypto'
24
24
  // truth for the running agent), and a log file (so failures are visible even
25
25
  // when the agent runs in a hidden window).
26
26
 
27
+ // Reported to the server on every sync so the web UI can flag outdated agents.
28
+ // Keep in lockstep with packages/agent/package.json "version" AND
29
+ // src/lib/agentVersion.ts LATEST_AGENT_VERSION.
30
+ const AGENT_VERSION = '0.3.1'
31
+
27
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
28
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
29
34
  const STABLE_AGENT = join(AGENT_DIR, 'agent.mjs')
@@ -102,10 +107,33 @@ function getNodePath() {
102
107
  return process.execPath
103
108
  }
104
109
 
110
+ // Kill any previously-running agent processes so an update applies WITHOUT a
111
+ // Windows re-login. Targets node processes launched from the agent dir
112
+ // (~/.gitdone-agent), excluding ourselves — the npx installer runs from the
113
+ // npx cache, so it won't match. Best-effort; failures are non-fatal.
114
+ function stopRunningAgents() {
115
+ const ps = [
116
+ "$ErrorActionPreference='SilentlyContinue'",
117
+ "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" |",
118
+ ` Where-Object { $_.CommandLine -like '*.gitdone-agent*' -and $_.ProcessId -ne ${process.pid} } |`,
119
+ ' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
120
+ ].join('\n')
121
+ try {
122
+ ensureAgentDir()
123
+ const scriptPath = join(AGENT_DIR, 'stop-agents.ps1')
124
+ writeFileSync(scriptPath, ps, 'utf8')
125
+ execSync(`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${scriptPath}"`, { stdio: 'ignore' })
126
+ } catch { /* best-effort — at worst the old agent lingers until next login */ }
127
+ }
128
+
105
129
  function installStartup() {
106
130
  ensureAgentDir()
107
131
  const nodePath = getNodePath()
108
132
 
133
+ // Stop the previously-running agent(s) first so we don't end up with the old
134
+ // build still reporting alongside the new one until the next Windows login.
135
+ stopRunningAgents()
136
+
109
137
  // Copy this script to a stable location. process.argv[1] may point inside an
110
138
  // npx cache dir that gets purged later — referencing it from autostart would
111
139
  // silently break on the next login. A stable copy survives.
@@ -171,6 +199,49 @@ function git(cmd, cwd) {
171
199
  }
172
200
  }
173
201
 
202
+ // Like git(), but surfaces failures (with stderr) instead of swallowing them —
203
+ // used for push/pull where we need to detect auth rejection.
204
+ function gitTry(cmd, cwd) {
205
+ try {
206
+ const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
207
+ return { ok: true, out }
208
+ } catch (err) {
209
+ const out = (err.stderr || err.stdout || err.message || '').toString().trim()
210
+ return { ok: false, out }
211
+ }
212
+ }
213
+
214
+ // Never let a token reach the log file or the server's command-result.
215
+ function redact(text, token) {
216
+ if (!text || !token) return text
217
+ return text.split(token).join('***')
218
+ }
219
+
220
+ // https://x-access-token:<token>@github.com/<owner>/<repo>.git — an ad-hoc URL
221
+ // passed straight to push/pull, so it never gets written into .git/config.
222
+ function authUrl(auth) {
223
+ const repo = auth.repo.replace(/\.git$/i, '')
224
+ return `https://x-access-token:${auth.token}@github.com/${repo}.git`
225
+ }
226
+
227
+ // Push/pull using the server-issued token when we have one; otherwise fall back
228
+ // to the machine's own git credentials (old behaviour). Throws on failure.
229
+ function pushOrPull(type, repoPath, auth) {
230
+ const branch = git('git branch --show-current', repoPath) || 'HEAD'
231
+ let cmd
232
+ if (auth) {
233
+ const url = authUrl(auth)
234
+ cmd = type === 'push' ? `git push "${url}" HEAD:${branch}` : `git pull "${url}" ${branch}`
235
+ } else {
236
+ cmd = type === 'push' ? 'git push' : 'git pull'
237
+ }
238
+ const r = gitTry(cmd, repoPath)
239
+ if (!r.ok) throw new Error(r.out || `${type} failed`)
240
+ return r.out || (type === 'push' ? 'pushed' : 'pulled')
241
+ }
242
+
243
+ const AUTH_FAIL = /authentication|authorization|403|401|denied|could not read Username|invalid username or password|terminal prompts disabled/i
244
+
174
245
  function parseDiffByFile(diffOutput) {
175
246
  const files = {}
176
247
  if (!diffOutput) return files
@@ -274,20 +345,26 @@ async function reportCommandResult(cfg, id, status, result) {
274
345
  async function executeCommand(cfg, cmd, repoPath) {
275
346
  let result = 'ok'
276
347
  let status = 'done'
348
+ const auth = cfg.auth?.[repoPath] ?? null
277
349
  try {
278
350
  if (cmd.type === 'commit') {
279
351
  const msg = (cmd.payload?.message ?? 'commit').replace(/"/g, '\\"')
280
352
  git('git add -A', repoPath)
281
353
  result = git(`git commit -m "${msg}"`, repoPath) || 'committed'
282
- } else if (cmd.type === 'push') {
283
- result = git('git push', repoPath) || 'pushed'
284
- } else if (cmd.type === 'pull') {
285
- result = git('git pull', repoPath) || 'pulled'
354
+ } else if (cmd.type === 'push' || cmd.type === 'pull') {
355
+ result = pushOrPull(cmd.type, repoPath, auth)
286
356
  }
287
- log(`✓ ${cmd.type} @ ${repoPath}: ${result}`)
357
+ log(`✓ ${cmd.type} @ ${repoPath}: ${redact(result, auth?.token)}`)
288
358
  } catch (err) {
289
359
  status = 'error'
290
- result = err.message
360
+ result = redact(err.message, auth?.token)
361
+ // Cached token rejected (e.g. rotated/expired on the server) → drop it so
362
+ // the next snapshot reports hasGithubAuth:false and the server reissues one.
363
+ if (auth && AUTH_FAIL.test(err.message)) {
364
+ delete cfg.auth[repoPath]
365
+ writeConfig(cfg)
366
+ result += ' — токенът е изчистен, пробвай командата пак'
367
+ }
291
368
  log(`✗ ${cmd.type} failed @ ${repoPath}: ${result}`)
292
369
  }
293
370
  await reportCommandResult(cfg, cmd.id, status, result)
@@ -298,6 +375,7 @@ async function sync(cfg, discovered) {
298
375
  const data = await api(cfg, '/api/v1/agent/sync', {
299
376
  machineId: cfg.machineId,
300
377
  hostname: cfg.hostname,
378
+ agentVersion: AGENT_VERSION,
301
379
  roots: cfg.roots,
302
380
  repos: discovered,
303
381
  })
@@ -311,8 +389,17 @@ async function pushSnapshot(cfg, repo) {
311
389
  machineId: cfg.machineId,
312
390
  path: repo.path,
313
391
  repoName: repo.name,
392
+ // Tells the server whether we already hold a push token for this repo.
393
+ // Always a boolean → marks us as a token-capable agent.
394
+ hasGithubAuth: !!cfg.auth?.[repo.path],
314
395
  ...snapshot,
315
396
  })
397
+ // Server issued a (fresh) push token — cache it on disk so we ask only once.
398
+ if (data.githubAuth?.token && data.githubAuth?.repo) {
399
+ cfg.auth = cfg.auth ?? {}
400
+ cfg.auth[repo.path] = { token: data.githubAuth.token, repo: data.githubAuth.repo }
401
+ writeConfig(cfg)
402
+ }
316
403
  for (const cmd of data.commands ?? []) {
317
404
  await executeCommand(cfg, cmd, repo.path)
318
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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": {