termcast 1.3.33 → 1.3.35

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 (88) hide show
  1. package/dist/apis/cache.d.ts +1 -2
  2. package/dist/apis/cache.d.ts.map +1 -1
  3. package/dist/apis/cache.js +134 -52
  4. package/dist/apis/cache.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +25 -0
  7. package/dist/build.js.map +1 -1
  8. package/dist/cli.js +6 -8
  9. package/dist/cli.js.map +1 -1
  10. package/dist/components/dropdown.js +3 -3
  11. package/dist/components/dropdown.js.map +1 -1
  12. package/dist/components/footer.d.ts.map +1 -1
  13. package/dist/components/footer.js +1 -1
  14. package/dist/components/footer.js.map +1 -1
  15. package/dist/components/icon.d.ts.map +1 -1
  16. package/dist/components/icon.js +386 -23
  17. package/dist/components/icon.js.map +1 -1
  18. package/dist/components/list.d.ts.map +1 -1
  19. package/dist/components/list.js +90 -22
  20. package/dist/components/list.js.map +1 -1
  21. package/dist/examples/list-controlled-search.d.ts +2 -0
  22. package/dist/examples/list-controlled-search.d.ts.map +1 -0
  23. package/dist/examples/list-controlled-search.js +12 -0
  24. package/dist/examples/list-controlled-search.js.map +1 -0
  25. package/dist/extensions/home.js +1 -1
  26. package/dist/extensions/home.js.map +1 -1
  27. package/dist/extensions/react-refresh-init.d.ts.map +1 -1
  28. package/dist/extensions/react-refresh-init.js +4 -3
  29. package/dist/extensions/react-refresh-init.js.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal/dialog.d.ts.map +1 -1
  34. package/dist/internal/dialog.js +4 -5
  35. package/dist/internal/dialog.js.map +1 -1
  36. package/dist/internal/providers.d.ts.map +1 -1
  37. package/dist/internal/providers.js +18 -5
  38. package/dist/internal/providers.js.map +1 -1
  39. package/dist/state.d.ts +1 -0
  40. package/dist/state.d.ts.map +1 -1
  41. package/dist/state.js.map +1 -1
  42. package/dist/theme.d.ts.map +1 -1
  43. package/dist/theme.js +6 -2
  44. package/dist/theme.js.map +1 -1
  45. package/dist/utils/run-command.js +3 -3
  46. package/dist/utils/run-command.js.map +1 -1
  47. package/dist/utils.d.ts +16 -1
  48. package/dist/utils.d.ts.map +1 -1
  49. package/dist/utils.js +28 -1
  50. package/dist/utils.js.map +1 -1
  51. package/dist/watcher.d.ts.map +1 -1
  52. package/dist/watcher.js +24 -4
  53. package/dist/watcher.js.map +1 -1
  54. package/package.json +10 -9
  55. package/src/apis/cache.test.ts +35 -3
  56. package/src/apis/cache.tsx +180 -57
  57. package/src/build.tsx +28 -0
  58. package/src/cli.tsx +8 -10
  59. package/src/compile.vitest.tsx +42 -24
  60. package/src/components/dropdown.tsx +3 -3
  61. package/src/components/footer.tsx +4 -2
  62. package/src/components/icon.tsx +385 -23
  63. package/src/components/list.tsx +104 -28
  64. package/src/examples/github.vitest.tsx +37 -37
  65. package/src/examples/list-controlled-search.tsx +28 -0
  66. package/src/examples/list-controlled-search.vitest.tsx +49 -0
  67. package/src/examples/list-detail-metadata.vitest.tsx +1 -1
  68. package/src/examples/list-dropdown-default.vitest.tsx +9 -9
  69. package/src/examples/list-scrollbox.vitest.tsx +55 -41
  70. package/src/examples/list-with-detail.vitest.tsx +35 -36
  71. package/src/examples/list-with-dropdown.vitest.tsx +2 -2
  72. package/src/examples/list-with-sections.vitest.tsx +153 -118
  73. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  74. package/src/examples/simple-grid.vitest.tsx +44 -44
  75. package/src/examples/simple-navigation.vitest.tsx +43 -12
  76. package/src/examples/store.vitest.tsx +1 -1
  77. package/src/examples/swift-extension.vitest.tsx +3 -3
  78. package/src/extensions/dev.vitest.tsx +69 -34
  79. package/src/extensions/home.tsx +1 -1
  80. package/src/extensions/react-refresh-init.tsx +4 -3
  81. package/src/index.tsx +1 -0
  82. package/src/internal/dialog.tsx +21 -23
  83. package/src/internal/providers.tsx +18 -5
  84. package/src/state.tsx +1 -0
  85. package/src/theme.tsx +6 -2
  86. package/src/utils/run-command.tsx +3 -3
  87. package/src/utils.tsx +41 -1
  88. package/src/watcher.tsx +26 -6
