@bulkgrid/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.
@@ -0,0 +1,351 @@
1
+ // src/init.ts
2
+ import { spawnSync } from "child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, join } from "path";
6
+ import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
7
+ var DEFAULT_SERVER_NAME = "bulkgrid";
8
+ var API_KEY_ENV_VAR = "BULKGRID_API_KEY";
9
+ var MCP_URL_ENV_VAR = "BULKGRID_MCP_URL";
10
+ var DASHBOARD_API_KEYS_URL = "https://bulkgrid.com/dashboard/settings/api-keys";
11
+ var AGENT_CHOICES = [
12
+ { label: "Cursor", value: "cursor" },
13
+ { label: "VS Code", value: "vscode" },
14
+ { label: "Claude Code", value: "claude" },
15
+ { label: "Codex", value: "codex" }
16
+ ];
17
+ function isJsonObject(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function parseJsonObject(filePath) {
21
+ if (!existsSync(filePath)) {
22
+ return {};
23
+ }
24
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
25
+ if (!isJsonObject(parsed)) {
26
+ throw new Error(`${filePath} must contain a JSON object`);
27
+ }
28
+ return parsed;
29
+ }
30
+ function writeJsonFile(filePath, data) {
31
+ mkdirSync(dirname(filePath), { recursive: true });
32
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
33
+ `);
34
+ }
35
+ function getObjectProperty(parent, key) {
36
+ const current = parent[key];
37
+ if (isJsonObject(current)) {
38
+ return current;
39
+ }
40
+ const next = {};
41
+ parent[key] = next;
42
+ return next;
43
+ }
44
+ function buildAuthorizationHeader(context, inputExpression) {
45
+ if (context.writeApiKey && context.apiKey) {
46
+ return `Bearer ${context.apiKey}`;
47
+ }
48
+ return `Bearer ${inputExpression}`;
49
+ }
50
+ function buildCursorServer(context) {
51
+ return {
52
+ url: context.mcpUrl,
53
+ headers: {
54
+ Authorization: buildAuthorizationHeader(context, `\${env:${API_KEY_ENV_VAR}}`)
55
+ }
56
+ };
57
+ }
58
+ function mergeCursorConfig(existing, context) {
59
+ const next = { ...existing };
60
+ const servers = getObjectProperty(next, "mcpServers");
61
+ servers[DEFAULT_SERVER_NAME] = buildCursorServer(context);
62
+ return next;
63
+ }
64
+ function buildVsCodeConfig(context) {
65
+ return {
66
+ inputs: [
67
+ {
68
+ type: "promptString",
69
+ id: "bulkgrid-api-key",
70
+ description: "Bulkgrid API Key",
71
+ password: true
72
+ }
73
+ ],
74
+ servers: {
75
+ [DEFAULT_SERVER_NAME]: {
76
+ type: "http",
77
+ url: context.mcpUrl,
78
+ headers: {
79
+ Authorization: buildAuthorizationHeader(context, "${input:bulkgrid-api-key}")
80
+ }
81
+ }
82
+ }
83
+ };
84
+ }
85
+ function mergeVsCodeConfig(existing, context) {
86
+ const next = { ...existing };
87
+ const config = buildVsCodeConfig(context);
88
+ const existingServers = getObjectProperty(next, "servers");
89
+ const configServers = getObjectProperty(config, "servers");
90
+ existingServers[DEFAULT_SERVER_NAME] = configServers[DEFAULT_SERVER_NAME] ?? {};
91
+ if (!Array.isArray(next.inputs)) {
92
+ next.inputs = config.inputs ?? [];
93
+ return next;
94
+ }
95
+ const hasInput = next.inputs.some((item) => isJsonObject(item) && item.id === "bulkgrid-api-key");
96
+ if (!hasInput && Array.isArray(config.inputs)) {
97
+ next.inputs = [...next.inputs, ...config.inputs];
98
+ }
99
+ return next;
100
+ }
101
+ function resolveCursorPath(scope) {
102
+ if (scope === "global") {
103
+ return join(homedir(), ".cursor", "mcp.json");
104
+ }
105
+ return join(process.cwd(), ".cursor", "mcp.json");
106
+ }
107
+ function resolveVsCodePath(scope) {
108
+ if (scope === "global") {
109
+ if (process.platform === "darwin") {
110
+ return join(homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
111
+ }
112
+ if (process.platform === "win32") {
113
+ return join(homedir(), "AppData", "Roaming", "Code", "User", "mcp.json");
114
+ }
115
+ return join(homedir(), ".config", "Code", "User", "mcp.json");
116
+ }
117
+ return join(process.cwd(), ".vscode", "mcp.json");
118
+ }
119
+ function setupCursor(context) {
120
+ const filePath = resolveCursorPath(context.scope);
121
+ const config = mergeCursorConfig(parseJsonObject(filePath), context);
122
+ writeJsonFile(filePath, config);
123
+ return {
124
+ agent: "cursor",
125
+ status: "configured",
126
+ message: `Wrote ${filePath}`
127
+ };
128
+ }
129
+ function setupVsCode(context) {
130
+ const filePath = resolveVsCodePath(context.scope);
131
+ const config = mergeVsCodeConfig(parseJsonObject(filePath), context);
132
+ writeJsonFile(filePath, config);
133
+ return {
134
+ agent: "vscode",
135
+ status: "configured",
136
+ message: `Wrote ${filePath}`
137
+ };
138
+ }
139
+ function commandExists(command) {
140
+ const result = spawnSync(command, ["--version"], { encoding: "utf8", stdio: "ignore" });
141
+ return result.status === 0;
142
+ }
143
+ function runCommand(command, args) {
144
+ const result = spawnSync(command, args, { encoding: "utf8", stdio: "inherit" });
145
+ if (result.error) {
146
+ throw result.error;
147
+ }
148
+ if (result.status !== 0) {
149
+ throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`);
150
+ }
151
+ }
152
+ function printInitBanner() {
153
+ console.log("");
154
+ console.log(" \u25A6 Bulkgrid init");
155
+ console.log("");
156
+ }
157
+ async function promptForConfirmation(message, defaultValue) {
158
+ return confirm({
159
+ message,
160
+ default: defaultValue
161
+ });
162
+ }
163
+ async function promptForSelect(message, choices) {
164
+ return select({
165
+ message,
166
+ choices: choices.map((choice) => ({
167
+ name: choice.label,
168
+ value: choice.value
169
+ }))
170
+ });
171
+ }
172
+ async function promptForMultiSelect(message, choices) {
173
+ return checkbox({
174
+ message,
175
+ choices: choices.map((choice) => ({
176
+ name: choice.label,
177
+ value: choice.value,
178
+ checked: true
179
+ })),
180
+ required: true,
181
+ pageSize: choices.length
182
+ });
183
+ }
184
+ async function maybeInstallGlobally(options) {
185
+ if (options.installGlobal) {
186
+ runCommand("npm", ["install", "-g", "@bulkgrid/cli"]);
187
+ return;
188
+ }
189
+ if (options.yes) {
190
+ return;
191
+ }
192
+ const shouldInstall = await promptForConfirmation("Install @bulkgrid/cli globally?", false);
193
+ if (shouldInstall) {
194
+ runCommand("npm", ["install", "-g", "@bulkgrid/cli"]);
195
+ }
196
+ }
197
+ function openBrowser(url) {
198
+ let command;
199
+ let args;
200
+ if (process.platform === "darwin") {
201
+ command = "open";
202
+ args = [url];
203
+ } else if (process.platform === "win32") {
204
+ command = "cmd";
205
+ args = ["/c", "start", "", url];
206
+ } else {
207
+ command = "xdg-open";
208
+ args = [url];
209
+ }
210
+ const result = spawnSync(command, args, { encoding: "utf8", stdio: "ignore" });
211
+ if (result.status !== 0) {
212
+ console.log(`Open ${url} to create a Bulkgrid API key.`);
213
+ }
214
+ }
215
+ async function resolveApiKey(options) {
216
+ const configuredApiKey = options.apiKey ?? process.env[API_KEY_ENV_VAR];
217
+ if (configuredApiKey || options.yes) {
218
+ return configuredApiKey;
219
+ }
220
+ const authMode = options.auth ?? await promptForSelect("Authenticate to Bulkgrid", [
221
+ { label: "Open the dashboard and paste a new API key", value: "browser" },
222
+ { label: "Enter an existing API key", value: "manual" },
223
+ { label: "Skip this step", value: "skip" }
224
+ ]);
225
+ if (authMode === "skip") {
226
+ console.log(`Skipped. Set ${API_KEY_ENV_VAR} later or use client-side prompts.`);
227
+ return void 0;
228
+ }
229
+ if (authMode === "browser") {
230
+ openBrowser(DASHBOARD_API_KEYS_URL);
231
+ console.log("Create an API key with mcp:use and search:query scopes.");
232
+ }
233
+ const apiKey = await password({
234
+ message: "Bulkgrid API key",
235
+ mask: "*",
236
+ validate: (value) => value.trim().length > 0 || "Enter a Bulkgrid API key."
237
+ });
238
+ return apiKey || void 0;
239
+ }
240
+ function setupCodex(context) {
241
+ if (!commandExists("codex")) {
242
+ return {
243
+ agent: "codex",
244
+ status: "skipped",
245
+ message: "codex command was not found"
246
+ };
247
+ }
248
+ runCommand("codex", ["mcp", "add", DEFAULT_SERVER_NAME, "--url", context.mcpUrl, "--bearer-token-env-var", API_KEY_ENV_VAR]);
249
+ return {
250
+ agent: "codex",
251
+ status: "configured",
252
+ message: `Registered ${DEFAULT_SERVER_NAME} with codex mcp add`
253
+ };
254
+ }
255
+ function setupClaude(context) {
256
+ if (!commandExists("claude")) {
257
+ return {
258
+ agent: "claude",
259
+ status: "skipped",
260
+ message: "claude command was not found"
261
+ };
262
+ }
263
+ if (!context.apiKey) {
264
+ return {
265
+ agent: "claude",
266
+ status: "skipped",
267
+ message: `Set ${API_KEY_ENV_VAR} or pass --api-key to configure Claude Code`
268
+ };
269
+ }
270
+ const args = ["mcp", "add", "--transport", "http"];
271
+ if (context.scope === "global") {
272
+ args.push("--scope", "user");
273
+ }
274
+ args.push(DEFAULT_SERVER_NAME, context.mcpUrl, "--header", `Authorization: Bearer ${context.apiKey}`);
275
+ runCommand("claude", args);
276
+ return {
277
+ agent: "claude",
278
+ status: "configured",
279
+ message: `Registered ${DEFAULT_SERVER_NAME} with claude mcp add`
280
+ };
281
+ }
282
+ async function resolveAgents(options) {
283
+ if (options.all || !options.cursor && !options.vscode && !options.claude && !options.codex) {
284
+ if (!options.all && !options.yes) {
285
+ const shouldConfigure = await promptForConfirmation("Configure the Bulkgrid MCP server for clients?", true);
286
+ if (!shouldConfigure) {
287
+ return [];
288
+ }
289
+ return promptForMultiSelect("Choose MCP clients to configure:", AGENT_CHOICES);
290
+ }
291
+ return ["cursor", "vscode", "claude", "codex"];
292
+ }
293
+ const agents = [];
294
+ if (options.cursor) {
295
+ agents.push("cursor");
296
+ }
297
+ if (options.vscode) {
298
+ agents.push("vscode");
299
+ }
300
+ if (options.claude) {
301
+ agents.push("claude");
302
+ }
303
+ if (options.codex) {
304
+ agents.push("codex");
305
+ }
306
+ return agents;
307
+ }
308
+ async function promptForValue(message) {
309
+ return input({
310
+ message,
311
+ validate: (value) => value.trim().length > 0 || "Enter a value."
312
+ });
313
+ }
314
+ async function buildContext(options) {
315
+ const configuredUrl = options.mcpUrl ?? process.env[MCP_URL_ENV_VAR];
316
+ const mcpUrl = configuredUrl ?? (options.yes ? void 0 : await promptForValue("Bulkgrid MCP URL: "));
317
+ if (!mcpUrl) {
318
+ throw new Error(`Missing MCP URL. Pass --mcp-url or set ${MCP_URL_ENV_VAR}.`);
319
+ }
320
+ const apiKey = await resolveApiKey(options);
321
+ const agents = await resolveAgents(options);
322
+ return {
323
+ agents,
324
+ mcpUrl,
325
+ apiKey,
326
+ scope: options.global ? "global" : "project",
327
+ writeApiKey: options.writeApiKey ?? false
328
+ };
329
+ }
330
+ async function runInitCommand(options) {
331
+ printInitBanner();
332
+ await maybeInstallGlobally(options);
333
+ const context = await buildContext(options);
334
+ const results = [];
335
+ for (const agent of context.agents) {
336
+ if (agent === "cursor") {
337
+ results.push(setupCursor(context));
338
+ } else if (agent === "vscode") {
339
+ results.push(setupVsCode(context));
340
+ } else if (agent === "claude") {
341
+ results.push(setupClaude(context));
342
+ } else if (agent === "codex") {
343
+ results.push(setupCodex(context));
344
+ }
345
+ }
346
+ return results;
347
+ }
348
+
349
+ export {
350
+ runInitCommand
351
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runInitCommand
4
+ } from "./chunk-OUGLFLZG.js";
5
+
6
+ // src/cli.ts
7
+ import { Command } from "commander";
8
+ var program = new Command();
9
+ program.name("bulkgrid").description("Bulkgrid developer tooling").version("0.1.0");
10
+ program.command("init").description("Configure the Bulkgrid MCP server").option("--all", "configure all supported agents").option("--cursor", "configure Cursor").option("--vscode", "configure VS Code").option("--claude", "configure Claude Code").option("--codex", "configure Codex").option("--mcp-url <url>", "Bulkgrid MCP server URL").option("--api-key <key>", "Bulkgrid API key; defaults to BULKGRID_API_KEY").option("--global", "write global user configuration where supported").option("--project", "write project configuration where supported").option("--yes", "do not prompt for missing values").option("--write-api-key", "write the API key into generated JSON config files").option("--install-global", "install @bulkgrid/cli globally before configuring agents").option("--auth <mode>", "authentication mode: browser, manual, or skip").action(async (options) => {
11
+ try {
12
+ const results = await runInitCommand(options);
13
+ if (results.length === 0) {
14
+ console.log("No MCP clients configured.");
15
+ return;
16
+ }
17
+ for (const result of results) {
18
+ const marker = result.status === "configured" ? "configured" : "skipped";
19
+ console.log(`${result.agent}: ${marker} - ${result.message}`);
20
+ }
21
+ } catch (error) {
22
+ const message = error instanceof Error ? error.message : "Unknown error";
23
+ console.error(`bulkgrid init failed: ${message}`);
24
+ process.exitCode = 1;
25
+ }
26
+ });
27
+ program.parse();
@@ -0,0 +1,27 @@
1
+ type AgentName = 'cursor' | 'vscode' | 'claude' | 'codex';
2
+ type AuthMode = 'browser' | 'manual' | 'skip';
3
+ interface InitFlags {
4
+ readonly all?: boolean;
5
+ readonly cursor?: boolean;
6
+ readonly vscode?: boolean;
7
+ readonly claude?: boolean;
8
+ readonly codex?: boolean;
9
+ }
10
+ interface InitCommandOptions extends InitFlags {
11
+ readonly mcpUrl?: string;
12
+ readonly apiKey?: string;
13
+ readonly global?: boolean;
14
+ readonly project?: boolean;
15
+ readonly yes?: boolean;
16
+ readonly writeApiKey?: boolean;
17
+ readonly installGlobal?: boolean;
18
+ readonly auth?: AuthMode;
19
+ }
20
+ interface SetupResult {
21
+ readonly agent: AgentName;
22
+ readonly status: 'configured' | 'skipped';
23
+ readonly message: string;
24
+ }
25
+ declare function runInitCommand(options: InitCommandOptions): Promise<readonly SetupResult[]>;
26
+
27
+ export { type InitCommandOptions, runInitCommand };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ runInitCommand
3
+ } from "./chunk-OUGLFLZG.js";
4
+ export {
5
+ runInitCommand
6
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@bulkgrid/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "bin": {
13
+ "bulkgrid": "./dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "lint": "eslint .",
21
+ "check-types": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "format": "prettier --write \"src/**/*.{ts,js,md}\"",
25
+ "format:check": "prettier --check \"src/**/*.{ts,js,md}\""
26
+ },
27
+ "dependencies": {
28
+ "@inquirer/prompts": "7.10.1",
29
+ "commander": "14.0.3"
30
+ },
31
+ "devDependencies": {
32
+ "@repo/eslint-config": "*",
33
+ "@repo/typescript-config": "*",
34
+ "tsup": "8.5.1",
35
+ "vitest": "4.1.2"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }