@tsproxy/cli 0.0.1

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 (2) hide show
  1. package/dist/index.js +676 -0
  2. package/package.json +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,676 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import * as p from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import { writeFileSync, existsSync } from "fs";
10
+ import { execSync } from "child_process";
11
+ import { resolve } from "path";
12
+ async function init() {
13
+ p.intro(pc.bgCyan(pc.black(" tsproxy init ")));
14
+ const scope = await p.select({
15
+ message: "What do you want to set up?",
16
+ options: [
17
+ { value: "both", label: "Backend + Frontend", hint: "proxy server and search UI" },
18
+ { value: "backend", label: "Backend only", hint: "proxy server" },
19
+ { value: "frontend", label: "Frontend only", hint: "search client and components" }
20
+ ]
21
+ });
22
+ if (p.isCancel(scope)) return process.exit(0);
23
+ const needsBackend = scope === "both" || scope === "backend";
24
+ const needsFrontend = scope === "both" || scope === "frontend";
25
+ let typesenseMode = "docker";
26
+ let tsHost = "localhost";
27
+ let tsPort = "8108";
28
+ let tsApiKey = "test-api-key";
29
+ let tsProtocol = "http";
30
+ let wantsRedis = false;
31
+ let redisHost = "localhost";
32
+ let redisPort = "6379";
33
+ if (needsBackend) {
34
+ typesenseMode = await p.select({
35
+ message: "How will you run Typesense?",
36
+ options: [
37
+ { value: "docker", label: "Docker (local)", hint: "generates docker-compose.yml" },
38
+ { value: "cloud", label: "Typesense Cloud" },
39
+ { value: "self-hosted", label: "Self-hosted" }
40
+ ]
41
+ });
42
+ if (p.isCancel(typesenseMode)) return process.exit(0);
43
+ if (typesenseMode === "cloud" || typesenseMode === "self-hosted") {
44
+ const hostInput = await p.text({
45
+ message: "Typesense host",
46
+ placeholder: typesenseMode === "cloud" ? "xyz.a1.typesense.net" : "localhost",
47
+ validate: (v) => v.length === 0 ? "Host is required" : void 0
48
+ });
49
+ if (p.isCancel(hostInput)) return process.exit(0);
50
+ tsHost = hostInput;
51
+ const portInput = await p.text({
52
+ message: "Typesense port",
53
+ initialValue: typesenseMode === "cloud" ? "443" : "8108"
54
+ });
55
+ if (p.isCancel(portInput)) return process.exit(0);
56
+ tsPort = portInput;
57
+ if (typesenseMode === "cloud") {
58
+ tsProtocol = "https";
59
+ }
60
+ const keyInput = await p.text({
61
+ message: "Typesense API key",
62
+ validate: (v) => v.length === 0 ? "API key is required" : void 0
63
+ });
64
+ if (p.isCancel(keyInput)) return process.exit(0);
65
+ tsApiKey = keyInput;
66
+ }
67
+ const redisChoice = await p.confirm({
68
+ message: "Use Redis for persistent queue?",
69
+ initialValue: typesenseMode === "docker"
70
+ });
71
+ if (p.isCancel(redisChoice)) return process.exit(0);
72
+ wantsRedis = redisChoice;
73
+ if (wantsRedis && typesenseMode !== "docker") {
74
+ const rHost = await p.text({
75
+ message: "Redis host",
76
+ initialValue: "localhost"
77
+ });
78
+ if (p.isCancel(rHost)) return process.exit(0);
79
+ redisHost = rHost;
80
+ const rPort = await p.text({
81
+ message: "Redis port",
82
+ initialValue: "6379"
83
+ });
84
+ if (p.isCancel(rPort)) return process.exit(0);
85
+ redisPort = rPort;
86
+ }
87
+ }
88
+ let frontendType = "react";
89
+ if (needsFrontend) {
90
+ frontendType = await p.select({
91
+ message: "Frontend framework?",
92
+ options: [
93
+ { value: "react", label: "React", hint: "headless components + InstantSearch" },
94
+ { value: "vanilla", label: "Vanilla JS", hint: "search client only" }
95
+ ]
96
+ });
97
+ if (p.isCancel(frontendType)) return process.exit(0);
98
+ }
99
+ const s = p.spinner();
100
+ s.start("Generating files");
101
+ const cwd = process.cwd();
102
+ if (needsBackend) {
103
+ const configContent = generateConfig({
104
+ tsHost,
105
+ tsPort,
106
+ tsProtocol,
107
+ tsApiKey,
108
+ wantsRedis,
109
+ redisHost,
110
+ redisPort,
111
+ isDocker: typesenseMode === "docker"
112
+ });
113
+ writeFileSync(resolve(cwd, "tsproxy.config.ts"), configContent);
114
+ }
115
+ if (typesenseMode === "docker") {
116
+ const dockerContent = generateDockerCompose(wantsRedis);
117
+ if (!existsSync(resolve(cwd, "docker-compose.yml"))) {
118
+ writeFileSync(resolve(cwd, "docker-compose.yml"), dockerContent);
119
+ } else {
120
+ p.log.warn("docker-compose.yml already exists, skipping");
121
+ }
122
+ }
123
+ const envContent = generateEnv({
124
+ tsHost,
125
+ tsPort,
126
+ tsProtocol,
127
+ tsApiKey,
128
+ isDocker: typesenseMode === "docker",
129
+ wantsRedis,
130
+ redisHost,
131
+ redisPort
132
+ });
133
+ if (!existsSync(resolve(cwd, ".env"))) {
134
+ writeFileSync(resolve(cwd, ".env"), envContent);
135
+ }
136
+ s.stop("Files generated");
137
+ const deps = [];
138
+ if (needsBackend) deps.push("@tsproxy/api");
139
+ if (needsFrontend) {
140
+ deps.push("@tsproxy/js");
141
+ if (frontendType === "react") {
142
+ deps.push("@tsproxy/react", "react-instantsearch");
143
+ }
144
+ }
145
+ if (deps.length > 0) {
146
+ const pm = detectPackageManager();
147
+ s.start(`Installing ${deps.join(", ")}`);
148
+ try {
149
+ const installCmd = pm === "pnpm" ? `pnpm add ${deps.join(" ")}` : pm === "yarn" ? `yarn add ${deps.join(" ")}` : pm === "bun" ? `bun add ${deps.join(" ")}` : `npm install ${deps.join(" ")}`;
150
+ execSync(installCmd, { stdio: "pipe", cwd });
151
+ s.stop("Dependencies installed");
152
+ } catch {
153
+ s.stop("Failed to install \u2014 run manually:");
154
+ p.log.info(` ${pm} add ${deps.join(" ")}`);
155
+ }
156
+ }
157
+ p.note(
158
+ [
159
+ typesenseMode === "docker" && "docker compose up -d",
160
+ needsBackend && "npx tsproxy dev",
161
+ needsFrontend && "# Import in your app:",
162
+ needsFrontend && frontendType === "react" ? ' import { SearchBox, Hits } from "@tsproxy/react"' : needsFrontend ? ' import { createSearchClient } from "@tsproxy/js"' : null
163
+ ].filter(Boolean).join("\n"),
164
+ "Next steps"
165
+ );
166
+ p.outro(pc.green("Ready!"));
167
+ }
168
+ function generateConfig(opts) {
169
+ const redis = opts.wantsRedis ? `
170
+ redis: { host: "${opts.isDocker ? "localhost" : opts.redisHost}", port: ${opts.isDocker ? 6379 : opts.redisPort} },` : "";
171
+ return `import { defineConfig } from "@tsproxy/api";
172
+
173
+ export default defineConfig({
174
+ typesense: {
175
+ host: "${opts.isDocker ? "localhost" : opts.tsHost}",
176
+ port: ${opts.isDocker ? 8108 : opts.tsPort},
177
+ protocol: "${opts.tsProtocol}",
178
+ apiKey: process.env.TYPESENSE_API_KEY || "${opts.tsApiKey}",
179
+ },
180
+
181
+ server: {
182
+ port: 3000,
183
+ apiKey: process.env.PROXY_API_KEY || "change-me",
184
+ },
185
+
186
+ cache: {
187
+ ttl: 60,
188
+ maxSize: 1000,
189
+ },
190
+
191
+ queue: {
192
+ concurrency: 5,
193
+ maxSize: 10000,${redis}
194
+ },
195
+
196
+ rateLimit: {
197
+ search: 100,
198
+ ingest: 30,
199
+ },
200
+
201
+ collections: {
202
+ // Define your collections here:
203
+ // products: {
204
+ // fields: {
205
+ // name: { type: "string", searchable: true },
206
+ // price: { type: "float", sortable: true },
207
+ // category: { type: "string", facet: true },
208
+ // },
209
+ // },
210
+ },
211
+ });
212
+ `;
213
+ }
214
+ function generateDockerCompose(wantsRedis) {
215
+ let content = `services:
216
+ typesense:
217
+ image: typesense/typesense:30.0
218
+ restart: unless-stopped
219
+ ports:
220
+ - "8108:8108"
221
+ volumes:
222
+ - typesense-data:/data
223
+ command: >
224
+ --data-dir /data
225
+ --api-key=\${TYPESENSE_API_KEY:-test-api-key}
226
+ --enable-cors
227
+ healthcheck:
228
+ test: ["CMD", "curl", "-sf", "http://localhost:8108/health"]
229
+ interval: 10s
230
+ timeout: 5s
231
+ retries: 3
232
+ `;
233
+ if (wantsRedis) {
234
+ content += `
235
+ redis:
236
+ image: redis:7-alpine
237
+ restart: unless-stopped
238
+ ports:
239
+ - "6379:6379"
240
+ volumes:
241
+ - redis-data:/data
242
+ command: redis-server --appendonly yes
243
+ healthcheck:
244
+ test: ["CMD", "redis-cli", "ping"]
245
+ interval: 10s
246
+ timeout: 5s
247
+ retries: 3
248
+ `;
249
+ }
250
+ content += `
251
+ volumes:
252
+ typesense-data:`;
253
+ if (wantsRedis) {
254
+ content += `
255
+ redis-data:`;
256
+ }
257
+ return content + "\n";
258
+ }
259
+ function generateEnv(opts) {
260
+ let content = `# Typesense
261
+ TYPESENSE_HOST=${opts.isDocker ? "localhost" : opts.tsHost}
262
+ TYPESENSE_PORT=${opts.isDocker ? "8108" : opts.tsPort}
263
+ TYPESENSE_PROTOCOL=${opts.tsProtocol}
264
+ TYPESENSE_API_KEY=${opts.tsApiKey}
265
+
266
+ # Proxy
267
+ PROXY_PORT=3000
268
+ PROXY_API_KEY=change-me
269
+ `;
270
+ if (opts.wantsRedis) {
271
+ content += `
272
+ # Redis
273
+ REDIS_HOST=${opts.isDocker ? "localhost" : opts.redisHost}
274
+ REDIS_PORT=${opts.isDocker ? "6379" : opts.redisPort}
275
+ `;
276
+ }
277
+ return content;
278
+ }
279
+ function detectPackageManager() {
280
+ const cwd = process.cwd();
281
+ if (existsSync(resolve(cwd, "pnpm-lock.yaml"))) return "pnpm";
282
+ if (existsSync(resolve(cwd, "yarn.lock"))) return "yarn";
283
+ if (existsSync(resolve(cwd, "bun.lockb"))) return "bun";
284
+ return "npm";
285
+ }
286
+
287
+ // src/commands/dev.ts
288
+ import { execSync as execSync2 } from "child_process";
289
+ import { resolve as resolve2 } from "path";
290
+ import { existsSync as existsSync2 } from "fs";
291
+ async function dev(opts) {
292
+ const args = ["dev"];
293
+ if (opts.port) args.push("--port", opts.port);
294
+ if (opts.config) args.push("--config", opts.config);
295
+ const hasTsx = existsSync2(resolve2(process.cwd(), "node_modules/.bin/tsx"));
296
+ const cliPath = findCliEntry();
297
+ if (!cliPath) {
298
+ console.error("@tsproxy/api not found. Run: npm install @tsproxy/api");
299
+ process.exit(1);
300
+ }
301
+ const cmd = hasTsx ? `npx tsx watch ${cliPath} ${args.join(" ")}` : `node ${cliPath} ${args.join(" ")}`;
302
+ try {
303
+ execSync2(cmd, { stdio: "inherit", cwd: process.cwd() });
304
+ } catch {
305
+ process.exit(1);
306
+ }
307
+ }
308
+ function findCliEntry() {
309
+ const paths = [
310
+ resolve2(process.cwd(), "node_modules/@tsproxy/api/dist/cli.js"),
311
+ resolve2(process.cwd(), "node_modules/@tsproxy/api/src/cli.ts")
312
+ ];
313
+ for (const p2 of paths) {
314
+ if (existsSync2(p2)) return p2;
315
+ }
316
+ return null;
317
+ }
318
+
319
+ // src/commands/start.ts
320
+ import { execSync as execSync3 } from "child_process";
321
+ import { resolve as resolve3 } from "path";
322
+ import { existsSync as existsSync3 } from "fs";
323
+ async function start(opts) {
324
+ const cliPath = resolve3(process.cwd(), "node_modules/@tsproxy/api/dist/cli.js");
325
+ if (!existsSync3(cliPath)) {
326
+ console.error("@tsproxy/api not found or not built. Run: npm install @tsproxy/api");
327
+ process.exit(1);
328
+ }
329
+ const args = ["start"];
330
+ if (opts.port) args.push("--port", opts.port);
331
+ if (opts.config) args.push("--config", opts.config);
332
+ try {
333
+ execSync3(`node ${cliPath} ${args.join(" ")}`, {
334
+ stdio: "inherit",
335
+ cwd: process.cwd()
336
+ });
337
+ } catch {
338
+ process.exit(1);
339
+ }
340
+ }
341
+
342
+ // src/commands/build.ts
343
+ import { execSync as execSync4 } from "child_process";
344
+ async function build() {
345
+ console.log("Building @tsproxy/api for production...");
346
+ try {
347
+ execSync4("npx tsup node_modules/@tsproxy/api/src/index.ts node_modules/@tsproxy/api/src/server.ts node_modules/@tsproxy/api/src/cli.ts --format esm --outDir dist", {
348
+ stdio: "inherit",
349
+ cwd: process.cwd()
350
+ });
351
+ console.log("Build complete. Start with: tsproxy start");
352
+ } catch {
353
+ console.error("Build failed.");
354
+ process.exit(1);
355
+ }
356
+ }
357
+
358
+ // src/commands/seed.ts
359
+ import { readFileSync } from "fs";
360
+ import { resolve as resolve4 } from "path";
361
+ import pc2 from "picocolors";
362
+ async function seed(file, opts) {
363
+ const proxyUrl = process.env.PROXY_URL || "http://localhost:3000";
364
+ const apiKey = process.env.PROXY_API_KEY || "change-me";
365
+ const collection = opts.collection || "products";
366
+ if (!file) {
367
+ console.error("Usage: tsproxy seed <file.json> --collection <name>");
368
+ console.error(" File should be a JSON array of documents.");
369
+ process.exit(1);
370
+ }
371
+ const filePath = resolve4(process.cwd(), file);
372
+ let documents;
373
+ try {
374
+ const content = readFileSync(filePath, "utf-8");
375
+ if (content.trim().startsWith("[")) {
376
+ documents = JSON.parse(content);
377
+ } else {
378
+ documents = content.trim().split("\n").map((line) => JSON.parse(line));
379
+ }
380
+ } catch (err) {
381
+ console.error(`Failed to read ${filePath}:`, err.message);
382
+ process.exit(1);
383
+ }
384
+ console.log(
385
+ `Seeding ${pc2.bold(String(documents.length))} documents into ${pc2.bold(collection)} via ingest API...`
386
+ );
387
+ const url = `${proxyUrl}/api/ingest/${collection}/documents/import`;
388
+ const headers = {
389
+ "Content-Type": "application/json",
390
+ "X-API-Key": apiKey
391
+ };
392
+ if (opts.locale) {
393
+ headers["X-Locale"] = opts.locale;
394
+ }
395
+ try {
396
+ const res = await fetch(url, {
397
+ method: "POST",
398
+ headers,
399
+ body: JSON.stringify(documents)
400
+ });
401
+ if (!res.ok) {
402
+ const body = await res.json().catch(() => ({}));
403
+ console.error(
404
+ pc2.red(`Failed: ${res.status} ${body.error || res.statusText}`)
405
+ );
406
+ process.exit(1);
407
+ }
408
+ const result = await res.json();
409
+ const results = Array.isArray(result) ? result : [result];
410
+ const successes = results.filter((r) => r.success !== false).length;
411
+ const failures = results.length - successes;
412
+ console.log(
413
+ pc2.green(`Imported ${successes} documents`) + (failures > 0 ? pc2.red(` (${failures} failures)`) : "")
414
+ );
415
+ if (opts.locales) {
416
+ console.log("\nTo seed locale-specific collections, run:");
417
+ console.log(
418
+ ` tsproxy seed ${file} --collection ${collection} --locale en`
419
+ );
420
+ console.log(
421
+ ` tsproxy seed ${file} --collection ${collection} --locale fr`
422
+ );
423
+ }
424
+ } catch (err) {
425
+ console.error(pc2.red("Connection failed:"), err.message);
426
+ console.error("Is the proxy running? Start with: tsproxy dev");
427
+ process.exit(1);
428
+ }
429
+ }
430
+
431
+ // src/commands/migrate.ts
432
+ import { resolve as resolve5 } from "path";
433
+ import { existsSync as existsSync4 } from "fs";
434
+ import { pathToFileURL } from "url";
435
+ import pc3 from "picocolors";
436
+ async function migrate(opts) {
437
+ const configPath = findConfig();
438
+ if (!configPath) {
439
+ console.error("No tsproxy.config.ts found. Run: tsproxy init");
440
+ process.exit(1);
441
+ }
442
+ let config;
443
+ try {
444
+ const mod = await import(pathToFileURL(configPath).href);
445
+ config = mod.default || mod;
446
+ } catch (err) {
447
+ console.error("Failed to load config:", err.message);
448
+ process.exit(1);
449
+ }
450
+ const collections = config.collections || {};
451
+ const collectionNames = Object.keys(collections);
452
+ if (collectionNames.length === 0) {
453
+ console.log("No collections defined in config.");
454
+ return;
455
+ }
456
+ const tsHost = config.typesense?.host || process.env.TYPESENSE_HOST || "localhost";
457
+ const tsPort = config.typesense?.port || process.env.TYPESENSE_PORT || 8108;
458
+ const tsProtocol = config.typesense?.protocol || "http";
459
+ const tsApiKey = config.typesense?.apiKey || process.env.TYPESENSE_API_KEY;
460
+ if (!tsApiKey) {
461
+ console.error("TYPESENSE_API_KEY not set.");
462
+ process.exit(1);
463
+ }
464
+ const baseUrl = `${tsProtocol}://${tsHost}:${tsPort}`;
465
+ let existing = {};
466
+ try {
467
+ const res = await fetch(`${baseUrl}/collections`, {
468
+ headers: { "X-TYPESENSE-API-KEY": tsApiKey }
469
+ });
470
+ const cols = await res.json();
471
+ for (const col of cols) {
472
+ existing[col.name] = col;
473
+ }
474
+ } catch (err) {
475
+ console.error("Cannot connect to Typesense:", err.message);
476
+ process.exit(1);
477
+ }
478
+ console.log(pc3.bold("\nMigration plan:\n"));
479
+ const actions = [];
480
+ for (const [name, def] of Object.entries(collections)) {
481
+ const names = [name];
482
+ if (def.locales?.length) {
483
+ for (const locale of def.locales) {
484
+ names.push(`${name}_${locale}`);
485
+ }
486
+ }
487
+ for (const colName of names) {
488
+ if (existing[colName]) {
489
+ if (opts.drop) {
490
+ actions.push({
491
+ type: "drop+create",
492
+ name: colName,
493
+ details: `Drop and recreate with ${Object.keys(def.fields || {}).length} fields`
494
+ });
495
+ } else {
496
+ const existingFields = new Set(
497
+ (existing[colName].fields || []).map((f) => f.name)
498
+ );
499
+ const configFields = Object.keys(def.fields || {}).filter(
500
+ (f) => !def.fields[f].compute
501
+ );
502
+ const newFields = configFields.filter((f) => !existingFields.has(f));
503
+ if (newFields.length > 0) {
504
+ actions.push({
505
+ type: "update",
506
+ name: colName,
507
+ details: `Add fields: ${newFields.join(", ")}`
508
+ });
509
+ } else {
510
+ actions.push({
511
+ type: "ok",
512
+ name: colName,
513
+ details: "Up to date"
514
+ });
515
+ }
516
+ }
517
+ } else {
518
+ actions.push({
519
+ type: "create",
520
+ name: colName,
521
+ details: `Create with ${Object.keys(def.fields || {}).length} fields`
522
+ });
523
+ }
524
+ }
525
+ }
526
+ for (const action of actions) {
527
+ const icon = action.type === "ok" ? pc3.green("\u2713") : action.type === "create" ? pc3.cyan("+") : action.type === "update" ? pc3.yellow("~") : pc3.red("!");
528
+ console.log(` ${icon} ${pc3.bold(action.name)} \u2014 ${action.details}`);
529
+ }
530
+ const pendingActions = actions.filter((a) => a.type !== "ok");
531
+ if (pendingActions.length === 0) {
532
+ console.log(pc3.green("\nAll collections are up to date."));
533
+ return;
534
+ }
535
+ if (!opts.apply) {
536
+ console.log(
537
+ pc3.yellow(`
538
+ Dry run \u2014 ${pendingActions.length} change(s). Run with --apply to execute.`)
539
+ );
540
+ return;
541
+ }
542
+ console.log(pc3.bold("\nApplying...\n"));
543
+ for (const action of pendingActions) {
544
+ const colName = action.name;
545
+ const baseName = colName.includes("_") ? colName.split("_").slice(0, -1).join("_") : colName;
546
+ const def = collections[baseName] || collections[colName];
547
+ if (!def) continue;
548
+ const schema = buildSchema(colName, def);
549
+ try {
550
+ if (action.type === "drop+create") {
551
+ await fetch(`${baseUrl}/collections/${colName}`, {
552
+ method: "DELETE",
553
+ headers: { "X-TYPESENSE-API-KEY": tsApiKey }
554
+ });
555
+ await fetch(`${baseUrl}/collections`, {
556
+ method: "POST",
557
+ headers: {
558
+ "Content-Type": "application/json",
559
+ "X-TYPESENSE-API-KEY": tsApiKey
560
+ },
561
+ body: JSON.stringify(schema)
562
+ });
563
+ console.log(` ${pc3.green("\u2713")} ${colName} \u2014 dropped and recreated`);
564
+ } else if (action.type === "create") {
565
+ await fetch(`${baseUrl}/collections`, {
566
+ method: "POST",
567
+ headers: {
568
+ "Content-Type": "application/json",
569
+ "X-TYPESENSE-API-KEY": tsApiKey
570
+ },
571
+ body: JSON.stringify(schema)
572
+ });
573
+ console.log(` ${pc3.green("\u2713")} ${colName} \u2014 created`);
574
+ } else if (action.type === "update") {
575
+ const existingFields = new Set(
576
+ (existing[colName].fields || []).map((f) => f.name)
577
+ );
578
+ const newFields = Object.entries(def.fields || {}).filter(([name, f]) => !existingFields.has(name) && !f.compute).map(([name, f]) => ({
579
+ name,
580
+ type: f.type,
581
+ ...f.facet ? { facet: true } : {},
582
+ ...f.optional ? { optional: true } : {}
583
+ }));
584
+ await fetch(`${baseUrl}/collections/${colName}`, {
585
+ method: "PATCH",
586
+ headers: {
587
+ "Content-Type": "application/json",
588
+ "X-TYPESENSE-API-KEY": tsApiKey
589
+ },
590
+ body: JSON.stringify({ fields: newFields })
591
+ });
592
+ console.log(` ${pc3.green("\u2713")} ${colName} \u2014 updated`);
593
+ }
594
+ } catch (err) {
595
+ console.error(
596
+ ` ${pc3.red("\u2717")} ${colName} \u2014 ${err.message}`
597
+ );
598
+ }
599
+ }
600
+ console.log(pc3.green("\nMigration complete."));
601
+ }
602
+ function buildSchema(name, def) {
603
+ const fields = Object.entries(def.fields || {}).filter(([, f]) => !f.compute).map(([fieldName, f]) => ({
604
+ name: fieldName,
605
+ type: f.type,
606
+ ...f.facet ? { facet: true } : {},
607
+ ...f.optional ? { optional: true } : {},
608
+ ...f.sortable && !["int32", "int64", "float", "bool"].includes(f.type) ? { sort: true } : {},
609
+ ...f.infix ? { infix: true } : {}
610
+ }));
611
+ return {
612
+ name,
613
+ fields,
614
+ ...def.defaultSortBy ? { default_sorting_field: def.defaultSortBy } : {}
615
+ };
616
+ }
617
+ function findConfig() {
618
+ const names = ["tsproxy.config.ts", "tsproxy.config.js", "tsproxy.config.mjs"];
619
+ let dir = process.cwd();
620
+ const root = resolve5("/");
621
+ while (dir !== root) {
622
+ for (const name of names) {
623
+ const p2 = resolve5(dir, name);
624
+ if (existsSync4(p2)) return p2;
625
+ }
626
+ dir = resolve5(dir, "..");
627
+ }
628
+ return null;
629
+ }
630
+
631
+ // src/commands/health.ts
632
+ import pc4 from "picocolors";
633
+ async function health() {
634
+ const proxyUrl = process.env.PROXY_URL || "http://localhost:3000";
635
+ console.log(pc4.bold("\nHealth check\n"));
636
+ try {
637
+ const res = await fetch(`${proxyUrl}/api/health`);
638
+ const data = await res.json();
639
+ const proxyOk = data.proxy?.status === "ok";
640
+ const tsOk = data.typesense?.status === "ok";
641
+ const redisStatus = data.redis?.status;
642
+ const redisOk = redisStatus === "ok" || redisStatus === "not_configured";
643
+ console.log(
644
+ ` ${proxyOk ? pc4.green("\u2713") : pc4.red("\u2717")} Proxy ${proxyOk ? "ok" : "error"}`
645
+ );
646
+ console.log(
647
+ ` ${tsOk ? pc4.green("\u2713") : pc4.red("\u2717")} Typesense ${tsOk ? "ok" : data.typesense?.error || "error"} ${pc4.dim(data.typesense?.host || "")}`
648
+ );
649
+ console.log(
650
+ ` ${redisStatus === "ok" ? pc4.green("\u2713") : redisStatus === "not_configured" ? pc4.dim("-") : pc4.red("\u2717")} Redis ${redisStatus} ${pc4.dim(data.redis?.host || "")}`
651
+ );
652
+ console.log(
653
+ `
654
+ Status: ${data.status === "healthy" ? pc4.green("healthy") : pc4.red("degraded")}`
655
+ );
656
+ } catch (err) {
657
+ console.error(
658
+ ` ${pc4.red("\u2717")} Cannot connect to proxy at ${proxyUrl}`
659
+ );
660
+ console.error(` ${err.message}`);
661
+ console.error("\n Is the proxy running? Start with: tsproxy dev");
662
+ process.exit(1);
663
+ }
664
+ }
665
+
666
+ // src/index.ts
667
+ var program = new Command();
668
+ program.name("tsproxy").description("Typesense search proxy framework").version("0.1.0");
669
+ program.command("init").description("Initialize a new tsproxy project").action(init);
670
+ program.command("dev").description("Start the proxy in development mode").option("-p, --port <port>", "Port to listen on").option("-c, --config <path>", "Path to config file").action(dev);
671
+ program.command("start").description("Start the proxy in production mode").option("-p, --port <port>", "Port to listen on").option("-c, --config <path>", "Path to config file").action(start);
672
+ program.command("build").description("Build the proxy for production").action(build);
673
+ program.command("seed [file]").description("Seed Typesense with data from a JSON/JSONL file").option("--collection <name>", "Collection name").option("--locale <locale>", "Locale for the collection").option("--locales", "Create locale-specific collections").action(seed);
674
+ program.command("migrate").description("Sync Typesense schema with config").option("--apply", "Apply changes (default is dry-run)").option("--drop", "Drop and recreate collections").action(migrate);
675
+ program.command("health").description("Check Typesense and Redis connectivity").action(health);
676
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@tsproxy/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI for tsproxy — Typesense search proxy framework",
5
+ "type": "module",
6
+ "bin": {
7
+ "tsproxy": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/akshitkrnagpal/tsproxy.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "typesense",
23
+ "search",
24
+ "proxy",
25
+ "cli"
26
+ ],
27
+ "dependencies": {
28
+ "@clack/prompts": "^0.10.0",
29
+ "commander": "^13.1.0",
30
+ "picocolors": "^1.1.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20",
34
+ "tsup": "^8.4.0",
35
+ "tsx": "^4.19.0",
36
+ "typescript": "^5"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup src/index.ts --format esm --target node22",
40
+ "dev": "tsx src/index.ts"
41
+ }
42
+ }