litmus-cli 1.0.20 → 1.1.0

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 (61) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +4 -3
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/status.js +1 -1
  6. package/dist/commands/status.js.map +1 -1
  7. package/dist/commands/submit.d.ts.map +1 -1
  8. package/dist/commands/submit.js +35 -11
  9. package/dist/commands/submit.js.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/api.d.ts +6 -16
  13. package/dist/lib/api.d.ts.map +1 -1
  14. package/dist/lib/api.js +67 -5
  15. package/dist/lib/api.js.map +1 -1
  16. package/dist/lib/config.d.ts +0 -3
  17. package/dist/lib/config.d.ts.map +1 -1
  18. package/dist/lib/config.js +0 -3
  19. package/dist/lib/config.js.map +1 -1
  20. package/dist/lib/detect-project.d.ts.map +1 -0
  21. package/dist/lib/detect-project.js.map +1 -0
  22. package/dist/lib/env-hosts.d.ts +12 -0
  23. package/dist/lib/env-hosts.d.ts.map +1 -0
  24. package/dist/lib/env-hosts.js +19 -0
  25. package/dist/lib/env-hosts.js.map +1 -0
  26. package/dist/{utils → lib}/errors.d.ts +1 -1
  27. package/dist/lib/errors.d.ts.map +1 -0
  28. package/dist/{utils → lib}/errors.js +2 -3
  29. package/dist/lib/errors.js.map +1 -0
  30. package/dist/lib/extract.d.ts +1 -1
  31. package/dist/lib/extract.d.ts.map +1 -1
  32. package/dist/lib/extract.js +19 -3
  33. package/dist/lib/extract.js.map +1 -1
  34. package/dist/lib/hook-logger.cjs +6 -4
  35. package/dist/lib/platform.d.ts +37 -0
  36. package/dist/lib/platform.d.ts.map +1 -0
  37. package/dist/lib/platform.js +69 -0
  38. package/dist/lib/platform.js.map +1 -0
  39. package/dist/lib/schemas.d.ts +24 -0
  40. package/dist/lib/schemas.d.ts.map +1 -0
  41. package/dist/lib/schemas.js +21 -0
  42. package/dist/lib/schemas.js.map +1 -0
  43. package/dist/lib/tracker.d.ts +1 -1
  44. package/dist/lib/tracker.js +2 -2
  45. package/dist/lib/tracker.js.map +1 -1
  46. package/dist/lib/watcher.d.ts +17 -0
  47. package/dist/lib/watcher.d.ts.map +1 -0
  48. package/dist/lib/watcher.js +752 -0
  49. package/dist/lib/watcher.js.map +1 -0
  50. package/package.json +23 -8
  51. package/dist/lib/watcher.cjs +0 -592
  52. package/dist/staging.d.ts +0 -2
  53. package/dist/staging.d.ts.map +0 -1
  54. package/dist/staging.js +0 -6
  55. package/dist/staging.js.map +0 -1
  56. package/dist/utils/detect-project.d.ts.map +0 -1
  57. package/dist/utils/detect-project.js.map +0 -1
  58. package/dist/utils/errors.d.ts.map +0 -1
  59. package/dist/utils/errors.js.map +0 -1
  60. /package/dist/{utils → lib}/detect-project.d.ts +0 -0
  61. /package/dist/{utils → lib}/detect-project.js +0 -0
