termcast 1.3.32 → 1.3.34

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 (327) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +8 -0
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +1 -2
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +138 -54
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts.map +1 -1
  9. package/dist/apis/clipboard.js +4 -0
  10. package/dist/apis/clipboard.js.map +1 -1
  11. package/dist/apis/oauth.d.ts.map +1 -1
  12. package/dist/apis/oauth.js +31 -4
  13. package/dist/apis/oauth.js.map +1 -1
  14. package/dist/build.d.ts +0 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +30 -51
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.js +31 -14
  19. package/dist/cli.js.map +1 -1
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +5 -1
  22. package/dist/compile.js.map +1 -1
  23. package/dist/components/actions.d.ts +14 -0
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +151 -59
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/alert.d.ts.map +1 -1
  28. package/dist/components/alert.js +6 -5
  29. package/dist/components/alert.js.map +1 -1
  30. package/dist/components/animation-tick.d.ts +1 -1
  31. package/dist/components/animation-tick.js +1 -1
  32. package/dist/components/animation-tick.js.map +1 -1
  33. package/dist/components/detail.d.ts +5 -31
  34. package/dist/components/detail.d.ts.map +1 -1
  35. package/dist/components/detail.js +36 -52
  36. package/dist/components/detail.js.map +1 -1
  37. package/dist/components/dropdown.d.ts +1 -1
  38. package/dist/components/dropdown.d.ts.map +1 -1
  39. package/dist/components/dropdown.js +50 -22
  40. package/dist/components/dropdown.js.map +1 -1
  41. package/dist/components/footer.d.ts.map +1 -1
  42. package/dist/components/footer.js +19 -18
  43. package/dist/components/footer.js.map +1 -1
  44. package/dist/components/form/checkbox.d.ts.map +1 -1
  45. package/dist/components/form/checkbox.js +12 -11
  46. package/dist/components/form/checkbox.js.map +1 -1
  47. package/dist/components/form/date-picker.d.ts.map +1 -1
  48. package/dist/components/form/date-picker.js +7 -22
  49. package/dist/components/form/date-picker.js.map +1 -1
  50. package/dist/components/form/description.d.ts +1 -1
  51. package/dist/components/form/description.d.ts.map +1 -1
  52. package/dist/components/form/description.js +6 -5
  53. package/dist/components/form/description.js.map +1 -1
  54. package/dist/components/form/dropdown.d.ts.map +1 -1
  55. package/dist/components/form/dropdown.js +53 -50
  56. package/dist/components/form/dropdown.js.map +1 -1
  57. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  58. package/dist/components/form/file-autocomplete.js +5 -4
  59. package/dist/components/form/file-autocomplete.js.map +1 -1
  60. package/dist/components/form/file-picker.d.ts.map +1 -1
  61. package/dist/components/form/file-picker.js +23 -22
  62. package/dist/components/form/file-picker.js.map +1 -1
  63. package/dist/components/form/form-end.d.ts.map +1 -1
  64. package/dist/components/form/form-end.js +6 -4
  65. package/dist/components/form/form-end.js.map +1 -1
  66. package/dist/components/form/form-field-wrapper.d.ts +15 -0
  67. package/dist/components/form/form-field-wrapper.d.ts.map +1 -0
  68. package/dist/components/form/form-field-wrapper.js +29 -0
  69. package/dist/components/form/form-field-wrapper.js.map +1 -0
  70. package/dist/components/form/index.d.ts.map +1 -1
  71. package/dist/components/form/index.js +31 -30
  72. package/dist/components/form/index.js.map +1 -1
  73. package/dist/components/form/password-field.d.ts.map +1 -1
  74. package/dist/components/form/password-field.js +7 -6
  75. package/dist/components/form/password-field.js.map +1 -1
  76. package/dist/components/form/separator.d.ts.map +1 -1
  77. package/dist/components/form/separator.js +3 -2
  78. package/dist/components/form/separator.js.map +1 -1
  79. package/dist/components/form/tagpicker.d.ts.map +1 -1
  80. package/dist/components/form/tagpicker.js +2 -1
  81. package/dist/components/form/tagpicker.js.map +1 -1
  82. package/dist/components/form/text-area.d.ts.map +1 -1
  83. package/dist/components/form/text-area.js +7 -6
  84. package/dist/components/form/text-area.js.map +1 -1
  85. package/dist/components/form/text-field.d.ts.map +1 -1
  86. package/dist/components/form/text-field.js +7 -6
  87. package/dist/components/form/text-field.js.map +1 -1
  88. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  89. package/dist/components/form/use-form-navigation.js +4 -4
  90. package/dist/components/form/use-form-navigation.js.map +1 -1
  91. package/dist/components/form/with-left-border.d.ts +15 -0
  92. package/dist/components/form/with-left-border.d.ts.map +1 -1
  93. package/dist/components/form/with-left-border.js +21 -9
  94. package/dist/components/form/with-left-border.js.map +1 -1
  95. package/dist/components/icon.d.ts +14 -0
  96. package/dist/components/icon.d.ts.map +1 -1
  97. package/dist/components/icon.js +60 -0
  98. package/dist/components/icon.js.map +1 -1
  99. package/dist/components/image.d.ts +47 -2
  100. package/dist/components/image.d.ts.map +1 -1
  101. package/dist/components/image.js +46 -7
  102. package/dist/components/image.js.map +1 -1
  103. package/dist/components/list.d.ts +5 -0
  104. package/dist/components/list.d.ts.map +1 -1
  105. package/dist/components/list.js +188 -132
  106. package/dist/components/list.js.map +1 -1
  107. package/dist/components/loading-bar.d.ts.map +1 -1
  108. package/dist/components/loading-bar.js +4 -3
  109. package/dist/components/loading-bar.js.map +1 -1
  110. package/dist/components/metadata.d.ts +70 -0
  111. package/dist/components/metadata.d.ts.map +1 -0
  112. package/dist/components/metadata.js +82 -0
  113. package/dist/components/metadata.js.map +1 -0
  114. package/dist/components/theme-picker.d.ts.map +1 -1
  115. package/dist/components/theme-picker.js +3 -2
  116. package/dist/components/theme-picker.js.map +1 -1
  117. package/dist/descendants-v2.d.ts +60 -0
  118. package/dist/descendants-v2.d.ts.map +1 -0
  119. package/dist/descendants-v2.js +144 -0
  120. package/dist/descendants-v2.js.map +1 -0
  121. package/dist/examples/actions-context.d.ts +2 -0
  122. package/dist/examples/actions-context.d.ts.map +1 -0
  123. package/dist/examples/actions-context.js +33 -0
  124. package/dist/examples/actions-context.js.map +1 -0
  125. package/dist/examples/form-basic.d.ts.map +1 -1
  126. package/dist/examples/form-basic.js +1 -1
  127. package/dist/examples/form-basic.js.map +1 -1
  128. package/dist/examples/form-dropdown.js +1 -1
  129. package/dist/examples/form-dropdown.js.map +1 -1
  130. package/dist/examples/internal/custom-action-renderables.d.ts +70 -0
  131. package/dist/examples/internal/custom-action-renderables.d.ts.map +1 -0
  132. package/dist/examples/internal/custom-action-renderables.js +163 -0
  133. package/dist/examples/internal/custom-action-renderables.js.map +1 -0
  134. package/dist/examples/internal/custom-dropdown.d.ts +99 -0
  135. package/dist/examples/internal/custom-dropdown.d.ts.map +1 -0
  136. package/dist/examples/internal/custom-dropdown.js +270 -0
  137. package/dist/examples/internal/custom-dropdown.js.map +1 -0
  138. package/dist/examples/internal/custom-renderable-form.d.ts +43 -0
  139. package/dist/examples/internal/custom-renderable-form.d.ts.map +1 -0
  140. package/dist/examples/internal/custom-renderable-form.js +284 -0
  141. package/dist/examples/internal/custom-renderable-form.js.map +1 -0
  142. package/dist/examples/internal/custom-renderable-list-default-search.d.ts +2 -0
  143. package/dist/examples/internal/custom-renderable-list-default-search.d.ts.map +1 -0
  144. package/dist/examples/internal/custom-renderable-list-default-search.js +16 -0
  145. package/dist/examples/internal/custom-renderable-list-default-search.js.map +1 -0
  146. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts +2 -0
  147. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts.map +1 -0
  148. package/dist/examples/internal/custom-renderable-list-v2-default-search.js +24 -0
  149. package/dist/examples/internal/custom-renderable-list-v2-default-search.js.map +1 -0
  150. package/dist/examples/internal/custom-renderable-list-v2.d.ts +189 -0
  151. package/dist/examples/internal/custom-renderable-list-v2.d.ts.map +1 -0
  152. package/dist/examples/internal/custom-renderable-list-v2.js +708 -0
  153. package/dist/examples/internal/custom-renderable-list-v2.js.map +1 -0
  154. package/dist/examples/internal/custom-renderable-list.d.ts +72 -0
  155. package/dist/examples/internal/custom-renderable-list.d.ts.map +1 -0
  156. package/dist/examples/internal/custom-renderable-list.js +544 -0
  157. package/dist/examples/internal/custom-renderable-list.js.map +1 -0
  158. package/dist/examples/internal/rhf-custom-ref.js +5 -4
  159. package/dist/examples/internal/rhf-custom-ref.js.map +1 -1
  160. package/dist/examples/internal/scrollbox-with-descendants.js +4 -2
  161. package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
  162. package/dist/examples/list-controlled-search.d.ts +2 -0
  163. package/dist/examples/list-controlled-search.d.ts.map +1 -0
  164. package/dist/examples/list-controlled-search.js +12 -0
  165. package/dist/examples/list-controlled-search.js.map +1 -0
  166. package/dist/examples/list-detail-metadata.js +1 -1
  167. package/dist/examples/list-detail-metadata.js.map +1 -1
  168. package/dist/examples/simple-image-mask.d.ts +8 -0
  169. package/dist/examples/simple-image-mask.d.ts.map +1 -0
  170. package/dist/examples/simple-image-mask.js +12 -0
  171. package/dist/examples/simple-image-mask.js.map +1 -0
  172. package/dist/examples/toast-variations.js +1 -1
  173. package/dist/examples/toast-variations.js.map +1 -1
  174. package/dist/extensions/dev.d.ts.map +1 -1
  175. package/dist/extensions/dev.js +3 -2
  176. package/dist/extensions/dev.js.map +1 -1
  177. package/dist/extensions/react-refresh-init.d.ts.map +1 -1
  178. package/dist/extensions/react-refresh-init.js +4 -3
  179. package/dist/extensions/react-refresh-init.js.map +1 -1
  180. package/dist/index.d.ts +3 -2
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +1 -1
  183. package/dist/index.js.map +1 -1
  184. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  185. package/dist/internal/date-picker-widget.js +2 -1
  186. package/dist/internal/date-picker-widget.js.map +1 -1
  187. package/dist/internal/dialog.d.ts +6 -0
  188. package/dist/internal/dialog.d.ts.map +1 -1
  189. package/dist/internal/dialog.js +59 -18
  190. package/dist/internal/dialog.js.map +1 -1
  191. package/dist/internal/navigation.d.ts.map +1 -1
  192. package/dist/internal/navigation.js +8 -1
  193. package/dist/internal/navigation.js.map +1 -1
  194. package/dist/internal/offscreen.d.ts +3 -0
  195. package/dist/internal/offscreen.d.ts.map +1 -1
  196. package/dist/internal/offscreen.js +5 -0
  197. package/dist/internal/offscreen.js.map +1 -1
  198. package/dist/internal/providers.d.ts.map +1 -1
  199. package/dist/internal/providers.js +20 -3
  200. package/dist/internal/providers.js.map +1 -1
  201. package/dist/internal/scrollbox.d.ts.map +1 -1
  202. package/dist/internal/scrollbox.js +3 -2
  203. package/dist/internal/scrollbox.js.map +1 -1
  204. package/dist/logger.d.ts.map +1 -1
  205. package/dist/logger.js +4 -0
  206. package/dist/logger.js.map +1 -1
  207. package/dist/preload.js +5 -17
  208. package/dist/preload.js.map +1 -1
  209. package/dist/state.d.ts +4 -0
  210. package/dist/state.d.ts.map +1 -1
  211. package/dist/state.js +4 -0
  212. package/dist/state.js.map +1 -1
  213. package/dist/test-border-overlay.d.ts +2 -0
  214. package/dist/test-border-overlay.d.ts.map +1 -0
  215. package/dist/test-border-overlay.js +7 -0
  216. package/dist/test-border-overlay.js.map +1 -0
  217. package/dist/test-layout-2.d.ts +2 -0
  218. package/dist/test-layout-2.d.ts.map +1 -0
  219. package/dist/test-layout-2.js +5 -0
  220. package/dist/test-layout-2.js.map +1 -0
  221. package/dist/test-layout.d.ts +2 -0
  222. package/dist/test-layout.d.ts.map +1 -0
  223. package/dist/test-layout.js +7 -0
  224. package/dist/test-layout.js.map +1 -0
  225. package/dist/theme.d.ts +1 -2
  226. package/dist/theme.d.ts.map +1 -1
  227. package/dist/theme.js +5 -9
  228. package/dist/theme.js.map +1 -1
  229. package/dist/utils/run-command.d.ts +1 -1
  230. package/dist/utils/run-command.d.ts.map +1 -1
  231. package/dist/utils/run-command.js +27 -7
  232. package/dist/utils/run-command.js.map +1 -1
  233. package/dist/utils.d.ts +1 -0
  234. package/dist/utils.d.ts.map +1 -1
  235. package/dist/utils.js +44 -23
  236. package/dist/utils.js.map +1 -1
  237. package/dist/watcher.d.ts.map +1 -1
  238. package/dist/watcher.js +24 -4
  239. package/dist/watcher.js.map +1 -1
  240. package/package.json +14 -12
  241. package/src/action-utils.tsx +10 -0
  242. package/src/apis/cache.test.ts +35 -3
  243. package/src/apis/cache.tsx +184 -59
  244. package/src/apis/clipboard.tsx +5 -0
  245. package/src/apis/oauth.tsx +33 -4
  246. package/src/build.tsx +35 -58
  247. package/src/cli.tsx +156 -134
  248. package/src/compile.tsx +6 -3
  249. package/src/compile.vitest.tsx +33 -15
  250. package/src/components/actions.tsx +230 -99
  251. package/src/components/alert.tsx +11 -10
  252. package/src/components/animation-tick.tsx +1 -1
  253. package/src/components/detail.tsx +56 -151
  254. package/src/components/dropdown.tsx +70 -36
  255. package/src/components/footer.tsx +58 -33
  256. package/src/components/form/checkbox.tsx +30 -32
  257. package/src/components/form/date-picker.tsx +27 -47
  258. package/src/components/form/description.tsx +19 -18
  259. package/src/components/form/dropdown.tsx +95 -103
  260. package/src/components/form/file-autocomplete.tsx +9 -8
  261. package/src/components/form/file-picker.tsx +46 -46
  262. package/src/components/form/form-end.tsx +6 -4
  263. package/src/components/form/index.tsx +38 -48
  264. package/src/components/form/password-field.tsx +25 -27
  265. package/src/components/form/separator.tsx +3 -2
  266. package/src/components/form/tagpicker.tsx +2 -1
  267. package/src/components/form/text-area.tsx +25 -30
  268. package/src/components/form/text-field.tsx +25 -27
  269. package/src/components/form/use-form-navigation.tsx +4 -5
  270. package/src/components/form/with-left-border.tsx +48 -10
  271. package/src/components/icon.tsx +69 -0
  272. package/src/components/image.tsx +60 -7
  273. package/src/components/list.tsx +270 -202
  274. package/src/components/loading-bar.tsx +4 -3
  275. package/src/components/metadata.tsx +217 -0
  276. package/src/components/theme-picker.tsx +3 -2
  277. package/src/examples/actions-context.tsx +63 -0
  278. package/src/examples/actions-context.vitest.tsx +110 -0
  279. package/src/examples/actions-dialog-layout.vitest.tsx +2 -1
  280. package/src/examples/file-autocomplete.vitest.tsx +15 -15
  281. package/src/examples/form-basic.tsx +12 -0
  282. package/src/examples/form-basic.vitest.tsx +74 -74
  283. package/src/examples/form-dropdown.tsx +8 -0
  284. package/src/examples/form-dropdown.vitest.tsx +364 -421
  285. package/src/examples/form-tagpicker.vitest.tsx +56 -54
  286. package/src/examples/github.vitest.tsx +252 -0
  287. package/src/examples/internal/rhf-custom-ref.tsx +16 -15
  288. package/src/examples/internal/scrollbox-with-descendants.tsx +4 -2
  289. package/src/examples/internal/simple-dialog.tsx +1 -1
  290. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -9
  291. package/src/examples/list-controlled-search.tsx +28 -0
  292. package/src/examples/list-controlled-search.vitest.tsx +49 -0
  293. package/src/examples/list-detail-metadata.tsx +8 -5
  294. package/src/examples/list-detail-metadata.vitest.tsx +22 -22
  295. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  296. package/src/examples/list-scrollbox.vitest.tsx +52 -38
  297. package/src/examples/list-with-detail.vitest.tsx +45 -41
  298. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  299. package/src/examples/list-with-sections.vitest.tsx +65 -12
  300. package/src/examples/list-with-toast.vitest.tsx +4 -4
  301. package/src/examples/simple-file-picker.vitest.tsx +12 -12
  302. package/src/examples/simple-grid.vitest.tsx +53 -53
  303. package/src/examples/simple-image-mask.tsx +58 -0
  304. package/src/examples/simple-navigation.vitest.tsx +19 -19
  305. package/src/examples/store.vitest.tsx +1 -1
  306. package/src/examples/swift-extension.vitest.tsx +4 -2
  307. package/src/examples/synonyms.vitest.tsx +31 -9
  308. package/src/examples/toast-action.vitest.tsx +8 -8
  309. package/src/examples/toast-variations.tsx +1 -1
  310. package/src/examples/toast-variations.vitest.tsx +69 -134
  311. package/src/extensions/dev.tsx +3 -2
  312. package/src/extensions/dev.vitest.tsx +65 -28
  313. package/src/extensions/react-refresh-init.tsx +4 -3
  314. package/src/index.tsx +3 -1
  315. package/src/internal/date-picker-widget.tsx +2 -1
  316. package/src/internal/dialog.tsx +100 -28
  317. package/src/internal/navigation.tsx +8 -1
  318. package/src/internal/offscreen.tsx +10 -0
  319. package/src/internal/providers.tsx +34 -8
  320. package/src/internal/scrollbox.tsx +4 -2
  321. package/src/logger.tsx +4 -0
  322. package/src/preload.tsx +5 -17
  323. package/src/state.tsx +12 -0
  324. package/src/theme.tsx +6 -9
  325. package/src/utils/run-command.tsx +32 -8
  326. package/src/utils.tsx +58 -23
  327. package/src/watcher.tsx +26 -6
