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.
Files changed (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,240 +1,240 @@
1
- /**
2
- * Resolve OpenCode data paths (XDG + Windows + OPENCODE_DB_PATH).
3
- * Zero deps — node:fs / path / os only.
4
- */
5
- import fs from "node:fs"
6
- import os from "node:os"
7
- import path from "node:path"
8
- import { fileURLToPath } from "node:url"
9
-
10
- export type Env = Record<string, string | undefined>
11
-
12
- /** Absolute path to the plugin's root directory (one level above `src/`). */
13
- export function pluginRoot(): string {
14
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")
15
- }
16
-
17
- export function getDataDir(env: Env = process.env, homedir = os.homedir()): string {
18
- let dataDir = env.XDG_DATA_HOME
19
- if (dataDir) {
20
- if (dataDir === "~") dataDir = homedir
21
- else if (dataDir.startsWith("~/") || dataDir.startsWith("~\\")) {
22
- dataDir = path.join(homedir, dataDir.slice(2).replace(/[\\/]+/g, path.sep))
23
- }
24
- }
25
- return dataDir ?? path.join(homedir, ".local", "share")
26
- }
27
-
28
- export function getOpenCodeConfigDir(env: Env = process.env, homedir = os.homedir()): string {
29
- const xdg = env.XDG_CONFIG_HOME
30
- if (xdg) {
31
- const root = xdg === "~" ? homedir : xdg.startsWith("~/") || xdg.startsWith("~\\")
32
- ? path.join(homedir, xdg.slice(2).replace(/[\\/]+/g, path.sep))
33
- : xdg
34
- return path.join(root, "opencode")
35
- }
36
- return path.join(homedir, ".config", "opencode")
37
- }
38
-
39
- export function getOpenCodeRoot(env: Env = process.env, homedir = os.homedir()): string {
40
- return path.join(getDataDir(env, homedir), "opencode")
41
- }
42
-
43
- /** Local agentize DB path relative to a project root, or null when absent. */
44
- export function localAgentizeDbPath(projectRoot: string): string | null {
45
- const p = path.join(projectRoot, ".agentize", "opencode", "opencode.db")
46
- return fs.existsSync(p) ? p : null
47
- }
48
-
49
- /**
50
- * Resolve the opencode.db path — mirrors opencode's own resolution
51
- * (`packages/opencode/src/storage/db.ts`): `OPENCODE_DB` first, then the
52
- * XDG / home-based global path. agentize pins the exact DB with
53
- * `OPENCODE_DB` (an absolute path), so it must win over the heuristics.
54
- *
55
- * Priority:
56
- * 1. OPENCODE_DB env var (absolute → verbatim; relative → under the data dir)
57
- * 2. OPENCODE_DB_PATH env var (legacy alias)
58
- * 3. .agentize/opencode/opencode.db under projectRoot (when supplied)
59
- * 4. XDG / home-based global path
60
- */
61
- export function getOpenCodeDbPath(
62
- env: Env = process.env,
63
- homedir = os.homedir(),
64
- projectRoot?: string | null,
65
- ): string {
66
- const db = env.OPENCODE_DB
67
- if (db && db !== ":memory:") {
68
- return path.isAbsolute(db)
69
- ? path.resolve(db)
70
- : path.join(getOpenCodeRoot(env, homedir), db)
71
- }
72
- if (env.OPENCODE_DB_PATH) return path.resolve(env.OPENCODE_DB_PATH)
73
- if (projectRoot) {
74
- const local = localAgentizeDbPath(projectRoot)
75
- if (local) return local
76
- }
77
- return path.join(getOpenCodeRoot(env, homedir), "opencode.db")
78
- }
79
-
80
- export function getSessionDiffPath(
81
- sessionId: string,
82
- env: Env = process.env,
83
- homedir = os.homedir(),
84
- ): string {
85
- return path.join(getOpenCodeRoot(env, homedir), "storage", "session_diff", `${sessionId}.json`)
86
- }
87
-
88
- export function realpathSafe(p: string): string | null {
89
- try {
90
- return fs.realpathSync(p)
91
- } catch {
92
- return null
93
- }
94
- }
95
-
96
- /** Last path segment. Never a directory. */
97
- export function basenameOf(p: string): string {
98
- const t = p.replace(/\\/g, "/").replace(/\/+$/, "").trim()
99
- if (!t) return "file"
100
- const i = t.lastIndexOf("/")
101
- const base = i >= 0 ? t.slice(i + 1) : t
102
- return base || "file"
103
- }
104
-
105
- /** Positive finite number, else 0. */
106
- export function finiteNum(v: unknown): number {
107
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0
108
- }
109
-
110
- /** Trimmed non-empty string, else null. */
111
- export function str(v: unknown): string | null {
112
- if (typeof v === "string" && v.trim()) return v.trim()
113
- return null
114
- }
115
-
116
- export function canonicalizePath(p: string): string {
117
- const resolved = path.resolve(p)
118
- return path.normalize(realpathSafe(resolved) ?? resolved)
119
- }
120
-
121
- /**
122
- * OpenCode/git often emit posix paths (`D:/a/b` or `/d/a/b`).
123
- * Fold those onto the host path so Windows `D:\` matches.
124
- */
125
- export function normalizeIncomingPath(p: string): string {
126
- let t = p.trim().replace(/\\/g, "/")
127
- const m = t.match(/^\/([A-Za-z])\/(.*)$/)
128
- if (m) t = `${m[1]}:/${m[2]}`
129
- return t
130
- }
131
-
132
- /**
133
- * Fold an absolute path/directory into one comparison key so two spellings of
134
- * the same location compare equal: posix separators (`D:/` and `D:\`), a
135
- * git-bash drive prefix (`/d/…`), trailing slashes, and drive-letter case
136
- * (`d:/` from git-bash vs `D:/`). Path case is folded only on Windows, where
137
- * the filesystem is case-insensitive.
138
- */
139
- export function foldPathKey(p: string | null | undefined): string {
140
- if (!p) return ""
141
- let t = normalizeIncomingPath(p).replace(/\/+$/, "")
142
- const drive = t.match(/^([a-z]):\//)
143
- if (drive) t = `${drive[1]!.toUpperCase()}:${t.slice(2)}`
144
- return process.platform === "win32" ? t.toLowerCase() : t
145
- }
146
-
147
- /** True when two directory/file paths point at the same location. */
148
- export function samePath(a: string | null | undefined, b: string | null | undefined): boolean {
149
- return foldPathKey(a) === foldPathKey(b)
150
- }
151
-
152
- function isInsideRoot(root: string, abs: string): boolean {
153
- const rel = path.relative(root, abs)
154
- if (!rel || path.isAbsolute(rel)) return false
155
- const n = rel.replace(/\\/g, "/")
156
- if (n === ".." || n.startsWith("../")) return false
157
- return true
158
- }
159
-
160
- function asRootList(roots: string | readonly string[] | null | undefined): string[] {
161
- if (!roots) return []
162
- const list = typeof roots === "string" ? [roots] : [...roots]
163
- const out: string[] = []
164
- const seen = new Set<string>()
165
- for (const r of list) {
166
- if (!r || typeof r !== "string") continue
167
- let key = r
168
- try {
169
- key = canonicalizePath(r)
170
- } catch {
171
- key = r
172
- }
173
- const fold = process.platform === "win32" ? key.toLowerCase() : key
174
- if (seen.has(fold)) continue
175
- seen.add(fold)
176
- out.push(r)
177
- }
178
- return out
179
- }
180
-
181
- /** Absolute path + project-relative posix path, or null if outside / missing. */
182
- export function resolveProjectFile(
183
- roots: string | readonly string[] | null | undefined,
184
- filePath: string | null | undefined,
185
- ): { abs: string; rel: string } | null {
186
- if (!filePath) return null
187
- const incoming = normalizeIncomingPath(filePath)
188
- for (const rawRoot of asRootList(roots)) {
189
- try {
190
- const root = canonicalizePath(rawRoot)
191
- const abs = path.isAbsolute(incoming)
192
- ? canonicalizePath(incoming)
193
- : canonicalizePath(path.join(root, incoming))
194
- if (!isInsideRoot(root, abs)) continue
195
- if (!fs.existsSync(abs)) continue
196
- const rel = path.relative(root, abs).replace(/\\/g, "/")
197
- if (!rel) continue
198
- return { abs, rel }
199
- } catch {
200
- // next root
201
- }
202
- }
203
- return null
204
- }
205
-
206
- /** Project-relative path for detail dialogs — never escapes above project root. */
207
- export function relativeProjectPath(
208
- projectRoot: string | null | undefined,
209
- filePath: string | null | undefined,
210
- ): string | null {
211
- return resolveProjectFile(projectRoot, filePath)?.rel ?? null
212
- }
213
-
214
- /** Read a JSON file as a plain object, soft-failing to null (missing / invalid / non-object). */
215
- export function readJson(p: string): Record<string, unknown> | null {
216
- try {
217
- if (!fs.existsSync(p)) return null
218
- const raw = JSON.parse(fs.readFileSync(p, "utf8"))
219
- return raw && typeof raw === "object" && !Array.isArray(raw)
220
- ? (raw as Record<string, unknown>)
221
- : null
222
- } catch {
223
- return null
224
- }
225
- }
226
-
227
- export function fileStamp(p: string | null | undefined): string {
228
- if (!p) return "0"
229
- try {
230
- const st = fs.statSync(p)
231
- return `${st.mtimeMs}:${st.size}`
232
- } catch {
233
- return "0"
234
- }
235
- }
236
-
237
- /** DB + WAL/SHM stamps — WAL writers change companion files, not always .db mtime. */
238
- export function dbStamp(dbPath: string): string {
239
- return [dbPath, `${dbPath}-wal`, `${dbPath}-shm`].map(fileStamp).join("|")
240
- }
1
+ /**
2
+ * Resolve OpenCode data paths (XDG + Windows + OPENCODE_DB_PATH).
3
+ * Zero deps — node:fs / path / os only.
4
+ */
5
+ import fs from "node:fs"
6
+ import os from "node:os"
7
+ import path from "node:path"
8
+ import { fileURLToPath } from "node:url"
9
+
10
+ export type Env = Record<string, string | undefined>
11
+
12
+ /** Absolute path to the plugin's root directory (one level above `src/`). */
13
+ export function pluginRoot(): string {
14
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")
15
+ }
16
+
17
+ export function getDataDir(env: Env = process.env, homedir = os.homedir()): string {
18
+ let dataDir = env.XDG_DATA_HOME
19
+ if (dataDir) {
20
+ if (dataDir === "~") dataDir = homedir
21
+ else if (dataDir.startsWith("~/") || dataDir.startsWith("~\\")) {
22
+ dataDir = path.join(homedir, dataDir.slice(2).replace(/[\\/]+/g, path.sep))
23
+ }
24
+ }
25
+ return dataDir ?? path.join(homedir, ".local", "share")
26
+ }
27
+
28
+ export function getOpenCodeConfigDir(env: Env = process.env, homedir = os.homedir()): string {
29
+ const xdg = env.XDG_CONFIG_HOME
30
+ if (xdg) {
31
+ const root = xdg === "~" ? homedir : xdg.startsWith("~/") || xdg.startsWith("~\\")
32
+ ? path.join(homedir, xdg.slice(2).replace(/[\\/]+/g, path.sep))
33
+ : xdg
34
+ return path.join(root, "opencode")
35
+ }
36
+ return path.join(homedir, ".config", "opencode")
37
+ }
38
+
39
+ export function getOpenCodeRoot(env: Env = process.env, homedir = os.homedir()): string {
40
+ return path.join(getDataDir(env, homedir), "opencode")
41
+ }
42
+
43
+ /** Local agentize DB path relative to a project root, or null when absent. */
44
+ export function localAgentizeDbPath(projectRoot: string): string | null {
45
+ const p = path.join(projectRoot, ".agentize", "opencode", "opencode.db")
46
+ return fs.existsSync(p) ? p : null
47
+ }
48
+
49
+ /**
50
+ * Resolve the opencode.db path — mirrors opencode's own resolution
51
+ * (`packages/opencode/src/storage/db.ts`): `OPENCODE_DB` first, then the
52
+ * XDG / home-based global path. agentize pins the exact DB with
53
+ * `OPENCODE_DB` (an absolute path), so it must win over the heuristics.
54
+ *
55
+ * Priority:
56
+ * 1. OPENCODE_DB env var (absolute → verbatim; relative → under the data dir)
57
+ * 2. OPENCODE_DB_PATH env var (legacy alias)
58
+ * 3. .agentize/opencode/opencode.db under projectRoot (when supplied)
59
+ * 4. XDG / home-based global path
60
+ */
61
+ export function getOpenCodeDbPath(
62
+ env: Env = process.env,
63
+ homedir = os.homedir(),
64
+ projectRoot?: string | null,
65
+ ): string {
66
+ const db = env.OPENCODE_DB
67
+ if (db && db !== ":memory:") {
68
+ return path.isAbsolute(db)
69
+ ? path.resolve(db)
70
+ : path.join(getOpenCodeRoot(env, homedir), db)
71
+ }
72
+ if (env.OPENCODE_DB_PATH) return path.resolve(env.OPENCODE_DB_PATH)
73
+ if (projectRoot) {
74
+ const local = localAgentizeDbPath(projectRoot)
75
+ if (local) return local
76
+ }
77
+ return path.join(getOpenCodeRoot(env, homedir), "opencode.db")
78
+ }
79
+
80
+ export function getSessionDiffPath(
81
+ sessionId: string,
82
+ env: Env = process.env,
83
+ homedir = os.homedir(),
84
+ ): string {
85
+ return path.join(getOpenCodeRoot(env, homedir), "storage", "session_diff", `${sessionId}.json`)
86
+ }
87
+
88
+ export function realpathSafe(p: string): string | null {
89
+ try {
90
+ return fs.realpathSync(p)
91
+ } catch {
92
+ return null
93
+ }
94
+ }
95
+
96
+ /** Last path segment. Never a directory. */
97
+ export function basenameOf(p: string): string {
98
+ const t = p.replace(/\\/g, "/").replace(/\/+$/, "").trim()
99
+ if (!t) return "file"
100
+ const i = t.lastIndexOf("/")
101
+ const base = i >= 0 ? t.slice(i + 1) : t
102
+ return base || "file"
103
+ }
104
+
105
+ /** Positive finite number, else 0. */
106
+ export function finiteNum(v: unknown): number {
107
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0
108
+ }
109
+
110
+ /** Trimmed non-empty string, else null. */
111
+ export function str(v: unknown): string | null {
112
+ if (typeof v === "string" && v.trim()) return v.trim()
113
+ return null
114
+ }
115
+
116
+ export function canonicalizePath(p: string): string {
117
+ const resolved = path.resolve(p)
118
+ return path.normalize(realpathSafe(resolved) ?? resolved)
119
+ }
120
+
121
+ /**
122
+ * OpenCode/git often emit posix paths (`D:/a/b` or `/d/a/b`).
123
+ * Fold those onto the host path so Windows `D:\` matches.
124
+ */
125
+ export function normalizeIncomingPath(p: string): string {
126
+ let t = p.trim().replace(/\\/g, "/")
127
+ const m = t.match(/^\/([A-Za-z])\/(.*)$/)
128
+ if (m) t = `${m[1]}:/${m[2]}`
129
+ return t
130
+ }
131
+
132
+ /**
133
+ * Fold an absolute path/directory into one comparison key so two spellings of
134
+ * the same location compare equal: posix separators (`D:/` and `D:\`), a
135
+ * git-bash drive prefix (`/d/…`), trailing slashes, and drive-letter case
136
+ * (`d:/` from git-bash vs `D:/`). Path case is folded only on Windows, where
137
+ * the filesystem is case-insensitive.
138
+ */
139
+ export function foldPathKey(p: string | null | undefined): string {
140
+ if (!p) return ""
141
+ let t = normalizeIncomingPath(p).replace(/\/+$/, "")
142
+ const drive = t.match(/^([a-z]):\//)
143
+ if (drive) t = `${drive[1]!.toUpperCase()}:${t.slice(2)}`
144
+ return process.platform === "win32" ? t.toLowerCase() : t
145
+ }
146
+
147
+ /** True when two directory/file paths point at the same location. */
148
+ export function samePath(a: string | null | undefined, b: string | null | undefined): boolean {
149
+ return foldPathKey(a) === foldPathKey(b)
150
+ }
151
+
152
+ function isInsideRoot(root: string, abs: string): boolean {
153
+ const rel = path.relative(root, abs)
154
+ if (!rel || path.isAbsolute(rel)) return false
155
+ const n = rel.replace(/\\/g, "/")
156
+ if (n === ".." || n.startsWith("../")) return false
157
+ return true
158
+ }
159
+
160
+ function asRootList(roots: string | readonly string[] | null | undefined): string[] {
161
+ if (!roots) return []
162
+ const list = typeof roots === "string" ? [roots] : [...roots]
163
+ const out: string[] = []
164
+ const seen = new Set<string>()
165
+ for (const r of list) {
166
+ if (!r || typeof r !== "string") continue
167
+ let key = r
168
+ try {
169
+ key = canonicalizePath(r)
170
+ } catch {
171
+ key = r
172
+ }
173
+ const fold = process.platform === "win32" ? key.toLowerCase() : key
174
+ if (seen.has(fold)) continue
175
+ seen.add(fold)
176
+ out.push(r)
177
+ }
178
+ return out
179
+ }
180
+
181
+ /** Absolute path + project-relative posix path, or null if outside / missing. */
182
+ export function resolveProjectFile(
183
+ roots: string | readonly string[] | null | undefined,
184
+ filePath: string | null | undefined,
185
+ ): { abs: string; rel: string } | null {
186
+ if (!filePath) return null
187
+ const incoming = normalizeIncomingPath(filePath)
188
+ for (const rawRoot of asRootList(roots)) {
189
+ try {
190
+ const root = canonicalizePath(rawRoot)
191
+ const abs = path.isAbsolute(incoming)
192
+ ? canonicalizePath(incoming)
193
+ : canonicalizePath(path.join(root, incoming))
194
+ if (!isInsideRoot(root, abs)) continue
195
+ if (!fs.existsSync(abs)) continue
196
+ const rel = path.relative(root, abs).replace(/\\/g, "/")
197
+ if (!rel) continue
198
+ return { abs, rel }
199
+ } catch {
200
+ // next root
201
+ }
202
+ }
203
+ return null
204
+ }
205
+
206
+ /** Project-relative path for detail dialogs — never escapes above project root. */
207
+ export function relativeProjectPath(
208
+ projectRoot: string | null | undefined,
209
+ filePath: string | null | undefined,
210
+ ): string | null {
211
+ return resolveProjectFile(projectRoot, filePath)?.rel ?? null
212
+ }
213
+
214
+ /** Read a JSON file as a plain object, soft-failing to null (missing / invalid / non-object). */
215
+ export function readJson(p: string): Record<string, unknown> | null {
216
+ try {
217
+ if (!fs.existsSync(p)) return null
218
+ const raw = JSON.parse(fs.readFileSync(p, "utf8"))
219
+ return raw && typeof raw === "object" && !Array.isArray(raw)
220
+ ? (raw as Record<string, unknown>)
221
+ : null
222
+ } catch {
223
+ return null
224
+ }
225
+ }
226
+
227
+ export function fileStamp(p: string | null | undefined): string {
228
+ if (!p) return "0"
229
+ try {
230
+ const st = fs.statSync(p)
231
+ return `${st.mtimeMs}:${st.size}`
232
+ } catch {
233
+ return "0"
234
+ }
235
+ }
236
+
237
+ /** DB + WAL/SHM stamps — WAL writers change companion files, not always .db mtime. */
238
+ export function dbStamp(dbPath: string): string {
239
+ return [dbPath, `${dbPath}-wal`, `${dbPath}-shm`].map(fileStamp).join("|")
240
+ }
@@ -1,74 +1,74 @@
1
- /**
2
- * File preview helpers (no OpenTUI). Unit tests import this, not the dialogs
3
- * module (pware.oc.ui.menudialogs.tsx).
4
- */
5
- import fs from "node:fs"
6
- import path from "node:path"
7
-
8
- const PREVIEW_MAX_LINES = 200
9
- const PREVIEW_MAX_BYTES = 128_000
10
- /** DialogPad bottom 1 + header 2 + footer 2 + slack 1. Extra is the optional relative-path line. */
11
- const PREVIEW_CHROME = 6
12
- const PREVIEW_MIN_ROWS = 8
13
-
14
- const PREVIEW_EXT = new Set([
15
- ".md",
16
- ".txt",
17
- ".json",
18
- ".jsonc",
19
- ".yaml",
20
- ".yml",
21
- ".toml",
22
- ".xml",
23
- ".html",
24
- ".css",
25
- ".scss",
26
- ".ts",
27
- ".tsx",
28
- ".js",
29
- ".jsx",
30
- ".mjs",
31
- ".cjs",
32
- ".sh",
33
- ".py",
34
- ".rs",
35
- ".go",
36
- ".sql",
37
- ".csv",
38
- ])
39
-
40
- export function previewViewportRows(termHeight: number, extraChrome = 0, tall = false): number {
41
- const h = Number.isFinite(termHeight) && termHeight > 0 ? termHeight : 24
42
- const ratio = tall ? 0.9 : 0.75
43
- const min = tall ? 12 : PREVIEW_MIN_ROWS
44
- return Math.max(min, Math.floor(h * ratio) - PREVIEW_CHROME - extraChrome)
45
- }
46
-
47
- export function canPreviewPath(filePath: string): boolean {
48
- const ext = path.extname(filePath).toLowerCase()
49
- return PREVIEW_EXT.has(ext)
50
- }
51
-
52
- export function isMarkdownPath(filePath: string): boolean {
53
- return path.extname(filePath).toLowerCase() === ".md"
54
- }
55
-
56
- export function readTextPreview(absPath: string): { text: string; truncated: boolean } | null {
57
- try {
58
- const st = fs.statSync(absPath)
59
- if (!st.isFile()) return null
60
- const buf = fs.readFileSync(absPath)
61
- const slice = buf.subarray(0, Math.min(buf.length, PREVIEW_MAX_BYTES))
62
- if (slice.includes(0)) return null
63
- let text = slice.toString("utf8")
64
- const lines = text.split(/\r?\n/)
65
- let truncated = buf.length > PREVIEW_MAX_BYTES
66
- if (lines.length > PREVIEW_MAX_LINES) {
67
- text = lines.slice(0, PREVIEW_MAX_LINES).join("\n")
68
- truncated = true
69
- }
70
- return { text, truncated }
71
- } catch {
72
- return null
73
- }
74
- }
1
+ /**
2
+ * File preview helpers (no OpenTUI). Unit tests import this, not the dialogs
3
+ * module (pware.oc.ui.menudialogs.tsx).
4
+ */
5
+ import fs from "node:fs"
6
+ import path from "node:path"
7
+
8
+ const PREVIEW_MAX_LINES = 200
9
+ const PREVIEW_MAX_BYTES = 128_000
10
+ /** DialogPad bottom 1 + header 2 + footer 2 + slack 1. Extra is the optional relative-path line. */
11
+ const PREVIEW_CHROME = 6
12
+ const PREVIEW_MIN_ROWS = 8
13
+
14
+ const PREVIEW_EXT = new Set([
15
+ ".md",
16
+ ".txt",
17
+ ".json",
18
+ ".jsonc",
19
+ ".yaml",
20
+ ".yml",
21
+ ".toml",
22
+ ".xml",
23
+ ".html",
24
+ ".css",
25
+ ".scss",
26
+ ".ts",
27
+ ".tsx",
28
+ ".js",
29
+ ".jsx",
30
+ ".mjs",
31
+ ".cjs",
32
+ ".sh",
33
+ ".py",
34
+ ".rs",
35
+ ".go",
36
+ ".sql",
37
+ ".csv",
38
+ ])
39
+
40
+ export function previewViewportRows(termHeight: number, extraChrome = 0, tall = false): number {
41
+ const h = Number.isFinite(termHeight) && termHeight > 0 ? termHeight : 24
42
+ const ratio = tall ? 0.9 : 0.75
43
+ const min = tall ? 12 : PREVIEW_MIN_ROWS
44
+ return Math.max(min, Math.floor(h * ratio) - PREVIEW_CHROME - extraChrome)
45
+ }
46
+
47
+ export function canPreviewPath(filePath: string): boolean {
48
+ const ext = path.extname(filePath).toLowerCase()
49
+ return PREVIEW_EXT.has(ext)
50
+ }
51
+
52
+ export function isMarkdownPath(filePath: string): boolean {
53
+ return path.extname(filePath).toLowerCase() === ".md"
54
+ }
55
+
56
+ export function readTextPreview(absPath: string): { text: string; truncated: boolean } | null {
57
+ try {
58
+ const st = fs.statSync(absPath)
59
+ if (!st.isFile()) return null
60
+ const buf = fs.readFileSync(absPath)
61
+ const slice = buf.subarray(0, Math.min(buf.length, PREVIEW_MAX_BYTES))
62
+ if (slice.includes(0)) return null
63
+ let text = slice.toString("utf8")
64
+ const lines = text.split(/\r?\n/)
65
+ let truncated = buf.length > PREVIEW_MAX_BYTES
66
+ if (lines.length > PREVIEW_MAX_LINES) {
67
+ text = lines.slice(0, PREVIEW_MAX_LINES).join("\n")
68
+ truncated = true
69
+ }
70
+ return { text, truncated }
71
+ } catch {
72
+ return null
73
+ }
74
+ }