@headband/cli 0.1.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 (3) hide show
  1. package/README.md +69 -0
  2. package/dist/index.js +259 -0
  3. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @headband/cli
2
+
3
+ Interactive CLI for setting up [Headband](https://headband.dev) search in any project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @headband/cli init
9
+ ```
10
+
11
+ This will:
12
+
13
+ 1. Ask for your Headband URL (default: `https://headband.dev`)
14
+ 2. Ask for your search API key (`hb_src_...` or `hb_adm_...`)
15
+ 3. Ask for your default search index (e.g. `products`, `movies`)
16
+ 4. Detect your package manager (npm, pnpm, yarn, or bun)
17
+ 5. Install `@headband/react`
18
+ 6. Create `headband.config.ts` with your settings
19
+ 7. Create an example `<HeadbandSearch />` component
20
+
21
+ ## Options
22
+
23
+ ```bash
24
+ npx @headband/cli init --yes # Skip prompts, use defaults
25
+ npx @headband/cli --help # Show help
26
+ ```
27
+
28
+ ## What gets created
29
+
30
+ **`headband.config.ts`** -- Search configuration that reads from `NEXT_PUBLIC_HEADBAND_KEY` env var with a fallback:
31
+
32
+ ```typescript
33
+ export const headbandConfig = {
34
+ url: "https://headband.dev",
35
+ searchKey: process.env.NEXT_PUBLIC_HEADBAND_KEY || "hb_src_...",
36
+ defaultIndex: "products",
37
+ };
38
+ ```
39
+
40
+ **`components/headband-search.tsx`** (or `src/components/` if your project uses a `src/` directory) -- A ready-to-use search component:
41
+
42
+ ```tsx
43
+ "use client";
44
+
45
+ import {
46
+ headband,
47
+ HeadbandProvider,
48
+ SearchBox,
49
+ Hits,
50
+ Stats,
51
+ } from "@headband/react";
52
+ import { headbandConfig } from "../headband.config";
53
+
54
+ const client = headband(headbandConfig.url, headbandConfig.searchKey);
55
+
56
+ export function HeadbandSearch() {
57
+ return (
58
+ <HeadbandProvider client={client} index={headbandConfig.defaultIndex}>
59
+ <SearchBox placeholder={`Search products...`} />
60
+ <Stats />
61
+ <Hits />
62
+ </HeadbandProvider>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## License
68
+
69
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/init.ts
27
+ var import_prompts = __toESM(require("prompts"));
28
+ var import_picocolors = __toESM(require("picocolors"));
29
+ var import_path2 = require("path");
30
+
31
+ // src/utils.ts
32
+ var import_fs = require("fs");
33
+ var import_child_process = require("child_process");
34
+ var import_path = require("path");
35
+ function detectPackageManager(cwd) {
36
+ if ((0, import_fs.existsSync)((0, import_path.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
37
+ if ((0, import_fs.existsSync)((0, import_path.join)(cwd, "yarn.lock"))) return "yarn";
38
+ if ((0, import_fs.existsSync)((0, import_path.join)(cwd, "bun.lockb"))) return "bun";
39
+ return "npm";
40
+ }
41
+ function getInstallCommand(pm, pkg) {
42
+ switch (pm) {
43
+ case "pnpm":
44
+ return `pnpm add ${pkg}`;
45
+ case "yarn":
46
+ return `yarn add ${pkg}`;
47
+ case "bun":
48
+ return `bun add ${pkg}`;
49
+ default:
50
+ return `npm install ${pkg}`;
51
+ }
52
+ }
53
+ function runCommand(command2, cwd) {
54
+ (0, import_child_process.execSync)(command2, { cwd, stdio: "inherit" });
55
+ }
56
+ function hasSrcDir(cwd) {
57
+ return (0, import_fs.existsSync)((0, import_path.join)(cwd, "src"));
58
+ }
59
+ function writeFileSafe(filePath, content) {
60
+ const dir = (0, import_path.dirname)(filePath);
61
+ if (!(0, import_fs.existsSync)(dir)) {
62
+ (0, import_fs.mkdirSync)(dir, { recursive: true });
63
+ }
64
+ (0, import_fs.writeFileSync)(filePath, content, "utf-8");
65
+ }
66
+ function generateConfig(opts) {
67
+ return `export const headbandConfig = {
68
+ url: "${opts.url}",
69
+ searchKey: process.env.NEXT_PUBLIC_HEADBAND_KEY || "${opts.searchKey}",
70
+ defaultIndex: "${opts.defaultIndex}",
71
+ };
72
+ `;
73
+ }
74
+ function generateSearchComponent(opts) {
75
+ return `"use client";
76
+
77
+ import {
78
+ headband,
79
+ HeadbandProvider,
80
+ SearchBox,
81
+ Hits,
82
+ Stats,
83
+ } from "@headband/react";
84
+ import { headbandConfig } from "${opts.configImportPath}";
85
+
86
+ const client = headband(headbandConfig.url, headbandConfig.searchKey);
87
+
88
+ export function HeadbandSearch() {
89
+ return (
90
+ <HeadbandProvider client={client} index={headbandConfig.defaultIndex}>
91
+ <SearchBox placeholder={\`Search ${opts.defaultIndex}...\`} />
92
+ <Stats />
93
+ <Hits />
94
+ </HeadbandProvider>
95
+ );
96
+ }
97
+ `;
98
+ }
99
+
100
+ // src/init.ts
101
+ async function init(options = {}) {
102
+ const cwd = process.cwd();
103
+ console.log();
104
+ console.log(import_picocolors.default.bold(import_picocolors.default.cyan(" Setting up Headband search...")));
105
+ console.log();
106
+ const onCancel = () => {
107
+ console.log();
108
+ console.log(import_picocolors.default.yellow("Setup cancelled."));
109
+ process.exit(0);
110
+ };
111
+ let url = "https://headband.dev";
112
+ let searchKey = "";
113
+ let defaultIndex = "products";
114
+ if (options.yes) {
115
+ console.log(import_picocolors.default.dim(" Using defaults (--yes flag)"));
116
+ searchKey = "hb_src_your_key_here";
117
+ } else {
118
+ const answers = await (0, import_prompts.default)(
119
+ [
120
+ {
121
+ type: "text",
122
+ name: "url",
123
+ message: "Headband URL",
124
+ initial: "https://headband.dev",
125
+ validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "Must be a valid URL (http:// or https://)"
126
+ },
127
+ {
128
+ type: "text",
129
+ name: "searchKey",
130
+ message: "Paste your search API key",
131
+ hint: "hb_src_... or hb_adm_...",
132
+ validate: (v) => {
133
+ if (!v) return "API key is required";
134
+ if (!v.startsWith("hb_src_") && !v.startsWith("hb_adm_")) {
135
+ return "Key must start with hb_src_ or hb_adm_";
136
+ }
137
+ return true;
138
+ }
139
+ },
140
+ {
141
+ type: "text",
142
+ name: "defaultIndex",
143
+ message: "Which index to search?",
144
+ initial: "products",
145
+ hint: "e.g. products, movies, articles",
146
+ validate: (v) => v.trim() ? true : "Index name is required"
147
+ }
148
+ ],
149
+ { onCancel }
150
+ );
151
+ url = answers.url;
152
+ searchKey = answers.searchKey;
153
+ defaultIndex = answers.defaultIndex;
154
+ }
155
+ const pm = detectPackageManager(cwd);
156
+ console.log();
157
+ console.log(
158
+ import_picocolors.default.dim(` Detected package manager: ${import_picocolors.default.bold(pm)}`)
159
+ );
160
+ console.log();
161
+ console.log(import_picocolors.default.cyan(" Installing @headband/react..."));
162
+ console.log();
163
+ try {
164
+ const installCmd = getInstallCommand(pm, "@headband/react");
165
+ runCommand(installCmd, cwd);
166
+ } catch {
167
+ console.log();
168
+ console.log(
169
+ import_picocolors.default.yellow(
170
+ " Could not install @headband/react automatically."
171
+ )
172
+ );
173
+ console.log(
174
+ import_picocolors.default.dim(
175
+ ` Run manually: ${getInstallCommand(pm, "@headband/react")}`
176
+ )
177
+ );
178
+ }
179
+ const useSrc = hasSrcDir(cwd);
180
+ const componentsDir = useSrc ? (0, import_path2.join)(cwd, "src", "components") : (0, import_path2.join)(cwd, "components");
181
+ const configImportPath = useSrc ? "../headband.config" : "../headband.config";
182
+ const configPath = (0, import_path2.join)(cwd, "headband.config.ts");
183
+ const configContent = generateConfig({ url, searchKey, defaultIndex });
184
+ writeFileSafe(configPath, configContent);
185
+ const componentPath = (0, import_path2.join)(componentsDir, "headband-search.tsx");
186
+ const componentContent = generateSearchComponent({
187
+ defaultIndex,
188
+ configImportPath
189
+ });
190
+ writeFileSafe(componentPath, componentContent);
191
+ console.log();
192
+ console.log(import_picocolors.default.green(import_picocolors.default.bold(" Headband is ready!")));
193
+ console.log();
194
+ console.log(import_picocolors.default.dim(" Created files:"));
195
+ console.log(` ${import_picocolors.default.cyan("headband.config.ts")} \u2014 search configuration`);
196
+ console.log(
197
+ ` ${import_picocolors.default.cyan(
198
+ useSrc ? "src/components/headband-search.tsx" : "components/headband-search.tsx"
199
+ )} \u2014 example search component`
200
+ );
201
+ console.log();
202
+ console.log(import_picocolors.default.dim(" Recommended: add your API key to .env.local:"));
203
+ console.log();
204
+ console.log(
205
+ import_picocolors.default.yellow(` NEXT_PUBLIC_HEADBAND_KEY=${searchKey}`)
206
+ );
207
+ console.log();
208
+ console.log(import_picocolors.default.dim(" Next steps:"));
209
+ console.log(` 1. Import ${import_picocolors.default.cyan("<HeadbandSearch />")} into a page`);
210
+ console.log(
211
+ ` 2. Move your API key to ${import_picocolors.default.cyan(".env.local")} for safety`
212
+ );
213
+ console.log(
214
+ ` 3. Check the docs at ${import_picocolors.default.cyan("https://headband.dev/docs")}`
215
+ );
216
+ console.log();
217
+ }
218
+
219
+ // src/index.ts
220
+ var args = process.argv.slice(2);
221
+ var flags = new Set(args.filter((a) => a.startsWith("-")));
222
+ var positional = args.filter((a) => !a.startsWith("-"));
223
+ var command = positional[0];
224
+ function printHelp() {
225
+ console.log(`
226
+ @headband/cli \u2014 Set up Headband search in your project
227
+
228
+ Usage:
229
+ npx @headband/cli init [options]
230
+
231
+ Commands:
232
+ init Set up Headband in the current project
233
+
234
+ Options:
235
+ --yes Skip prompts and use defaults
236
+ --help Show this help message
237
+
238
+ Examples:
239
+ npx @headband/cli init
240
+ npx @headband/cli init --yes
241
+ `);
242
+ }
243
+ async function main() {
244
+ if (flags.has("--help") || flags.has("-h") || command === "help") {
245
+ printHelp();
246
+ process.exit(0);
247
+ }
248
+ if (!command || command === "init") {
249
+ await init({ yes: flags.has("--yes") || flags.has("-y") });
250
+ } else {
251
+ console.error(`Unknown command: ${command}`);
252
+ printHelp();
253
+ process.exit(1);
254
+ }
255
+ }
256
+ main().catch((err) => {
257
+ console.error(err);
258
+ process.exit(1);
259
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@headband/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for setting up Headband search in any project",
5
+ "bin": {
6
+ "headband": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch"
15
+ },
16
+ "dependencies": {
17
+ "picocolors": "^1.1.1",
18
+ "prompts": "^2.4.2"
19
+ },
20
+ "devDependencies": {
21
+ "@types/prompts": "^2.4.9",
22
+ "tsup": "^8.5.1",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "keywords": [
29
+ "headband",
30
+ "search",
31
+ "cli",
32
+ "setup",
33
+ "meilisearch",
34
+ "elasticsearch",
35
+ "typesense"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/zvndev/headband",
40
+ "directory": "packages/cli"
41
+ },
42
+ "homepage": "https://headband.dev",
43
+ "license": "MIT"
44
+ }