gitdone-agent 0.1.1 → 0.1.3

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 +123 -15
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -2,11 +2,32 @@
2
2
  // gitdone-agent — watches a local git repo and pushes snapshots to gitdone.eu
3
3
  // Usage: bun index.js --key=gdo_xxx --name=my-repo --path=/path/to/repo [--install]
4
4
 
5
- import { execSync } from 'node:child_process'
6
- import { existsSync, writeFileSync, unlinkSync } from 'node:fs'
5
+ import { execSync, spawn } from 'node:child_process'
6
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync, copyFileSync, appendFileSync } from 'node:fs'
7
7
  import { resolve, join } from 'node:path'
8
8
  import { homedir } from 'node:os'
9
9
 
10
+ // ─── Stable agent dir + logging ────────────────────────────────────────────────
11
+ // Everything persistent lives here: a stable copy of the agent script (so
12
+ // autostart never points at a purged npx temp dir) and a per-repo log file
13
+ // (so failures are visible even when the agent runs in a hidden window).
14
+
15
+ const AGENT_DIR = join(homedir(), '.gitdone-agent')
16
+
17
+ function ensureAgentDir() {
18
+ if (!existsSync(AGENT_DIR)) mkdirSync(AGENT_DIR, { recursive: true })
19
+ }
20
+
21
+ let LOG_PATH = null
22
+
23
+ function log(msg) {
24
+ const line = `[${new Date().toISOString()}] ${msg}`
25
+ console.log(line)
26
+ if (LOG_PATH) {
27
+ try { appendFileSync(LOG_PATH, line + '\n') } catch { /* best-effort */ }
28
+ }
29
+ }
30
+
10
31
  // ─── Arg parsing ─────────────────────────────────────────────────────────────
11
32
 
