termcast 1.3.30 → 1.3.32

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 (294) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +4 -39
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/hud.d.ts.map +1 -1
  5. package/dist/apis/hud.js +13 -31
  6. package/dist/apis/hud.js.map +1 -1
  7. package/dist/apis/localstorage.d.ts.map +1 -1
  8. package/dist/apis/localstorage.js +3 -27
  9. package/dist/apis/localstorage.js.map +1 -1
  10. package/dist/apis/toast.d.ts +16 -43
  11. package/dist/apis/toast.d.ts.map +1 -1
  12. package/dist/apis/toast.js +78 -177
  13. package/dist/apis/toast.js.map +1 -1
  14. package/dist/build.d.ts +3 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +52 -2
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +206 -25
  21. package/dist/cli.js.map +1 -1
  22. package/dist/colors.d.ts.map +1 -1
  23. package/dist/colors.js +1 -0
  24. package/dist/colors.js.map +1 -1
  25. package/dist/compile.d.ts +0 -1
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +18 -23
  28. package/dist/compile.js.map +1 -1
  29. package/dist/components/actions.d.ts.map +1 -1
  30. package/dist/components/actions.js +30 -15
  31. package/dist/components/actions.js.map +1 -1
  32. package/dist/components/animation-tick.d.ts +12 -0
  33. package/dist/components/animation-tick.d.ts.map +1 -0
  34. package/dist/components/animation-tick.js +63 -0
  35. package/dist/components/animation-tick.js.map +1 -0
  36. package/dist/components/detail.d.ts.map +1 -1
  37. package/dist/components/detail.js +10 -13
  38. package/dist/components/detail.js.map +1 -1
  39. package/dist/components/dropdown.d.ts +1 -0
  40. package/dist/components/dropdown.d.ts.map +1 -1
  41. package/dist/components/dropdown.js +27 -26
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/extension-preferences.d.ts.map +1 -1
  44. package/dist/components/extension-preferences.js +15 -10
  45. package/dist/components/extension-preferences.js.map +1 -1
  46. package/dist/components/footer.d.ts +13 -0
  47. package/dist/components/footer.d.ts.map +1 -0
  48. package/dist/components/footer.js +106 -0
  49. package/dist/components/footer.js.map +1 -0
  50. package/dist/components/form/file-autocomplete.d.ts +19 -4
  51. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  52. package/dist/components/form/file-autocomplete.js +56 -55
  53. package/dist/components/form/file-autocomplete.js.map +1 -1
  54. package/dist/components/form/file-picker.d.ts.map +1 -1
  55. package/dist/components/form/file-picker.js +26 -15
  56. package/dist/components/form/file-picker.js.map +1 -1
  57. package/dist/components/form/index.d.ts.map +1 -1
  58. package/dist/components/form/index.js +17 -15
  59. package/dist/components/form/index.js.map +1 -1
  60. package/dist/components/form/with-left-border.d.ts.map +1 -1
  61. package/dist/components/form/with-left-border.js +4 -12
  62. package/dist/components/form/with-left-border.js.map +1 -1
  63. package/dist/components/list.d.ts.map +1 -1
  64. package/dist/components/list.js +126 -86
  65. package/dist/components/list.js.map +1 -1
  66. package/dist/components/loading-bar.d.ts.map +1 -1
  67. package/dist/components/loading-bar.js +5 -22
  68. package/dist/components/loading-bar.js.map +1 -1
  69. package/dist/components/loading-text.d.ts.map +1 -1
  70. package/dist/components/loading-text.js +3 -22
  71. package/dist/components/loading-text.js.map +1 -1
  72. package/dist/components/theme-picker.d.ts +2 -0
  73. package/dist/components/theme-picker.d.ts.map +1 -0
  74. package/dist/components/theme-picker.js +37 -0
  75. package/dist/components/theme-picker.js.map +1 -0
  76. package/dist/descendants.d.ts +6 -0
  77. package/dist/descendants.d.ts.map +1 -1
  78. package/dist/descendants.js +74 -8
  79. package/dist/descendants.js.map +1 -1
  80. package/dist/examples/internal/descendants-rerender.d.ts +14 -0
  81. package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
  82. package/dist/examples/internal/descendants-rerender.js +145 -0
  83. package/dist/examples/internal/descendants-rerender.js.map +1 -0
  84. package/dist/examples/internal/simple-dialog.js +4 -1
  85. package/dist/examples/internal/simple-dialog.js.map +1 -1
  86. package/dist/examples/internal/simple-scrollbox.js +1 -1
  87. package/dist/examples/internal/simple-scrollbox.js.map +1 -1
  88. package/dist/examples/list-with-dropdown.js +1 -1
  89. package/dist/examples/list-with-dropdown.js.map +1 -1
  90. package/dist/examples/miscellaneous.js +1 -1
  91. package/dist/examples/miscellaneous.js.map +1 -1
  92. package/dist/examples/toast-action.d.ts +2 -0
  93. package/dist/examples/toast-action.d.ts.map +1 -0
  94. package/dist/examples/toast-action.js +76 -0
  95. package/dist/examples/toast-action.js.map +1 -0
  96. package/dist/examples/toast-variations.js +38 -36
  97. package/dist/examples/toast-variations.js.map +1 -1
  98. package/dist/extensions/dev.d.ts +1 -1
  99. package/dist/extensions/dev.d.ts.map +1 -1
  100. package/dist/extensions/dev.js +62 -30
  101. package/dist/extensions/dev.js.map +1 -1
  102. package/dist/extensions/home.d.ts.map +1 -1
  103. package/dist/extensions/home.js +4 -3
  104. package/dist/extensions/home.js.map +1 -1
  105. package/dist/extensions/react-refresh-init.d.ts +5 -0
  106. package/dist/extensions/react-refresh-init.d.ts.map +1 -0
  107. package/dist/extensions/react-refresh-init.js +52 -0
  108. package/dist/extensions/react-refresh-init.js.map +1 -0
  109. package/dist/internal/date-picker-widget.js +1 -1
  110. package/dist/internal/date-picker-widget.js.map +1 -1
  111. package/dist/internal/dialog.d.ts +8 -3
  112. package/dist/internal/dialog.d.ts.map +1 -1
  113. package/dist/internal/dialog.js +37 -53
  114. package/dist/internal/dialog.js.map +1 -1
  115. package/dist/internal/navigation.d.ts +1 -0
  116. package/dist/internal/navigation.d.ts.map +1 -1
  117. package/dist/internal/navigation.js +25 -1
  118. package/dist/internal/navigation.js.map +1 -1
  119. package/dist/internal/providers.d.ts.map +1 -1
  120. package/dist/internal/providers.js +9 -197
  121. package/dist/internal/providers.js.map +1 -1
  122. package/dist/internal/scrollbox.d.ts.map +1 -1
  123. package/dist/internal/scrollbox.js +1 -0
  124. package/dist/internal/scrollbox.js.map +1 -1
  125. package/dist/release.d.ts +1 -0
  126. package/dist/release.d.ts.map +1 -1
  127. package/dist/release.js +16 -9
  128. package/dist/release.js.map +1 -1
  129. package/dist/state.d.ts +27 -1
  130. package/dist/state.d.ts.map +1 -1
  131. package/dist/state.js +6 -0
  132. package/dist/state.js.map +1 -1
  133. package/dist/theme.d.ts +6 -19
  134. package/dist/theme.d.ts.map +1 -1
  135. package/dist/theme.js +76 -45
  136. package/dist/theme.js.map +1 -1
  137. package/dist/themes/aura.json +69 -0
  138. package/dist/themes/ayu.json +80 -0
  139. package/dist/themes/catppuccin-frappe.json +233 -0
  140. package/dist/themes/catppuccin-macchiato.json +233 -0
  141. package/dist/themes/catppuccin.json +112 -0
  142. package/dist/themes/cobalt2.json +228 -0
  143. package/dist/themes/cursor.json +249 -0
  144. package/dist/themes/dracula.json +219 -0
  145. package/dist/themes/everforest.json +241 -0
  146. package/dist/themes/flexoki.json +237 -0
  147. package/dist/themes/github-light.json +56 -0
  148. package/dist/themes/github.json +241 -0
  149. package/dist/themes/gruvbox.json +95 -0
  150. package/dist/themes/kanagawa.json +77 -0
  151. package/dist/themes/lucent-orng.json +227 -0
  152. package/dist/themes/material.json +235 -0
  153. package/dist/themes/matrix.json +77 -0
  154. package/dist/themes/mercury.json +245 -0
  155. package/dist/themes/monokai.json +221 -0
  156. package/dist/themes/nightowl.json +221 -0
  157. package/dist/themes/nord.json +223 -0
  158. package/dist/themes/one-dark.json +84 -0
  159. package/dist/themes/opencode-light.json +62 -0
  160. package/dist/themes/opencode.json +245 -0
  161. package/dist/themes/orng.json +245 -0
  162. package/dist/themes/palenight.json +222 -0
  163. package/dist/themes/rosepine.json +234 -0
  164. package/dist/themes/solarized.json +223 -0
  165. package/dist/themes/synthwave84.json +226 -0
  166. package/dist/themes/termcast.json +226 -0
  167. package/dist/themes/tokyonight.json +243 -0
  168. package/dist/themes/vercel.json +255 -0
  169. package/dist/themes/vesper.json +218 -0
  170. package/dist/themes/zenburn.json +223 -0
  171. package/dist/themes.d.ts +57 -0
  172. package/dist/themes.d.ts.map +1 -0
  173. package/dist/themes.js +181 -0
  174. package/dist/themes.js.map +1 -0
  175. package/dist/utils/run-command.d.ts +2 -1
  176. package/dist/utils/run-command.d.ts.map +1 -1
  177. package/dist/utils/run-command.js +20 -10
  178. package/dist/utils/run-command.js.map +1 -1
  179. package/dist/utils.d.ts +2 -1
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +90 -17
  182. package/dist/utils.js.map +1 -1
  183. package/dist/watcher.d.ts +3 -0
  184. package/dist/watcher.d.ts.map +1 -0
  185. package/dist/watcher.js +16 -0
  186. package/dist/watcher.js.map +1 -0
  187. package/package.json +16 -10
  188. package/src/apis/cache.tsx +5 -44
  189. package/src/apis/hud.tsx +17 -62
  190. package/src/apis/localstorage.tsx +3 -32
  191. package/src/apis/toast.tsx +91 -275
  192. package/src/build.test.tsx +10 -0
  193. package/src/build.tsx +61 -1
  194. package/src/cli.tsx +365 -103
  195. package/src/colors.tsx +1 -0
  196. package/src/compile.tsx +21 -29
  197. package/src/compile.vitest.tsx +300 -0
  198. package/src/components/actions.tsx +64 -45
  199. package/src/components/animation-tick.tsx +85 -0
  200. package/src/components/detail.tsx +31 -35
  201. package/src/components/dropdown.tsx +32 -21
  202. package/src/components/extension-preferences.tsx +14 -10
  203. package/src/components/footer.tsx +241 -0
  204. package/src/components/form/file-autocomplete.tsx +80 -60
  205. package/src/components/form/file-picker.tsx +37 -25
  206. package/src/components/form/index.tsx +45 -41
  207. package/src/components/form/with-left-border.tsx +4 -14
  208. package/src/components/list.tsx +181 -121
  209. package/src/components/loading-bar.tsx +5 -25
  210. package/src/components/loading-text.tsx +4 -23
  211. package/src/components/theme-picker.tsx +57 -0
  212. package/src/descendants.tsx +98 -9
  213. package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
  214. package/src/examples/file-autocomplete.vitest.tsx +131 -122
  215. package/src/examples/form-basic.vitest.tsx +463 -644
  216. package/src/examples/form-dropdown.vitest.tsx +553 -571
  217. package/src/examples/form-scroll.vitest.tsx +112 -102
  218. package/src/examples/form-tagpicker.vitest.tsx +364 -338
  219. package/src/examples/internal/descendants-rerender.tsx +273 -0
  220. package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
  221. package/src/examples/internal/simple-dialog.tsx +4 -4
  222. package/src/examples/internal/simple-scrollbox.tsx +2 -2
  223. package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
  224. package/src/examples/list-detail-metadata.vitest.tsx +34 -30
  225. package/src/examples/list-dropdown-default.vitest.tsx +84 -72
  226. package/src/examples/list-empty-view.vitest.tsx +93 -0
  227. package/src/examples/list-fetch-data.vitest.tsx +36 -30
  228. package/src/examples/list-scrollbox.vitest.tsx +59 -39
  229. package/src/examples/list-with-detail.vitest.tsx +339 -314
  230. package/src/examples/list-with-dropdown.tsx +1 -0
  231. package/src/examples/list-with-dropdown.vitest.tsx +176 -150
  232. package/src/examples/list-with-sections.vitest.tsx +289 -270
  233. package/src/examples/list-with-toast.vitest.tsx +44 -44
  234. package/src/examples/miscellaneous.tsx +10 -0
  235. package/src/examples/simple-file-picker.vitest.tsx +90 -86
  236. package/src/examples/simple-grid.vitest.tsx +275 -249
  237. package/src/examples/simple-navigation.vitest.tsx +192 -168
  238. package/src/examples/store.vitest.tsx +6 -4
  239. package/src/examples/swift-extension.vitest.tsx +31 -19
  240. package/src/examples/synonyms.vitest.tsx +93 -83
  241. package/src/examples/toast-action.tsx +160 -0
  242. package/src/examples/toast-action.vitest.tsx +404 -0
  243. package/src/examples/toast-variations.tsx +58 -57
  244. package/src/examples/toast-variations.vitest.tsx +186 -166
  245. package/src/extensions/dev.tsx +74 -33
  246. package/src/extensions/dev.vitest.tsx +162 -69
  247. package/src/extensions/home.tsx +5 -6
  248. package/src/extensions/react-refresh-init.tsx +59 -0
  249. package/src/internal/date-picker-widget.tsx +1 -1
  250. package/src/internal/dialog.tsx +59 -83
  251. package/src/internal/navigation.tsx +37 -4
  252. package/src/internal/providers.tsx +27 -315
  253. package/src/internal/scrollbox.tsx +1 -0
  254. package/src/release.tsx +16 -10
  255. package/src/state.tsx +36 -3
  256. package/src/theme.tsx +82 -51
  257. package/src/themes/aura.json +69 -0
  258. package/src/themes/ayu.json +80 -0
  259. package/src/themes/catppuccin-frappe.json +233 -0
  260. package/src/themes/catppuccin-macchiato.json +233 -0
  261. package/src/themes/catppuccin.json +112 -0
  262. package/src/themes/cobalt2.json +228 -0
  263. package/src/themes/cursor.json +249 -0
  264. package/src/themes/dracula.json +219 -0
  265. package/src/themes/everforest.json +241 -0
  266. package/src/themes/flexoki.json +237 -0
  267. package/src/themes/github-light.json +56 -0
  268. package/src/themes/github.json +241 -0
  269. package/src/themes/gruvbox.json +95 -0
  270. package/src/themes/kanagawa.json +77 -0
  271. package/src/themes/lucent-orng.json +227 -0
  272. package/src/themes/material.json +235 -0
  273. package/src/themes/matrix.json +77 -0
  274. package/src/themes/mercury.json +252 -0
  275. package/src/themes/monokai.json +221 -0
  276. package/src/themes/nightowl.json +221 -0
  277. package/src/themes/nord.json +223 -0
  278. package/src/themes/one-dark.json +84 -0
  279. package/src/themes/opencode-light.json +62 -0
  280. package/src/themes/opencode.json +245 -0
  281. package/src/themes/orng.json +245 -0
  282. package/src/themes/palenight.json +222 -0
  283. package/src/themes/rosepine.json +234 -0
  284. package/src/themes/solarized.json +223 -0
  285. package/src/themes/synthwave84.json +226 -0
  286. package/src/themes/termcast.json +227 -0
  287. package/src/themes/tokyonight.json +243 -0
  288. package/src/themes/vercel.json +255 -0
  289. package/src/themes/vesper.json +218 -0
  290. package/src/themes/zenburn.json +223 -0
  291. package/src/themes.ts +291 -0
  292. package/src/utils/run-command.tsx +23 -12
  293. package/src/utils.tsx +115 -18
  294. package/src/watcher.tsx +19 -0
