opencode-extended-sidebar 0.2.90 → 0.2.91
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/.oesignore +17 -17
- package/CHANGELOG.md +96 -95
- package/LICENSE +21 -21
- package/README.md +150 -235
- package/oes.json +13 -11
- package/package.json +100 -100
- package/scripts/install-git-hooks.mjs +29 -29
- package/src/pware.oc.core/constants/index.ts +18 -18
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
- package/src/pware.oc.core/git/index.ts +7 -7
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
- package/src/pware.oc.core/index.ts +19 -18
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
- package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
- package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
- package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
- package/src/pware.oc.omo/constants/index.ts +15 -15
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
- package/src/pware.oc.omo/index.ts +7 -7
- package/src/pware.oc.omo/resolver/index.ts +19 -19
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
- package/src/pware.oc.opencode/constants/index.ts +10 -10
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
- package/src/pware.oc.opencode/index.ts +8 -8
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
- package/src/pware.oc.opencode/resolver/index.ts +223 -170
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
- package/src/pware.oc.perf/index.ts +14 -14
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
- package/src/pware.oc.runtime/index.ts +13 -13
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
- package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
- package/src/pware.oc.runtime/resolver/index.ts +268 -224
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
- package/src/pware.oc.ui/index.ts +12 -12
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
- package/src/pware.oc.ui.tsx +98 -98
|
@@ -1,280 +1,280 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional file-based debug + profile loggers, plus a tiny on-screen event
|
|
3
|
-
* ring for the sidebar's read-only debug console.
|
|
4
|
-
*
|
|
5
|
-
* Debug: OES_DEBUG_OPENCODE=1 → writes to <plugin>/logs/
|
|
6
|
-
* Profile: OES_DEBUG_PROFILE=1 → writes to <plugin>/logs/
|
|
7
|
-
* Either OES_DEBUG_*=/some/dir → writes to that directory
|
|
8
|
-
*
|
|
9
|
-
* Each session appends a single oes-debug-<date>.log / oes-profile-<date>.log
|
|
10
|
-
* file. Lines are newline-delimited JSON:
|
|
11
|
-
* debug → { ts, tag, msg, data? }
|
|
12
|
-
* profile → { ts, tag, ms, data? }
|
|
13
|
-
* All writes are silent on error — the plugin never crashes because of logging.
|
|
14
|
-
*
|
|
15
|
-
* The screen ring (`pushScreenLine`) is a module-level 200-line buffer of short
|
|
16
|
-
* labels (e.g. `db open`) that the sidebar console subscribes to; it is only
|
|
17
|
-
* fed while a logger is active, so production pays nothing.
|
|
18
|
-
*/
|
|
19
|
-
import fs from "node:fs"
|
|
20
|
-
import path from "node:path"
|
|
21
|
-
import { pluginRoot } from "./pware.oc.core.paths.js"
|
|
22
|
-
|
|
23
|
-
const DEBUG_ENV_KEY = "OES_DEBUG_OPENCODE"
|
|
24
|
-
const PROFILE_ENV_KEY = "OES_DEBUG_PROFILE"
|
|
25
|
-
|
|
26
|
-
function pluginLogsDir(): string {
|
|
27
|
-
return path.join(pluginRoot(), "logs")
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resolveLogDir(
|
|
31
|
-
envKey: string,
|
|
32
|
-
env: Record<string, string | undefined>,
|
|
33
|
-
defaultDir?: string,
|
|
34
|
-
): string | null {
|
|
35
|
-
const val = env[envKey]
|
|
36
|
-
if (!val) return null
|
|
37
|
-
const trimmed = val.trim()
|
|
38
|
-
if (!trimmed) return null
|
|
39
|
-
const FALSY = new Set(["0", "false", "no", "off"])
|
|
40
|
-
if (FALSY.has(trimmed.toLowerCase())) return null
|
|
41
|
-
const TRUTHY = new Set(["1", "true", "yes", "on"])
|
|
42
|
-
if (TRUTHY.has(trimmed.toLowerCase())) return defaultDir ?? pluginLogsDir()
|
|
43
|
-
return trimmed
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Returns the debug log directory, or null when debug is off. Exported for tests. */
|
|
47
|
-
export function debugLogDir(
|
|
48
|
-
env: Record<string, string | undefined> = process.env,
|
|
49
|
-
defaultDir?: string,
|
|
50
|
-
): string | null {
|
|
51
|
-
return resolveLogDir(DEBUG_ENV_KEY, env, defaultDir)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Returns the profile log directory, or null when profiling is off. Exported for tests. */
|
|
55
|
-
export function profileLogDir(
|
|
56
|
-
env: Record<string, string | undefined> = process.env,
|
|
57
|
-
defaultDir?: string,
|
|
58
|
-
): string | null {
|
|
59
|
-
return resolveLogDir(PROFILE_ENV_KEY, env, defaultDir)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** True when the debug logger is active — env on AND its log dir resolved/writable. */
|
|
63
|
-
export function debugActive(): boolean {
|
|
64
|
-
return resolvedDir() !== null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function debugActiveDir(): string | null {
|
|
68
|
-
return resolvedDir()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** True when the profile logger is active — env on AND its log dir resolved/writable. */
|
|
72
|
-
export function profileActive(): boolean {
|
|
73
|
-
return profileResolvedDir() !== null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function profileActiveDir(): string | null {
|
|
77
|
-
return profileResolvedDir()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function datedLogFile(prefix: string): string {
|
|
81
|
-
const d = new Date()
|
|
82
|
-
const pad = (n: number) => String(n).padStart(2, "0")
|
|
83
|
-
return `oes-${prefix}-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.log`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let _dir: string | null | undefined = undefined // undefined = not resolved yet
|
|
87
|
-
let _pdir: string | null | undefined = undefined
|
|
88
|
-
|
|
89
|
-
function resolvedDir(): string | null {
|
|
90
|
-
if (_dir !== undefined) return _dir
|
|
91
|
-
_dir = debugLogDir()
|
|
92
|
-
if (_dir) {
|
|
93
|
-
try {
|
|
94
|
-
fs.mkdirSync(_dir, { recursive: true })
|
|
95
|
-
} catch {
|
|
96
|
-
_dir = null
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return _dir
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function profileResolvedDir(): string | null {
|
|
103
|
-
if (_pdir !== undefined) return _pdir
|
|
104
|
-
_pdir = profileLogDir()
|
|
105
|
-
if (_pdir) {
|
|
106
|
-
try {
|
|
107
|
-
fs.mkdirSync(_pdir, { recursive: true })
|
|
108
|
-
} catch {
|
|
109
|
-
_pdir = null
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return _pdir
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Reset cached state (test helper). */
|
|
116
|
-
export function resetDebug(): void {
|
|
117
|
-
_dir = undefined
|
|
118
|
-
_pdir = undefined
|
|
119
|
-
profileStats = {}
|
|
120
|
-
screenLines = []
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** One on-screen console line: a timestamp plus a short label (e.g. `db open`). */
|
|
124
|
-
export type ScreenLine = { at: number; text: string }
|
|
125
|
-
|
|
126
|
-
/** Ring cap — the console shows a 5-line window over these, scrolled. */
|
|
127
|
-
const SCREEN_MAX = 200
|
|
128
|
-
|
|
129
|
-
let screenLines: ScreenLine[] = []
|
|
130
|
-
const screenListeners = new Set<() => void>()
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Append one short label (e.g. `db open`) to the sidebar console's screen ring
|
|
134
|
-
* and notify subscribers. No-op unless a debug or profile logger is active, so
|
|
135
|
-
* a normal session pays nothing for it.
|
|
136
|
-
*/
|
|
137
|
-
export function pushScreenLine(text: string): void {
|
|
138
|
-
if (!debugActive() && !profileActive()) return
|
|
139
|
-
screenLines.push({ at: Date.now(), text })
|
|
140
|
-
if (screenLines.length > SCREEN_MAX) screenLines.splice(0, screenLines.length - SCREEN_MAX)
|
|
141
|
-
for (const listener of screenListeners) {
|
|
142
|
-
try {
|
|
143
|
-
listener()
|
|
144
|
-
} catch {
|
|
145
|
-
// a subscriber must never break the ring feed
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Newest-first copy of the screen ring (empty when nothing was pushed). */
|
|
151
|
-
export function readScreenLines(): ScreenLine[] {
|
|
152
|
-
return screenLines.slice().reverse()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Subscribe to screen-ring pushes; returns an unsubscribe fn. */
|
|
156
|
-
export function subscribeScreenLines(listener: () => void): () => void {
|
|
157
|
-
screenListeners.add(listener)
|
|
158
|
-
return () => {
|
|
159
|
-
screenListeners.delete(listener)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function appendLine(dir: string, file: string, line: Record<string, unknown>): void {
|
|
164
|
-
try {
|
|
165
|
-
const entry = { ts: new Date().toISOString(), ...line }
|
|
166
|
-
fs.appendFileSync(path.join(dir, file), JSON.stringify(entry) + "\n", "utf8")
|
|
167
|
-
} catch {
|
|
168
|
-
// silent — logging must never crash the plugin
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Append one JSON-line entry to the debug log.
|
|
174
|
-
* No-op when OES_DEBUG_OPENCODE is not set or falsy.
|
|
175
|
-
*/
|
|
176
|
-
export function dbg(tag: string, msg: string, data?: unknown): void {
|
|
177
|
-
const dir = resolvedDir()
|
|
178
|
-
if (!dir) return
|
|
179
|
-
appendLine(dir, datedLogFile("debug"), {
|
|
180
|
-
tag,
|
|
181
|
-
msg,
|
|
182
|
-
...(data !== undefined ? { data } : {}),
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export type ProfileStats = { n: number; sum: number; max: number }
|
|
187
|
-
|
|
188
|
-
/** In-memory per-tag totals, updated only while OES_DEBUG_PROFILE is on. */
|
|
189
|
-
let profileStats: Record<string, ProfileStats> = {}
|
|
190
|
-
|
|
191
|
-
/** Lazily-computed or plain profile data — a factory keeps the call site free when off. */
|
|
192
|
-
type ProfileData = Record<string, unknown> | (() => Record<string, unknown>)
|
|
193
|
-
|
|
194
|
-
function resolveData(data: ProfileData | undefined): Record<string, unknown> | undefined {
|
|
195
|
-
const d = typeof data === "function" ? data() : data
|
|
196
|
-
return d && Object.keys(d).length > 0 ? d : undefined
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function recordProfile(tag: string, ms: number, data?: ProfileData): void {
|
|
200
|
-
const dir = profileResolvedDir()
|
|
201
|
-
if (!dir) return
|
|
202
|
-
const s = (profileStats[tag] ??= { n: 0, sum: 0, max: 0 })
|
|
203
|
-
s.n += 1
|
|
204
|
-
s.sum += ms
|
|
205
|
-
if (ms > s.max) s.max = ms
|
|
206
|
-
const d = resolveData(data)
|
|
207
|
-
appendLine(dir, datedLogFile("profile"), {
|
|
208
|
-
tag,
|
|
209
|
-
ms,
|
|
210
|
-
...(d !== undefined ? { data: d } : {}),
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Measure fn with performance.now() and append one { tag, ms, data? } line to
|
|
216
|
-
* the profile log. No-op when OES_DEBUG_PROFILE is not set or falsy. Returns
|
|
217
|
-
* fn's result; throws propagate.
|
|
218
|
-
*/
|
|
219
|
-
export function profile<T>(tag: string, fn: () => T, data?: ProfileData): T {
|
|
220
|
-
if (!profileResolvedDir()) return fn()
|
|
221
|
-
const t0 = performance.now()
|
|
222
|
-
try {
|
|
223
|
-
return fn()
|
|
224
|
-
} finally {
|
|
225
|
-
recordProfile(tag, performance.now() - t0, data)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Same as profile(), but for an async fn — measures until the returned promise
|
|
231
|
-
* settles. The fn is invoked immediately (the promise is what is timed).
|
|
232
|
-
*/
|
|
233
|
-
export function profileAsync<T>(
|
|
234
|
-
tag: string,
|
|
235
|
-
fn: () => Promise<T>,
|
|
236
|
-
data?: ProfileData,
|
|
237
|
-
): Promise<T> {
|
|
238
|
-
if (!profileResolvedDir()) return fn()
|
|
239
|
-
const t0 = performance.now()
|
|
240
|
-
return fn().then(
|
|
241
|
-
(value) => {
|
|
242
|
-
recordProfile(tag, performance.now() - t0, data)
|
|
243
|
-
return value
|
|
244
|
-
},
|
|
245
|
-
(err) => {
|
|
246
|
-
recordProfile(tag, performance.now() - t0, data)
|
|
247
|
-
throw err
|
|
248
|
-
},
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Shallow copy of the per-tag profile totals. Empty when profiling is off. */
|
|
253
|
-
export function readProfileStats(): Record<string, ProfileStats> {
|
|
254
|
-
const out: Record<string, ProfileStats> = {}
|
|
255
|
-
for (const [tag, s] of Object.entries(profileStats)) out[tag] = { ...s }
|
|
256
|
-
return out
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Append one `summary` line with the per-tag totals (n, total ms, avg, max) to
|
|
261
|
-
* the profile log. No-op when profiling is off or nothing was measured.
|
|
262
|
-
*/
|
|
263
|
-
export function writeProfileSummary(): void {
|
|
264
|
-
const dir = profileResolvedDir()
|
|
265
|
-
if (!dir) return
|
|
266
|
-
const stats = readProfileStats()
|
|
267
|
-
const tags = Object.keys(stats)
|
|
268
|
-
if (tags.length === 0) return
|
|
269
|
-
const summary: Record<string, { n: number; total: number; avg: number; max: number }> = {}
|
|
270
|
-
for (const tag of tags) {
|
|
271
|
-
const s = stats[tag]
|
|
272
|
-
summary[tag] = {
|
|
273
|
-
n: s.n,
|
|
274
|
-
total: Math.round(s.sum * 10) / 10,
|
|
275
|
-
avg: Math.round((s.sum / s.n) * 10) / 10,
|
|
276
|
-
max: Math.round(s.max * 10) / 10,
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
appendLine(dir, datedLogFile("profile"), { tag: "summary", data: summary })
|
|
280
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Optional file-based debug + profile loggers, plus a tiny on-screen event
|
|
3
|
+
* ring for the sidebar's read-only debug console.
|
|
4
|
+
*
|
|
5
|
+
* Debug: OES_DEBUG_OPENCODE=1 → writes to <plugin>/logs/
|
|
6
|
+
* Profile: OES_DEBUG_PROFILE=1 → writes to <plugin>/logs/
|
|
7
|
+
* Either OES_DEBUG_*=/some/dir → writes to that directory
|
|
8
|
+
*
|
|
9
|
+
* Each session appends a single oes-debug-<date>.log / oes-profile-<date>.log
|
|
10
|
+
* file. Lines are newline-delimited JSON:
|
|
11
|
+
* debug → { ts, tag, msg, data? }
|
|
12
|
+
* profile → { ts, tag, ms, data? }
|
|
13
|
+
* All writes are silent on error — the plugin never crashes because of logging.
|
|
14
|
+
*
|
|
15
|
+
* The screen ring (`pushScreenLine`) is a module-level 200-line buffer of short
|
|
16
|
+
* labels (e.g. `db open`) that the sidebar console subscribes to; it is only
|
|
17
|
+
* fed while a logger is active, so production pays nothing.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs"
|
|
20
|
+
import path from "node:path"
|
|
21
|
+
import { pluginRoot } from "./pware.oc.core.paths.js"
|
|
22
|
+
|
|
23
|
+
const DEBUG_ENV_KEY = "OES_DEBUG_OPENCODE"
|
|
24
|
+
const PROFILE_ENV_KEY = "OES_DEBUG_PROFILE"
|
|
25
|
+
|
|
26
|
+
function pluginLogsDir(): string {
|
|
27
|
+
return path.join(pluginRoot(), "logs")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveLogDir(
|
|
31
|
+
envKey: string,
|
|
32
|
+
env: Record<string, string | undefined>,
|
|
33
|
+
defaultDir?: string,
|
|
34
|
+
): string | null {
|
|
35
|
+
const val = env[envKey]
|
|
36
|
+
if (!val) return null
|
|
37
|
+
const trimmed = val.trim()
|
|
38
|
+
if (!trimmed) return null
|
|
39
|
+
const FALSY = new Set(["0", "false", "no", "off"])
|
|
40
|
+
if (FALSY.has(trimmed.toLowerCase())) return null
|
|
41
|
+
const TRUTHY = new Set(["1", "true", "yes", "on"])
|
|
42
|
+
if (TRUTHY.has(trimmed.toLowerCase())) return defaultDir ?? pluginLogsDir()
|
|
43
|
+
return trimmed
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the debug log directory, or null when debug is off. Exported for tests. */
|
|
47
|
+
export function debugLogDir(
|
|
48
|
+
env: Record<string, string | undefined> = process.env,
|
|
49
|
+
defaultDir?: string,
|
|
50
|
+
): string | null {
|
|
51
|
+
return resolveLogDir(DEBUG_ENV_KEY, env, defaultDir)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Returns the profile log directory, or null when profiling is off. Exported for tests. */
|
|
55
|
+
export function profileLogDir(
|
|
56
|
+
env: Record<string, string | undefined> = process.env,
|
|
57
|
+
defaultDir?: string,
|
|
58
|
+
): string | null {
|
|
59
|
+
return resolveLogDir(PROFILE_ENV_KEY, env, defaultDir)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when the debug logger is active — env on AND its log dir resolved/writable. */
|
|
63
|
+
export function debugActive(): boolean {
|
|
64
|
+
return resolvedDir() !== null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function debugActiveDir(): string | null {
|
|
68
|
+
return resolvedDir()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when the profile logger is active — env on AND its log dir resolved/writable. */
|
|
72
|
+
export function profileActive(): boolean {
|
|
73
|
+
return profileResolvedDir() !== null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function profileActiveDir(): string | null {
|
|
77
|
+
return profileResolvedDir()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function datedLogFile(prefix: string): string {
|
|
81
|
+
const d = new Date()
|
|
82
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
83
|
+
return `oes-${prefix}-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.log`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let _dir: string | null | undefined = undefined // undefined = not resolved yet
|
|
87
|
+
let _pdir: string | null | undefined = undefined
|
|
88
|
+
|
|
89
|
+
function resolvedDir(): string | null {
|
|
90
|
+
if (_dir !== undefined) return _dir
|
|
91
|
+
_dir = debugLogDir()
|
|
92
|
+
if (_dir) {
|
|
93
|
+
try {
|
|
94
|
+
fs.mkdirSync(_dir, { recursive: true })
|
|
95
|
+
} catch {
|
|
96
|
+
_dir = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return _dir
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function profileResolvedDir(): string | null {
|
|
103
|
+
if (_pdir !== undefined) return _pdir
|
|
104
|
+
_pdir = profileLogDir()
|
|
105
|
+
if (_pdir) {
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(_pdir, { recursive: true })
|
|
108
|
+
} catch {
|
|
109
|
+
_pdir = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return _pdir
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reset cached state (test helper). */
|
|
116
|
+
export function resetDebug(): void {
|
|
117
|
+
_dir = undefined
|
|
118
|
+
_pdir = undefined
|
|
119
|
+
profileStats = {}
|
|
120
|
+
screenLines = []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One on-screen console line: a timestamp plus a short label (e.g. `db open`). */
|
|
124
|
+
export type ScreenLine = { at: number; text: string }
|
|
125
|
+
|
|
126
|
+
/** Ring cap — the console shows a 5-line window over these, scrolled. */
|
|
127
|
+
const SCREEN_MAX = 200
|
|
128
|
+
|
|
129
|
+
let screenLines: ScreenLine[] = []
|
|
130
|
+
const screenListeners = new Set<() => void>()
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Append one short label (e.g. `db open`) to the sidebar console's screen ring
|
|
134
|
+
* and notify subscribers. No-op unless a debug or profile logger is active, so
|
|
135
|
+
* a normal session pays nothing for it.
|
|
136
|
+
*/
|
|
137
|
+
export function pushScreenLine(text: string): void {
|
|
138
|
+
if (!debugActive() && !profileActive()) return
|
|
139
|
+
screenLines.push({ at: Date.now(), text })
|
|
140
|
+
if (screenLines.length > SCREEN_MAX) screenLines.splice(0, screenLines.length - SCREEN_MAX)
|
|
141
|
+
for (const listener of screenListeners) {
|
|
142
|
+
try {
|
|
143
|
+
listener()
|
|
144
|
+
} catch {
|
|
145
|
+
// a subscriber must never break the ring feed
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Newest-first copy of the screen ring (empty when nothing was pushed). */
|
|
151
|
+
export function readScreenLines(): ScreenLine[] {
|
|
152
|
+
return screenLines.slice().reverse()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Subscribe to screen-ring pushes; returns an unsubscribe fn. */
|
|
156
|
+
export function subscribeScreenLines(listener: () => void): () => void {
|
|
157
|
+
screenListeners.add(listener)
|
|
158
|
+
return () => {
|
|
159
|
+
screenListeners.delete(listener)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function appendLine(dir: string, file: string, line: Record<string, unknown>): void {
|
|
164
|
+
try {
|
|
165
|
+
const entry = { ts: new Date().toISOString(), ...line }
|
|
166
|
+
fs.appendFileSync(path.join(dir, file), JSON.stringify(entry) + "\n", "utf8")
|
|
167
|
+
} catch {
|
|
168
|
+
// silent — logging must never crash the plugin
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Append one JSON-line entry to the debug log.
|
|
174
|
+
* No-op when OES_DEBUG_OPENCODE is not set or falsy.
|
|
175
|
+
*/
|
|
176
|
+
export function dbg(tag: string, msg: string, data?: unknown): void {
|
|
177
|
+
const dir = resolvedDir()
|
|
178
|
+
if (!dir) return
|
|
179
|
+
appendLine(dir, datedLogFile("debug"), {
|
|
180
|
+
tag,
|
|
181
|
+
msg,
|
|
182
|
+
...(data !== undefined ? { data } : {}),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type ProfileStats = { n: number; sum: number; max: number }
|
|
187
|
+
|
|
188
|
+
/** In-memory per-tag totals, updated only while OES_DEBUG_PROFILE is on. */
|
|
189
|
+
let profileStats: Record<string, ProfileStats> = {}
|
|
190
|
+
|
|
191
|
+
/** Lazily-computed or plain profile data — a factory keeps the call site free when off. */
|
|
192
|
+
type ProfileData = Record<string, unknown> | (() => Record<string, unknown>)
|
|
193
|
+
|
|
194
|
+
function resolveData(data: ProfileData | undefined): Record<string, unknown> | undefined {
|
|
195
|
+
const d = typeof data === "function" ? data() : data
|
|
196
|
+
return d && Object.keys(d).length > 0 ? d : undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function recordProfile(tag: string, ms: number, data?: ProfileData): void {
|
|
200
|
+
const dir = profileResolvedDir()
|
|
201
|
+
if (!dir) return
|
|
202
|
+
const s = (profileStats[tag] ??= { n: 0, sum: 0, max: 0 })
|
|
203
|
+
s.n += 1
|
|
204
|
+
s.sum += ms
|
|
205
|
+
if (ms > s.max) s.max = ms
|
|
206
|
+
const d = resolveData(data)
|
|
207
|
+
appendLine(dir, datedLogFile("profile"), {
|
|
208
|
+
tag,
|
|
209
|
+
ms,
|
|
210
|
+
...(d !== undefined ? { data: d } : {}),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Measure fn with performance.now() and append one { tag, ms, data? } line to
|
|
216
|
+
* the profile log. No-op when OES_DEBUG_PROFILE is not set or falsy. Returns
|
|
217
|
+
* fn's result; throws propagate.
|
|
218
|
+
*/
|
|
219
|
+
export function profile<T>(tag: string, fn: () => T, data?: ProfileData): T {
|
|
220
|
+
if (!profileResolvedDir()) return fn()
|
|
221
|
+
const t0 = performance.now()
|
|
222
|
+
try {
|
|
223
|
+
return fn()
|
|
224
|
+
} finally {
|
|
225
|
+
recordProfile(tag, performance.now() - t0, data)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Same as profile(), but for an async fn — measures until the returned promise
|
|
231
|
+
* settles. The fn is invoked immediately (the promise is what is timed).
|
|
232
|
+
*/
|
|
233
|
+
export function profileAsync<T>(
|
|
234
|
+
tag: string,
|
|
235
|
+
fn: () => Promise<T>,
|
|
236
|
+
data?: ProfileData,
|
|
237
|
+
): Promise<T> {
|
|
238
|
+
if (!profileResolvedDir()) return fn()
|
|
239
|
+
const t0 = performance.now()
|
|
240
|
+
return fn().then(
|
|
241
|
+
(value) => {
|
|
242
|
+
recordProfile(tag, performance.now() - t0, data)
|
|
243
|
+
return value
|
|
244
|
+
},
|
|
245
|
+
(err) => {
|
|
246
|
+
recordProfile(tag, performance.now() - t0, data)
|
|
247
|
+
throw err
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Shallow copy of the per-tag profile totals. Empty when profiling is off. */
|
|
253
|
+
export function readProfileStats(): Record<string, ProfileStats> {
|
|
254
|
+
const out: Record<string, ProfileStats> = {}
|
|
255
|
+
for (const [tag, s] of Object.entries(profileStats)) out[tag] = { ...s }
|
|
256
|
+
return out
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Append one `summary` line with the per-tag totals (n, total ms, avg, max) to
|
|
261
|
+
* the profile log. No-op when profiling is off or nothing was measured.
|
|
262
|
+
*/
|
|
263
|
+
export function writeProfileSummary(): void {
|
|
264
|
+
const dir = profileResolvedDir()
|
|
265
|
+
if (!dir) return
|
|
266
|
+
const stats = readProfileStats()
|
|
267
|
+
const tags = Object.keys(stats)
|
|
268
|
+
if (tags.length === 0) return
|
|
269
|
+
const summary: Record<string, { n: number; total: number; avg: number; max: number }> = {}
|
|
270
|
+
for (const tag of tags) {
|
|
271
|
+
const s = stats[tag]
|
|
272
|
+
summary[tag] = {
|
|
273
|
+
n: s.n,
|
|
274
|
+
total: Math.round(s.sum * 10) / 10,
|
|
275
|
+
avg: Math.round((s.sum / s.n) * 10) / 10,
|
|
276
|
+
max: Math.round(s.max * 10) / 10,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
appendLine(dir, datedLogFile("profile"), { tag: "summary", data: summary })
|
|
280
|
+
}
|