@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.
- package/README.md +69 -0
- package/dist/index.js +259 -0
- 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
|
+
}
|