@@ -1,592 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Litmus file activity watcher.
4
- * Runs as a detached background process started by `litmus init`.
5
- * Usage: node watcher.cjs <projectDir> <activityLogPath> [cliBinPath]
6
- *
7
- * Tracks:
8
- * - File changes with size deltas and large-paste detection (debounced 500ms per file)
9
- * - Heartbeat events every 5 minutes
10
- * - Git commit events (polled every 30s)
11
- * - Editor and AI tool detection (polled every 10 min)
12
- * - Auto-submit at deadline (if deadline/timeLimit configured)
13
- *
14
- * Writes newline-delimited JSON to the activity log.
15
- */
16
-
17
- const fs = require("fs")
18
- const path = require("path")
19
- const crypto = require("crypto")
20
- const { execSync, execFile } = require("child_process")
21
- const dns = require("dns")
22
- const https = require("https")
23
- const http = require("http")
24
-
25
- const [, , projectDir, activityLogPath, cliBinPath] = process.argv
26
-
27
- process.stderr.write(`[watcher] starting: projectDir=${projectDir} log=${activityLogPath}\n`)
28
- process.stderr.write(`[watcher] node ${process.version}, platform=${process.platform}, arch=${process.arch}\n`)
29
-
30
- if (!projectDir || !activityLogPath) {
31
- process.stderr.write("[watcher] missing arguments\n")
32
- process.exit(1)
33
- }
34
-
35
- if (!fs.existsSync(projectDir)) {
36
- process.stderr.write(`[watcher] projectDir does not exist: ${projectDir}\n`)
37
- process.exit(1)
38
- }
39
-
40
- // Ensure the log directory exists
41
- fs.mkdirSync(path.dirname(activityLogPath), { recursive: true })
42
-
43
- // Open the log file for appending
44
- const log = fs.createWriteStream(activityLogPath, { flags: "a" })
45
-
46
- // ── Config ──────────────────────────────────────────────────────
47
- let litmusConfig = null
48
- try {
49
- const configPath = path.join(projectDir, ".litmus", "config.json")
50
- litmusConfig = JSON.parse(fs.readFileSync(configPath, "utf8"))
51
- } catch { /* config not available */ }
52
-
53
- // ── Server upload config ────────────────────────────────────────
54
- let uploadConfig = null
55
- let uploadBuffer = []
56
- const MAX_BUFFER_SIZE = 10_000
57
-
58
- if (litmusConfig && litmusConfig.token && litmusConfig.backendUrl) {
59
- uploadConfig = { token: litmusConfig.token, backendUrl: litmusConfig.backendUrl }
60
- process.stderr.write("[watcher] upload enabled\n")
61
- }
62
-
63
- /**
64
- * Upload buffered events to server.
65
- * Returns a Promise that resolves when the HTTP request completes (used by shutdown).
66
- * During normal operation the returned promise is ignored (fire-and-forget).
67
- */
68
- function uploadEvents() {
69
- if (!uploadConfig || uploadBuffer.length === 0) return Promise.resolve()
70
-
71
- const events = uploadBuffer.splice(0) // take all, clear buffer
72
- const body = JSON.stringify({ events })
73
- const url = new URL(
74
- `${uploadConfig.backendUrl}/cli/activity?token=${encodeURIComponent(uploadConfig.token)}`
75
- )
76
- const reqFn = url.protocol === "https:" ? https.request : http.request
77
-
78
- return new Promise((resolve) => {
79
- const req = reqFn(url, {
80
- method: "POST",
81
- headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
82
- timeout: 10_000,
83
- }, (res) => {
84
- // Drain response
85
- res.resume()
86
- res.on("end", () => {
87
- if (res.statusCode >= 500) {
88
- process.stderr.write(`[watcher] upload failed (will retry): ${res.statusCode}\n`)
89
- uploadBuffer = events.concat(uploadBuffer)
90
- if (uploadBuffer.length > MAX_BUFFER_SIZE) {
91
- uploadBuffer = uploadBuffer.slice(-MAX_BUFFER_SIZE)
92
- }
93
- } else if (res.statusCode >= 400) {
94
- process.stderr.write(`[watcher] upload rejected: ${res.statusCode}\n`)
95
- }
96
- resolve()
97
- })
98
- })
99
- req.on("error", () => {
100
- // Network error — retry on next heartbeat
101
- uploadBuffer = events.concat(uploadBuffer)
102
- if (uploadBuffer.length > MAX_BUFFER_SIZE) {
103
- uploadBuffer = uploadBuffer.slice(-MAX_BUFFER_SIZE)
104
- }
105
- resolve()
106
- })
107
- req.on("timeout", () => { req.destroy() })
108
- req.end(body)
109
- })
110
- }
111
-
112
- // ── Ignore patterns ──────────────────────────────────────────────
113
- const IGNORED = [
114
- /(?:^|[/\\])\./, // dotfiles and dotdirs (including .git, .litmus)
115
- /node_modules/,
116
- /__pycache__/,
117
- /\.pyc$/,
118
- /\.class$/,
119
- /(?:^|[/\\])venv[/\\]/,
120
- /(?:^|[/\\])\.venv[/\\]/,
121
- ]
122
-
123
- function shouldIgnore(relPath) {
124
- return IGNORED.some((re) => re.test(relPath))
125
- }
126
-
127
- // ── Hash chain for tamper evidence ──────────────────────────────
128
- let prevHash = "0".repeat(64) // genesis
129
- let nextSeq = 0
130
-
131
- function emit(event) {
132
- event._seq = nextSeq++
133
- event._prevHash = prevHash
134
- const hash = crypto.createHash("sha256").update(JSON.stringify(event)).digest("hex")
135
- event._hash = hash
136
- prevHash = hash
137
- log.write(JSON.stringify(event) + "\n")
138
- if (uploadConfig && uploadBuffer.length < MAX_BUFFER_SIZE) {
139
- uploadBuffer.push(event)
140
- }
141
- }
142
-
143
- // ── File size tracking ───────────────────────────────────────────
144
- const LARGE_PASTE_THRESHOLD = 5000 // bytes — matches backend constant
145
- const fileSizes = new Map() // relativePath -> lastKnownSize
146
- const debounceTimers = new Map() // relativePath -> timeoutId
147
-
148
- function emitFileEvent(type, relPath) {
149
- const fullPath = path.join(projectDir, relPath)
150
- const event = { ts: new Date().toISOString(), type, path: relPath }
151
-
152
- try {
153
- const stat = fs.statSync(fullPath)
154
- const fileSize = stat.size
155
- event.fileSize = fileSize
156
-
157
- const prev = fileSizes.get(relPath)
158
- const sizeDelta = prev !== undefined ? fileSize - prev : fileSize
159
- event.sizeDelta = sizeDelta
160
- fileSizes.set(relPath, fileSize)
161
-
162
- if (sizeDelta > LARGE_PASTE_THRESHOLD) {
163
- event.largePaste = true
164
- event.bytesAdded = sizeDelta
165
- }
166
- } catch {
167
- // File was deleted or unreadable — still log the event, just without size data
168
- fileSizes.delete(relPath)
169
- }
170
-
171
- emit(event)
172
- }
173
-
174
- // ── Burst-edit detection (AI-driven multi-file edits) ────────────
175
- const BURST_WINDOW_MS = 2000 // 2 seconds
176
- const BURST_MIN_FILES = 3 // 3+ distinct files = burst
177
- const recentEdits = [] // [{ file, ts }]
178
-
179
- function checkBurstEdit(filename) {
180
- const now = Date.now()
181
- recentEdits.push({ file: filename, ts: now })
182
- // Prune edits outside the window
183
- while (recentEdits.length > 0 && now - recentEdits[0].ts > BURST_WINDOW_MS) {
184
- recentEdits.shift()
185
- }
186
- // Count distinct files in the window
187
- const distinctFiles = new Set(recentEdits.map((e) => e.file))
188
- if (distinctFiles.size >= BURST_MIN_FILES) {
189
- const files = [...distinctFiles]
190
- emit({
191
- ts: new Date().toISOString(),
192
- type: "burst_edit",
193
- fileCount: files.length,
194
- files: files.slice(0, 10), // Cap at 10 filenames
195
- })
196
- recentEdits.length = 0 // Reset to avoid duplicate bursts
197
- }
198
- }
199
-
200
- // ── File system watcher (debounced) ──────────────────────────────
201
- process.on("uncaughtException", (err) => {
202
- process.stderr.write(`[watcher] uncaughtException: ${err.stack || err}\n`)
203
- process.exit(1)
204
- })
205
-
206
- try {
207
- const watcher = fs.watch(projectDir, { recursive: true }, (eventType, filename) => {
208
- if (!filename) return
209
-
210
- // Check for AI artifact files before applying ignore filter (artifacts are dotfiles)
211
- checkArtifact(filename)
212
-
213
- if (shouldIgnore(filename)) return
214
-
215
- // Check for burst edits on raw events (before debounce — needs real timing)
216
- checkBurstEdit(filename)
217
-
218
- // Debounce per file: collapse multiple events within 500ms into one
219
- if (debounceTimers.has(filename)) {
220
- clearTimeout(debounceTimers.get(filename))
221
- }
222
- debounceTimers.set(filename, setTimeout(() => {
223
- debounceTimers.delete(filename)
224
- emitFileEvent(eventType, filename)
225
- }, 500))
226
- })
227
-
228
- watcher.on("error", (err) => {
229
- process.stderr.write(`[watcher] fs.watch error: ${err}\n`)
230
- })
231
-
232
- process.stderr.write("[watcher] file watcher started\n")
233
- } catch (err) {
234
- // fs.watch with recursive: true is not supported on Linux < Node 22.
235
- // Continue without file watching — heartbeats and git commit polling still work.
236
- process.stderr.write(`[watcher] file watching unavailable: ${err.message} (heartbeat + git tracking still active)\n`)
237
- }
238
-
239
- // ── Heartbeats ───────────────────────────────────────────────────
240
- function emitHeartbeat() {
241
- emit({ ts: new Date().toISOString(), type: "heartbeat" })
242
- uploadEvents() // Upload buffered events to server
243
- }
244
-
245
- emitHeartbeat() // Immediate heartbeat + initial upload
246
- setInterval(emitHeartbeat, 5 * 60 * 1000) // Every 5 minutes
247
-
248
- // ── Git commit polling ───────────────────────────────────────────
249
- let lastCommitHash = null
250
-
251
- function readCurrentCommitHash() {
252
- try {
253
- const headContent = fs.readFileSync(path.join(projectDir, ".git", "HEAD"), "utf8").trim()
254
- if (headContent.startsWith("ref: ")) {
255
- const ref = headContent.slice(5)
256
- const refPath = path.join(projectDir, ".git", ref)
257
- // Try loose ref first, then fall back to packed-refs
258
- if (fs.existsSync(refPath)) {
259
- return fs.readFileSync(refPath, "utf8").trim()
260
- }
261
- const packedPath = path.join(projectDir, ".git", "packed-refs")
262
- if (fs.existsSync(packedPath)) {
263
- const packed = fs.readFileSync(packedPath, "utf8")
264
- for (const line of packed.split("\n")) {
265
- if (line.endsWith(" " + ref)) {
266
- return line.split(" ")[0]
267
- }
268
- }
269
- }
270
- } else {
271
- return headContent // Detached HEAD — the hash itself
272
- }
273
- } catch { /* .git may not exist */ }
274
- return null
275
- }
276
-
277
- // Initialize with current commit hash
278
- lastCommitHash = readCurrentCommitHash()
279
-
280
- function checkGitCommits() {
281
- try {
282
- const currentHash = readCurrentCommitHash()
283
- if (!currentHash || currentHash === lastCommitHash) return
284
-
285
- lastCommitHash = currentHash
286
-
287
- const info = execSync(
288
- 'git log -1 --numstat --pretty=format:"%H|%aI|%s"',
289
- { cwd: projectDir, timeout: 5000, encoding: "utf8" }
290
- )
291
- const lines = info.trim().split("\n")
292
- const header = lines[0].replace(/^"|"$/g, "")
293
- const firstPipe = header.indexOf("|")
294
- const secondPipe = header.indexOf("|", firstPipe + 1)
295
- const hash = header.slice(0, firstPipe)
296
- const message = secondPipe >= 0 ? header.slice(secondPipe + 1) : ""
297
- let linesAdded = 0, linesDeleted = 0, filesChanged = 0
298
-
299
- for (let i = 1; i < lines.length; i++) {
300
- const line = lines[i].trim()
301
- if (!line) continue
302
- const parts = line.split("\t")
303
- if (parts[0] !== "-") linesAdded += parseInt(parts[0], 10) || 0
304
- if (parts[1] !== "-") linesDeleted += parseInt(parts[1], 10) || 0
305
- filesChanged++
306
- }
307
-
308
- emit({
309
- ts: new Date().toISOString(),
310
- type: "commit",
311
- hash: hash ? hash.slice(0, 12) : currentHash.slice(0, 12),
312
- message: message || "",
313
- linesAdded,
314
- linesDeleted,
315
- filesChanged,
316
- })
317
- } catch { /* git not available or command failed — skip */ }
318
- }
319
-
320
- setInterval(checkGitCommits, 30 * 1000) // Every 30 seconds
321
-
322
- // ── Editor & AI tool detection ───────────────────────────────────
323
- const EDITOR_PATTERNS = {
324
- vscode: /\bcode\b/,
325
- cursor: /\bcursor\b/,
326
- vim: /\b[gn]?vim\b/,
327
- intellij: /\bidea\b/,
328
- webstorm: /\bwebstorm\b/,
329
- sublime: /\bsubl\b/,
330
- emacs: /\bemacs\b/,
331
- zed: /\bzed\b/,
332
- }
333
-
334
- const AI_TOOL_PATTERNS = {
335
- claude: /\bclaude\b/,
336
- aider: /\baider\b/,
337
- codeium: /\bcodeium\b/,
338
- codex: /\bcodex\b/,
339
- copilot: /\bcopilot\b/,
340
- windsurf: /\bwindsurf\b/,
341
- ollama: /\bollama\b/,
342
- tabnine: /\btabnine\b/,
343
- supermaven: /\bsupermaven\b/,
344
- cline: /\bcline\b/,
345
- cody: /\bsourcegraph-cody\b|\bcody-agent\b/,
346
- "amazon-q": /\bamazon-q\b/,
347
- "roo-code": /\broo-code\b|\broo_code\b/,
348
- pearai: /\bpearai\b/,
349
- ghostwriter: /\bghostwriter\b/,
350
- }
351
-
352
- function detectEnvironment() {
353
- try {
354
- const ps = execSync("ps aux", { timeout: 5000, encoding: "utf8" })
355
-
356
- const editors = Object.entries(EDITOR_PATTERNS)
357
- .filter(([, re]) => re.test(ps))
358
- .map(([name]) => name)
359
-
360
- const aiTools = Object.entries(AI_TOOL_PATTERNS)
361
- .filter(([, re]) => re.test(ps))
362
- .map(([name]) => name)
363
-
364
- // Cursor is both an editor and an AI tool
365
- if (editors.includes("cursor") && !aiTools.includes("cursor")) {
366
- aiTools.push("cursor")
367
- }
368
-
369
- if (editors.length > 0 || aiTools.length > 0) {
370
- const event = { ts: new Date().toISOString(), type: "env_detected" }
371
- if (editors.length) event.editors = editors
372
- if (aiTools.length) event.aiTools = aiTools
373
- emit(event)
374
- }
375
- } catch { /* ps may fail, skip */ }
376
- }
377
-
378
- detectEnvironment() // Run immediately on startup
379
- setInterval(detectEnvironment, 10 * 60 * 1000) // Every 10 minutes
380
-
381
- // ── AI artifact file detection ──────────────────────────────────
382
- const AI_ARTIFACT_PATTERNS = [
383
- { tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
384
- { tool: "windsurf", paths: [".windsurfrules"] },
385
- { tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
386
- { tool: "continue", paths: [".continue"] },
387
- { tool: "codeium", paths: [".codeium"] },
388
- { tool: "tabnine", paths: [".tabnine"] },
389
- { tool: "codex", paths: ["codex.md", ".codex"] },
390
- ]
391
-
392
- const seenArtifacts = new Set()
393
-
394
- function checkArtifact(relPath) {
395
- if (seenArtifacts.has(relPath)) return
396
- for (const { tool, paths } of AI_ARTIFACT_PATTERNS) {
397
- for (const p of paths) {
398
- if (relPath === p || relPath.startsWith(p + "/") || relPath.startsWith(p + "\\")) {
399
- seenArtifacts.add(relPath)
400
- emit({
401
- ts: new Date().toISOString(),
402
- type: "ai_artifact_detected",
403
- tool,
404
- file: relPath,
405
- })
406
- return
407
- }
408
- }
409
- }
410
- }
411
-
412
- // Startup scan for pre-existing artifacts
413
- for (const { tool, paths } of AI_ARTIFACT_PATTERNS) {
414
- for (const p of paths) {
415
- try {
416
- if (fs.existsSync(path.join(projectDir, p))) {
417
- seenArtifacts.add(p)
418
- emit({
419
- ts: new Date().toISOString(),
420
- type: "ai_artifact_detected",
421
- tool,
422
- file: p,
423
- })
424
- }
425
- } catch { /* skip */ }
426
- }
427
- }
428
-
429
- // ── Network monitoring (AI API endpoint detection) ──────────────
430
- const AI_NETWORK_HOSTS = {
431
- "api.openai.com": "openai",
432
- "chat.openai.com": "chatgpt",
433
- "chatgpt.com": "chatgpt",
434
- "api.anthropic.com": "claude_api",
435
- "claude.ai": "claude_web",
436
- "generativelanguage.googleapis.com": "gemini_api",
437
- "gemini.google.com": "gemini_web",
438
- "copilot-proxy.githubusercontent.com": "copilot",
439
- "githubcopilot.com": "copilot",
440
- "api2.cursor.sh": "cursor",
441
- "cursor.sh": "cursor",
442
- "server.codeium.com": "codeium",
443
- "api.perplexity.ai": "perplexity",
444
- "perplexity.ai": "perplexity",
445
- "api.deepseek.com": "deepseek",
446
- "api.groq.com": "groq",
447
- "api.mistral.ai": "mistral",
448
- }
449
-
450
- // IP -> tool name cache (refreshed every 30 min)
451
- const ipToTool = new Map()
452
- let lastDnsRefresh = 0
453
- const DNS_REFRESH_MS = 30 * 60 * 1000
454
-
455
- function refreshDnsCache() {
456
- lastDnsRefresh = Date.now()
457
- for (const [host, tool] of Object.entries(AI_NETWORK_HOSTS)) {
458
- dns.resolve4(host, { ttl: false }, (err, addresses) => {
459
- if (!err && addresses) {
460
- for (const ip of addresses) {
461
- ipToTool.set(ip, { tool, host })
462
- }
463
- }
464
- })
465
- }
466
- }
467
-
468
- refreshDnsCache() // Resolve on startup
469
-
470
- let lastNetworkTools = "" // Dedup: JSON stringified tool set from previous check
471
-
472
- function detectNetworkAI() {
473
- if (Date.now() - lastDnsRefresh > DNS_REFRESH_MS) refreshDnsCache()
474
- if (ipToTool.size === 0) return // DNS hasn't resolved yet
475
-
476
- execFile("lsof", ["-i", "-n", "-P"], { timeout: 10_000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
477
- if (err) {
478
- // lsof unavailable — try ss on Linux
479
- if (err.code === "ENOENT" && process.platform === "linux") {
480
- execFile("ss", ["-tnp"], { timeout: 5000, encoding: "utf8", maxBuffer: 1024 * 1024 }, (ssErr, ssOut) => {
481
- if (!ssErr && ssOut) parseNetworkOutput(ssOut, true)
482
- })
483
- }
484
- return
485
- }
486
- if (stdout) parseNetworkOutput(stdout, false)
487
- })
488
- }
489
-
490
- function parseNetworkOutput(output, isSS) {
491
- const detected = new Map() // tool -> Set<host>
492
-
493
- for (const line of output.split("\n")) {
494
- // lsof: match destination IP in "->IP:port" pattern
495
- // ss: match destination IP in "IP:port" pattern after local address
496
- const ipMatch = isSS
497
- ? line.match(/\s(\d+\.\d+\.\d+\.\d+):(\d+)\s*$/)
498
- : line.match(/->(\d+\.\d+\.\d+\.\d+):(\d+)/)
499
- if (!ipMatch) continue
500
-
501
- const ip = ipMatch[1]
502
- const entry = ipToTool.get(ip)
503
- if (entry) {
504
- if (!detected.has(entry.tool)) detected.set(entry.tool, new Set())
505
- detected.get(entry.tool).add(entry.host)
506
- }
507
- }
508
-
509
- if (detected.size === 0) return
510
-
511
- // Dedup: don't re-emit if same tools detected as last check
512
- const toolKey = JSON.stringify([...detected.keys()].sort())
513
- if (toolKey === lastNetworkTools) return
514
- lastNetworkTools = toolKey
515
-
516
- const tools = []
517
- const hosts = []
518
- for (const [tool, hostSet] of detected) {
519
- tools.push(tool)
520
- for (const h of hostSet) hosts.push(h)
521
- }
522
-
523
- emit({
524
- ts: new Date().toISOString(),
525
- type: "network_ai_detected",
526
- tools,
527
- hosts,
528
- })
529
- }
530
-
531
- detectNetworkAI() // Run immediately
532
- setInterval(detectNetworkAI, 5 * 60 * 1000) // Every 5 minutes
533
-
534
- // ── Auto-submit at deadline ──────────────────────────────────────
535
- // NOTE: Deadline calculation duplicated from config.ts getEffectiveDeadline()
536
- // (this file is CommonJS and cannot import the TS module). Keep both in sync.
537
- if (cliBinPath && litmusConfig) {
538
- const deadlineCandidates = [
539
- litmusConfig.deadline ? new Date(litmusConfig.deadline).getTime() : Infinity,
540
- litmusConfig.startedAt && litmusConfig.timeLimit
541
- ? new Date(litmusConfig.startedAt).getTime() + litmusConfig.timeLimit * 60_000
542
- : Infinity,
543
- ].filter(t => t !== Infinity)
544
-
545
- if (deadlineCandidates.length > 0) {
546
- const effectiveDeadline = Math.min(...deadlineCandidates)
547
- const msUntilDeadline = effectiveDeadline - Date.now()
548
-
549
- const MAX_TIMEOUT = 2_147_483_647 // setTimeout max (2^31 - 1 ms, ~24.8 days)
550
- if (msUntilDeadline > 0 && msUntilDeadline <= MAX_TIMEOUT) {
551
- process.stderr.write(`[watcher] deadline timer set for ${new Date(effectiveDeadline).toISOString()} (${Math.round(msUntilDeadline / 1000)}s)\n`)
552
- setTimeout(() => {
553
- process.stderr.write("[watcher] deadline reached, auto-submitting\n")
554
- execFile(process.execPath, [cliBinPath, "submit", "--yes"], { cwd: projectDir, timeout: 5 * 60_000 }, (err, _stdout, stderr) => {
555
- if (!err) {
556
- process.stderr.write("[watcher] auto-submit succeeded\n")
557
- emit({ ts: new Date().toISOString(), type: "auto_submit" })
558
- } else {
559
- process.stderr.write(`[watcher] auto-submit failed: ${stderr || err.message}\n`)
560
- emit({ ts: new Date().toISOString(), type: "auto_submit_failed", error: (stderr || err.message).slice(0, 500) })
561
- }
562
- })
563
- }, msUntilDeadline)
564
- } else if (msUntilDeadline > MAX_TIMEOUT) {
565
- process.stderr.write(`[watcher] deadline too far in the future (${Math.round(msUntilDeadline / 86_400_000)}d), skipping auto-submit timer\n`)
566
- } else {
567
- process.stderr.write("[watcher] deadline already passed, skipping auto-submit\n")
568
- }
569
- }
570
- }
571
-
572
- // ── Graceful shutdown ────────────────────────────────────────────
573
- let shuttingDown = false
574
- function shutdown() {
575
- if (shuttingDown) return
576
- shuttingDown = true
577
-
578
- // Flush buffered events to server, then close log and exit
579
- const done = () => log.end(() => process.exit(0))
580
- const uploadPromise = uploadEvents()
581
- if (uploadPromise) {
582
- uploadPromise.then(done, done)
583
- // Hard exit after 5s if upload hangs
584
- setTimeout(() => process.exit(0), 5000).unref()
585
- } else {
586
- done()
587
- }
588
- }
589
- process.on("SIGTERM", shutdown)
590
- process.on("SIGINT", shutdown)
591
-
592
- process.stderr.write("[watcher] all monitors started\n")
package/dist/staging.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- //# sourceMappingURL=staging.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":""}
package/dist/staging.js DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- // Staging entrypoint — points the CLI at the elena preview branch
4
- process.env.LITMUS_API_URL = "https://elena.litmus.build";
5
- import("./index.js");
6
- //# sourceMappingURL=staging.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"staging.js","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":";;AACA,kEAAkE;AAClE,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,4BAA4B,CAAC;AAC1D,MAAM,CAAC,YAAY,CAAC,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/utils/detect-project.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAwDtD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/utils/detect-project.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,YAAY,6BAA6B,CAAA;AAEtD,qBAAa,UAAW,SAAQ,KAAK;;CAEpC;AAID,QAAA,IAAI,YAAY,EAAE;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAEN,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAEvE;AAaD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AA0CnD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,GAC/D,KAAK,CA2BP;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"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"}
File without changes
File without changes