@@ -5,15 +5,27 @@ import * as fs from 'fs'
5
5
  import { logger } from '../logger'
6
6
  import { useStore } from '../state'
7
7
 
8
+ const CACHE_TABLE_NAME = 'cache_entries'
9
+ const DEFAULT_NAMESPACE = '__default__'
10
+ const initializedDatabasePaths = new Set<string>()
11
+ let logicalTimestamp = Date.now()
12
+
13
+ function nextTimestamp(): number {
14
+ logicalTimestamp += 1
15
+ return logicalTimestamp
16
+ }
17
+
8
18
  function getCurrentDatabasePath(): string {
9
19
  const { extensionPath } = useStore.getState()
20
+ const dbSuffix = process.env.TERMCAST_DB_SUFFIX?.replace(/[^a-zA-Z0-9_-]/g, '_')
21
+ const dbFileName = dbSuffix ? `data-${dbSuffix}.db` : 'data.db'
10
22
 
11
23
  if (extensionPath) {
12
- return path.join(extensionPath, '.termcast-bundle', 'data.db')
24
+ return path.join(extensionPath, '.termcast-bundle', dbFileName)
13
25
  }
14
26
 
15
27
  // Fallback for examples/tests that don't set extensionPath
16
- return path.join(os.homedir(), '.termcast', '.termcast-bundle', 'data.db')
28
+ return path.join(os.homedir(), '.termcast', '.termcast-bundle', dbFileName)
17
29
  }