package/src/cli.tsx CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ // CRITICAL: Import react-refresh-init FIRST before anything that imports @opentui/react
4
+ // This ensures the devtools hook exists before the reconciler calls injectIntoDevTools()
5
+ import './extensions/react-refresh-init'
6
+
3
7
  import fs from 'node:fs'
4
8
  import path from 'node:path'
5
9
  import { execSync, spawn } from 'node:child_process'
6
10
  import { cac } from 'cac'
7
- import * as watcher from '@parcel/watcher'
11
+ import { getWatcher } from './watcher'
8
12
  import { buildExtensionCommands } from './build'
9
13
  import { logger } from './logger'
10
14
  import { installExtension } from './utils'
@@ -32,7 +36,7 @@ async function checkForUpdates() {
32
36
  return
33
37
  }
34
38
 
35
- const latestRelease = await response.json()
39
+ const latestRelease = (await response.json()) as { tag_name?: string }
36
40
  const latestVersion =
37
41
  latestRelease.tag_name?.replace('termcast@', '') ||
38
42
  latestRelease.tag_name?.replace('v', '')
@@ -40,10 +44,14 @@ async function checkForUpdates() {
40
44
  // Compare versions
41
45
  if (latestVersion && latestVersion !== currentVersion) {
42
46
  // Run the install script in background
43
- const updateProcess = spawn('bash', ['-c', 'curl -sf https://termcast.app/install | bash'], {
44
- detached: true,
45
- stdio: 'ignore',
46
- })
47
+ const updateProcess = spawn(
48
+ 'bash',
49
+ ['-c', 'curl -sf https://termcast.app/install | bash'],
50
+ {
51
+ detached: true,
52
+ stdio: 'ignore',
53
+ },
54
+ )
47
55
 
48
56
  updateProcess.on('exit', async (code) => {
49
57
  if (code === 0) {
@@ -64,20 +72,22 @@ async function checkForUpdates() {
64
72
  }
65
73
  }
66
74
 
67
- // Check for updates when CLI starts
68
- checkForUpdates()
75
+ // TODO: re-enable auto-update check once install script temp dir issue is fixed
76
+ // checkForUpdates()
69
77
 
70
78
  cli
71
79
  .command('dev [path]', 'Run the extension in the current working directory')
72
80
  .action(async (rawExtensionPath, options) => {
73
81
  try {
74
82
  // Check if the provided arg looks like a path (contains / or . or is existing dir)
75
- const looksLikePath = rawExtensionPath && (
76
- rawExtensionPath.includes('/') ||
77
- rawExtensionPath.startsWith('.') ||
78
- fs.existsSync(rawExtensionPath)
83
+ const looksLikePath =
84
+ rawExtensionPath &&
85
+ (rawExtensionPath.includes('/') ||
86
+ rawExtensionPath.startsWith('.') ||
87
+ fs.existsSync(rawExtensionPath))
88
+ const extensionPath = path.resolve(
89
+ looksLikePath ? rawExtensionPath : process.cwd(),
79
90
  )
80
- const extensionPath = path.resolve(looksLikePath ? rawExtensionPath : process.cwd())
81
91
  let isBuilding = false
82
92
 
83
93
  // Start dev mode with initial render
@@ -96,16 +106,57 @@ cli
96
106
  console.log('\nWatching for file changes...')
97
107
 
98
108
  // Watch entire extension directory using @parcel/watcher
109
+ // Single source of truth for ignored patterns
110
+ const IGNORED_DIRS = [
111
+ 'node_modules',
112
+ '.termcast-bundle',
113
+ '.git',
114
+ '.build', // Swift build output
115
+ '.cache',
116
+ 'tmp',
117
+ '.tmp',
118
+ 'dist',
119
+ 'build',
120
+ ]
121
+ const IGNORED_EXTENSIONS = ['.log', '.db', '.sqlite']
122
+
123
+ // Glob patterns for @parcel/watcher (matched against relative paths using micromatch)
99
124
  const ignoredPatterns = [
100
- '**/node_modules/**',
101
- '**/.termcast-bundle/**',
102
- '**/.git/**',
103
- '**/.build/**', // Swift build output
104
- '**/*.log',
105
- '**/dist/**',
106
- '**/build/**',
125
+ ...IGNORED_DIRS.map((dir) => `**/${dir}/**`),
126
+ ...IGNORED_EXTENSIONS.map((ext) => `**/*${ext}`),
127
+ // SQLite creates .db-wal and .db-shm alongside .db
128
+ '**/*.db-*',
129
+ '**/*.sqlite-*',
107
130
  ]
108
131
 
132
+ // Backup filter for files that should never trigger rebuild
133
+ // This catches cases where @parcel/watcher ignore doesn't work as expected
134
+ const shouldIgnoreFile = (filePath: string): boolean => {
135
+ const relativePath = path.relative(extensionPath, filePath)
136
+ // Ignore files outside the extension directory
137
+ if (relativePath.startsWith('..')) {
138
+ return true
139
+ }
140
+ // Check if path contains any ignored directory
141
+ const hasIgnoredDir = IGNORED_DIRS.some(
142
+ (dir) =>
143
+ relativePath.includes(`/${dir}/`) ||
144
+ relativePath.startsWith(`${dir}/`),
145
+ )
146
+ if (hasIgnoredDir) {
147
+ return true
148
+ }
149
+ // Check if file has ignored extension
150
+ if (IGNORED_EXTENSIONS.some((ext) => relativePath.endsWith(ext))) {
151
+ return true
152
+ }
153
+ // Also catch .db-* and .sqlite-* patterns
154
+ if (/\.db-|\.sqlite-/.test(relativePath)) {
155
+ return true
156
+ }
157
+ return false
158
+ }
159
+
109
160
  const rebuild = async (filePath: string) => {
110
161
  if (isBuilding) {
111
162
  logger.log('Build already in progress, skipping...')
@@ -125,19 +176,24 @@ cli
125
176
  }
126
177
  }
127
178
 
128
- const subscription = await watcher.subscribe(
179
+ const subscription = await getWatcher().subscribe(
129
180
  extensionPath,
130
181
  (err, events) => {
131
182
  if (err) {
132
183
  logger.error('Watcher error:', err)
133
184
  return
134
185
  }
135
- // Trigger rebuild for any event (create, update, delete)
136
- if (events.length > 0) {
137
- rebuild(events[0].path)
186
+
187
+ // Filter out events for files that should be ignored
188
+ const relevantEvents = events.filter(
189
+ (event) => !shouldIgnoreFile(event.path),
190
+ )
191
+
192
+ if (relevantEvents.length > 0) {
193
+ rebuild(relevantEvents[0].path)
138
194
  }
139
195
  },
140
- { ignore: ignoredPatterns }
196
+ { ignore: ignoredPatterns },
141
197
  )
142
198
 
143
199
  // Clean up watcher on exit signals
@@ -182,6 +238,7 @@ cli
182
238
  extensionSourcePath: extensionPath,
183
239
  })
184
240
  console.log(`\nExtension installed to store as '${extensionName}'`)
241
+ process.exit(0)
185
242
  } catch (error: any) {
186
243
  console.error('Build failed:', error.message)
187
244
  process.exit(1)
@@ -205,6 +262,7 @@ cli
205
262
 
206
263
  console.log(`\nExecutable created: ${result.outfile}`)
207
264
  console.log(`Run it with: ${result.outfile}`)
265
+ process.exit(0)
208
266
  } catch (error: any) {
209
267
  console.error('Compile failed:', error.message)
210
268
  process.exit(1)
@@ -234,7 +292,10 @@ cli
234
292
  })
235
293
 
236
294
  cli
237
- .command('pr <prNumber>', 'Download extension from a GitHub PR in Raycast extensions repo. To test it with Termcast')
295
+ .command(
296
+ 'raycast-pr <prNumber>',
297
+ 'Download extension from a GitHub PR in Raycast extensions repo. To test it with Termcast',
298
+ )
238
299
  .action(async (prNumber: string) => {
239
300
  try {
240
301
  // Parse PR number from URL if provided
@@ -254,13 +315,34 @@ cli
254
315
  )
255
316
 
256
317
  if (!prResponse.ok) {
257
- console.error(
258
- `Failed to fetch PR #${parsedPrNumber}: ${prResponse.statusText}`,
259
- )
318
+ if (prResponse.status === 403) {
319
+ const rateLimitRemaining = prResponse.headers.get(
320
+ 'x-ratelimit-remaining',
321
+ )
322
+ if (rateLimitRemaining === '0') {
323
+ const resetTime = prResponse.headers.get('x-ratelimit-reset')
324
+ const resetDate = resetTime
325
+ ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString()
326
+ : 'soon'
327
+ console.error(
328
+ `GitHub API rate limit exceeded. Resets at ${resetDate}`,
329
+ )
330
+ } else {
331
+ console.error(`Access forbidden for PR #${parsedPrNumber}`)
332
+ }
333
+ } else if (prResponse.status === 404) {
334
+ console.error(`PR #${parsedPrNumber} not found`)
335
+ } else {
336
+ console.error(
337
+ `Failed to fetch PR #${parsedPrNumber}: ${prResponse.status} ${prResponse.statusText}`,
338
+ )
339
+ }
260
340
  process.exit(1)
261
341
  }
262
342
 
263
- const prData = await prResponse.json()
343
+ const prData = (await prResponse.json()) as {
344
+ head: { user: { login: string }; ref: string; repo: { clone_url: string } }
345
+ }
264
346
  const prAuthor = prData.head.user.login
265
347
  const branch = prData.head.ref
266
348
  const forkUrl = prData.head.repo.clone_url
@@ -376,8 +458,76 @@ cli
376
458
  })
377
459
 
378
460
  cli
379
- .command('raycast-search <query>', 'Search for extensions in the Raycast store')
380
- .option('-n, --limit <number>', 'Number of results to show', { default: '10' })
461
+ .command(
462
+ 'raycast-pr-diff <prNumber>',
463
+ 'Show the diff of a PR in Raycast extensions repo',
464
+ )
465
+ .action(async (prNumber: string) => {
466
+ try {
467
+ // Parse PR number from URL if provided
468
+ let parsedPrNumber = prNumber
469
+ const urlMatch = prNumber.match(
470
+ /github\.com\/raycast\/extensions\/pull\/(\d+)/,
471
+ )
472
+ if (urlMatch) {
473
+ parsedPrNumber = urlMatch[1]
474
+ }
475
+
476
+ console.error(`Fetching diff for PR #${parsedPrNumber}...`)
477
+
478
+ // Fetch diff directly from GitHub API
479
+ const response = await fetch(
480
+ `https://api.github.com/repos/raycast/extensions/pulls/${parsedPrNumber}`,
481
+ {
482
+ headers: {
483
+ Accept: 'application/vnd.github.v3.diff',
484
+ },
485
+ },
486
+ )
487
+
488
+ if (!response.ok) {
489
+ if (response.status === 403) {
490
+ const rateLimitRemaining = response.headers.get(
491
+ 'x-ratelimit-remaining',
492
+ )
493
+ if (rateLimitRemaining === '0') {
494
+ const resetTime = response.headers.get('x-ratelimit-reset')
495
+ const resetDate = resetTime
496
+ ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString()
497
+ : 'soon'
498
+ console.error(
499
+ `GitHub API rate limit exceeded. Resets at ${resetDate}`,
500
+ )
501
+ } else {
502
+ console.error(`Access forbidden for PR #${parsedPrNumber}`)
503
+ }
504
+ } else if (response.status === 404) {
505
+ console.error(`PR #${parsedPrNumber} not found`)
506
+ } else {
507
+ console.error(
508
+ `Failed to fetch PR #${parsedPrNumber}: ${response.status} ${response.statusText}`,
509
+ )
510
+ }
511
+ process.exit(1)
512
+ }
513
+
514
+ const diff = await response.text()
515
+ console.log(diff)
516
+ process.exit(0)
517
+ } catch (error) {
518
+ console.error('Error fetching PR diff:', error)
519
+ process.exit(1)
520
+ }
521
+ })
522
+
523
+ cli
524
+ .command(
525
+ 'raycast-search <query>',
526
+ 'Search for extensions in the Raycast store',
527
+ )
528
+ .option('-n, --limit <number>', 'Number of results to show', {
529
+ default: '10',
530
+ })
381
531
  .action(async (query: string, options: { limit: string }) => {
382
532
  try {
383
533
  const limit = parseInt(options.limit, 10)
@@ -394,14 +544,18 @@ cli
394
544
  const downloads = ext.download_count.toLocaleString()
395
545
  const commands = ext.commands.map((c) => c.name).join(', ')
396
546
  console.log(` ${ext.name}`)
397
- console.log(` Path: extensions/${ext.relative_path.replace('extensions/', '')}`)
547
+ console.log(
548
+ ` Path: extensions/${ext.relative_path.replace('extensions/', '')}`,
549
+ )
398
550
  console.log(` Downloads: ${downloads}`)
399
551
  console.log(` Commands: ${commands || 'none'}`)
400
- console.log(` Description: ${ext.description.slice(0, 100)}${ext.description.length > 100 ? '...' : ''}`)
552
+ console.log(
553
+ ` Description: ${ext.description.slice(0, 100)}${ext.description.length > 100 ? '...' : ''}`,
554
+ )
401
555
  console.log()
402
556
  }
403
557
 
404
- console.log(`Download with: termcast download <extension-name>`)
558
+ console.log(`Download with: termcast raycast-download <extension-name>`)
405
559
  process.exit(0)
406
560
  } catch (error: any) {
407
561
  console.error('Search failed:', error.message)
@@ -410,98 +564,206 @@ cli
410
564
  })
411
565
 
412
566
  cli
413
- .command('download <extensionName>', 'Download extension from Raycast extensions repo')
567
+ .command(
568
+ 'raycast-download <extensionName>',
569
+ 'Download extension from Raycast extensions repo',
570
+ )
414
571
  .option('-o, --output <path>', 'Output directory', { default: '.' })
415
- .option('--no-dir', 'Put files directly in output directory instead of creating extension subdirectory')
416
- .action(async (extensionName: string, options: { output: string; dir: boolean }) => {
417
- try {
418
- const destPath = path.resolve(options.output)
419
- // When --no-dir is passed, dir is false; put files directly in destPath
420
- const extensionDir = options.dir ? path.join(destPath, extensionName) : destPath
421
- const tempCloneDir = path.join(destPath, `.tmp-${extensionName}-${Date.now()}`)
572
+ .option(
573
+ '--no-dir',
574
+ 'Put files directly in output directory instead of creating extension subdirectory',
575
+ )
576
+ .action(
577
+ async (
578
+ extensionName: string,
579
+ options: { output: string; dir: boolean },
580
+ ) => {
581
+ try {
582
+ const destPath = path.resolve(options.output)
583
+ // When --no-dir is passed, dir is false; put files directly in destPath
584
+ const extensionDir = options.dir
585
+ ? path.join(destPath, extensionName)
586
+ : destPath
587
+ const tempCloneDir = path.join(
588
+ destPath,
589
+ `.tmp-${extensionName}-${Date.now()}`,
590
+ )
422
591
 
423
- console.log(`Downloading extension '${extensionName}' from raycast/extensions...`)
592
+ console.log(
593
+ `Downloading extension '${extensionName}' from raycast/extensions...`,
594
+ )
424
595
 
425
- if (options.dir && fs.existsSync(extensionDir)) {
426
- console.log(`Removing existing directory: ${extensionDir}`)
427
- fs.rmSync(extensionDir, { recursive: true, force: true })
428
- }
596
+ if (options.dir && fs.existsSync(extensionDir)) {
597
+ console.log(`Removing existing directory: ${extensionDir}`)
598
+ fs.rmSync(extensionDir, { recursive: true, force: true })
599
+ }
429
600
 
430
- fs.mkdirSync(destPath, { recursive: true })
601
+ fs.mkdirSync(destPath, { recursive: true })
431
602
 
432
- const repoUrl = 'https://github.com/raycast/extensions.git'
433
- const cloneDirName = path.basename(tempCloneDir)
434
- const cloneCmd = `git clone -n --depth=1 --filter=tree:0 "${repoUrl}" "${cloneDirName}"`
435
- console.log(`Running: ${cloneCmd}`)
436
- try {
437
- execSync(cloneCmd, {
438
- cwd: destPath,
439
- stdio: 'inherit',
440
- })
441
- } catch (error) {
442
- console.error(`Failed to clone repository`)
443
- process.exit(1)
444
- }
603
+ const repoUrl = 'https://github.com/raycast/extensions.git'
604
+ const cloneDirName = path.basename(tempCloneDir)
605
+ const cloneCmd = `git clone -n --depth=1 --filter=tree:0 "${repoUrl}" "${cloneDirName}"`
606
+ console.log(`Running: ${cloneCmd}`)
607
+ try {
608
+ execSync(cloneCmd, {
609
+ cwd: destPath,
610
+ stdio: 'inherit',
611
+ })
612
+ } catch (error) {
613
+ console.error(`Failed to clone repository`)
614
+ process.exit(1)
615
+ }
445
616
 
446
- const sparseCmd = `git sparse-checkout set --no-cone "extensions/${extensionName}"`
447
- console.log(`Running: ${sparseCmd}`)
448
- try {
449
- execSync(sparseCmd, {
450
- cwd: tempCloneDir,
451
- stdio: 'inherit',
452
- })
453
- } catch (error) {
454
- console.error(`Failed to set sparse-checkout`)
617
+ const sparseCmd = `git sparse-checkout set --no-cone "extensions/${extensionName}"`
618
+ console.log(`Running: ${sparseCmd}`)
619
+ try {
620
+ execSync(sparseCmd, {
621
+ cwd: tempCloneDir,
622
+ stdio: 'inherit',
623
+ })
624
+ } catch (error) {
625
+ console.error(`Failed to set sparse-checkout`)
626
+ fs.rmSync(tempCloneDir, { recursive: true, force: true })
627
+ process.exit(1)
628
+ }
629
+
630
+ const checkoutCmd = 'git checkout'
631
+ console.log(`Running: ${checkoutCmd}`)
632
+ try {
633
+ execSync(checkoutCmd, {
634
+ cwd: tempCloneDir,
635
+ stdio: 'inherit',
636
+ })
637
+ } catch (error) {
638
+ console.error(`Failed to checkout files`)
639
+ fs.rmSync(tempCloneDir, { recursive: true, force: true })
640
+ process.exit(1)
641
+ }
642
+
643
+ const extensionPath = path.join(
644
+ tempCloneDir,
645
+ 'extensions',
646
+ extensionName,
647
+ )
648
+
649
+ if (!fs.existsSync(extensionPath)) {
650
+ console.error(
651
+ `Extension '${extensionName}' not found in raycast/extensions repo`,
652
+ )
653
+ fs.rmSync(tempCloneDir, { recursive: true, force: true })
654
+ process.exit(1)
655
+ }
656
+
657
+ // Move files to final destination
658
+ if (options.dir) {
659
+ fs.mkdirSync(extensionDir, { recursive: true })
660
+ }
661
+ const filesToMove = fs.readdirSync(extensionPath)
662
+ for (const file of filesToMove) {
663
+ const src = path.join(extensionPath, file)
664
+ const dest = path.join(extensionDir, file)
665
+ fs.renameSync(src, dest)
666
+ }
667
+
668
+ // Clean up temp clone directory
455
669
  fs.rmSync(tempCloneDir, { recursive: true, force: true })
456
- process.exit(1)
457
- }
458
670
 
459
- const checkoutCmd = 'git checkout'
460
- console.log(`Running: ${checkoutCmd}`)
461
- try {
462
- execSync(checkoutCmd, {
463
- cwd: tempCloneDir,
671
+ console.log(`\nInstalling dependencies...`)
672
+ execSync('npm install', {
673
+ cwd: extensionDir,
464
674
  stdio: 'inherit',
465
675
  })
676
+
677
+ console.log(`\nāœ… Extension downloaded successfully!`)
678
+ console.log(`šŸ“ Path: ${extensionDir}`)
679
+ process.exit(0)
466
680
  } catch (error) {
467
- console.error(`Failed to checkout files`)
468
- fs.rmSync(tempCloneDir, { recursive: true, force: true })
681
+ console.error('Error downloading extension:', error)
469
682
  process.exit(1)
470
683
  }
684
+ },
685
+ )
471
686
 
472
- const extensionPath = path.join(tempCloneDir, 'extensions', extensionName)
687
+ cli
688
+ .command('new <name>', 'Create a new termcast extension')
689
+ .action(async (name: string) => {
690
+ try {
691
+ const targetDir = path.resolve(name)
473
692
 
474
- if (!fs.existsSync(extensionPath)) {
475
- console.error(`Extension '${extensionName}' not found in raycast/extensions repo`)
476
- fs.rmSync(tempCloneDir, { recursive: true, force: true })
693
+ if (fs.existsSync(targetDir)) {
694
+ console.error(`Directory "${name}" already exists`)
477
695
  process.exit(1)
478
696
  }
479
697
 
480
- // Move files to final destination
481
- if (options.dir) {
482
- fs.mkdirSync(extensionDir, { recursive: true })
698
+ console.log(`Creating new extension "${name}"...`)
699
+
700
+ // Download template from GitHub
701
+ const templateUrl =
702
+ 'https://github.com/remorses/termcast/archive/refs/heads/main.zip'
703
+
704
+ console.log('Downloading template...')
705
+ const response = await fetch(templateUrl)
706
+ if (!response.ok) {
707
+ throw new Error(`Failed to download template: ${response.status}`)
483
708
  }
484
- const filesToMove = fs.readdirSync(extensionPath)
485
- for (const file of filesToMove) {
486
- const src = path.join(extensionPath, file)
487
- const dest = path.join(extensionDir, file)
488
- fs.renameSync(src, dest)
709
+
710
+ console.log('Extracting...')
711
+ const JSZip = (await import('jszip')).default
712
+ const zip = await JSZip.loadAsync(await response.arrayBuffer())
713
+
714
+ // Find the template folder in the zip
715
+ const templatePrefix = 'termcast-main/termcast/template/'
716
+ const templateFiles = Object.keys(zip.files).filter((name) =>
717
+ name.startsWith(templatePrefix),
718
+ )
719
+
720
+ if (templateFiles.length === 0) {
721
+ throw new Error('Template not found in downloaded archive')
489
722
  }
490
723
 
491
- // Clean up temp clone directory
492
- fs.rmSync(tempCloneDir, { recursive: true, force: true })
724
+ // Extract template files to target directory
725
+ fs.mkdirSync(targetDir, { recursive: true })
493
726
 
494
- console.log(`\nInstalling dependencies...`)
495
- execSync('npm install', {
496
- cwd: extensionDir,
497
- stdio: 'inherit',
498
- })
727
+ for (const filePath of templateFiles) {
728
+ const relativePath = filePath.slice(templatePrefix.length)
729
+ if (!relativePath) {
730
+ continue
731
+ }
499
732
 
500
- console.log(`\nāœ… Extension downloaded successfully!`)
501
- console.log(`šŸ“ Path: ${extensionDir}`)
733
+ const targetPath = path.join(targetDir, relativePath)
734
+ const zipEntry = zip.files[filePath]
735
+
736
+ if (zipEntry.dir) {
737
+ fs.mkdirSync(targetPath, { recursive: true })
738
+ } else {
739
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true })
740
+ const content = await zipEntry.async('nodebuffer')
741
+ fs.writeFileSync(targetPath, content)
742
+ }
743
+ }
744
+
745
+ // Replace placeholders in package.json
746
+ const packageJsonPath = path.join(targetDir, 'package.json')
747
+ let packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8')
748
+ const title = name
749
+ .split('-')
750
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
751
+ .join(' ')
752
+ packageJsonContent = packageJsonContent
753
+ .replace(/\{\{name\}\}/g, name)
754
+ .replace(/\{\{title\}\}/g, title)
755
+ fs.writeFileSync(packageJsonPath, packageJsonContent)
756
+
757
+ console.log('\nInstalling dependencies...')
758
+ execSync('bun install', { cwd: targetDir, stdio: 'inherit' })
759
+
760
+ console.log(`\nāœ… Created "${name}" successfully!`)
761
+ console.log(`\nNext steps:`)
762
+ console.log(` cd ${name}`)
763
+ console.log(` termcast dev`)
502
764
  process.exit(0)
503
- } catch (error) {
504
- console.error('Error downloading extension:', error)
765
+ } catch (error: any) {
766
+ console.error('Failed to create extension:', error.message)
505
767
  process.exit(1)
506
768
  }
507
769
  })
package/src/colors.tsx CHANGED
@@ -30,6 +30,7 @@ export function resolveColor(color: Color.ColorLike | undefined | null): string
30
30
  return color
31
31
  }
32
32
  if (typeof color === 'object' && 'dark' in color) {
33
+ // Color.Dynamic - just use dark variant (light themes should use flat colors)
33
34
  return color.dark
34
35
  }
35
36
  return undefined