12
33
  function parseArgs() {
@@ -27,17 +48,18 @@ function parseArgs() {
27
48
  const url = (args.url ?? 'https://gitdone.eu').replace(/\/$/, '')
28
49
  const install = 'install' in args
29
50
  const uninstall = 'uninstall' in args
51
+ const doctor = 'doctor' in args
30
52
 
31
- if (!key && !uninstall) {
53
+ if (!key && !uninstall && !doctor) {
32
54
  console.error('Error: --key is required (e.g. --key=gdo_xxx)')
33
55
  process.exit(1)
34
56
  }
35
- if (!existsSync(repoPath) && !uninstall) {
57
+ if (!existsSync(repoPath) && !uninstall && !doctor) {
36
58
  console.error(`Error: path does not exist: ${repoPath}`)
37
59
  process.exit(1)
38
60
  }
39
61
 
40
- return { key, path: repoPath, name, projectId, interval, url, install, uninstall }
62
+ return { key, path: repoPath, name, projectId, interval, url, install, uninstall, doctor }
41
63
  }
42
64
 
43
65
  // ─── Windows auto-start ───────────────────────────────────────────────────────
@@ -53,8 +75,19 @@ function getNodePath() {
53
75
  }
54
76
 
55
77
  function installStartup({ name, key, path: repoPath, url, interval, projectId }) {
78
+ ensureAgentDir()
56
79
  const nodePath = getNodePath()
57
- const agentPath = resolve(process.argv[1])
80
+
81
+ // Copy this script to a stable location. process.argv[1] may point inside an
82
+ // npx cache dir that gets purged later — referencing it from autostart would
83
+ // silently break on the next login. A stable copy survives.
84
+ const stableAgent = join(AGENT_DIR, 'agent.mjs')
85
+ try {
86
+ copyFileSync(resolve(process.argv[1]), stableAgent)
87
+ } catch (err) {
88
+ console.error(`Warning: could not copy agent to ${stableAgent}: ${err.message}`)
89
+ }
90
+ const agentPath = existsSync(stableAgent) ? stableAgent : resolve(process.argv[1])
58
91
  const startupDir = getStartupDir()
59
92
 
60
93
  const argLine = [
@@ -76,9 +109,11 @@ function installStartup({ name, key, path: repoPath, url, interval, projectId })
76
109
  writeFileSync(vbsPath, vbs, 'utf8')
77
110
 
78
111
  console.log(`✓ Инсталиран! При следващото влизане в Windows агентът ще стартира автоматично.`)
79
- console.log(` Файл: ${vbsPath}`)
80
- console.log(` Repo : ${repoPath}`)
81
- console.log(` Name : ${name}`)
112
+ console.log(` Файл : ${vbsPath}`)
113
+ console.log(` Агент : ${agentPath}`)
114
+ console.log(` Repo : ${repoPath}`)
115
+ console.log(` Name : ${name}`)
116
+ console.log(` Лог : ${join(AGENT_DIR, `${name}.log`)}`)
82
117
  console.log()
83
118
  }
84
119
 
@@ -92,6 +127,20 @@ function uninstallStartup(name) {
92
127
  }
93
128
  }
94
129
 
130
+ // ─── Doctor: print where things are so "offline" is debuggable ─────────────────
131
+
132
+ function runDoctor(name) {
133
+ const vbsPath = join(getStartupDir(), `gitdone-${name}.vbs`)
134
+ const stableAgent = join(AGENT_DIR, 'agent.mjs')
135
+ const logPath = join(AGENT_DIR, `${name}.log`)
136
+ console.log('gitdone-agent doctor')
137
+ console.log(` node : ${getNodePath()}`)
138
+ console.log(` agent dir : ${AGENT_DIR} ${existsSync(AGENT_DIR) ? '(ok)' : '(MISSING)'}`)
139
+ console.log(` stable agent : ${stableAgent} ${existsSync(stableAgent) ? '(ok)' : '(not installed)'}`)
140
+ console.log(` autostart vbs : ${vbsPath} ${existsSync(vbsPath) ? '(ok)' : '(not installed)'}`)
141
+ console.log(` log file : ${logPath} ${existsSync(logPath) ? '(ok)' : '(none yet)'}`)
142
+ }
143
+
95
144
  // ─── Git helpers ─────────────────────────────────────────────────────────────
96
145
 
97
146
  function git(cmd, cwd) {
@@ -148,9 +197,42 @@ function getSnapshot(repoPath) {
148
197
  return { branch, modified, staged, aheadBy, behindBy, lastCommit, statuses, diffs }
149
198
  }
150
199
 
151
- // ─── Push to API ─────────────────────────────────────────────────────────────
200
+ // ─── Push to API & execute commands ─────────────────────────────────────────
152
201
 
153
- async function push({ url, key, name, projectId, snapshot }) {
202
+ async function reportCommandResult(url, key, id, status, result) {
203
+ await fetch(`${url}/api/v1/agent/command-result`, {
204
+ method: 'POST',
205
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({ id, status, result }),
207
+ }).catch(() => {})
208
+ }
209
+
210
+ async function executeCommand(cmd, repoPath, url, key) {
211
+ let result = 'ok'
212
+ let status = 'done'
213
+ try {
214
+ if (cmd.type === 'commit') {
215
+ const msg = (cmd.payload?.message ?? 'commit').replace(/"/g, '\\"')
216
+ git('git add -A', repoPath)
217
+ const out = git(`git commit -m "${msg}"`, repoPath)
218
+ result = out || 'committed'
219
+ } else if (cmd.type === 'push') {
220
+ const out = git('git push', repoPath)
221
+ result = out || 'pushed'
222
+ } else if (cmd.type === 'pull') {
223
+ const out = git('git pull', repoPath)
224
+ result = out || 'pulled'
225
+ }
226
+ log(`✓ ${cmd.type}: ${result}`)
227
+ } catch (err) {
228
+ status = 'error'
229
+ result = err.message
230
+ log(`✗ ${cmd.type} failed: ${result}`)
231
+ }
232
+ await reportCommandResult(url, key, cmd.id, status, result)
233
+ }
234
+
235
+ async function push({ url, key, name, projectId, repoPath, snapshot }) {
154
236
  const res = await fetch(`${url}/api/v1/agent/snapshot`, {
155
237
  method: 'POST',
156
238
  headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
@@ -160,13 +242,29 @@ async function push({ url, key, name, projectId, snapshot }) {
160
242
  const text = await res.text().catch(() => '')
161
243
  throw new Error(`HTTP ${res.status}: ${text}`)
162
244
  }
245
+ const data = await res.json().catch(() => ({}))
246
+ for (const cmd of data.commands ?? []) {
247
+ await executeCommand(cmd, repoPath, url, key)
248
+ }
163
249
  }
164
250
 
165
251
  // ─── Main ────────────────────────────────────────────────────────────────────
166
252
 
167
253
  async function main() {
168
254
  const config = parseArgs()
169
- const { key, path: repoPath, name, projectId, interval, url, install, uninstall } = config
255
+ const { key, path: repoPath, name, projectId, interval, url, install, uninstall, doctor } = config
256
+
257
+ if (doctor) {
258
+ runDoctor(name)
259
+ process.exit(0)
260
+ }
261
+
262
+ // Route all runtime logs to a per-repo file too, so a silently-running
263
+ // (hidden-window) agent still leaves a trail to debug "why is it offline".
264
+ if (!uninstall) {
265
+ ensureAgentDir()
266
+ LOG_PATH = join(AGENT_DIR, `${name}.log`)
267
+ }
170
268
 
171
269
  if (uninstall) {
172
270
  uninstallStartup(name)
@@ -175,6 +273,16 @@ async function main() {
175
273
 
176
274
  if (install) {
177
275
  installStartup(config)
276
+
277
+ // Launch the agent silently in the background right now via wscript.exe
278
+ const vbsPath = join(getStartupDir(), `gitdone-${name}.vbs`)
279
+ const child = spawn('wscript.exe', [vbsPath], { detached: true, stdio: 'ignore' })
280
+ child.unref()
281
+
282
+ console.log()
283
+ console.log('✓ Агентът е стартиран в заден план — можеш да затвориш прозореца.')
284
+ console.log(' При следващо влизане в Windows ще стартира автоматично.')
285
+ process.exit(0)
178
286
  }
179
287
 
180
288
  if (!isGitRepo(repoPath)) {
@@ -194,11 +302,11 @@ async function main() {
194
302
  async function tick() {
195
303
  try {
196
304
  const snapshot = getSnapshot(repoPath)
197
- await push({ url, key, name, projectId, snapshot })
305
+ await push({ url, key, name, projectId, repoPath, snapshot })
198
306
  const changed = snapshot.modified.length + snapshot.staged.length
199
- console.log(`[${new Date().toLocaleTimeString()}] ✓ pushed — branch: ${snapshot.branch}, changes: ${changed}`)
307
+ log(`✓ pushed — branch: ${snapshot.branch}, changes: ${changed}`)
200
308
  } catch (err) {
201
- console.error(`[${new Date().toLocaleTimeString()}] ✗ error: ${err.message}`)
309
+ log(`✗ error: ${err.message}`)
202
310
  }
203
311
  }
204
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": {