Files
claude-mem/src/npx-cli/commands/runtime.ts
T
Alex Newman 85eb796b18 feat: add npx CLI entry point with install, runtime, and IDE detection commands
Replaces the old git-clone installer with a direct npm package copy workflow.
Supports 13 IDE auto-detection targets, runtime delegation to Bun worker,
and pure Node.js install path (no Bun required for installation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 23:02:18 -07:00

185 lines
5.4 KiB
TypeScript

/**
* Runtime command routing for `npx claude-mem start|stop|restart|status|search|transcript`.
*
* These commands delegate to the installed plugin's worker-service.cjs via Bun,
* or hit the worker's HTTP API directly (for `search`).
*
* Pure Node.js — no Bun APIs used.
*/
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import pc from 'picocolors';
import { resolveBunBinaryPath } from '../utils/bun-resolver.js';
import { isPluginInstalled, marketplaceDirectory } from '../utils/paths.js';
// ---------------------------------------------------------------------------
// Installation guard
// ---------------------------------------------------------------------------
function ensureInstalledOrExit(): void {
if (!isPluginInstalled()) {
console.error(pc.red('claude-mem is not installed.'));
console.error(`Run: ${pc.bold('npx claude-mem install')}`);
process.exit(1);
}
}
// ---------------------------------------------------------------------------
// Bun guard
// ---------------------------------------------------------------------------
function resolveBunOrExit(): string {
const bunPath = resolveBunBinaryPath();
if (!bunPath) {
console.error(pc.red('Bun not found.'));
console.error('Install Bun: https://bun.sh');
console.error('After installation, restart your terminal.');
process.exit(1);
}
return bunPath;
}
// ---------------------------------------------------------------------------
// Worker-service path
// ---------------------------------------------------------------------------
function workerServiceScriptPath(): string {
return join(marketplaceDirectory(), 'plugin', 'scripts', 'worker-service.cjs');
}
// ---------------------------------------------------------------------------
// Spawn helper
// ---------------------------------------------------------------------------
function spawnBunWorkerCommand(command: string, extraArgs: string[] = []): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const workerScript = workerServiceScriptPath();
if (!existsSync(workerScript)) {
console.error(pc.red(`Worker script not found at: ${workerScript}`));
console.error('The installation may be corrupted. Try: npx claude-mem install');
process.exit(1);
}
const args = [workerScript, command, ...extraArgs];
const child = spawn(bunPath, args, {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start Bun: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function runStartCommand(): void {
spawnBunWorkerCommand('start');
}
export function runStopCommand(): void {
spawnBunWorkerCommand('stop');
}
export function runRestartCommand(): void {
spawnBunWorkerCommand('restart');
}
export function runStatusCommand(): void {
spawnBunWorkerCommand('status');
}
/**
* Search the worker API at `GET /api/search?q=<query>`.
*/
export async function runSearchCommand(queryParts: string[]): Promise<void> {
ensureInstalledOrExit();
const query = queryParts.join(' ').trim();
if (!query) {
console.error(pc.red('Usage: npx claude-mem search <query>'));
process.exit(1);
}
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
const searchUrl = `http://127.0.0.1:${workerPort}/api/search?q=${encodeURIComponent(query)}`;
try {
const response = await fetch(searchUrl);
if (!response.ok) {
if (response.status === 404) {
console.error(pc.red('Search endpoint not found. Is the worker running?'));
console.error(`Try: ${pc.bold('npx claude-mem start')}`);
process.exit(1);
}
console.error(pc.red(`Search failed: HTTP ${response.status}`));
process.exit(1);
}
const data = await response.json();
if (typeof data === 'object' && data !== null) {
console.log(JSON.stringify(data, null, 2));
} else {
console.log(data);
}
} catch (error: any) {
if (error?.cause?.code === 'ECONNREFUSED' || error?.message?.includes('ECONNREFUSED')) {
console.error(pc.red('Worker is not running.'));
console.error(`Start it with: ${pc.bold('npx claude-mem start')}`);
process.exit(1);
}
console.error(pc.red(`Search failed: ${error.message}`));
process.exit(1);
}
}
/**
* Start the transcript watcher via Bun.
*/
export function runTranscriptWatchCommand(): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const transcriptWatcherPath = join(
marketplaceDirectory(),
'plugin',
'scripts',
'transcript-watcher.cjs',
);
if (!existsSync(transcriptWatcherPath)) {
// Fall back to worker-service with transcript subcommand
spawnBunWorkerCommand('transcript', ['watch']);
return;
}
const child = spawn(bunPath, [transcriptWatcherPath, 'watch'], {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start transcript watcher: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}