termcast 1.3.54 → 1.4.0

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 (169) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +17 -132
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +8 -30
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +9 -271
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts +4 -2
  9. package/dist/apis/clipboard.d.ts.map +1 -1
  10. package/dist/apis/clipboard.js +18 -31
  11. package/dist/apis/clipboard.js.map +1 -1
  12. package/dist/apis/environment.d.ts.map +1 -1
  13. package/dist/apis/environment.js +14 -49
  14. package/dist/apis/environment.js.map +1 -1
  15. package/dist/apis/localstorage.d.ts +7 -12
  16. package/dist/apis/localstorage.d.ts.map +1 -1
  17. package/dist/apis/localstorage.js +7 -184
  18. package/dist/apis/localstorage.js.map +1 -1
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js +16 -15
  21. package/dist/app.js.map +1 -1
  22. package/dist/cli.js +7 -6
  23. package/dist/cli.js.map +1 -1
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +13 -2
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/extension-preferences.d.ts.map +1 -1
  28. package/dist/components/extension-preferences.js +7 -8
  29. package/dist/components/extension-preferences.js.map +1 -1
  30. package/dist/components/form/file-autocomplete.js +2 -2
  31. package/dist/components/form/file-autocomplete.js.map +1 -1
  32. package/dist/components/list.d.ts.map +1 -1
  33. package/dist/components/list.js +242 -14
  34. package/dist/components/list.js.map +1 -1
  35. package/dist/e2e-node.d.ts.map +1 -1
  36. package/dist/e2e-node.js +5 -4
  37. package/dist/e2e-node.js.map +1 -1
  38. package/dist/extensions/dev.d.ts.map +1 -1
  39. package/dist/extensions/dev.js +5 -2
  40. package/dist/extensions/dev.js.map +1 -1
  41. package/dist/globals.d.ts.map +1 -1
  42. package/dist/globals.js +2 -1
  43. package/dist/globals.js.map +1 -1
  44. package/dist/internal/error-handler.d.ts.map +1 -1
  45. package/dist/internal/error-handler.js +21 -19
  46. package/dist/internal/error-handler.js.map +1 -1
  47. package/dist/internal/providers.d.ts.map +1 -1
  48. package/dist/internal/providers.js +41 -1
  49. package/dist/internal/providers.js.map +1 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js +31 -29
  52. package/dist/logger.js.map +1 -1
  53. package/dist/platform/browser/cache.d.ts +41 -0
  54. package/dist/platform/browser/cache.d.ts.map +1 -0
  55. package/dist/platform/browser/cache.js +262 -0
  56. package/dist/platform/browser/cache.js.map +1 -0
  57. package/dist/platform/browser/localstorage.d.ts +20 -0
  58. package/dist/platform/browser/localstorage.d.ts.map +1 -0
  59. package/dist/platform/browser/localstorage.js +102 -0
  60. package/dist/platform/browser/localstorage.js.map +1 -0
  61. package/dist/platform/browser/runtime.d.ts +51 -0
  62. package/dist/platform/browser/runtime.d.ts.map +1 -0
  63. package/dist/platform/browser/runtime.js +164 -0
  64. package/dist/platform/browser/runtime.js.map +1 -0
  65. package/dist/platform/bun/sqlite.d.ts +17 -0
  66. package/dist/platform/bun/sqlite.d.ts.map +1 -0
  67. package/dist/platform/bun/sqlite.js +6 -0
  68. package/dist/platform/bun/sqlite.js.map +1 -0
  69. package/dist/platform/node/cache.d.ts +35 -0
  70. package/dist/platform/node/cache.d.ts.map +1 -0
  71. package/dist/platform/node/cache.js +269 -0
  72. package/dist/platform/node/cache.js.map +1 -0
  73. package/dist/platform/node/localstorage.d.ts +17 -0
  74. package/dist/platform/node/localstorage.d.ts.map +1 -0
  75. package/dist/platform/node/localstorage.js +186 -0
  76. package/dist/platform/node/localstorage.js.map +1 -0
  77. package/dist/platform/node/runtime.d.ts +52 -0
  78. package/dist/platform/node/runtime.d.ts.map +1 -0
  79. package/dist/platform/node/runtime.js +230 -0
  80. package/dist/platform/node/runtime.js.map +1 -0
  81. package/dist/platform/node/sqlite.d.ts +27 -0
  82. package/dist/platform/node/sqlite.d.ts.map +1 -0
  83. package/dist/platform/node/sqlite.js +21 -0
  84. package/dist/platform/node/sqlite.js.map +1 -0
  85. package/dist/state.d.ts +5 -0
  86. package/dist/state.d.ts.map +1 -1
  87. package/dist/state.js +6 -28
  88. package/dist/state.js.map +1 -1
  89. package/dist/utils/file-system.d.ts.map +1 -1
  90. package/dist/utils/file-system.js +17 -22
  91. package/dist/utils/file-system.js.map +1 -1
  92. package/dist/utils.d.ts +1 -1
  93. package/dist/utils.d.ts.map +1 -1
  94. package/dist/utils.js +42 -47
  95. package/dist/utils.js.map +1 -1
  96. package/dist/vim-mode.d.ts +40 -0
  97. package/dist/vim-mode.d.ts.map +1 -0
  98. package/dist/vim-mode.js +135 -0
  99. package/dist/vim-mode.js.map +1 -0
  100. package/fonts/Inconsolata.otf +0 -0
  101. package/fonts/SIL Open Font License.txt +41 -0
  102. package/package.json +60 -8
  103. package/src/action-utils.tsx +27 -124
  104. package/src/apis/cache.test.ts +1 -1
  105. package/src/apis/cache.tsx +9 -373
  106. package/src/apis/clipboard.tsx +29 -38
  107. package/src/apis/environment.tsx +25 -52
  108. package/src/apis/localstorage.tsx +8 -214
  109. package/src/app.tsx +16 -15
  110. package/src/cli.tsx +14 -15
  111. package/src/compile.vitest.tsx +2 -2
  112. package/src/components/actions.tsx +19 -1
  113. package/src/components/extension-preferences.tsx +7 -8
  114. package/src/components/form/file-autocomplete.tsx +2 -2
  115. package/src/components/list.tsx +279 -14
  116. package/src/e2e-node.tsx +7 -7
  117. package/src/examples/action-shortcut.vitest.tsx +2 -2
  118. package/src/examples/actions-context.vitest.tsx +1 -1
  119. package/src/examples/bar-graph-weekly.vitest.tsx +10 -36
  120. package/src/examples/detail-metadata-showcase.vitest.tsx +36 -36
  121. package/src/examples/form-basic.vitest.tsx +21 -17
  122. package/src/examples/github.vitest.tsx +4 -4
  123. package/src/examples/graph-bar-chart.vitest.tsx +13 -11
  124. package/src/examples/graph-polymarket.vitest.tsx +2 -2
  125. package/src/examples/graph-row.vitest.tsx +66 -66
  126. package/src/examples/graph-styles.vitest.tsx +12 -12
  127. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -48
  128. package/src/examples/list-detail-metadata.vitest.tsx +5 -5
  129. package/src/examples/list-fetch-data.vitest.tsx +3 -3
  130. package/src/examples/list-item-accessories.vitest.tsx +2 -2
  131. package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
  132. package/src/examples/list-no-actions.vitest.tsx +2 -2
  133. package/src/examples/list-scrollbox.vitest.tsx +5 -5
  134. package/src/examples/list-spacing-mode.vitest.tsx +3 -3
  135. package/src/examples/list-with-detail.vitest.tsx +68 -68
  136. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  137. package/src/examples/list-with-sections.vitest.tsx +27 -27
  138. package/src/examples/simple-candle-chart.vitest.tsx +7 -7
  139. package/src/examples/simple-detail-markdown.vitest.tsx +8 -8
  140. package/src/examples/simple-detail-table.vitest.tsx +8 -8
  141. package/src/examples/simple-graph.vitest.tsx +3 -3
  142. package/src/examples/simple-grid.vitest.tsx +14 -14
  143. package/src/examples/simple-heatmap.vitest.tsx +1 -1
  144. package/src/examples/simple-navigation.vitest.tsx +17 -17
  145. package/src/examples/simple-progress-bar.vitest.tsx +1 -1
  146. package/src/examples/store.vitest.tsx +1 -1
  147. package/src/examples/swift-extension.vitest.tsx +2 -2
  148. package/src/examples/table-edge-cases.vitest.tsx +18 -18
  149. package/src/examples/toast-action.vitest.tsx +2 -2
  150. package/src/extensions/dev.tsx +5 -2
  151. package/src/extensions/dev.vitest.tsx +3 -3
  152. package/src/globals.ts +2 -1
  153. package/src/internal/error-handler.tsx +19 -21
  154. package/src/internal/providers.tsx +39 -0
  155. package/src/logger.tsx +38 -41
  156. package/src/platform/browser/cache.ts +327 -0
  157. package/src/platform/browser/localstorage.ts +119 -0
  158. package/src/platform/browser/runtime.ts +209 -0
  159. package/src/platform/bun/sqlite.ts +19 -0
  160. package/src/platform/node/cache.ts +372 -0
  161. package/src/platform/node/localstorage.ts +214 -0
  162. package/src/platform/node/runtime.ts +264 -0
  163. package/src/platform/node/sqlite.ts +43 -0
  164. package/src/state.tsx +17 -28
  165. package/src/utils/file-system.ts +17 -22
  166. package/src/utils.test.tsx +1 -1
  167. package/src/utils.tsx +56 -47
  168. package/src/vim-mode.tsx +153 -0
  169. package/src/apis/sqlite.ts +0 -14
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Browser Cache implementation backed by IndexedDB with in-memory Map for sync reads.
3
+ *
4
+ * On construction, all entries for the namespace are loaded into a Map.
5
+ * Sync reads (get, has, isEmpty) hit the Map.
6
+ * Writes update both the Map and IndexedDB (fire-and-forget).
7
+ * LRU eviction is done in-memory.
8
+ */
9
+
10
+ import { byteLength } from '#platform/runtime'
11
+
12
+ const DB_NAME = 'termcast-cache'
13
+ const STORE_NAME = 'cache_entries'
14
+ const DB_VERSION = 1
15
+ const DEFAULT_NAMESPACE = '__default__'
16
+
17
+ interface CacheEntry {
18
+ namespace: string
19
+ key: string
20
+ data: string
21
+ size: number
22
+ last_accessed_at: number
23
+ updated_at: number
24
+ }
25
+
26
+ // Shared IDB connection — lazily opened, reused across Cache instances
27
+ let dbPromise: Promise<IDBDatabase> | null = null
28
+
29
+ function openDB(): Promise<IDBDatabase> {
30
+ if (dbPromise) return dbPromise
31
+
32
+ dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
33
+ const request = indexedDB.open(DB_NAME, DB_VERSION)
34
+
35
+ request.onupgradeneeded = () => {
36
+ const db = request.result
37
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
38
+ const store = db.createObjectStore(STORE_NAME, {
39
+ keyPath: ['namespace', 'key'],
40
+ })
41
+ store.createIndex('by_namespace', 'namespace', { unique: false })
42
+ store.createIndex('by_lru', ['namespace', 'last_accessed_at'], {
43
+ unique: false,
44
+ })
45
+ }
46
+ }
47
+
48
+ request.onsuccess = () => {
49
+ resolve(request.result)
50
+ }
51
+ request.onerror = () => {
52
+ reject(request.error)
53
+ }
54
+ })
55
+
56
+ return dbPromise
57
+ }
58
+
59
+ // Load all entries for a namespace into memory (called once per Cache instance)
60
+ async function loadNamespace(namespace: string): Promise<Map<string, CacheEntry>> {
61
+ const db = await openDB()
62
+ return new Promise((resolve, reject) => {
63
+ const tx = db.transaction(STORE_NAME, 'readonly')
64
+ const store = tx.objectStore(STORE_NAME)
65
+ const index = store.index('by_namespace')
66
+ const request = index.getAll(namespace)
67
+
68
+ request.onsuccess = () => {
69
+ const map = new Map<string, CacheEntry>()
70
+ for (const entry of request.result as CacheEntry[]) {
71
+ map.set(entry.key, entry)
72
+ }
73
+ resolve(map)
74
+ }
75
+ request.onerror = () => {
76
+ reject(request.error)
77
+ }
78
+ })
79
+ }
80
+
81
+ async function idbPut(entry: CacheEntry): Promise<void> {
82
+ const db = await openDB()
83
+ return new Promise((resolve, reject) => {
84
+ const tx = db.transaction(STORE_NAME, 'readwrite')
85
+ const store = tx.objectStore(STORE_NAME)
86
+ const request = store.put(entry)
87
+ request.onsuccess = () => {
88
+ resolve()
89
+ }
90
+ request.onerror = () => {
91
+ reject(request.error)
92
+ }
93
+ })
94
+ }
95
+
96
+ async function idbDelete(namespace: string, key: string): Promise<void> {
97
+ const db = await openDB()
98
+ return new Promise((resolve, reject) => {
99
+ const tx = db.transaction(STORE_NAME, 'readwrite')
100
+ const store = tx.objectStore(STORE_NAME)
101
+ const request = store.delete([namespace, key])
102
+ request.onsuccess = () => {
103
+ resolve()
104
+ }
105
+ request.onerror = () => {
106
+ reject(request.error)
107
+ }
108
+ })
109
+ }
110
+
111
+ async function idbClearNamespace(namespace: string): Promise<void> {
112
+ const db = await openDB()
113
+ return new Promise((resolve, reject) => {
114
+ const tx = db.transaction(STORE_NAME, 'readwrite')
115
+ const store = tx.objectStore(STORE_NAME)
116
+ const index = store.index('by_namespace')
117
+ const request = index.openCursor(namespace)
118
+
119
+ request.onsuccess = () => {
120
+ const cursor = request.result
121
+ if (cursor) {
122
+ cursor.delete()
123
+ cursor.continue()
124
+ } else {
125
+ resolve()
126
+ }
127
+ }
128
+ request.onerror = () => {
129
+ reject(request.error)
130
+ }
131
+ })
132
+ }
133
+
134
+ let logicalTimestamp = Date.now()
135
+ function nextTimestamp(): number {
136
+ logicalTimestamp += 1
137
+ return logicalTimestamp
138
+ }
139
+
140
+ export class Cache {
141
+ static get STORAGE_DIRECTORY_NAME(): string {
142
+ return 'cache'
143
+ }
144
+
145
+ static get DEFAULT_CAPACITY(): number {
146
+ return 10 * 1024 * 1024 // 10 MB
147
+ }
148
+
149
+ private entries: Map<string, CacheEntry> = new Map()
150
+ private capacity: number
151
+ private namespace: string
152
+ private subscribers: Cache.Subscriber[] = []
153
+ private currentSize: number = 0
154
+ private initialized: boolean = false
155
+
156
+ constructor(options?: Cache.Options) {
157
+ this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
158
+ this.namespace = options?.namespace || DEFAULT_NAMESPACE
159
+
160
+ // Kick off async load — sync reads return undefined until loaded
161
+ this.init()
162
+
163
+ // Bind all methods
164
+ this.get = this.get.bind(this)
165
+ this.has = this.has.bind(this)
166
+ this.set = this.set.bind(this)
167
+ this.remove = this.remove.bind(this)
168
+ this.clear = this.clear.bind(this)
169
+ this.subscribe = this.subscribe.bind(this)
170
+ }
171
+
172
+ private async init(): Promise<void> {
173
+ try {
174
+ const loaded = await loadNamespace(this.namespace)
175
+ // Merge: keep in-memory writes that happened during async load,
176
+ // preferring the newer entry (by updated_at) on key conflicts.
177
+ for (const [key, inMemoryEntry] of this.entries) {
178
+ const loadedEntry = loaded.get(key)
179
+ if (!loadedEntry || inMemoryEntry.updated_at >= loadedEntry.updated_at) {
180
+ loaded.set(key, inMemoryEntry)
181
+ }
182
+ }
183
+ this.entries = loaded
184
+ this.currentSize = 0
185
+ for (const entry of this.entries.values()) {
186
+ this.currentSize += entry.size
187
+ }
188
+ this.initialized = true
189
+ } catch {
190
+ // IndexedDB might not be available (e.g. incognito with restrictions)
191
+ this.initialized = true
192
+ }
193
+ }
194
+
195
+ get storageDirectory(): string {
196
+ return `/termcast/cache/${this.namespace}`
197
+ }
198
+
199
+ get(key: string): string | undefined {
200
+ const entry = this.entries.get(key)
201
+ if (!entry) return undefined
202
+
203
+ // Update LRU timestamp in-memory and async in IDB
204
+ const now = nextTimestamp()
205
+ entry.last_accessed_at = now
206
+ idbPut(entry).catch(() => {})
207
+
208
+ return entry.data
209
+ }
210
+
211
+ has(key: string): boolean {
212
+ return this.entries.has(key)
213
+ }
214
+
215
+ get isEmpty(): boolean {
216
+ return this.entries.size === 0
217
+ }
218
+
219
+ set(key: string, data: string): void {
220
+ const now = nextTimestamp()
221
+ const dataSize = byteLength(data)
222
+
223
+ const existing = this.entries.get(key)
224
+ const oldSize = existing?.size || 0
225
+ const newTotalSize = this.currentSize - oldSize + dataSize
226
+
227
+ if (newTotalSize > this.capacity) {
228
+ this.maintainCapacity(newTotalSize - this.capacity)
229
+ }
230
+
231
+ const entry: CacheEntry = {
232
+ namespace: this.namespace,
233
+ key,
234
+ data,
235
+ size: dataSize,
236
+ last_accessed_at: now,
237
+ updated_at: now,
238
+ }
239
+
240
+ this.entries.set(key, entry)
241
+ this.currentSize = this.currentSize - oldSize + dataSize
242
+
243
+ // Persist async
244
+ idbPut(entry).catch(() => {})
245
+
246
+ this.notifySubscribers(key, data)
247
+ }
248
+
249
+ remove(key: string): boolean {
250
+ const entry = this.entries.get(key)
251
+ if (!entry) return false
252
+
253
+ this.entries.delete(key)
254
+ this.currentSize -= entry.size
255
+
256
+ // Persist async
257
+ idbDelete(this.namespace, key).catch(() => {})
258
+
259
+ this.notifySubscribers(key, undefined)
260
+ return true
261
+ }
262
+
263
+ clear(options?: { notifySubscribers: boolean }): void {
264
+ this.entries.clear()
265
+ this.currentSize = 0
266
+
267
+ // Persist async
268
+ idbClearNamespace(this.namespace).catch(() => {})
269
+
270
+ if (options?.notifySubscribers !== false) {
271
+ this.notifySubscribers(undefined, undefined)
272
+ }
273
+ }
274
+
275
+ subscribe(subscriber: Cache.Subscriber): Cache.Subscription {
276
+ this.subscribers.push(subscriber)
277
+ return () => {
278
+ const index = this.subscribers.indexOf(subscriber)
279
+ if (index > -1) {
280
+ this.subscribers.splice(index, 1)
281
+ }
282
+ }
283
+ }
284
+
285
+ private maintainCapacity(bytesToFree: number): void {
286
+ // Sort by LRU — oldest first
287
+ const sorted = [...this.entries.values()].sort(
288
+ (a, b) => a.last_accessed_at - b.last_accessed_at,
289
+ )
290
+
291
+ let freedBytes = 0
292
+ for (const entry of sorted) {
293
+ if (freedBytes >= bytesToFree) break
294
+ this.entries.delete(entry.key)
295
+ freedBytes += entry.size
296
+ // Persist async
297
+ idbDelete(this.namespace, entry.key).catch(() => {})
298
+ }
299
+ this.currentSize -= freedBytes
300
+ }
301
+
302
+ private notifySubscribers(
303
+ key: string | undefined,
304
+ data: string | undefined,
305
+ ): void {
306
+ for (const subscriber of this.subscribers) {
307
+ try {
308
+ subscriber(key, data)
309
+ } catch {
310
+ // Ignore subscriber errors in browser
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ export namespace Cache {
317
+ export interface Options {
318
+ namespace?: string
319
+ capacity?: number
320
+ }
321
+
322
+ export type Subscriber = (
323
+ key: string | undefined,
324
+ data: string | undefined,
325
+ ) => void
326
+ export type Subscription = () => void
327
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Browser LocalStorage implementation backed by window.localStorage.
3
+ *
4
+ * Values are stored as JSON objects with type metadata to preserve
5
+ * number/boolean types across serialization, matching the SQLite
6
+ * implementation's behavior.
7
+ */
8
+
9
+ const PREFIX = 'termcast:'
10
+
11
+ interface StoredEntry {
12
+ value: string
13
+ type: string
14
+ }
15
+
16
+ export namespace LocalStorage {
17
+ export type Value = string | number | boolean
18
+
19
+ export interface Values {
20
+ [key: string]: any
21
+ }
22
+
23
+ export async function getItem<T extends Value = Value>(
24
+ key: string,
25
+ ): Promise<T | undefined> {
26
+ return getItemSync<T>(key)
27
+ }
28
+
29
+ export function getItemSync<T extends Value = Value>(
30
+ key: string,
31
+ ): T | undefined {
32
+ try {
33
+ const raw = window.localStorage.getItem(PREFIX + key)
34
+ if (raw === null) return undefined
35
+
36
+ const entry: StoredEntry = JSON.parse(raw)
37
+
38
+ let value: Value
39
+ switch (entry.type) {
40
+ case 'number':
41
+ value = parseFloat(entry.value)
42
+ break
43
+ case 'boolean':
44
+ value = entry.value === 'true'
45
+ break
46
+ default:
47
+ value = entry.value
48
+ }
49
+
50
+ return value as T
51
+ } catch {
52
+ return undefined
53
+ }
54
+ }
55
+
56
+ export async function setItem(key: string, value: Value): Promise<void> {
57
+ try {
58
+ const entry: StoredEntry = {
59
+ value: String(value),
60
+ type: typeof value,
61
+ }
62
+ window.localStorage.setItem(PREFIX + key, JSON.stringify(entry))
63
+ } catch {
64
+ // localStorage might be full or disabled
65
+ }
66
+ }
67
+
68
+ export async function removeItem(key: string): Promise<void> {
69
+ window.localStorage.removeItem(PREFIX + key)
70
+ }
71
+
72
+ export async function allItems<T extends Values = Values>(): Promise<T> {
73
+ const result: Values = {}
74
+
75
+ for (let i = 0; i < window.localStorage.length; i++) {
76
+ const fullKey = window.localStorage.key(i)
77
+ if (!fullKey?.startsWith(PREFIX)) continue
78
+
79
+ const key = fullKey.slice(PREFIX.length)
80
+ const raw = window.localStorage.getItem(fullKey)
81
+ if (raw === null) continue
82
+
83
+ try {
84
+ const entry: StoredEntry = JSON.parse(raw)
85
+
86
+ let value: Value
87
+ switch (entry.type) {
88
+ case 'number':
89
+ value = parseFloat(entry.value)
90
+ break
91
+ case 'boolean':
92
+ value = entry.value === 'true'
93
+ break
94
+ default:
95
+ value = entry.value
96
+ }
97
+
98
+ result[key] = value
99
+ } catch {
100
+ // Skip corrupted entries
101
+ }
102
+ }
103
+
104
+ return result as T
105
+ }
106
+
107
+ export async function clear(): Promise<void> {
108
+ const keysToRemove: string[] = []
109
+ for (let i = 0; i < window.localStorage.length; i++) {
110
+ const fullKey = window.localStorage.key(i)
111
+ if (fullKey?.startsWith(PREFIX)) {
112
+ keysToRemove.push(fullKey)
113
+ }
114
+ }
115
+ for (const key of keysToRemove) {
116
+ window.localStorage.removeItem(key)
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Browser platform runtime.
3
+ *
4
+ * Provides the same exports as the Node runtime but backed by browser APIs
5
+ * or sensible no-ops for features that don't exist in the browser.
6
+ */
7
+
8
+ // ── filesystem (no-ops / stubs) ─────────────────────────────────────
9
+
10
+ export function ensureDir(_dir: string): void {
11
+ // no-op: browser storage (IndexedDB) doesn't need directory creation
12
+ }
13
+
14
+ export function fileExists(_p: string): boolean {
15
+ return false
16
+ }
17
+
18
+ export function readFileSync(_p: string): string {
19
+ throw new Error('readFileSync is not available in the browser')
20
+ }
21
+
22
+ export function appendToFile(_p: string, _data: string): void {
23
+ // no-op: browser logger uses console only
24
+ }
25
+
26
+ export function unlinkIfExists(_p: string): void {
27
+ // no-op
28
+ }
29
+
30
+ export function readdirSync(_dir: string): Array<{ name: string; isDirectory(): boolean }> {
31
+ return []
32
+ }
33
+
34
+ export function rmSync(_p: string): void {
35
+ // no-op
36
+ }
37
+
38
+ export function mkdirSync(_dir: string): void {
39
+ // no-op
40
+ }
41
+
42
+ export function cpSync(_src: string, _dest: string): void {
43
+ throw new Error('cpSync is not available in the browser')
44
+ }
45
+
46
+ // ── async filesystem ────────────────────────────────────────────────
47
+
48
+ export async function readdirAsync(_dir: string): Promise<Array<{ name: string; isDirectory(): boolean }>> {
49
+ return []
50
+ }
51
+
52
+ export async function accessAsync(_p: string): Promise<boolean> {
53
+ return false
54
+ }
55
+
56
+ // ── path (pure string ops) ──────────────────────────────────────────
57
+
58
+ export function joinPath(...parts: string[]): string {
59
+ return parts
60
+ .filter(Boolean)
61
+ .join('/')
62
+ .replace(/\/+/g, '/')
63
+ }
64
+
65
+ export function dirname(p: string): string {
66
+ const parts = p.split('/')
67
+ parts.pop()
68
+ return parts.join('/') || '/'
69
+ }
70
+
71
+ export function basename(p: string): string {
72
+ return p.split('/').pop() || ''
73
+ }
74
+
75
+ export function resolvePath(...parts: string[]): string {
76
+ return joinPath(...parts)
77
+ }
78
+
79
+ export function isAbsolute(p: string): boolean {
80
+ return p.startsWith('/')
81
+ }
82
+
83
+ export function relativePath(from: string, to: string): string {
84
+ const fromParts = from.split('/').filter(Boolean)
85
+ const toParts = to.split('/').filter(Boolean)
86
+ let common = 0
87
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
88
+ common++
89
+ }
90
+ const ups = fromParts.length - common
91
+ return [...Array(ups).fill('..'), ...toParts.slice(common)].join('/')
92
+ }
93
+
94
+ // ── os ──────────────────────────────────────────────────────────────
95
+
96
+ export function homedir(): string {
97
+ return '/home'
98
+ }
99
+
100
+ // ── process ─────────────────────────────────────────────────────────
101
+
102
+ export const platform = 'browser'
103
+
104
+ export function exit(_code?: number): void {
105
+ // no-op: can't exit the browser
106
+ }
107
+
108
+ export function getEnv(key: string): string | undefined {
109
+ // Browser env can be populated by the host app before boot
110
+ return (globalThis as any).__termcast_env?.[key]
111
+ }
112
+
113
+ export function setEnv(key: string, value: string): void {
114
+ const g = globalThis as any
115
+ if (!g.__termcast_env) {
116
+ g.__termcast_env = {}
117
+ }
118
+ g.__termcast_env[key] = value
119
+ }
120
+
121
+ export function cwd(): string {
122
+ return '/'
123
+ }
124
+
125
+ export function stdoutWrite(_data: string): void {
126
+ // no-op: browser renderer handles output
127
+ }
128
+
129
+ export function isTTY(): boolean {
130
+ return false
131
+ }
132
+
133
+ export function getArgv(): string[] {
134
+ return []
135
+ }
136
+
137
+ // ── appearance ──────────────────────────────────────────────────────
138
+
139
+ export function getSystemAppearance(): 'dark' | 'light' {
140
+ if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
141
+ return 'dark'
142
+ }
143
+ return 'light'
144
+ }
145
+
146
+ // ── util ────────────────────────────────────────────────────────────
147
+
148
+ export function byteLength(str: string): number {
149
+ return new TextEncoder().encode(str).length
150
+ }
151
+
152
+ export function inspectValue(val: unknown, _depth = 3): string {
153
+ if (typeof val === 'string') {
154
+ return val
155
+ }
156
+ try {
157
+ return JSON.stringify(val, null, 2)
158
+ } catch {
159
+ return String(val)
160
+ }
161
+ }
162
+
163
+ // ── error handling ──────────────────────────────────────────────────
164
+
165
+ export function setupErrorHandlers(handler: (err: Error, type: string) => void): void {
166
+ window.addEventListener('error', (event) => {
167
+ handler(event.error ?? new Error(event.message), 'uncaughtException')
168
+ })
169
+ window.addEventListener('unhandledrejection', (event) => {
170
+ const err = event.reason instanceof Error
171
+ ? event.reason
172
+ : new Error(String(event.reason))
173
+ handler(err, 'unhandledRejection')
174
+ })
175
+ }
176
+
177
+ // ── shell / clipboard ───────────────────────────────────────────────
178
+
179
+ export function execWithInput(_command: string, _input: string): Promise<void> {
180
+ return Promise.reject(new Error('execWithInput is not available in the browser'))
181
+ }
182
+
183
+ export async function execCommand(_command: string): Promise<string> {
184
+ throw new Error('execCommand is not available in the browser')
185
+ }
186
+
187
+ export async function copyToClipboard(text: string): Promise<void> {
188
+ await navigator.clipboard.writeText(text)
189
+ }
190
+
191
+ export async function readClipboard(): Promise<string> {
192
+ return navigator.clipboard.readText()
193
+ }
194
+
195
+ export async function openUrl(url: string): Promise<void> {
196
+ window.open(url, '_blank')
197
+ }
198
+
199
+ export async function openFile(_target: string, _app?: string): Promise<void> {
200
+ throw new Error('openFile is not supported in the browser')
201
+ }
202
+
203
+ export async function showInFileManager(_filePath: string): Promise<void> {
204
+ throw new Error('showInFileManager is not supported in the browser')
205
+ }
206
+
207
+ export async function moveToTrash(_filePath: string): Promise<void> {
208
+ throw new Error('moveToTrash is not supported in the browser')
209
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SQLite Database for Bun — re-exports the built-in bun:sqlite.
3
+ */
4
+
5
+ import { Database as BunDatabase } from 'bun:sqlite'
6
+
7
+ export const Database = BunDatabase
8
+
9
+ /** Instance type returned by `new Database(...)` */
10
+ export type DatabaseInstance = {
11
+ exec(sql: string): void
12
+ prepare(sql: string): {
13
+ get(...params: any[]): any
14
+ all(...params: any[]): any[]
15
+ run(...params: any[]): any
16
+ }
17
+ transaction<T>(fn: () => T): () => T
18
+ close(): void
19
+ }