18
30
 
19
31
  function getCurrentCacheDir(namespace?: string): string {
@@ -26,6 +38,109 @@ function getCurrentCacheDir(namespace?: string): string {
26
38
  return namespace ? path.join(baseDir, namespace) : baseDir
27
39
  }
28
40
 
41
+ function getNamespace(namespace?: string): string {
42
+ return namespace || DEFAULT_NAMESPACE
43
+ }
44
+
45
+ function initializeDatabaseOnce({ db, dbPath }: { db: Database; dbPath: string }): void {
46
+ if (initializedDatabasePaths.has(dbPath)) {
47
+ return
48
+ }
49
+
50
+ db.exec('PRAGMA journal_mode = WAL')
51
+ db.exec('PRAGMA wal_autocheckpoint = 1000')
52
+ db.exec('PRAGMA synchronous = NORMAL')
53
+
54
+ db.exec(`
55
+ CREATE TABLE IF NOT EXISTS ${CACHE_TABLE_NAME} (
56
+ namespace TEXT NOT NULL,
57
+ key TEXT NOT NULL,
58
+ data TEXT NOT NULL,
59
+ size INTEGER NOT NULL,
60
+ last_accessed_at INTEGER NOT NULL,
61
+ updated_at INTEGER NOT NULL,
62
+ PRIMARY KEY(namespace, key)
63
+ )
64
+ `)
65
+
66
+ db.exec(`
67
+ CREATE INDEX IF NOT EXISTS idx_${CACHE_TABLE_NAME}_namespace_lru
68
+ ON ${CACHE_TABLE_NAME}(namespace, last_accessed_at)
69
+ `)
70
+
71
+ cleanupLegacyCacheTables(db)
72
+ initializedDatabasePaths.add(dbPath)
73
+ }
74
+
75
+ function cleanupLegacyCacheTables(db: Database): void {
76
+ const rows = db
77
+ .prepare(
78
+ `SELECT name FROM sqlite_master
79
+ WHERE type = 'table'
80
+ AND (name = 'cache' OR name LIKE 'cache_%')
81
+ AND name != ?`,
82
+ )
83
+ .all(CACHE_TABLE_NAME) as Array<{ name: string }>
84
+
85
+ if (rows.length === 0) {
86
+ return
87
+ }
88
+
89
+ const tx = db.transaction(() => {
90
+ const upsert = db.prepare(
91
+ `INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
92
+ VALUES (?, ?, ?, ?, ?, ?)
93
+ ON CONFLICT(namespace, key)
94
+ DO UPDATE SET
95
+ data = excluded.data,
96
+ size = excluded.size,
97
+ last_accessed_at = excluded.last_accessed_at,
98
+ updated_at = excluded.updated_at`,
99
+ )
100
+
101
+ for (const { name } of rows) {
102
+ const namespace =
103
+ name === 'cache'
104
+ ? DEFAULT_NAMESPACE
105
+ : name === 'cache_tanstack_query'
106
+ ? 'tanstack-query'
107
+ : `legacy:${name.slice('cache_'.length)}`
108
+
109
+ try {
110
+ const values = db
111
+ .prepare(`SELECT key, data, size, rowid FROM ${name}`)
112
+ .all() as Array<{ key: string; data: string; size: number; rowid: number }>
113
+
114
+ values.forEach((entry) => {
115
+ const timestamp = entry.rowid
116
+ upsert.run(
117
+ namespace,
118
+ entry.key,
119
+ entry.data,
120
+ entry.size,
121
+ timestamp,
122
+ timestamp,
123
+ )
124
+ })
125
+ } catch {
126
+ // Ignore invalid legacy tables and continue cleanup.
127
+ }
128
+
129
+ db.exec(`DROP TABLE IF EXISTS ${name}`)
130
+ }
131
+ })
132
+
133
+ tx()
134
+ }
135
+
136
+ function hashString(value: string): string {
137
+ let hash = 0
138
+ for (let i = 0; i < value.length; i++) {
139
+ hash = (hash * 31 + value.charCodeAt(i)) | 0
140
+ }
141
+ return Math.abs(hash).toString(36)
142
+ }
143
+
29
144
  export class Cache {
30
145
  static get STORAGE_DIRECTORY_NAME(): string {
31
146
  const extensionPath = useStore.getState().extensionPath
@@ -38,17 +153,14 @@ export class Cache {
38
153
 
39
154
  private db: Database
40
155
  private capacity: number
41
- private namespace?: string
42
- private tableName: string
156
+ private namespace: string
43
157
  private subscribers: Cache.Subscriber[] = []
44
158
  private currentSize: number = 0
45
159
 
46
160
  constructor(options?: Cache.Options) {
161
+ const sqliteLoadStart = Date.now()
47
162
  this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
48
- this.namespace = options?.namespace
49
- // Replace non-alphanumeric characters with underscores for valid SQL table names
50
- const safeNamespace = this.namespace?.replace(/[^a-zA-Z0-9]/g, '_')
51
- this.tableName = safeNamespace ? `cache_${safeNamespace}` : 'cache'
163
+ this.namespace = getNamespace(options?.namespace)
52
164
 
53
165
  const dbPath = getCurrentDatabasePath()
54
166
 
@@ -64,32 +176,23 @@ export class Cache {
64
176
  readwrite: true,
65
177
  })
66
178
 
67
- // Use WAL mode and optimize for single file usage
68
- this.db.exec('PRAGMA journal_mode = WAL')
69
- this.db.exec('PRAGMA wal_autocheckpoint = 1000')
70
- this.db.exec('PRAGMA synchronous = NORMAL')
71
-
72
- // Use rowid for ordering - it auto-increments and provides natural LRU order
73
- this.db.exec(`
74
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
75
- rowid INTEGER PRIMARY KEY AUTOINCREMENT,
76
- key TEXT UNIQUE NOT NULL,
77
- data TEXT NOT NULL,
78
- size INTEGER NOT NULL
79
- )
80
- `)
81
-
82
- // Create index on key for fast lookups
83
- this.db.exec(`
84
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key ON ${this.tableName}(key)
85
- `)
179
+ initializeDatabaseOnce({ db: this.db, dbPath })
86
180
 
87
181
  // Calculate initial size
88
182
  const row = this.db
89
- .prepare(`SELECT SUM(size) as total FROM ${this.tableName}`)
90
- .get() as { total: number | null } | undefined
183
+ .prepare(
184
+ `SELECT COALESCE(SUM(size), 0) as total FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
185
+ )
186
+ .get(this.namespace) as { total: number | null } | undefined
91
187
  this.currentSize = row?.total || 0
92
188
 
189
+ const sqliteLoadMs = Date.now() - sqliteLoadStart
190
+ if (sqliteLoadMs > 500) {
191
+ logger.log(
192
+ `[perf] sqlite cache init took ${sqliteLoadMs}ms (namespace=${this.namespace})`,
193
+ )
194
+ }
195
+
93
196
  // Bind all methods to this instance
94
197
  this.get = this.get.bind(this)
95
198
  this.has = this.has.bind(this)
@@ -106,21 +209,19 @@ export class Cache {
106
209
  }
107
210
 
108
211
  get(key: string): string | undefined {
212
+ const now = nextTimestamp()
109
213
  const row = this.db
110
- .prepare(`SELECT rowid, data, size FROM ${this.tableName} WHERE key = ?`)
111
- .get(key) as { rowid: number; data: string; size: number } | undefined
214
+ .prepare(
215
+ `SELECT data, size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
216
+ )
217
+ .get(this.namespace, key) as { data: string; size: number } | undefined
112
218
 
113
219
  if (row) {
114
- // Move to end of LRU by deleting and reinserting (gets new rowid)
115
- const tx = this.db.transaction(() => {
116
- this.db.prepare(`DELETE FROM ${this.tableName} WHERE key = ?`).run(key)
117
- this.db
118
- .prepare(
119
- `INSERT INTO ${this.tableName} (key, data, size) VALUES (?, ?, ?)`,
120
- )
121
- .run(key, row.data, row.size)
122
- })
123
- tx()
220
+ this.db
221
+ .prepare(
222
+ `UPDATE ${CACHE_TABLE_NAME} SET last_accessed_at = ? WHERE namespace = ? AND key = ?`,
223
+ )
224
+ .run(now, this.namespace, key)
124
225
 
125
226
  return row.data
126
227
  }
@@ -130,25 +231,30 @@ export class Cache {
130
231
 
131
232
  has(key: string): boolean {
132
233
  const row = this.db
133
- .prepare(`SELECT 1 FROM ${this.tableName} WHERE key = ?`)
134
- .get(key)
234
+ .prepare(`SELECT 1 FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
235
+ .get(this.namespace, key)
135
236
  return !!row
136
237
  }
137
238
 
138
239
  get isEmpty(): boolean {
139
240
  const row = this.db
140
- .prepare(`SELECT COUNT(*) as count FROM ${this.tableName}`)
141
- .get() as { count: number }
241
+ .prepare(
242
+ `SELECT COUNT(*) as count FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
243
+ )
244
+ .get(this.namespace) as { count: number }
142
245
  return row.count === 0
143
246
  }
144
247
 
145
248
  set(key: string, data: string): void {
249
+ const now = nextTimestamp()
146
250
  const dataSize = Buffer.byteLength(data, 'utf8')
147
251
 
148
252
  // Get existing size if any
149
253
  const existingRow = this.db
150
- .prepare(`SELECT size FROM ${this.tableName} WHERE key = ?`)
151
- .get(key) as { size: number } | undefined
254
+ .prepare(
255
+ `SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
256
+ )
257
+ .get(this.namespace, key) as { size: number } | undefined
152
258
  const oldSize = existingRow?.size || 0
153
259
  const newTotalSize = this.currentSize - oldSize + dataSize
154
260
 
@@ -159,9 +265,16 @@ export class Cache {
159
265
  // Insert or update the cache entry
160
266
  this.db
161
267
  .prepare(
162
- `INSERT OR REPLACE INTO ${this.tableName} (key, data, size) VALUES (?, ?, ?)`,
268
+ `INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
269
+ VALUES (?, ?, ?, ?, ?, ?)
270
+ ON CONFLICT(namespace, key)
271
+ DO UPDATE SET
272
+ data = excluded.data,
273
+ size = excluded.size,
274
+ last_accessed_at = excluded.last_accessed_at,
275
+ updated_at = excluded.updated_at`,
163
276
  )
164
- .run(key, data, dataSize)
277
+ .run(this.namespace, key, data, dataSize, now, now)
165
278
 
166
279
  this.currentSize = this.currentSize - oldSize + dataSize
167
280
  this.notifySubscribers(key, data)
@@ -170,12 +283,16 @@ export class Cache {
170
283
  remove(key: string): boolean {
171
284
  // Check if key exists and get its size
172
285
  const row = this.db
173
- .prepare(`SELECT size FROM ${this.tableName} WHERE key = ?`)
174
- .get(key) as { size: number } | undefined
286
+ .prepare(
287
+ `SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
288
+ )
289
+ .get(this.namespace, key) as { size: number } | undefined
175
290
 
176
291
  if (row) {
177
292
  // Delete the key
178
- this.db.prepare(`DELETE FROM ${this.tableName} WHERE key = ?`).run(key)
293
+ this.db
294
+ .prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
295
+ .run(this.namespace, key)
179
296
 
180
297
  this.currentSize -= row.size
181
298
  this.notifySubscribers(key, undefined)
@@ -186,7 +303,9 @@ export class Cache {
186
303
  }
187
304
 
188
305
  clear(options?: { notifySubscribers: boolean }): void {
189
- this.db.exec(`DELETE FROM ${this.tableName}`)
306
+ this.db
307
+ .prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`)
308
+ .run(this.namespace)
190
309
  this.currentSize = 0
191
310
 
192
311
  if (options?.notifySubscribers !== false) {
@@ -205,10 +324,14 @@ export class Cache {
205
324
  }
206
325
 
207
326
  private maintainCapacity(bytesToFree: number): void {
208
- // Order by rowid ASC to get oldest entries first
327
+ // Order by oldest last-access time first to evict least-recently-used rows.
209
328
  const rows = this.db
210
- .prepare(`SELECT key, size FROM ${this.tableName} ORDER BY rowid ASC`)
211
- .all() as Array<{ key: string; size: number }>
329
+ .prepare(
330
+ `SELECT key, size FROM ${CACHE_TABLE_NAME}
331
+ WHERE namespace = ?
332
+ ORDER BY last_accessed_at ASC`,
333
+ )
334
+ .all(this.namespace) as Array<{ key: string; size: number }>
212
335
 
213
336
  let freedBytes = 0
214
337
  const keysToRemove: string[] = []
@@ -224,9 +347,10 @@ export class Cache {
224
347
  if (keysToRemove.length > 0) {
225
348
  const placeholders = keysToRemove.map(() => '?').join(',')
226
349
  const stmt = this.db.prepare(
227
- `DELETE FROM ${this.tableName} WHERE key IN (${placeholders})`,
350
+ `DELETE FROM ${CACHE_TABLE_NAME}
351
+ WHERE namespace = ? AND key IN (${placeholders})`,
228
352
  )
229
- stmt.run(...(keysToRemove as [string, ...string[]]))
353
+ stmt.run(this.namespace, ...(keysToRemove as [string, ...string[]]))
230
354
  this.currentSize -= freedBytes
231
355
  }
232
356
  }
@@ -324,7 +448,8 @@ export function withCache<Fn extends (...args: any[]) => Promise<any>>(
324
448
  const validate = options?.validate || (() => true)
325
449
 
326
450
  if (!functionCacheMap.has(fnKey)) {
327
- functionCacheMap.set(fnKey, new Cache({ namespace: `fn-${Date.now()}` }))
451
+ const functionNamespace = `fn-${hashString(fnKey)}`
452
+ functionCacheMap.set(fnKey, new Cache({ namespace: functionNamespace }))
328
453
  functionCacheData.set(fnKey, new Map())
329
454
  }
330
455
 
@@ -12,6 +12,11 @@ const platform = process.platform
12
12
  async function copyFileToClipboard(filePath: string): Promise<void> {
13
13
  const absolutePath = path.resolve(filePath)
14
14
 
15
+ if (process.env.VITEST) {
16
+ logger.log(`📋 [VITEST] Skipping copy file to clipboard: ${filePath}`)
17
+ return
18
+ }
19
+
15
20
  if (!fs.existsSync(absolutePath)) {
16
21
  throw new Error(`File not found: ${absolutePath}`)
17
22
  }
@@ -444,11 +444,16 @@ export namespace OAuth {
444
444
  <h1 id="status">Processing...</h1>
445
445
  <div id="message"></div>
446
446
  <script>
447
- // Extract tokens from URL fragment
447
+ // Extract tokens from URL fragment (implicit flow) or query params (auth code flow)
448
448
  const hash = window.location.hash.substring(1);
449
- const params = new URLSearchParams(hash);
449
+ const hashParams = new URLSearchParams(hash);
450
+ const queryParams = new URLSearchParams(window.location.search);
451
+
452
+ // Use query params if we have a code (auth code flow), otherwise use hash (implicit flow)
453
+ const params = queryParams.get('code') ? queryParams : hashParams;
450
454
 
451
455
  const data = {
456
+ code: params.get('code'),
452
457
  access_token: params.get('access_token'),
453
458
  id_token: params.get('id_token'),
454
459
  expires_in: params.get('expires_in'),
@@ -458,6 +463,8 @@ export namespace OAuth {
458
463
  error: params.get('error'),
459
464
  error_description: params.get('error_description')
460
465
  };
466
+
467
+ console.log('OAuth callback data:', data);
461
468
 
462
469
  // Send tokens back to our server
463
470
  fetch('/oauth/implicit-callback', {
@@ -499,6 +506,14 @@ export namespace OAuth {
499
506
  req.on('end', () => {
500
507
  try {
501
508
  const data = JSON.parse(body)
509
+
510
+ logger.log('OAuth callback received data:', {
511
+ hasCode: !!data.code,
512
+ hasAccessToken: !!data.access_token,
513
+ receivedState: data.state,
514
+ expectedState: expectedState,
515
+ error: data.error,
516
+ })
502
517
 
503
518
  if (data.error) {
504
519
  res.writeHead(400, {
@@ -516,6 +531,10 @@ export namespace OAuth {
516
531
 
517
532
  // Validate state
518
533
  if (data.state !== expectedState) {
534
+ logger.error('OAuth state mismatch:', {
535
+ received: data.state,
536
+ expected: expectedState,
537
+ })
519
538
  res.writeHead(400, {
520
539
  'Content-Type': 'application/json',
521
540
  })
@@ -546,6 +565,17 @@ export namespace OAuth {
546
565
  scope: data.scope,
547
566
  }
548
567
 
568
+ // For authorization code flow, return the code for token exchange
569
+ if (data.code) {
570
+ logger.log('Authorization code flow - returning code for token exchange')
571
+ resolve({
572
+ authorizationCode: data.code,
573
+ state: data.state,
574
+ })
575
+ return
576
+ }
577
+
578
+ // For implicit flow, tokens come directly
549
579
  // Store tokens in memory for later retrieval
550
580
  this.implicitFlowTokens = tokens
551
581
 
@@ -554,9 +584,8 @@ export namespace OAuth {
554
584
  .then(() => {
555
585
  logger.log('Implicit flow tokens saved directly')
556
586
 
557
- // Return the tokens (authorizationCode is a dummy value for compatibility)
558
587
  resolve({
559
- authorizationCode: 'implicit_flow_dummy_code', // Dummy code for compatibility
588
+ authorizationCode: 'implicit_flow_complete',
560
589
  accessToken: data.access_token,
561
590
  idToken: data.id_token,
562
591
  state: data.state,
package/src/build.tsx CHANGED
@@ -1,8 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { execSync } from 'node:child_process'
4
3
  import type { BunPlugin } from 'bun'
5
- import * as swc from '@swc/core'
6
4
  import { logger } from './logger'
7
5
  import { getCommandsWithFiles, CommandWithFile } from './package-json'
8
6
  import * as termcastApi from './index'
@@ -43,13 +41,23 @@ export const aliasPlugin: BunPlugin = {
43
41
 
44
42
  // Alias @raycast/api to termcast using namespace
45
43
  build.onResolve({ filter: /@raycast\/api/ }, () => {
46
- logger.log('Resolving @raycast/api to termcast')
44
+ // logger.log('Resolving @raycast/api to termcast')
47
45
  return {
48
46
  path: 'termcast',
49
47
  namespace: GLOBALS_NAMESPACE,
50
48
  }
51
49
  })
52
50
 
51
+ // Alias @raycast/utils to our fork with termcast OAuth proxy URLs
52
+ build.onResolve({ filter: /@raycast\/utils/ }, () => {
53
+ // logger.log('Resolving @raycast/utils to termcast fork')
54
+ return {
55
+ path: require.resolve('@termcast/utils'),
56
+
57
+ // external: true,
58
+ }
59
+ })
60
+
53
61
  // Resolve external packages to globals namespace
54
62
  build.onResolve({ filter: /^termcast/ }, (args) => {
55
63
  return {
@@ -180,62 +188,19 @@ export const aliasPlugin: BunPlugin = {
180
188
  },
181
189
  }
182
190
 
183
- // React Refresh transform plugin for hot reloading using SWC (Rust-based, ~20x faster than Babel)
184
- // Transforms extension source files to inject $RefreshReg$ and $RefreshSig$ calls
185
- export const reactRefreshPlugin: BunPlugin = {
186
- name: 'react-refresh-transform',
187
- async setup(build) {
188
- build.onLoad({ filter: /\.[tj]sx?$/ }, async (args) => {
189
- // Skip node_modules
190
- if (args.path.includes('node_modules')) {
191
- return undefined
192
- }
193
-
194
- const code = await Bun.file(args.path).text()
195
-
196
- const isTypeScript =
197
- args.path.endsWith('.ts') || args.path.endsWith('.tsx')
198
- const hasJsx = args.path.endsWith('.tsx') || args.path.endsWith('.jsx')
199
-
200
- const result = await swc.transform(code, {
201
- filename: args.path,
202
- jsc: {
203
- parser: isTypeScript
204
- ? {
205
- syntax: 'typescript',
206
- tsx: hasJsx,
207
- }
208
- : {
209
- syntax: 'ecmascript',
210
- jsx: hasJsx,
211
- },
212
- transform: {
213
- react: {
214
- development: true,
215
- refresh: true, // Built-in React Refresh support in SWC
216
- runtime: 'automatic',
217
- },
218
- },
219
- },
220
- sourceMaps: 'inline',
221
- })
222
-
223
- if (!result?.code) {
224
- return undefined
225
- }
226
191
 
227
- return {
228
- contents: result.code,
229
- loader: 'js', // SWC transforms JSX to JS
230
- }
231
- })
232
- },
233
- }
234
192
 
235
193
  interface BundledCommand extends CommandWithFile {
236
194
  bundledPath: string
237
195
  }
238
196
 
197
+ function sanitizeHotReloadBundle({ code }: { code: string }): string {
198
+ // Bun reactFastRefresh can emit invalid self-redeclarations like:
199
+ // `var Component = Component;`
200
+ // which throws in ESM during import. Strip those lines in hot-reload bundles.
201
+ return code.replace(/^var\s+([A-Za-z_$][\w$]*)\s*=\s*\1;\s*$/gm, '')
202
+ }
203
+
239
204
  export interface BuildResult {
240
205
  commands: BundledCommand[]
241
206
  bundleDir: string
@@ -276,20 +241,17 @@ export async function buildExtensionCommands({
276
241
 
277
242
  logger.log(`Building ${entrypoints.length} commands...`)
278
243
 
279
- const plugins = [aliasPlugin, swiftLoaderPlugin]
280
- if (hotReload) {
281
- plugins.push(reactRefreshPlugin)
282
- }
244
+ const plugins: BunPlugin[] = [aliasPlugin, swiftLoaderPlugin]
283
245
 
284
246
  const result = await Bun.build({
285
247
  entrypoints,
286
248
  outdir: bundleDir,
287
249
  target: target || (format === 'cjs' ? 'node' : 'bun'),
288
250
  format,
289
- // external: [],
290
251
  plugins,
291
252
  naming: '[name].js',
292
253
  throw: false,
254
+ reactFastRefresh: hotReload,
293
255
  })
294
256
 
295
257
  if (!result.success) {
@@ -299,6 +261,21 @@ export async function buildExtensionCommands({
299
261
  throw new Error(`Build failed: ${errorMessage}`)
300
262
  }
301
263
 
264
+ if (hotReload) {
265
+ for (const output of result.outputs) {
266
+ const outputPath = output.path
267
+ if (!outputPath.endsWith('.js')) {
268
+ continue
269
+ }
270
+ const code = fs.readFileSync(outputPath, 'utf-8')
271
+ const sanitizedCode = sanitizeHotReloadBundle({ code })
272
+ if (sanitizedCode === code) {
273
+ continue
274
+ }
275
+ fs.writeFileSync(outputPath, sanitizedCode)
276
+ }
277
+ }
278
+
302
279
  // Map outputs back to commands
303
280
  const bundledCommands: BundledCommand[] = commandsData.commands.map(
304
281
  (command) => {