@tsctl/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +735 -0
  2. package/bin/tsctl.js +2 -0
  3. package/package.json +65 -0
  4. package/src/__tests__/analyticsrules.test.ts +303 -0
  5. package/src/__tests__/apikeys.test.ts +223 -0
  6. package/src/__tests__/apply.test.ts +245 -0
  7. package/src/__tests__/client.test.ts +48 -0
  8. package/src/__tests__/collection-advanced.test.ts +274 -0
  9. package/src/__tests__/config-loader.test.ts +217 -0
  10. package/src/__tests__/curationsets.test.ts +190 -0
  11. package/src/__tests__/helpers.ts +17 -0
  12. package/src/__tests__/import-drift.test.ts +231 -0
  13. package/src/__tests__/migrate-advanced.test.ts +197 -0
  14. package/src/__tests__/migrate.test.ts +220 -0
  15. package/src/__tests__/plan-new-resources.test.ts +258 -0
  16. package/src/__tests__/plan.test.ts +337 -0
  17. package/src/__tests__/presets.test.ts +97 -0
  18. package/src/__tests__/resources.test.ts +592 -0
  19. package/src/__tests__/setup.ts +77 -0
  20. package/src/__tests__/state.test.ts +312 -0
  21. package/src/__tests__/stemmingdictionaries.test.ts +111 -0
  22. package/src/__tests__/stopwords.test.ts +109 -0
  23. package/src/__tests__/synonymsets.test.ts +170 -0
  24. package/src/__tests__/types.test.ts +416 -0
  25. package/src/apply/index.ts +336 -0
  26. package/src/cli/index.ts +1106 -0
  27. package/src/client/index.ts +55 -0
  28. package/src/config/loader.ts +158 -0
  29. package/src/index.ts +45 -0
  30. package/src/migrate/index.ts +220 -0
  31. package/src/plan/index.ts +1333 -0
  32. package/src/resources/alias.ts +59 -0
  33. package/src/resources/analyticsrule.ts +134 -0
  34. package/src/resources/apikey.ts +203 -0
  35. package/src/resources/collection.ts +424 -0
  36. package/src/resources/curationset.ts +155 -0
  37. package/src/resources/index.ts +11 -0
  38. package/src/resources/override.ts +174 -0
  39. package/src/resources/preset.ts +83 -0
  40. package/src/resources/stemmingdictionary.ts +103 -0
  41. package/src/resources/stopword.ts +100 -0
  42. package/src/resources/synonym.ts +152 -0
  43. package/src/resources/synonymset.ts +144 -0
  44. package/src/state/index.ts +206 -0
  45. package/src/types/index.ts +451 -0
