Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/fluffy-lines-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@mixedbread/cli": patch
---

Improved CLI I/O, concurrency, and code deduplication

- Routed log and spinner output to stderr to keep stdout clean for piped data
- Replaced internal `mergeCommandOptions` with Commander's built-in `optsWithGlobals()`
- Switched to sliding-window concurrency (`p-limit`) for file uploads
- Used async file I/O in upload and sync hot paths
- Extracted shared `checkExistingFiles` utility
- Fixed missing `process.exit(1)` in completion command error handlers
- Fixed spinners not being stopped in catch blocks for sync and upload commands
- Changed `formatBytes` to return `"-"` instead of `"0 B"` for undefined/NaN values
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.3",
"dotenv": "^17.3.1",
"glob": "^13.0.3",
"mime-types": "^3.0.2",
"minimatch": "^10.2.0",
Expand Down
35 changes: 17 additions & 18 deletions packages/cli/src/bin/mxbai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,19 @@ import { setupGlobalOptions } from "../utils/global-options";
import { checkForUpdates } from "../utils/update-checker";

// Find package.json relative to the compiled file location
// In the published package, from bin/mxbai.js, package.json is one level up
const VERSION_PATHS = [
join(__dirname, "..", "package.json"),
join(__dirname, "..", "..", "package.json"),
];
let version = "0.0.0";
try {
// First try one level up (for published package)
const packageJsonPath = join(__dirname, "..", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
version = packageJson.version;
} catch (_error) {
for (const pkgPath of VERSION_PATHS) {
try {
// Fallback to two levels up (for development/build environment)
const packageJsonPath = join(__dirname, "..", "..", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
version = packageJson.version;
} catch (_error2) {
// Final fallback if package.json is not found
console.warn(
"Warning: Could not read package.json for version information"
);
}
version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
break;
} catch {}
}
if (version === "0.0.0") {
console.warn("Warning: Could not read package.json for version information");
}

const program = new Command();
Expand Down Expand Up @@ -76,14 +70,19 @@ program.on("command:*", () => {
// Parse arguments
async function main() {
try {
await checkForUpdates(version);
const updateCheck = checkForUpdates(version);

// Show help if no arguments provided
if (process.argv.length === 2) {
program.help();
}

await program.parseAsync(process.argv);

const banner = await updateCheck;
if (banner) {
console.error(banner);
}
} catch (error) {
if (error instanceof Error) {
console.error(chalk.red("\n✗"), error.message);
Expand Down
35 changes: 20 additions & 15 deletions packages/cli/src/commands/completion.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import path from "node:path";
import { log as clackLog, spinner } from "@clack/prompts";
import {
getShellFromEnv,
install,
log,
parseEnv,
log as tabtabLog,
uninstall,
} from "@pnpm/tabtab";
import chalk from "chalk";
Expand All @@ -17,10 +16,9 @@ import {
import {
addGlobalOptions,
BaseGlobalOptionsSchema,
type GlobalOptions,
mergeCommandOptions,
parseOptions,
} from "../utils/global-options";
import { log, spinner } from "../utils/logger";

const SUPPORTED_SHELLS = ["bash", "zsh", "fish", "pwsh"] as const;
export type SupportedShell = (typeof SUPPORTED_SHELLS)[number];
Expand Down Expand Up @@ -141,6 +139,7 @@ export function createCompletionCommand(): Command {
}
} catch (error) {
console.error(chalk.red("✗"), "Error installing completion:", error);
process.exit(1);
}
});

Expand All @@ -158,6 +157,7 @@ export function createCompletionCommand(): Command {
);
} catch (error) {
console.error(chalk.red("✗"), "Error uninstalling completion:", error);
process.exit(1);
}
});

Expand All @@ -167,8 +167,8 @@ export function createCompletionCommand(): Command {
)
);

refreshCommand.action(async (options: GlobalOptions) => {
const mergedOptions = mergeCommandOptions(refreshCommand, options);
refreshCommand.action(async () => {
const mergedOptions = refreshCommand.optsWithGlobals();
const parsedOptions = parseOptions(BaseGlobalOptionsSchema, {
...mergedOptions,
});
Expand All @@ -180,11 +180,12 @@ export function createCompletionCommand(): Command {
refreshSpinner.stop("Completion cache refreshed successfully");
} catch (error) {
refreshSpinner.stop();
clackLog.error(
log.error(
error instanceof Error
? error.message
: "Failed to refresh completion cache"
);
process.exit(1);
}
});

Expand Down Expand Up @@ -215,7 +216,7 @@ export function createCompletionServerCommand(): Command {

// Root level completions - "mxbai " (when no previous command)
if (env.words === 1) {
return log(
return tabtabLog(
["config", "store", "completion", "--help", "--version"],
shell,
console.log
Expand All @@ -241,15 +242,15 @@ export function createCompletionServerCommand(): Command {
if (keyName) {
const stores = getStoresForCompletion(keyName);
if (stores.length > 0) {
return log(stores, shell, console.log);
return tabtabLog(stores, shell, console.log);
}
}
}
}

// Store completions
if (env.prev === "store") {
return log(
return tabtabLog(
[
"create",
"delete",
Expand All @@ -272,7 +273,7 @@ export function createCompletionServerCommand(): Command {
// Check if we're in "mxbai store files " context
const words = env.line.trim().split(/\s+/);
if (words.length >= 3 && words[1] === "store") {
return log(["list", "get", "delete"], shell, console.log);
return tabtabLog(["list", "get", "delete"], shell, console.log);
}
}

Expand All @@ -291,22 +292,22 @@ export function createCompletionServerCommand(): Command {
if (keyName) {
const stores = getStoresForCompletion(keyName);
if (stores.length > 0) {
return log(stores, shell, console.log);
return tabtabLog(stores, shell, console.log);
}
}
}
}

// Config completions
if (env.prev === "config") {
return log(["get", "set", "keys"], shell, console.log);
return tabtabLog(["get", "set", "keys"], shell, console.log);
}

if (env.prev === "keys") {
// Check if we're in "mxbai config keys " context
const words = env.line.trim().split(/\s+/);
if (words.length >= 3 && words[1] === "config") {
return log(
return tabtabLog(
["list", "add", "remove", "set-default"],
shell,
console.log
Expand All @@ -316,7 +317,11 @@ export function createCompletionServerCommand(): Command {

// Completion completions
if (env.prev === "completion") {
return log(["install", "uninstall", "refresh"], shell, console.log);
return tabtabLog(
["install", "uninstall", "refresh"],
shell,
console.log
);
}
});

Expand Down
13 changes: 6 additions & 7 deletions packages/cli/src/commands/config/keys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cancel, confirm, isCancel, log, text } from "@clack/prompts";
import { cancel, confirm, isCancel, text } from "@clack/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { z } from "zod";
Expand All @@ -16,10 +16,9 @@ import {
import {
addGlobalOptions,
BaseGlobalOptionsSchema,
type GlobalOptions,
mergeCommandOptions,
parseOptions,
} from "../../utils/global-options";
import { log } from "../../utils/logger";

const RemoveKeySchema = z.object({
name: z.string().min(1, { error: '"name" is required' }),
Expand All @@ -34,8 +33,8 @@ export function createKeysCommand(): Command {
keysCommand
.command("add <key> [name]")
.description("Add a new API key")
.action(async (key: string, name?: string, options?: GlobalOptions) => {
const mergedOptions = mergeCommandOptions(keysCommand, options);
.action(async (key: string, name?: string) => {
const mergedOptions = keysCommand.optsWithGlobals();
const parsedOptions = parseOptions(BaseGlobalOptionsSchema, {
...mergedOptions,
});
Expand Down Expand Up @@ -185,8 +184,8 @@ export function createKeysCommand(): Command {
keysCommand
.command("set-default <name>")
.description("Set the default API key")
.action(async (name: string, options: GlobalOptions) => {
const mergedOptions = mergeCommandOptions(keysCommand, options);
.action(async (name: string) => {
const mergedOptions = keysCommand.optsWithGlobals();
const parsedOptions = parseOptions(BaseGlobalOptionsSchema, {
...mergedOptions,
});
Expand Down
16 changes: 3 additions & 13 deletions packages/cli/src/commands/store/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { log, spinner } from "@clack/prompts";
import { Command } from "commander";
import { z } from "zod";
import { createClient } from "../../utils/client";
Expand All @@ -9,10 +8,9 @@ import {
import {
addGlobalOptions,
extendGlobalOptions,
type GlobalOptions,
mergeCommandOptions,
parseOptions,
} from "../../utils/global-options";
import { log, spinner } from "../../utils/logger";
import { validateMetadata } from "../../utils/metadata";
import { formatOutput } from "../../utils/output";
import { buildStoreConfig, parsePublicFlag } from "../../utils/store";
Expand All @@ -30,14 +28,6 @@ const CreateStoreSchema = extendGlobalOptions({
metadata: z.string().optional(),
});

interface CreateOptions extends GlobalOptions {
description?: string;
public?: boolean | string;
contextualization?: boolean | string;
expiresAfter?: number;
metadata?: string;
}

export function createCreateCommand(): Command {
const command = addGlobalOptions(
new Command("create")
Expand All @@ -56,11 +46,11 @@ export function createCreateCommand(): Command {
.option("--metadata <json>", "Additional metadata as JSON string")
);

command.action(async (name: string, options: CreateOptions) => {
command.action(async (name: string) => {
const createSpinner = spinner();

try {
const mergedOptions = mergeCommandOptions(command, options);
const mergedOptions = command.optsWithGlobals();
const client = createClient(mergedOptions);

const parsedOptions = parseOptions(CreateStoreSchema, {
Expand Down
14 changes: 4 additions & 10 deletions packages/cli/src/commands/store/delete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { confirm, isCancel, log, spinner } from "@clack/prompts";
import { confirm, isCancel } from "@clack/prompts";
import { Command } from "commander";
import { z } from "zod";
import { createClient } from "../../utils/client";
Expand All @@ -9,21 +9,16 @@ import {
import {
addGlobalOptions,
extendGlobalOptions,
type GlobalOptions,
mergeCommandOptions,
parseOptions,
} from "../../utils/global-options";
import { log, spinner } from "../../utils/logger";
import { resolveStore } from "../../utils/store";

const DeleteStoreSchema = extendGlobalOptions({
nameOrId: z.string().min(1, { error: '"name-or-id" is required' }),
yes: z.boolean().optional(),
});

interface DeleteOptions extends GlobalOptions {
yes?: boolean;
}

export function createDeleteCommand(): Command {
const command = addGlobalOptions(
new Command("delete")
Expand All @@ -33,11 +28,10 @@ export function createDeleteCommand(): Command {
.option("-y, --yes", "Skip confirmation prompt")
);

command.action(async (nameOrId: string, options: DeleteOptions) => {
command.action(async (nameOrId: string) => {
const deleteSpinner = spinner();

try {
const mergedOptions = mergeCommandOptions(command, options);
const mergedOptions = command.optsWithGlobals();

const parsedOptions = parseOptions(DeleteStoreSchema, {
...mergedOptions,
Expand Down
Loading