@hacky.software/opencode-todo-supervisor 0.0.0-stage → 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.
package/src/log.ts ADDED
@@ -0,0 +1,77 @@
1
+ // todo-supervisor — logging.
2
+ //
3
+ // Why this exists (verified 2026-10-15, OpenCode 2.0.24):
4
+ // - `console.log` inside plugins is swallowed by the plugin runtime;
5
+ // - `console.error` reaches stderr, which is visible in standalone/CLI
6
+ // contexts (`opencode2 run --standalone --print-logs`) but is discarded by
7
+ // the detached background service;
8
+ // - there is no logger API on the plugin context.
9
+ //
10
+ // So: mirror to stderr AND append to `<data>/opencode/log/todo-supervisor.log`
11
+ // so the background service stays greppable by the `[todo-supervisor]` prefix.
12
+ import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs"
13
+ import { homedir } from "node:os"
14
+ import { join } from "node:path"
15
+
16
+ const MAX_BYTES = 2_000_000
17
+
18
+ let cached: string | undefined
19
+
20
+ function targetFile(): string | undefined {
21
+ if (cached !== undefined) return cached || undefined
22
+ try {
23
+ const dataHome = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share")
24
+ const dir = join(dataHome, "opencode", "log")
25
+ mkdirSync(dir, { recursive: true })
26
+ cached = join(dir, "todo-supervisor.log")
27
+ } catch {
28
+ cached = ""
29
+ }
30
+ return cached || undefined
31
+ }
32
+
33
+ function rotate(file: string) {
34
+ try {
35
+ if (statSync(file).size > MAX_BYTES) renameSync(file, `${file}.1`)
36
+ } catch {
37
+ // file missing or rename failed — appending either way is fine
38
+ }
39
+ }
40
+
41
+ function format(value: unknown): string {
42
+ if (typeof value === "string") return value
43
+ try {
44
+ return JSON.stringify(value)
45
+ } catch {
46
+ return String(value)
47
+ }
48
+ }
49
+
50
+ export function log(...args: unknown[]): void {
51
+ const line = `${new Date().toISOString()} [todo-supervisor] ${args.map(format).join(" ")}`
52
+ console.error(line)
53
+ const file = targetFile()
54
+ if (!file) return
55
+ try {
56
+ rotate(file)
57
+ appendFileSync(file, `${line}\n`)
58
+ } catch {
59
+ // never let logging break the plugin
60
+ }
61
+ }
62
+
63
+ /**
64
+ * File-only variant (no stderr) for environments that own the terminal — the
65
+ * TUI plugin shares this log so both sides stay greppable by the prefix.
66
+ */
67
+ export function logFile(prefix: string, ...args: unknown[]): void {
68
+ const line = `${new Date().toISOString()} [todo-supervisor/${prefix}] ${args.map(format).join(" ")}`
69
+ const file = targetFile()
70
+ if (!file) return
71
+ try {
72
+ rotate(file)
73
+ appendFileSync(file, `${line}\n`)
74
+ } catch {
75
+ // never let logging break the plugin
76
+ }
77
+ }