@@ -5,6 +5,16 @@ 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()
10
20
  const dbSuffix = process.env.TERMCAST_DB_SUFFIX?.replace(/[^a-zA-Z0-9_-]/g, '_')
@@ -28,6 +38,109 @@ function getCurrentCacheDir(namespace?: string): string {
28
38
  return namespace ? path.join(baseDir, namespace) : baseDir
29
39
  }
30
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
+
31
144
  export class Cache {
32
145
  static get STORAGE_DIRECTORY_NAME(): string {
33
146
  const extensionPath = useStore.getState().extensionPath
@@ -40,17 +153,14 @@ export class Cache {
40
153
 
41
154
  private db: Database
42
155
  private capacity: number
43
- private namespace?: string
44
- private tableName: string
156
+ private namespace: string
45
157
  private subscribers: Cache.Subscriber[] = []
46
158
  private currentSize: number = 0
47
159
 
48
160
  constructor(options?: Cache.Options) {
161
+ const sqliteLoadStart = Date.now()
49
162
  this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
50
- this.namespace = options?.namespace
51
- // Replace non-alphanumeric characters with underscores for valid SQL table names
52
- const safeNamespace = this.namespace?.replace(/[^a-zA-Z0-9]/g, '_')
53
- this.tableName = safeNamespace ? `cache_${safeNamespace}` : 'cache'
163
+ this.namespace = getNamespace(options?.namespace)
54
164
 
55
165
  const dbPath = getCurrentDatabasePath()
56
166
 
@@ -66,32 +176,23 @@ export class Cache {
66
176
  readwrite: true,
67
177
  })
68
178
 
69
- // Use WAL mode and optimize for single file usage
70
- this.db.exec('PRAGMA journal_mode = WAL')
71
- this.db.exec('PRAGMA wal_autocheckpoint = 1000')
72
- this.db.exec('PRAGMA synchronous = NORMAL')
73
-
74
- // Use rowid for ordering - it auto-increments and provides natural LRU order
75
- this.db.exec(`
76
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
77
- rowid INTEGER PRIMARY KEY AUTOINCREMENT,
78
- key TEXT UNIQUE NOT NULL,
79
- data TEXT NOT NULL,
80
- size INTEGER NOT NULL
81
- )
82
- `)
83
-
84
- // Create index on key for fast lookups
85
- this.db.exec(`
86
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key ON ${this.tableName}(key)
87
- `)
179
+ initializeDatabaseOnce({ db: this.db, dbPath })
88
180
 
89
181
  // Calculate initial size
90
182
  const row = this.db
91
- .prepare(`SELECT SUM(size) as total FROM ${this.tableName}`)
92
- .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
93
187
  this.currentSize = row?.total || 0
94
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
+
95
196
  // Bind all methods to this instance
96
197
  this.get = this.get.bind(this)
97
198
  this.has = this.has.bind(this)
@@ -108,21 +209,19 @@ export class Cache {
108
209
  }
109
210
 
110
211
  get(key: string): string | undefined {
212
+ const now = nextTimestamp()
111
213
  const row = this.db
112
- .prepare(`SELECT rowid, data, size FROM ${this.tableName} WHERE key = ?`)
113
- .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
114
218
 
115
219
  if (row) {
116
- // Move to end of LRU by deleting and reinserting (gets new rowid)
117
- const tx = this.db.transaction(() => {
118
- this.db.prepare(`DELETE FROM ${this.tableName} WHERE key = ?`).run(key)
119
- this.db
120
- .prepare(
121
- `INSERT INTO ${this.tableName} (key, data, size) VALUES (?, ?, ?)`,
122
- )
123
- .run(key, row.data, row.size)
124
- })
125
- 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)
126
225
 
127
226
  return row.data
128
227
  }
@@ -132,25 +231,30 @@ export class Cache {
132
231
 
133
232
  has(key: string): boolean {
134
233
  const row = this.db
135
- .prepare(`SELECT 1 FROM ${this.tableName} WHERE key = ?`)
136
- .get(key)
234
+ .prepare(`SELECT 1 FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
235
+ .get(this.namespace, key)
137
236
  return !!row
138
237
  }
139
238
 
140
239
  get isEmpty(): boolean {
141
240
  const row = this.db
142
- .prepare(`SELECT COUNT(*) as count FROM ${this.tableName}`)
143
- .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 }
144
245
  return row.count === 0
145
246
  }
146
247
 
147
248
  set(key: string, data: string): void {
249
+ const now = nextTimestamp()
148
250
  const dataSize = Buffer.byteLength(data, 'utf8')
149
251
 
150
252
  // Get existing size if any
151
253
  const existingRow = this.db
152
- .prepare(`SELECT size FROM ${this.tableName} WHERE key = ?`)
153
- .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
154
258
  const oldSize = existingRow?.size || 0
155
259
  const newTotalSize = this.currentSize - oldSize + dataSize
156
260
 
@@ -161,9 +265,16 @@ export class Cache {
161
265
  // Insert or update the cache entry
162
266
  this.db
163
267
  .prepare(
164
- `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`,
165
276
  )
166
- .run(key, data, dataSize)
277
+ .run(this.namespace, key, data, dataSize, now, now)
167
278
 
168
279
  this.currentSize = this.currentSize - oldSize + dataSize
169
280
  this.notifySubscribers(key, data)
@@ -172,12 +283,16 @@ export class Cache {
172
283
  remove(key: string): boolean {
173
284
  // Check if key exists and get its size
174
285
  const row = this.db
175
- .prepare(`SELECT size FROM ${this.tableName} WHERE key = ?`)
176
- .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
177
290
 
178
291
  if (row) {
179
292
  // Delete the key
180
- 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)
181
296
 
182
297
  this.currentSize -= row.size
183
298
  this.notifySubscribers(key, undefined)
@@ -188,7 +303,9 @@ export class Cache {
188
303
  }
189
304
 
190
305
  clear(options?: { notifySubscribers: boolean }): void {
191
- this.db.exec(`DELETE FROM ${this.tableName}`)
306
+ this.db
307
+ .prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`)
308
+ .run(this.namespace)
192
309
  this.currentSize = 0
193
310
 
194
311
  if (options?.notifySubscribers !== false) {
@@ -207,10 +324,14 @@ export class Cache {
207
324
  }
208
325
 
209
326
  private maintainCapacity(bytesToFree: number): void {
210
- // Order by rowid ASC to get oldest entries first
327
+ // Order by oldest last-access time first to evict least-recently-used rows.
211
328
  const rows = this.db
212
- .prepare(`SELECT key, size FROM ${this.tableName} ORDER BY rowid ASC`)
213
- .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 }>
214
335
 
215
336
  let freedBytes = 0
216
337
  const keysToRemove: string[] = []
@@ -226,9 +347,10 @@ export class Cache {
226
347
  if (keysToRemove.length > 0) {
227
348
  const placeholders = keysToRemove.map(() => '?').join(',')
228
349
  const stmt = this.db.prepare(
229
- `DELETE FROM ${this.tableName} WHERE key IN (${placeholders})`,
350
+ `DELETE FROM ${CACHE_TABLE_NAME}
351
+ WHERE namespace = ? AND key IN (${placeholders})`,
230
352
  )
231
- stmt.run(...(keysToRemove as [string, ...string[]]))
353
+ stmt.run(this.namespace, ...(keysToRemove as [string, ...string[]]))
232
354
  this.currentSize -= freedBytes
233
355
  }
234
356
  }
@@ -326,7 +448,8 @@ export function withCache<Fn extends (...args: any[]) => Promise<any>>(
326
448
  const validate = options?.validate || (() => true)
327
449
 
328
450
  if (!functionCacheMap.has(fnKey)) {
329
- functionCacheMap.set(fnKey, new Cache({ namespace: `fn-${Date.now()}` }))
451
+ const functionNamespace = `fn-${hashString(fnKey)}`
452
+ functionCacheMap.set(fnKey, new Cache({ namespace: functionNamespace }))
330
453
  functionCacheData.set(fnKey, new Map())
331
454
  }
332
455
 
package/src/build.tsx CHANGED
@@ -241,6 +241,33 @@ export async function buildExtensionCommands({
241
241
 
242
242
  logger.log(`Building ${entrypoints.length} commands...`)
243
243
 
244
+ // Externalize the extension's declared dependencies so native modules
245
+ // (@libsql/client, @prisma/client, etc.) resolve from node_modules at runtime
246
+ // instead of being inlined into the bundle where binary addons fail to load.
247
+ // Only direct deps are externalized — NOT packages:'external' which would also
248
+ // externalize transitive deps of termcast internals (like dequal/lite) that
249
+ // don't exist in the extension's node_modules.
250
+ const rawPackageJson = JSON.parse(
251
+ fs.readFileSync(path.join(resolvedPath, 'package.json'), 'utf-8'),
252
+ )
253
+ const allDeps = {
254
+ ...rawPackageJson.dependencies,
255
+ ...rawPackageJson.devDependencies,
256
+ }
257
+ // Deps handled by aliasPlugin (mapped to globalThis), not externalized
258
+ const aliasedPackages = new Set([
259
+ '@raycast/api',
260
+ '@raycast/utils',
261
+ 'react',
262
+ 'termcast',
263
+ ])
264
+ const externalDeps = Object.keys(allDeps).filter((dep) => {
265
+ return !aliasedPackages.has(dep)
266
+ })
267
+ if (externalDeps.length > 0) {
268
+ logger.log(`Externalizing ${externalDeps.length} deps: ${externalDeps.join(', ')}`)
269
+ }
270
+
244
271
  const plugins: BunPlugin[] = [aliasPlugin, swiftLoaderPlugin]
245
272
 
246
273
  const result = await Bun.build({
@@ -249,6 +276,7 @@ export async function buildExtensionCommands({
249
276
  target: target || (format === 'cjs' ? 'node' : 'bun'),
250
277
  format,
251
278
  plugins,
279
+ external: externalDeps,
252
280
  naming: '[name].js',
253
281
  throw: false,
254
282
  reactFastRefresh: hotReload,
package/src/cli.tsx CHANGED
@@ -7,7 +7,7 @@ import './extensions/react-refresh-init'
7
7
  import fs from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import { execSync, spawn } from 'node:child_process'
10
- import { cac } from 'cac'
10
+ import { goke } from 'goke'
11
11
  import { getWatcher } from './watcher'
12
12
  import { buildExtensionCommands } from './build'
13
13
  import { logger } from './logger'
@@ -21,7 +21,7 @@ import { runHomeCommand } from './extensions/home'
21
21
  import { showToast, Toast } from './apis/toast'
22
22
  import packageJson from '../package.json'
23
23
 
24
- const cli = cac('termcast')
24
+ const cli = goke('termcast')
25
25
 
26
26
  // Auto-update check
27
27
  async function checkForUpdates() {
@@ -535,12 +535,10 @@ cli
535
535
  'raycast-search <query>',
536
536
  'Search for extensions in the Raycast store',
537
537
  )
538
- .option('-n, --limit <number>', 'Number of results to show', {
539
- default: '10',
540
- })
541
- .action(async (query: string, options: { limit: string }) => {
538
+ .option('-n, --limit [number]', 'Number of results to show (default: 10)')
539
+ .action(async (query: string, options: { limit?: string }) => {
542
540
  try {
543
- const limit = parseInt(options.limit, 10)
541
+ const limit = parseInt(options.limit || '10', 10)
544
542
  const result = await searchStoreListings({ query, perPage: limit })
545
543
 
546
544
  if (result.data.length === 0) {
@@ -578,7 +576,7 @@ cli
578
576
  'raycast-download <extensionName>',
579
577
  'Download extension from Raycast extensions repo',
580
578
  )
581
- .option('-o, --output <path>', 'Output directory', { default: '.' })
579
+ .option('-o, --output [path]', 'Output directory (default: .)')
582
580
  .option(
583
581
  '--no-dir',
584
582
  'Put files directly in output directory instead of creating extension subdirectory',
@@ -586,10 +584,10 @@ cli
586
584
  .action(
587
585
  async (
588
586
  extensionName: string,
589
- options: { output: string; dir: boolean },
587
+ options: { output?: string; dir: boolean },
590
588
  ) => {
591
589
  try {
592
- const destPath = path.resolve(options.output)
590
+ const destPath = path.resolve(options.output || '.')
593
591
  // When --no-dir is passed, dir is false; put files directly in destPath
594
592
  const extensionDir = options.dir
595
593
  ? path.join(destPath, extensionName)
@@ -6,8 +6,14 @@ import { execSync } from 'node:child_process'
6
6
 
7
7
  const fixtureDir = path.resolve(__dirname, '../fixtures/simple-extension')
8
8
  const distDir = path.join(fixtureDir, 'dist')
9
+ const bundleDir = path.join(fixtureDir, '.termcast-bundle')
9
10
  const executablePath = path.join(distDir, 'simple-extension')
10
11
 
12
+ const singleErrorFixtureDir = path.resolve(__dirname, '../fixtures/single-error-extension')
13
+ const singleErrorDistDir = path.join(singleErrorFixtureDir, 'dist')
14
+ const singleErrorBundleDir = path.join(singleErrorFixtureDir, '.termcast-bundle')
15
+ const singleErrorExecutablePath = path.join(singleErrorDistDir, 'single-error-extension')
16
+
11
17
  let session: Session
12
18
 
13
19
  afterEach(() => {
@@ -38,6 +44,22 @@ beforeAll(() => {
38
44
  if (fs.existsSync(executablePath)) {
39
45
  fs.unlinkSync(executablePath)
40
46
  }
47
+ if (fs.existsSync(distDir)) {
48
+ fs.rmSync(distDir, { recursive: true, force: true })
49
+ }
50
+ if (fs.existsSync(bundleDir)) {
51
+ fs.rmSync(bundleDir, { recursive: true, force: true })
52
+ }
53
+
54
+ if (fs.existsSync(singleErrorExecutablePath)) {
55
+ fs.unlinkSync(singleErrorExecutablePath)
56
+ }
57
+ if (fs.existsSync(singleErrorDistDir)) {
58
+ fs.rmSync(singleErrorDistDir, { recursive: true, force: true })
59
+ }
60
+ if (fs.existsSync(singleErrorBundleDir)) {
61
+ fs.rmSync(singleErrorBundleDir, { recursive: true, force: true })
62
+ }
41
63
  })
42
64
 
43
65
 
@@ -66,12 +88,12 @@ test('compile extension and run executable', async () => {
66
88
 
67
89
  > Search commands...
68
90
 
69
- Commands
70
- ›List Items Displays a simple list with some ite view
71
- Search Items Search and filter through a list o view
72
- Google Oauth view
73
- usePromise Demo Shows how to use the usePromise view
74
- Show State Shows the current application state view
91
+ Commands
92
+ ›List Items Displays a simple list with some items view
93
+ Search Items Search and filter through a list of view
94
+ Google Oauth view
95
+ usePromise Demo Shows how to use the usePromise h view
96
+ Show State Shows the current application state in view
75
97
 
76
98
 
77
99
  ↵ run command ↑↓ navigate ^k actions
@@ -112,12 +134,12 @@ test('compiled executable can run command', async () => {
112
134
 
113
135
  > Search...
114
136
 
115
- Items
116
- ›▲ First Item This is the first item
117
- Second Item This is the second item
118
- Third Item This is the third item
119
- Fourth Item This is the fourth item
120
- Fifth Item This is the fifth item
137
+ Items
138
+ ›○ First Item This is the first item
139
+ Second Item This is the second item
140
+ Third Item This is the third item
141
+ Fourth Item This is the fourth item
142
+ Fifth Item This is the fifth item
121
143
 
122
144
 
123
145
  ✓ Copied to Clipboard First Item
@@ -168,12 +190,12 @@ test('compiled executable can navigate back', async () => {
168
190
 
169
191
  > Search...
170
192
 
171
- Items
172
- ›▲ First Item This is the first item
173
- Second Item This is the second item
174
- Third Item This is the third item
175
- Fourth Item This is the fourth item
176
- Fifth Item This is the fifth item
193
+ Items
194
+ ›○ First Item This is the first item
195
+ Second Item This is the second item
196
+ Third Item This is the third item
197
+ Fourth Item This is the fourth item
198
+ Fifth Item This is the fifth item
177
199
 
178
200
 
179
201
  ↵ copy item title ↑↓ navigate ^k actions
@@ -237,11 +259,6 @@ test('compiled executable shows error when command throws at root scope', async
237
259
  `)
238
260
  }, 60000)
239
261
 
240
- // Test for single-command extension with root-level error
241
- const singleErrorFixtureDir = path.resolve(__dirname, '../fixtures/single-error-extension')
242
- const singleErrorDistDir = path.join(singleErrorFixtureDir, 'dist')
243
- const singleErrorExecutablePath = path.join(singleErrorDistDir, 'single-error-extension')
244
-
245
262
  function ensureSingleErrorCompiled() {
246
263
  if (!fs.existsSync(singleErrorExecutablePath)) {
247
264
  if (fs.existsSync(singleErrorDistDir)) {
@@ -273,6 +290,8 @@ test('single command extension shows error when command throws at root scope', a
273
290
  await session.waitIdle()
274
291
 
275
292
  const errorSnapshot = await session.text()
293
+ expect(errorSnapshot).not.toContain('Failed to load native binding')
294
+ expect(errorSnapshot).not.toContain('@swc/core/binding.js')
276
295
  expect(errorSnapshot).toMatchInlineSnapshot(`
277
296
  "
278
297
 
@@ -297,4 +316,3 @@ test('single command extension shows error when command throws at root scope', a
297
316
  "
298
317
  `)
299
318
  }, 60000)
300
-
@@ -1,7 +1,7 @@
1
1
  import React, {
2
2
  ReactNode,
3
3
  useState,
4
- useEffect,
4
+ useLayoutEffect,
5
5
  useMemo,
6
6
  useRef,
7
7
  useCallback,
@@ -178,8 +178,8 @@ const Dropdown: DropdownType = (props) => {
178
178
  [searchText, filtering, selected, currentValue, onSelectionChange],
179
179
  )
180
180
 
181
- // Update controlled value
182
- useEffect(() => {
181
+ // Update controlled value (before paint to avoid flash)
182
+ useLayoutEffect(() => {
183
183
  if (value !== undefined) {
184
184
  setCurrentValue(value)
185
185
  }
@@ -241,9 +241,11 @@ export function Footer({
241
241
  <ToastInline toast={toast} />
242
242
  ) : (
243
243
  <>
244
- {children}
244
+ <box flexDirection='row' overflow='hidden' height={1} flexShrink={1}>
245
+ {children}
246
+ </box>
245
247
  {showPoweredBy && (
246
- <box flexDirection='row' gap={1}>
248
+ <box flexDirection='row' gap={1} flexShrink={0}>
247
249
  <text flexShrink={0} fg={theme.textMuted}>
248
250
  powered by
249
251
  </text>