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,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
|
+
}
|