@tsproxy/cli 0.0.2 → 0.0.3

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 +200 -4
  2. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -310,8 +310,8 @@ function findCliEntry() {
310
310
  resolve2(process.cwd(), "node_modules/@tsproxy/api/dist/cli.js"),
311
311
  resolve2(process.cwd(), "node_modules/@tsproxy/api/src/cli.ts")
312
312
  ];
313
- for (const p2 of paths) {
314
- if (existsSync2(p2)) return p2;
313
+ for (const p3 of paths) {
314
+ if (existsSync2(p3)) return p3;
315
315
  }
316
316
  return null;
317
317
  }
@@ -597,6 +597,72 @@ Dry run \u2014 ${pendingActions.length} change(s). Run with --apply to execute.`
597
597
  );
598
598
  }
599
599
  }
600
+ for (const [name, def] of Object.entries(collections)) {
601
+ const names = [name];
602
+ if (def.locales?.length) {
603
+ for (const locale of def.locales) {
604
+ names.push(`${name}_${locale}`);
605
+ }
606
+ }
607
+ for (const colName of names) {
608
+ if (def.synonyms) {
609
+ for (const [synName, synDef] of Object.entries(def.synonyms)) {
610
+ try {
611
+ const body = {};
612
+ if (synDef.synonyms) {
613
+ body.synonyms = synDef.synonyms;
614
+ } else if (synDef.root && synDef.words) {
615
+ body.root = synDef.root;
616
+ body.synonyms = synDef.words;
617
+ }
618
+ await fetch(`${baseUrl}/collections/${colName}/synonyms/${synName}`, {
619
+ method: "PUT",
620
+ headers: {
621
+ "Content-Type": "application/json",
622
+ "X-TYPESENSE-API-KEY": tsApiKey
623
+ },
624
+ body: JSON.stringify(body)
625
+ });
626
+ console.log(` ${pc3.green("\u2713")} ${colName}/synonyms/${synName}`);
627
+ } catch (err) {
628
+ console.error(` ${pc3.red("\u2717")} ${colName}/synonyms/${synName} \u2014 ${err.message}`);
629
+ }
630
+ }
631
+ }
632
+ if (def.curations) {
633
+ for (const [curName, curDef] of Object.entries(def.curations)) {
634
+ try {
635
+ const body = {
636
+ rule: {
637
+ query: curDef.query,
638
+ match: curDef.match || "exact"
639
+ }
640
+ };
641
+ if (curDef.pinnedIds?.length) {
642
+ body.includes = curDef.pinnedIds.map((id, i) => ({
643
+ id,
644
+ position: i + 1
645
+ }));
646
+ }
647
+ if (curDef.hiddenIds?.length) {
648
+ body.excludes = curDef.hiddenIds.map((id) => ({ id }));
649
+ }
650
+ await fetch(`${baseUrl}/collections/${colName}/overrides/${curName}`, {
651
+ method: "PUT",
652
+ headers: {
653
+ "Content-Type": "application/json",
654
+ "X-TYPESENSE-API-KEY": tsApiKey
655
+ },
656
+ body: JSON.stringify(body)
657
+ });
658
+ console.log(` ${pc3.green("\u2713")} ${colName}/overrides/${curName}`);
659
+ } catch (err) {
660
+ console.error(` ${pc3.red("\u2717")} ${colName}/overrides/${curName} \u2014 ${err.message}`);
661
+ }
662
+ }
663
+ }
664
+ }
665
+ }
600
666
  console.log(pc3.green("\nMigration complete."));
601
667
  }
602
668
  function buildSchema(name, def) {
@@ -620,8 +686,8 @@ function findConfig() {
620
686
  const root = resolve5("/");
621
687
  while (dir !== root) {
622
688
  for (const name of names) {
623
- const p2 = resolve5(dir, name);
624
- if (existsSync4(p2)) return p2;
689
+ const p3 = resolve5(dir, name);
690
+ if (existsSync4(p3)) return p3;
625
691
  }
626
692
  dir = resolve5(dir, "..");
627
693
  }
@@ -663,6 +729,135 @@ async function health() {
663
729
  }
664
730
  }
665
731
 
732
+ // src/commands/generate.ts
733
+ import * as p2 from "@clack/prompts";
734
+ import pc5 from "picocolors";
735
+ import { writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
736
+ import { resolve as resolve6 } from "path";
737
+ async function generate(opts) {
738
+ p2.intro(pc5.bgCyan(pc5.black(" tsproxy generate ")));
739
+ const tsHost = opts.host || process.env.TYPESENSE_HOST || "localhost";
740
+ const tsPort = opts.port || process.env.TYPESENSE_PORT || "8108";
741
+ const tsApiKey = opts.key || process.env.TYPESENSE_API_KEY;
742
+ if (!tsApiKey) {
743
+ p2.log.error("Typesense API key is required. Set TYPESENSE_API_KEY or pass --key.");
744
+ process.exit(1);
745
+ }
746
+ const s = p2.spinner();
747
+ s.start("Fetching collections from Typesense");
748
+ let collections;
749
+ try {
750
+ const res = await fetch(`http://${tsHost}:${tsPort}/collections`, {
751
+ headers: { "X-TYPESENSE-API-KEY": tsApiKey }
752
+ });
753
+ if (!res.ok) {
754
+ throw new Error(`${res.status} ${res.statusText}`);
755
+ }
756
+ collections = await res.json();
757
+ } catch (err) {
758
+ s.stop("Failed to connect");
759
+ p2.log.error(`Cannot connect to Typesense at ${tsHost}:${tsPort}`);
760
+ p2.log.error(err.message);
761
+ process.exit(1);
762
+ }
763
+ s.stop(`Found ${collections.length} collection(s)`);
764
+ if (collections.length === 0) {
765
+ p2.log.warn("No collections found. Create some collections first.");
766
+ process.exit(0);
767
+ }
768
+ const selected = await p2.multiselect({
769
+ message: "Which collections to include?",
770
+ options: collections.map((col) => ({
771
+ value: col.name,
772
+ label: `${col.name} (${col.num_documents} docs, ${col.fields.length} fields)`
773
+ })),
774
+ required: true
775
+ });
776
+ if (p2.isCancel(selected)) return process.exit(0);
777
+ const TYPE_MAP = {
778
+ string: "string",
779
+ "string[]": "string[]",
780
+ int32: "int32",
781
+ "int32[]": "int32[]",
782
+ int64: "int64",
783
+ "int64[]": "int64[]",
784
+ float: "float",
785
+ "float[]": "float[]",
786
+ bool: "bool",
787
+ "bool[]": "bool[]",
788
+ geopoint: "geopoint",
789
+ "geopoint[]": "geopoint[]",
790
+ auto: "auto",
791
+ object: "object",
792
+ "object[]": "object[]"
793
+ };
794
+ const collectionConfigs = [];
795
+ for (const colName of selected) {
796
+ const col = collections.find((c) => c.name === colName);
797
+ if (!col) continue;
798
+ const fieldLines = [];
799
+ for (const field of col.fields) {
800
+ if (field.name === ".*") continue;
801
+ const type = TYPE_MAP[field.type] || "string";
802
+ const props = [`type: "${type}"`];
803
+ if ((field.type === "string" || field.type === "string[]") && field.index !== false) {
804
+ props.push("searchable: true");
805
+ }
806
+ if (field.facet) props.push("facet: true");
807
+ if (field.sort) props.push("sortable: true");
808
+ if (field.optional) props.push("optional: true");
809
+ if (field.infix) props.push("infix: true");
810
+ fieldLines.push(` ${field.name}: { ${props.join(", ")} },`);
811
+ }
812
+ const sortField = col.default_sorting_field ? `
813
+ defaultSortBy: "${col.default_sorting_field}",` : "";
814
+ collectionConfigs.push(` ${colName}: {
815
+ fields: {
816
+ ${fieldLines.join("\n")}
817
+ },${sortField}
818
+ },`);
819
+ }
820
+ const configContent = `import { defineConfig } from "@tsproxy/api";
821
+
822
+ export default defineConfig({
823
+ typesense: {
824
+ host: "${tsHost}",
825
+ port: ${tsPort},
826
+ protocol: "http",
827
+ apiKey: process.env.TYPESENSE_API_KEY || "${tsApiKey}",
828
+ },
829
+
830
+ server: {
831
+ port: 3000,
832
+ apiKey: process.env.PROXY_API_KEY || "change-me",
833
+ },
834
+
835
+ cache: { ttl: 60, maxSize: 1000 },
836
+ queue: { concurrency: 5, maxSize: 10000 },
837
+ rateLimit: { search: 100, ingest: 30 },
838
+
839
+ collections: {
840
+ ${collectionConfigs.join("\n\n")}
841
+ },
842
+ });
843
+ `;
844
+ const outputPath = resolve6(process.cwd(), opts.output || "tsproxy.config.ts");
845
+ if (existsSync5(outputPath)) {
846
+ const overwrite = await p2.confirm({
847
+ message: `${opts.output || "tsproxy.config.ts"} already exists. Overwrite?`,
848
+ initialValue: false
849
+ });
850
+ if (p2.isCancel(overwrite) || !overwrite) {
851
+ p2.log.info("Generated config:");
852
+ console.log(configContent);
853
+ p2.outro("Config printed to stdout (file not written).");
854
+ return;
855
+ }
856
+ }
857
+ writeFileSync2(outputPath, configContent);
858
+ p2.outro(pc5.green(`Config written to ${opts.output || "tsproxy.config.ts"}`));
859
+ }
860
+
666
861
  // src/index.ts
