opencode-extended-sidebar 0.2.90
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 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only git status for Files letters. No repo / no git → empty, never throws.
|
|
3
|
+
* Status is scoped to the listed paths — never the whole worktree.
|
|
4
|
+
* Spawn is async: the TUI keeps the last letters until git returns.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
import fs from "node:fs"
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import { fileStamp } from "../pware.oc.core.paths.js"
|
|
10
|
+
|
|
11
|
+
/** Porcelain XY letters only. `?` is untracked. Do not invent extras here. */
|
|
12
|
+
export type GitLetter = "M" | "A" | "D" | "R" | "C" | "U" | "T" | "?"
|
|
13
|
+
|
|
14
|
+
const GIT_PATH_CAP = 40
|
|
15
|
+
const GIT_DEBOUNCE_MS = 2500
|
|
16
|
+
const GIT_TIMEOUT_MS = 1500
|
|
17
|
+
|
|
18
|
+
export function findGitRoot(start?: string | null): string | null {
|
|
19
|
+
if (!start) return null
|
|
20
|
+
let dir = path.resolve(start)
|
|
21
|
+
for (let i = 0; i < 16; i++) {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir)
|
|
28
|
+
if (parent === dir) break
|
|
29
|
+
dir = parent
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function gitStatusStamp(projectRoot?: string | null): string {
|
|
35
|
+
const root = findGitRoot(projectRoot)
|
|
36
|
+
if (!root) return "0"
|
|
37
|
+
return [fileStamp(path.join(root, ".git")), fileStamp(path.join(root, ".git", "HEAD")), fileStamp(path.join(root, ".git", "index"))].join("|")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function porcelainLetter(xy: string): GitLetter | null {
|
|
41
|
+
if (xy === "??") return "?"
|
|
42
|
+
if (xy === "!!") return null
|
|
43
|
+
const x = xy[0] ?? " "
|
|
44
|
+
const y = xy[1] ?? " "
|
|
45
|
+
if (x === "U" || y === "U" || xy === "AA" || xy === "DD") return "U"
|
|
46
|
+
const rank = (c: string): GitLetter | null => {
|
|
47
|
+
if (c === "D") return "D"
|
|
48
|
+
if (c === "R") return "R"
|
|
49
|
+
if (c === "C") return "C"
|
|
50
|
+
if (c === "A") return "A"
|
|
51
|
+
if (c === "T") return "T"
|
|
52
|
+
if (c === "M") return "M"
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
return rank(x) ?? rank(y)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function xIsRename(x: string): boolean {
|
|
59
|
+
return x === "R" || x === "C"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Parse `git status --porcelain -z` stdout. Exported for tests. */
|
|
63
|
+
export function parsePorcelainZ(buf: string): Map<string, GitLetter> {
|
|
64
|
+
const out = new Map<string, GitLetter>()
|
|
65
|
+
let i = 0
|
|
66
|
+
while (i < buf.length) {
|
|
67
|
+
const xy = buf.slice(i, i + 2)
|
|
68
|
+
if (xy.length < 2) break
|
|
69
|
+
i += 2
|
|
70
|
+
if (buf[i] === " ") i += 1
|
|
71
|
+
const z = buf.indexOf("\0", i)
|
|
72
|
+
if (z < 0) break
|
|
73
|
+
const p1 = buf.slice(i, z).replace(/\\/g, "/")
|
|
74
|
+
i = z + 1
|
|
75
|
+
if (xIsRename(xy[0] ?? "")) {
|
|
76
|
+
const z2 = buf.indexOf("\0", i)
|
|
77
|
+
if (z2 >= 0) i = z2 + 1
|
|
78
|
+
}
|
|
79
|
+
const letter = porcelainLetter(xy)
|
|
80
|
+
if (letter && p1) out.set(p1.toLowerCase(), letter)
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function relToGitRoot(posixPath: string, root: string): string {
|
|
86
|
+
const file = posixPath.replace(/\\/g, "/")
|
|
87
|
+
const base = root.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
88
|
+
const lower = file.toLowerCase()
|
|
89
|
+
const rootLower = base.toLowerCase()
|
|
90
|
+
if (lower.startsWith(`${rootLower}/`)) return lower.slice(rootLower.length + 1)
|
|
91
|
+
return lower.replace(/^\.\//, "")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pathspecs for `git status`: repo paths only, deduped, capped. Exported for tests. */
|
|
95
|
+
export function relsFrom(posixPaths: string[], root: string): string[] {
|
|
96
|
+
const base = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase()
|
|
97
|
+
const seen = new Set<string>()
|
|
98
|
+
const out: string[] = []
|
|
99
|
+
for (const p of posixPaths) {
|
|
100
|
+
const file = p.replace(/\\/g, "/")
|
|
101
|
+
// git aborts the whole status with exit 128 on a single outside-repo
|
|
102
|
+
// pathspec — sessions touch temp/config files outside the tree all the time.
|
|
103
|
+
if (!file.toLowerCase().startsWith(`${base}/`)) continue
|
|
104
|
+
const rel = relToGitRoot(p, root)
|
|
105
|
+
if (!rel || seen.has(rel)) continue
|
|
106
|
+
seen.add(rel)
|
|
107
|
+
out.push(rel)
|
|
108
|
+
if (out.length >= GIT_PATH_CAP) break
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let cacheKey = ""
|
|
114
|
+
let cacheMarks = new Map<string, GitLetter>()
|
|
115
|
+
let cacheRoot: string | null = null
|
|
116
|
+
let lastPathKey = ""
|
|
117
|
+
let lastSpawn = 0
|
|
118
|
+
let pendingKey = ""
|
|
119
|
+
let spawnGen = 0
|
|
120
|
+
let inFlight = false
|
|
121
|
+
|
|
122
|
+
const listeners = new Set<() => void>()
|
|
123
|
+
|
|
124
|
+
function notify(): void {
|
|
125
|
+
for (const fn of listeners) {
|
|
126
|
+
try {
|
|
127
|
+
fn()
|
|
128
|
+
} catch {
|
|
129
|
+
// sidebar teardown
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Re-render when an async git status lands. Returns unsubscribe. */
|
|
135
|
+
export function onGitMarksChange(fn: () => void): () => void {
|
|
136
|
+
listeners.add(fn)
|
|
137
|
+
return () => {
|
|
138
|
+
listeners.delete(fn)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Drop cached letters and in-flight spawn accounting (tests). */
|
|
143
|
+
export function resetGitCache(): void {
|
|
144
|
+
spawnGen += 1
|
|
145
|
+
inFlight = false
|
|
146
|
+
cacheKey = ""
|
|
147
|
+
pendingKey = ""
|
|
148
|
+
lastPathKey = ""
|
|
149
|
+
lastSpawn = 0
|
|
150
|
+
cacheRoot = null
|
|
151
|
+
cacheMarks = new Map()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runGit(root: string, rels: string[], key: string, gen: number): void {
|
|
155
|
+
let child: ReturnType<typeof spawn>
|
|
156
|
+
try {
|
|
157
|
+
child = spawn(
|
|
158
|
+
"git",
|
|
159
|
+
["-c", "core.quotepath=false", "status", "--porcelain", "-z", "--", ...rels],
|
|
160
|
+
{ cwd: root, windowsHide: true },
|
|
161
|
+
)
|
|
162
|
+
} catch {
|
|
163
|
+
if (gen === spawnGen) inFlight = false
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
let out = ""
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
try {
|
|
169
|
+
child.kill()
|
|
170
|
+
} catch {
|
|
171
|
+
// already gone
|
|
172
|
+
}
|
|
173
|
+
}, GIT_TIMEOUT_MS)
|
|
174
|
+
child.stdout?.setEncoding("utf8")
|
|
175
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
176
|
+
out += chunk
|
|
177
|
+
})
|
|
178
|
+
child.stderr?.resume()
|
|
179
|
+
const finish = (ok: boolean) => {
|
|
180
|
+
clearTimeout(timer)
|
|
181
|
+
if (gen !== spawnGen) return
|
|
182
|
+
inFlight = false
|
|
183
|
+
cacheRoot = root
|
|
184
|
+
cacheKey = key
|
|
185
|
+
if (ok) cacheMarks = parsePorcelainZ(out)
|
|
186
|
+
notify()
|
|
187
|
+
}
|
|
188
|
+
child.on("error", () => finish(false))
|
|
189
|
+
child.on("close", (code) => finish(code === 0))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Last known porcelain letters. Never waits on git — a stale map is returned
|
|
194
|
+
* and a spawn is scheduled when the cache key is cold (2.5 s debounce on index noise).
|
|
195
|
+
*/
|
|
196
|
+
export function readGitMarksFor(
|
|
197
|
+
posixPaths: string[],
|
|
198
|
+
projectRoot?: string | null,
|
|
199
|
+
): {
|
|
200
|
+
root: string | null
|
|
201
|
+
marks: Map<string, GitLetter>
|
|
202
|
+
} {
|
|
203
|
+
const root = findGitRoot(projectRoot)
|
|
204
|
+
if (!root || posixPaths.length === 0) {
|
|
205
|
+
return { root, marks: new Map() }
|
|
206
|
+
}
|
|
207
|
+
const rels = relsFrom(posixPaths, root)
|
|
208
|
+
if (rels.length === 0) {
|
|
209
|
+
return { root, marks: new Map() }
|
|
210
|
+
}
|
|
211
|
+
const pathKey = rels.slice().sort().join("\0")
|
|
212
|
+
const stamp = gitStatusStamp(projectRoot)
|
|
213
|
+
const key = `${root}|${stamp}|${pathKey}`
|
|
214
|
+
cacheRoot = root
|
|
215
|
+
if (key === cacheKey) return { root, marks: cacheMarks }
|
|
216
|
+
if (inFlight && pendingKey === key) return { root, marks: cacheMarks }
|
|
217
|
+
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
if (pathKey === lastPathKey && lastSpawn > 0 && now - lastSpawn < GIT_DEBOUNCE_MS) {
|
|
220
|
+
return { root, marks: cacheMarks }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pendingKey = key
|
|
224
|
+
lastPathKey = pathKey
|
|
225
|
+
lastSpawn = now
|
|
226
|
+
spawnGen += 1
|
|
227
|
+
inFlight = true
|
|
228
|
+
runGit(root, rels, key, spawnGen)
|
|
229
|
+
return { root, marks: cacheMarks }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function gitLetterFor(posixPath: string, projectRoot?: string | null): GitLetter | null {
|
|
233
|
+
const { root, marks } = readGitMarksFor([posixPath], projectRoot)
|
|
234
|
+
if (!root || marks.size === 0) return null
|
|
235
|
+
const rel = relToGitRoot(posixPath, root)
|
|
236
|
+
return marks.get(rel) ?? marks.get(posixPath.replace(/\\/g, "/").toLowerCase()) ?? null
|
|
237
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ignore sources for the Files panel: the project `.gitignore` (only when
|
|
3
|
+
* `skipGitignore` is on) and `.oesignore` (always, when present). Both use the
|
|
4
|
+
* `ignore` package for full gitignore semantics.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs"
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import ignore from "ignore"
|
|
9
|
+
import { fileStamp } from "../pware.oc.core.paths.js"
|
|
10
|
+
import { createStampCache } from "../pware.oc.core.cache.js"
|
|
11
|
+
|
|
12
|
+
export function gitignorePath(projectRoot?: string | null): string | null {
|
|
13
|
+
if (!projectRoot) return null
|
|
14
|
+
return path.join(projectRoot, ".gitignore")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function oesignorePath(projectRoot?: string | null): string | null {
|
|
18
|
+
if (!projectRoot) return null
|
|
19
|
+
return path.join(projectRoot, ".oesignore")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Stamp both ignore files so the fingerprint reloads when either changes. */
|
|
23
|
+
export function gitignoreStamp(projectRoot?: string | null): string {
|
|
24
|
+
return [gitignorePath(projectRoot), oesignorePath(projectRoot)].map(fileStamp).join("|")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readIfExists(p: string | null): string | null {
|
|
28
|
+
if (!p) return null
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(p)) return null
|
|
31
|
+
return fs.readFileSync(p, "utf8")
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Absolute/relative path folded to a project-relative posix path, or null. */
|
|
38
|
+
function toRelPath(posixFile: string, projectRoot: string): string | null {
|
|
39
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
40
|
+
const file = posixFile.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
41
|
+
if (!root) return null
|
|
42
|
+
if (file.toLowerCase().startsWith(`${root.toLowerCase()}/`)) return file.slice(root.length + 1)
|
|
43
|
+
if (/^[a-z]:\//i.test(file) || file.startsWith("/")) return null
|
|
44
|
+
return file.replace(/^\.\//, "")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compile(patterns: string | null): ReturnType<typeof ignore> {
|
|
48
|
+
const ig = ignore()
|
|
49
|
+
if (patterns != null) ig.add(patterns)
|
|
50
|
+
return ig
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type IgnoreSet = { gitignore: ReturnType<typeof ignore>; oesignore: ReturnType<typeof ignore> }
|
|
54
|
+
|
|
55
|
+
const ignoreCache = createStampCache<IgnoreSet>()
|
|
56
|
+
|
|
57
|
+
function setFor(projectRoot: string): IgnoreSet {
|
|
58
|
+
return ignoreCache.get(`${projectRoot}|${gitignoreStamp(projectRoot)}`, () => ({
|
|
59
|
+
gitignore: compile(readIfExists(gitignorePath(projectRoot))),
|
|
60
|
+
oesignore: compile(readIfExists(oesignorePath(projectRoot))),
|
|
61
|
+
}))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ignoredBy(
|
|
65
|
+
which: keyof IgnoreSet,
|
|
66
|
+
posixPath: string,
|
|
67
|
+
projectRoot?: string | null,
|
|
68
|
+
): boolean {
|
|
69
|
+
if (!projectRoot) return false
|
|
70
|
+
const rel = toRelPath(posixPath, projectRoot)
|
|
71
|
+
if (!rel) return false
|
|
72
|
+
try {
|
|
73
|
+
return setFor(projectRoot)[which].ignores(rel)
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True when the path matches the project's root `.gitignore`. */
|
|
80
|
+
export function ignoredByGitignore(posixPath: string, projectRoot?: string | null): boolean {
|
|
81
|
+
return ignoredBy("gitignore", posixPath, projectRoot)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when the path matches the project's `.oesignore` (always on when present). */
|
|
85
|
+
export function ignoredByOesignore(posixPath: string, projectRoot?: string | null): boolean {
|
|
86
|
+
return ignoredBy("oesignore", posixPath, projectRoot)
|
|
87
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core
|
|
3
|
+
*
|
|
4
|
+
* Shared infrastructure + pure logic. Core never imports a domain or the UI.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./pware.oc.core.cache.js"
|
|
7
|
+
export * from "./pware.oc.core.clipboard.js"
|
|
8
|
+
export * from "./pware.oc.core.debug.js"
|
|
9
|
+
export * from "./pware.oc.core.events.js"
|
|
10
|
+
export * from "./pware.oc.core.layout.js"
|
|
11
|
+
export * from "./pware.oc.core.oes.js"
|
|
12
|
+
export * from "./pware.oc.core.paths.js"
|
|
13
|
+
export * from "./pware.oc.core.preview.js"
|
|
14
|
+
export * from "./pware.oc.core.pulse.js"
|
|
15
|
+
export * from "./pware.oc.core.sqlite.js"
|
|
16
|
+
export * from "./pware.oc.core.status.js"
|
|
17
|
+
export * from "./pware.oc.core.bus.js"
|
|
18
|
+
export * from "./git/index.js"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PwareEvent = {
|
|
2
|
+
type: string
|
|
3
|
+
ts: number
|
|
4
|
+
data?: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type PwareEventListener = (evt: PwareEvent) => void
|
|
8
|
+
|
|
9
|
+
export type PwareEventBus = {
|
|
10
|
+
emit: (evt: PwareEvent) => void
|
|
11
|
+
on: (type: string, listener: PwareEventListener) => () => void
|
|
12
|
+
off: (type: string, listener: PwareEventListener) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createEventBus(): PwareEventBus {
|
|
16
|
+
const byType = new Map<string, Set<PwareEventListener>>()
|
|
17
|
+
|
|
18
|
+
const off = (type: string, listener: PwareEventListener): void => {
|
|
19
|
+
const set = byType.get(type)
|
|
20
|
+
if (!set) return
|
|
21
|
+
set.delete(listener)
|
|
22
|
+
if (set.size === 0) byType.delete(type)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const on = (type: string, listener: PwareEventListener): (() => void) => {
|
|
26
|
+
const set = byType.get(type)
|
|
27
|
+
if (set) {
|
|
28
|
+
set.add(listener)
|
|
29
|
+
} else {
|
|
30
|
+
byType.set(type, new Set([listener]))
|
|
31
|
+
}
|
|
32
|
+
return () => off(type, listener)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const emit = (evt: PwareEvent): void => {
|
|
36
|
+
const set = byType.get(evt.type)
|
|
37
|
+
if (!set || set.size === 0) return
|
|
38
|
+
for (const listener of set) listener(evt)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { emit, on, off }
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp-keyed in-memory cache. Optional TTL for filesystem snapshots.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function createStampCache<T>(opts?: { ttlMs?: number }): {
|
|
6
|
+
get: (key: string, load: () => T) => T
|
|
7
|
+
peek: (key: string) => T | undefined
|
|
8
|
+
set: (key: string, value: T) => void
|
|
9
|
+
reset: () => void
|
|
10
|
+
} {
|
|
11
|
+
const ttl = opts?.ttlMs
|
|
12
|
+
let cacheKey = ""
|
|
13
|
+
let cached: T | undefined
|
|
14
|
+
let at = 0
|
|
15
|
+
return {
|
|
16
|
+
get(key, load) {
|
|
17
|
+
const now = Date.now()
|
|
18
|
+
if (
|
|
19
|
+
key &&
|
|
20
|
+
key === cacheKey &&
|
|
21
|
+
cached !== undefined &&
|
|
22
|
+
(ttl == null || now - at < ttl)
|
|
23
|
+
) {
|
|
24
|
+
return cached
|
|
25
|
+
}
|
|
26
|
+
const next = load()
|
|
27
|
+
cacheKey = key
|
|
28
|
+
cached = next
|
|
29
|
+
at = now
|
|
30
|
+
return next
|
|
31
|
+
},
|
|
32
|
+
peek(key) {
|
|
33
|
+
if (!key || key !== cacheKey || cached === undefined) return undefined
|
|
34
|
+
if (ttl != null && Date.now() - at >= ttl) return undefined
|
|
35
|
+
return cached
|
|
36
|
+
},
|
|
37
|
+
set(key, value) {
|
|
38
|
+
cacheKey = key
|
|
39
|
+
cached = value
|
|
40
|
+
at = Date.now()
|
|
41
|
+
},
|
|
42
|
+
reset() {
|
|
43
|
+
cacheKey = ""
|
|
44
|
+
cached = undefined
|
|
45
|
+
at = 0
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy text to the system clipboard without npm deps.
|
|
3
|
+
* Native tools are async so a click never freezes the TUI.
|
|
4
|
+
* Windows prefers clip.exe; PowerShell is last resort (slow to start).
|
|
5
|
+
* OSC 52 is always sent so SSH / remote terminals can still receive the copy.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process"
|
|
8
|
+
import os from "node:os"
|
|
9
|
+
|
|
10
|
+
const COPY_TIMEOUT_MS = 1500
|
|
11
|
+
|
|
12
|
+
/** Terminal clipboard sequence (OSC 52). Exported for tests. */
|
|
13
|
+
export function osc52Payload(text: string): string {
|
|
14
|
+
return `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeOsc52(text: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
process.stdout.write(osc52Payload(text))
|
|
20
|
+
return true
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pipe(command: string, args: string[], input: string | Buffer): Promise<boolean> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let child: ReturnType<typeof spawn>
|
|
29
|
+
try {
|
|
30
|
+
child = spawn(command, args, {
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
33
|
+
})
|
|
34
|
+
} catch {
|
|
35
|
+
resolve(false)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
let done = false
|
|
39
|
+
const finish = (ok: boolean) => {
|
|
40
|
+
if (done) return
|
|
41
|
+
done = true
|
|
42
|
+
clearTimeout(timer)
|
|
43
|
+
resolve(ok)
|
|
44
|
+
}
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
try {
|
|
47
|
+
child.kill()
|
|
48
|
+
} catch {
|
|
49
|
+
// already gone
|
|
50
|
+
}
|
|
51
|
+
finish(false)
|
|
52
|
+
}, COPY_TIMEOUT_MS)
|
|
53
|
+
child.on("error", () => finish(false))
|
|
54
|
+
child.on("close", (code) => finish(code === 0))
|
|
55
|
+
try {
|
|
56
|
+
if (!child.stdin) {
|
|
57
|
+
finish(false)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
child.stdin.end(input)
|
|
61
|
+
} catch {
|
|
62
|
+
finish(false)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** clip.exe expects UTF-16 LE with BOM for non-ASCII. */
|
|
68
|
+
function clipUtf16(text: string): Buffer {
|
|
69
|
+
return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, "utf16le")])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function copyText(text: string): Promise<boolean> {
|
|
73
|
+
if (!text) return false
|
|
74
|
+
const osc = writeOsc52(text)
|
|
75
|
+
const platform = os.platform()
|
|
76
|
+
if (platform === "win32") {
|
|
77
|
+
if (await pipe("clip", [], clipUtf16(text))) return true
|
|
78
|
+
if (
|
|
79
|
+
await pipe(
|
|
80
|
+
"powershell.exe",
|
|
81
|
+
["-NoProfile", "-NonInteractive", "-Command", "$input | Set-Clipboard"],
|
|
82
|
+
text,
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
return osc
|
|
88
|
+
}
|
|
89
|
+
if (platform === "darwin") {
|
|
90
|
+
if (await pipe("pbcopy", [], text)) return true
|
|
91
|
+
return osc
|
|
92
|
+
}
|
|
93
|
+
if (process.env.WAYLAND_DISPLAY && (await pipe("wl-copy", [], text))) return true
|
|
94
|
+
if (await pipe("xclip", ["-selection", "clipboard"], text)) return true
|
|
95
|
+
if (await pipe("xsel", ["--clipboard", "--input"], text)) return true
|
|
96
|
+
return osc
|
|
97
|
+
}
|