package/README.md ADDED
@@ -0,0 +1,735 @@
1
+ # tsctl - Terraform-like CLI for Typesense
2
+
3
+ A declarative infrastructure-as-code CLI for managing Typesense collections, aliases, synonyms, curations, analytics, API keys, stopwords, presets, and more.
4
+
5
+ Supports **Typesense v27+** and **v30+** with full backward compatibility.
6
+
7
+ ## Features
8
+
9
+ - **Declarative configuration**: Define your Typesense schema in TypeScript, JSON, or YAML config files
10
+ - **Plan/Apply workflow**: See what will change before applying
11
+ - **State management**: State stored in Typesense itself—no external dependencies
12
+ - **Type-safe**: Full TypeScript support with autocomplete and validation
13
+ - **Import existing**: Import existing Typesense resources into managed state
14
+ - **Drift detection**: Detect changes made outside of tsctl
15
+ - **Blue/green migrations**: Zero-downtime collection schema updates
16
+ - **Multi-environment**: Manage development, staging, and production environments
17
+ - **v29/v30 compatible**: Supports both legacy per-collection synonyms/overrides and v30 global synonym sets/curation sets
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install -g @tsctl/cli
23
+ # or
24
+ npx @tsctl/cli
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Initialize a project
30
+
31
+ ```bash
32
+ tsctl init
33
+ ```
34
+
35
+ This creates:
36
+ - `tsctl.config.ts` - Your infrastructure definition
37
+ - `.env` - Connection settings
38
+
39
+ ### 2. Configure connection
40
+
41
+ Edit `.env`:
42
+
43
+ ```env
44
+ TYPESENSE_HOST=localhost
45
+ TYPESENSE_PORT=8108
46
+ TYPESENSE_PROTOCOL=http
47
+ TYPESENSE_API_KEY=your-api-key-here
48
+ ```
49
+
50
+ ### 3. Define your schema
51
+
52
+ Edit `tsctl.config.ts`:
53
+
54
+ ```typescript
55
+ import { defineConfig } from "@tsctl/cli";
56
+
57
+ export default defineConfig({
58
+ collections: [
59
+ {
60
+ name: "products",
61
+ fields: [
62
+ { name: "name", type: "string" },
63
+ { name: "description", type: "string", optional: true },
64
+ { name: "price", type: "float" },
65
+ { name: "category", type: "string", facet: true },
66
+ { name: "tags", type: "string[]", facet: true, optional: true },
67
+ ],
68
+ default_sorting_field: "price",
69
+ },
70
+ ],
71
+ aliases: [
72
+ {
73
+ name: "products_live",
74
+ collection: "products",
75
+ },
76
+ ],
77
+ });
78
+ ```
79
+
80
+ ### 4. Plan changes
81
+
82
+ ```bash
83
+ tsctl plan
84
+ ```
85
+
86
+ Output:
87
+ ```
88
+ Typesense Plan:
89
+
90
+ + collection.products (create)
91
+ + name: "products"
92
+ + fields: [...]
93
+
94
+ + alias.products_live (create)
95
+ + name: "products_live"
96
+ + collection: "products"
97
+
98
+ Summary:
99
+ 2 to create, 0 to update, 0 to delete, 0 unchanged
100
+ ```
101
+
102
+ ### 5. Apply changes
103
+
104
+ ```bash
105
+ tsctl apply
106
+ ```
107
+
108
+ ## Commands
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `tsctl init` | Initialize a new project |
113
+ | `tsctl validate` | Validate config file |
114
+ | `tsctl plan` | Show planned changes |
115
+ | `tsctl apply` | Apply changes to Typesense |
116
+ | `tsctl destroy` | Destroy all managed resources |
117
+ | `tsctl import` | Import existing resources |
118
+ | `tsctl state list` | List managed resources |
119
+ | `tsctl state show` | Show full state JSON |
120
+ | `tsctl state clear` | Clear state (keeps resources) |
121
+ | `tsctl env list` | List available environments |
122
+ | `tsctl env show` | Show current environment config |
123
+ | `tsctl drift` | Detect changes made outside of tsctl |
124
+ | `tsctl migrate` | Blue/green migration for collections |
125
+
126
+ **Global Options:**
127
+ - `--env <name>` - Use environment-specific `.env.<name>` file
128
+
129
+ ## Configuration Files
130
+
131
+ tsctl supports multiple configuration file formats and locations. Files are searched in the following order:
132
+
133
+ | File | Format |
134
+ |------|--------|
135
+ | `package.json` | `"tsctl"` property |
136
+ | `.tsctlrc` | JSON or YAML |
137
+ | `.tsctlrc.json` | JSON |
138
+ | `.tsctlrc.yaml` / `.tsctlrc.yml` | YAML |
139
+ | `.tsctlrc.js` / `.tsctlrc.cjs` / `.tsctlrc.mjs` | JavaScript |
140
+ | `.tsctlrc.ts` / `.tsctlrc.cts` / `.tsctlrc.mts` | TypeScript |
141
+ | `tsctl.config.js` / `tsctl.config.cjs` / `tsctl.config.mjs` | JavaScript |
142
+ | `tsctl.config.ts` / `tsctl.config.cts` / `tsctl.config.mts` | TypeScript |
143
+ | `tsctl.config.json` | JSON |
144
+ | `tsctl.config.yaml` / `tsctl.config.yml` | YAML |
145
+ | `typesense.config.*` | Legacy (all formats) |
146
+
147
+ ### Examples
148
+
149
+ **TypeScript (recommended):**
150
+ ```typescript
151
+ // tsctl.config.ts
152
+ import { defineConfig } from "@tsctl/cli";
153
+
154
+ export default defineConfig({
155
+ collections: [{ name: "products", fields: [...] }],
156
+ });
157
+ ```
158
+
159
+ **JSON:**
160
+ ```json
161
+ // tsctl.config.json
162
+ {
163
+ "collections": [{ "name": "products", "fields": [...] }]
164
+ }
165
+ ```
166
+
167
+ **YAML:**
168
+ ```yaml
169
+ # tsctl.config.yaml
170
+ collections:
171
+ - name: products
172
+ fields:
173
+ - name: title
174
+ type: string
175
+ ```
176
+
177
+ **package.json:**
178
+ ```json
179
+ {
180
+ "name": "my-app",
181
+ "tsctl": {
182
+ "collections": [{ "name": "products", "fields": [...] }]
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Configuration Reference
188
+
189
+ ### Collections
190
+
191
+ ```typescript
192
+ {
193
+ name: "products",
194
+ fields: [
195
+ {
196
+ name: "title",
197
+ type: "string", // Required
198
+ optional: true, // Allow null/missing
199
+ facet: true, // Enable faceting
200
+ index: true, // Index for search (default: true)
201
+ sort: true, // Enable sorting
202
+ infix: true, // Enable infix search
203
+ locale: "en", // Language for stemming
204
+ stem: true, // Enable stemming
205
+ stem_dictionary: "en-plurals", // Custom stemming dictionary
206
+ store: true, // Store original value
207
+ num_dim: 384, // Vector dimensions
208
+ vec_dist: "cosine", // Vector distance metric
209
+ reference: "users.id", // JOINs
210
+ range_index: true, // For numeric range queries
211
+ truncate_len: 200, // Max chars per token (default: 100)
212
+ token_separators: ["-"], // Field-level token separators
213
+ symbols_to_index: ["#"], // Field-level symbols to index
214
+ },
215
+ ],
216
+ default_sorting_field: "created_at",
217
+ token_separators: ["-", "/"],
218
+ symbols_to_index: ["#", "@"],
219
+ enable_nested_fields: true,
220
+ metadata: { team: "search" }, // Custom metadata
221
+ synonym_sets: ["clothing-synonyms"], // Link synonym sets (v30+)
222
+ curation_sets: ["product-curations"], // Link curation sets (v30+)
223
+ }
224
+ ```
225
+
226
+ ### Field Types
227
+
228
+ - `string`, `string[]` - Text
229
+ - `int32`, `int32[]`, `int64`, `int64[]` - Integers
230
+ - `float`, `float[]` - Decimals
231
+ - `bool`, `bool[]` - Booleans
232
+ - `geopoint`, `geopoint[]` - Coordinates
233
+ - `geopolygon` - Geographic polygon
234
+ - `object`, `object[]` - Nested objects
235
+ - `auto` - Auto-detect type
236
+ - `string*` - Auto-embedding
237
+ - `image` - Image embedding
238
+
239
+ ### Aliases
240
+
241
+ ```typescript
242
+ {
243
+ name: "products_live",
244
+ collection: "products",
245
+ }
246
+ ```
247
+
248
+ ### Synonyms (Legacy - Typesense < 30.0)
249
+
250
+ Per-collection synonyms for Typesense versions before 30.0.
251
+
252
+ ```typescript
253
+ {
254
+ id: "smartphone-synonyms",
255
+ collection: "products",
256
+ synonyms: ["phone", "mobile", "smartphone", "cell phone"],
257
+ }
258
+ ```
259
+
260
+ For one-way synonyms (root word):
261
+
262
+ ```typescript
263
+ {
264
+ id: "tv-synonym",
265
+ collection: "products",
266
+ root: "television",
267
+ synonyms: ["tv", "telly", "television set"],
268
+ }
269
+ ```
270
+
271
+ ### Synonym Sets (Typesense 30.0+ - Global)
272
+
273
+ Global synonym sets that can be shared across collections.
274
+
275
+ ```typescript
276
+ {
277
+ name: "clothing-synonyms",
278
+ items: [
279
+ { id: "pants", synonyms: ["pants", "trousers", "slacks"] },
280
+ { id: "shirt", synonyms: ["shirt", "top", "blouse"] },
281
+ { id: "tv", root: "television", synonyms: ["tv", "telly"] },
282
+ ],
283
+ }
284
+ ```
285
+
286
+ Link to collections:
287
+ ```typescript
288
+ {
289
+ name: "products",
290
+ fields: [...],
291
+ synonym_sets: ["clothing-synonyms"],
292
+ }
293
+ ```
294
+
295
+ ### Overrides/Curations (Legacy - Typesense < 30.0)
296
+
297
+ Per-collection overrides for Typesense versions before 30.0.
298
+
299
+ ```typescript
300
+ {
301
+ id: "pin-featured",
302
+ collection: "products",
303
+ rule: {
304
+ query: "featured",
305
+ match: "exact",
306
+ },
307
+ includes: [
308
+ { id: "product-123", position: 1 },
309
+ { id: "product-456", position: 2 },
310
+ ],
311
+ }
312
+ ```
313
+
314
+ ### Curation Sets (Typesense 30.0+ - Global)
315
+
316
+ Global curation rules that can be shared across collections.
317
+
318
+ ```typescript
319
+ {
320
+ name: "product-curations",
321
+ items: [
322
+ {
323
+ id: "pin-featured",
324
+ rule: { query: "featured", match: "exact" },
325
+ includes: [{ id: "product-123", position: 1 }],
326
+ },
327
+ {
328
+ id: "boost-shoes",
329
+ rule: { query: "shoes", match: "contains" },
330
+ filter_by: "category:=footwear",
331
+ sort_by: "popularity:desc",
332
+ remove_matched_tokens: true,
333
+ effective_from_ts: 1672531200,
334
+ effective_to_ts: 1704067200,
335
+ },
336
+ ],
337
+ }
338
+ ```
339
+
340
+ Link to collections:
341
+ ```typescript
342
+ {
343
+ name: "products",
344
+ fields: [...],
345
+ curation_sets: ["product-curations"],
346
+ }
347
+ ```
348
+
349
+ ### Stopwords
350
+
351
+ Define stopword sets to remove common words from search queries.
352
+
353
+ ```typescript
354
+ {
355
+ id: "english-stopwords",
356
+ stopwords: ["the", "a", "an", "is", "are", "was", "were"],
357
+ locale: "en", // optional
358
+ }
359
+ ```
360
+
361
+ ### Search Presets
362
+
363
+ Store reusable search parameter configurations.
364
+
365
+ ```typescript
366
+ {
367
+ name: "listing_view",
368
+ value: {
369
+ searches: [
370
+ {
371
+ collection: "products",
372
+ q: "*",
373
+ sort_by: "popularity:desc",
374
+ },
375
+ ],
376
+ },
377
+ }
378
+ ```
379
+
380
+ ### Analytics Rules
381
+
382
+ ```typescript
383
+ {
384
+ name: "popular-queries",
385
+ type: "popular_queries", // popular_queries | nohits_queries | counter | log
386
+ collection: "products",
387
+ event_type: "search", // search | click | conversion | visit | custom
388
+ params: {
389
+ destination_collection: "popular_queries",
390
+ limit: 1000,
391
+ expand_query: true,
392
+ },
393
+ }
394
+ ```
395
+
396
+ ### API Keys
397
+
398
+ ```typescript
399
+ {
400
+ description: "Search-only key for frontend",
401
+ actions: ["documents:search"],
402
+ collections: ["products", "categories"],
403
+ }
404
+ ```
405
+
406
+ With expiration:
407
+
408
+ ```typescript
409
+ {
410
+ description: "Temporary admin key",
411
+ actions: ["*"],
412
+ collections: ["*"],
413
+ expires_at: 1735689600, // Unix timestamp
414
+ autodelete: true,
415
+ }
416
+ ```
417
+
418
+ ### Stemming Dictionaries
419
+
420
+ Custom word-to-root mappings for stemming.
421
+
422
+ ```typescript
423
+ {
424
+ id: "english-plurals",
425
+ words: [
426
+ { word: "dogs", root: "dog" },
427
+ { word: "cats", root: "cat" },
428
+ { word: "mice", root: "mouse" },
429
+ ],
430
+ }
431
+ ```
432
+
433
+ Reference from fields:
434
+ ```typescript
435
+ {
436
+ name: "title",
437
+ type: "string",
438
+ stem_dictionary: "english-plurals",
439
+ }
440
+ ```
441
+
442
+ ## Full Configuration Example
443
+
444
+ ```typescript
445
+ import { defineConfig } from "@tsctl/cli";
446
+
447
+ export default defineConfig({
448
+ collections: [
449
+ {
450
+ name: "products",
451
+ fields: [
452
+ { name: "name", type: "string" },
453
+ { name: "description", type: "string", optional: true },
454
+ { name: "price", type: "float" },
455
+ { name: "category", type: "string", facet: true },
456
+ ],
457
+ default_sorting_field: "price",
458
+ synonym_sets: ["product-synonyms"],
459
+ curation_sets: ["product-curations"],
460
+ },
461
+ ],
462
+
463
+ aliases: [
464
+ { name: "products_live", collection: "products" },
465
+ ],
466
+
467
+ // v30+ global synonym sets
468
+ synonymSets: [
469
+ {
470
+ name: "product-synonyms",
471
+ items: [
472
+ { id: "phones", synonyms: ["phone", "mobile", "smartphone"] },
473
+ ],
474
+ },
475
+ ],
476
+
477
+ // v30+ global curation sets
478
+ curationSets: [
479
+ {
480
+ name: "product-curations",
481
+ items: [
482
+ {
483
+ id: "featured",
484
+ rule: { query: "featured", match: "exact" },
485
+ includes: [{ id: "product-1", position: 1 }],
486
+ },
487
+ ],
488
+ },
489
+ ],
490
+
491
+ stopwords: [
492
+ { id: "english", stopwords: ["the", "a", "an"] },
493
+ ],
494
+
495
+ presets: [
496
+ { name: "default_search", value: { q: "*", sort_by: "price:asc" } },
497
+ ],
498
+
499
+ apiKeys: [
500
+ {
501
+ description: "Search-only key",
502
+ actions: ["documents:search"],
503
+ collections: ["products"],
504
+ },
505
+ ],
506
+
507
+ stemmingDictionaries: [
508
+ {
509
+ id: "en-plurals",
510
+ words: [{ word: "shoes", root: "shoe" }],
511
+ },
512
+ ],
513
+ });
514
+ ```
515
+
516
+ ## State Management
517
+
518
+ State is stored in a special Typesense collection (`_tsctl_state`). This means:
519
+
520
+ - No external state storage needed
521
+ - State travels with your Typesense instance
522
+ - Easy backup/restore with Typesense snapshots
523
+
524
+ ### Import Existing Resources
525
+
526
+ If you have existing collections/aliases:
527
+
528
+ ```bash
529
+ tsctl import
530
+ ```
531
+
532
+ This will:
533
+ 1. Scan your Typesense instance for all resource types
534
+ 2. Generate a `tsctl.imported.config.ts` file
535
+ 3. Save the current state
536
+
537
+ Review the generated config, then rename it to `tsctl.config.ts`.
538
+
539
+ ## Environment Variables
540
+
541
+ | Variable | Default | Description |
542
+ |----------|---------|-------------|
543
+ | `TYPESENSE_HOST` | `localhost` | Typesense host |
544
+ | `TYPESENSE_PORT` | `8108` | Typesense port |
545
+ | `TYPESENSE_PROTOCOL` | `http` | `http` or `https` |
546
+ | `TYPESENSE_API_KEY` | - | API key (required) |
547
+
548
+ ## Multi-Environment Support
549
+
550
+ Manage multiple Typesense environments (development, staging, production) using environment-specific `.env` files.
551
+
552
+ ### Setup
553
+
554
+ Initialize with environment files:
555
+
556
+ ```bash
557
+ tsctl init --with-environments
558
+ ```
559
+
560
+ This creates:
561
+ - `.env` - Default/development settings
562
+ - `.env.development` - Development environment
563
+ - `.env.staging` - Staging environment
564
+ - `.env.production` - Production environment
565
+
566
+ ### Usage
567
+
568
+ Use the `--env` flag to target a specific environment:
569
+
570
+ ```bash
571
+ # Plan changes against staging
572
+ tsctl plan --env staging
573
+
574
+ # Apply to production
575
+ tsctl apply --env production
576
+
577
+ # Import from development
578
+ tsctl import --env development
579
+ ```
580
+
581
+ ## Drift Detection
582
+
583
+ Detect when resources have been modified outside of tsctl (e.g., via Typesense dashboard or API).
584
+
585
+ ```bash
586
+ tsctl drift
587
+ ```
588
+
589
+ Output shows:
590
+ - **Modified**: Resources changed outside of tsctl
591
+ - **Deleted**: Resources removed outside of tsctl
592
+ - **Unmanaged**: Resources that exist but aren't in your config
593
+
594
+ Drift detection covers all resource types: collections, aliases, stopwords, presets, curation sets, and more.
595
+
596
+ ### CI/CD Integration
597
+
598
+ The `drift` command exits with code 1 if drift is detected, making it useful for CI pipelines:
599
+
600
+ ```bash
601
+ # In your CI pipeline
602
+ tsctl drift --env production || echo "Drift detected!"
603
+ ```
604
+
605
+ ### JSON Output
606
+
607
+ For programmatic use:
608
+
609
+ ```bash
610
+ tsctl drift --json
611
+ ```
612
+
613
+ ## Blue/Green Migrations
614
+
615
+ Perform zero-downtime collection schema updates using the blue/green deployment pattern.
616
+
617
+ ### How It Works
618
+
619
+ 1. **Create** a new versioned collection (e.g., `products_1706486400000`)
620
+ 2. **Index** your data to the new collection
621
+ 3. **Switch** the alias to point to the new collection
622
+ 4. **Cleanup** the old collection when ready
623
+
624
+ ### Quick Migration
625
+
626
+ Full migration in one command:
627
+
628
+ ```bash
629
+ tsctl migrate -a products_live -c tsctl.config.ts
630
+ ```
631
+
632
+ ### Step-by-Step Migration
633
+
634
+ For more control, migrate in stages:
635
+
636
+ ```bash
637
+ # Step 1: Create the new collection
638
+ tsctl migrate -a products_live -c tsctl.config.ts --create-only
639
+
640
+ # Step 2: Index your data to the new collection
641
+ # (use your own indexing process)
642
+
643
+ # Step 3: Switch the alias to the new collection
644
+ tsctl migrate -a products_live -c tsctl.config.ts --switch-only
645
+
646
+ # Step 4: Delete the old collection when ready
647
+ tsctl migrate -a products_live -c tsctl.config.ts --cleanup products_1706486400000
648
+ ```
649
+
650
+ ### Options
651
+
652
+ | Option | Description |
653
+ |--------|-------------|
654
+ | `-a, --alias <name>` | Alias to migrate (required) |
655
+ | `-c, --config <path>` | Path to config file (required) |
656
+ | `--collection <name>` | Collection from config (if multiple) |
657
+ | `--skip-delete` | Keep old collection for rollback |
658
+ | `--create-only` | Only create new collection |
659
+ | `--switch-only` | Only switch alias |
660
+ | `--cleanup <name>` | Delete old collection |
661
+
662
+ ## Typesense Version Compatibility
663
+
664
+ | Feature | v27 | v28 | v29 | v30+ |
665
+ |---------|-----|-----|-----|------|
666
+ | Collections | Yes | Yes | Yes | Yes |
667
+ | Aliases | Yes | Yes | Yes | Yes |
668
+ | Per-collection Synonyms | Yes | Yes | Yes | Deprecated* |
669
+ | Per-collection Overrides | Yes | Yes | Yes | Deprecated* |
670
+ | Global Synonym Sets | - | Yes | Yes | Yes |
671
+ | Global Curation Sets | - | - | - | Yes |
672
+ | API Keys | Yes | Yes | Yes | Yes |
673
+ | Analytics Rules | Yes | Yes | Yes | Yes |
674
+ | Stopwords | Yes | Yes | Yes | Yes |
675
+ | Presets | Yes | Yes | Yes | Yes |
676
+ | Stemming Dictionaries | - | Yes | Yes | Yes |
677
+
678
+ \* Auto-migrated to global sets on upgrade to v30
679
+
680
+ ## Development
681
+
682
+ ### Prerequisites
683
+
684
+ - [Bun](https://bun.sh) v1.0+
685
+ - [Docker](https://www.docker.com/) (for running Typesense locally)
686
+
687
+ ### Setup
688
+
689
+ ```bash
690
+ # Clone the repo
691
+ git clone https://github.com/akshitkrnagpal/tsctl.git
692
+ cd tsctl
693
+
694
+ # Install dependencies
695
+ bun install
696
+
697
+ # Start Typesense (v27)
698
+ docker compose up -d
699
+
700
+ # Or start Typesense v30
701
+ docker compose -f docker-compose.v30.yml up -d
702
+
703
+ # Run tests
704
+ bun test
705
+
706
+ # Type check
707
+ bun run typecheck
708
+ ```
709
+
710
+ ### Running Tests
711
+
712
+ Tests run against a live Typesense instance. Start one with Docker Compose before running tests:
713
+
714
+ ```bash
715
+ # Test against Typesense v27 (default)
716
+ docker compose up -d
717
+ bun test
718
+
719
+ # Test against Typesense v30
720
+ docker compose -f docker-compose.v30.yml up -d
721
+ bun test
722
+
723
+ # Stop Typesense
724
+ docker compose down -v
725
+ ```
726
+
727
+ Tests are version-aware and automatically skip tests for features not available in the running Typesense version.
728
+
729
+ ### CI/CD
730
+
731
+ The project uses GitHub Actions to run tests against both Typesense v27 and v30 on every push and pull request. See `.github/workflows/ci.yml`.
732
+
733
+ ## License
734
+
735
+ MIT