667
862
  var program = new Command();
668
863
  program.name("tsproxy").description("Typesense search proxy framework").version("0.1.0");
@@ -673,4 +868,5 @@ program.command("build").description("Build the proxy for production").action(bu
673
868
  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
869
  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
870
  program.command("health").description("Check Typesense and Redis connectivity").action(health);
871
+ program.command("generate").description("Generate tsproxy.config.ts from existing Typesense schema").option("--host <host>", "Typesense host").option("--port <port>", "Typesense port").option("--key <key>", "Typesense API key").option("-o, --output <path>", "Output file path", "tsproxy.config.ts").action(generate);
676
872
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsproxy/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "CLI for tsproxy — Typesense search proxy framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,8 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "build": "tsup src/index.ts --format esm --target node22",
30
- "dev": "tsx src/index.ts"
30
+ "dev": "tsx src/index.ts",
31
+ "test": "vitest run"
31
32
  },
32
33
  "dependencies": {
33
34
  "@clack/prompts": "^0.10.0",
@@ -38,6 +39,7 @@
38
39
  "@types/node": "^20",
39
40
  "tsup": "^8.4.0",
40
41
  "tsx": "^4.19.0",
41
- "typescript": "^5"
42
+ "typescript": "^5",
43
+ "vitest": "^3.2.4"
42
44
  }
43
45
  }