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>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* IDE Auto-Detection
|
||||
*
|
||||
* Detects which AI coding IDEs / tools are installed on the system by
|
||||
* probing known config directories and checking for binaries in PATH.
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { IS_WINDOWS } from '../utils/paths.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IDE type and metadata
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IDEInfo {
|
||||
/** Machine-readable identifier. */
|
||||
id: string;
|
||||
/** Human-readable label for display in prompts. */
|
||||
label: string;
|
||||
/** Whether the IDE was detected on this system. */
|
||||
detected: boolean;
|
||||
/** Whether claude-mem has implemented setup for this IDE. */
|
||||
supported: boolean;
|
||||
/** Short hint text shown in the multi-select. */
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATH helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isCommandInPath(command: string): boolean {
|
||||
try {
|
||||
const whichCommand = IS_WINDOWS ? 'where' : 'which';
|
||||
execSync(`${whichCommand} ${command}`, { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VS Code extension directory scanner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function hasVscodeExtension(extensionNameFragment: string): boolean {
|
||||
const extensionsDirectory = join(homedir(), '.vscode', 'extensions');
|
||||
if (!existsSync(extensionsDirectory)) return false;
|
||||
try {
|
||||
const entries = readdirSync(extensionsDirectory);
|
||||
return entries.some((entry) => entry.toLowerCase().includes(extensionNameFragment.toLowerCase()));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detection map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect all known IDEs and return an array of `IDEInfo` objects.
|
||||
* Each entry indicates whether the IDE was found and whether claude-mem
|
||||
* currently supports setting it up.
|
||||
*/
|
||||
export function detectInstalledIDEs(): IDEInfo[] {
|
||||
const home = homedir();
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'claude-code',
|
||||
label: 'Claude Code',
|
||||
detected: existsSync(join(home, '.claude')),
|
||||
supported: true,
|
||||
hint: 'recommended',
|
||||
},
|
||||
{
|
||||
id: 'gemini-cli',
|
||||
label: 'Gemini CLI',
|
||||
detected: existsSync(join(home, '.gemini')),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
label: 'OpenCode',
|
||||
detected:
|
||||
existsSync(join(home, '.config', 'opencode')) || isCommandInPath('opencode'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'openclaw',
|
||||
label: 'OpenClaw',
|
||||
detected: existsSync(join(home, '.openclaw')),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'windsurf',
|
||||
label: 'Windsurf',
|
||||
detected: existsSync(join(home, '.codeium', 'windsurf')),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'codex-cli',
|
||||
label: 'Codex CLI',
|
||||
detected: existsSync(join(home, '.codex')),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
label: 'Cursor',
|
||||
detected: existsSync(join(home, '.cursor')),
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
id: 'copilot-cli',
|
||||
label: 'Copilot CLI',
|
||||
detected: isCommandInPath('copilot'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'antigravity',
|
||||
label: 'Antigravity',
|
||||
detected: existsSync(join(home, '.gemini', 'antigravity')),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'goose',
|
||||
label: 'Goose',
|
||||
detected:
|
||||
existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'crush',
|
||||
label: 'Crush',
|
||||
detected: isCommandInPath('crush'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'roo-code',
|
||||
label: 'Roo Code',
|
||||
detected: hasVscodeExtension('roo-code'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
{
|
||||
id: 'warp',
|
||||
label: 'Warp',
|
||||
detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'),
|
||||
supported: false,
|
||||
hint: 'coming soon',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the IDEs that were detected on this system.
|
||||
*/
|
||||
export function getDetectedIDEs(): IDEInfo[] {
|
||||
return detectInstalledIDEs().filter((ide) => ide.detected);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* Install command for `npx claude-mem install`.
|
||||
*
|
||||
* Replaces the git-clone + build workflow. The npm package already ships
|
||||
* a pre-built `plugin/` directory; this command copies it into the right
|
||||
* locations and registers it with Claude Code.
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
*/
|
||||
import * as p from '@clack/prompts';
|
||||
import pc from 'picocolors';
|
||||
import { execSync } from 'child_process';
|
||||
import { cpSync, existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
claudeSettingsPath,
|
||||
ensureDirectoryExists,
|
||||
installedPluginsPath,
|
||||
IS_WINDOWS,
|
||||
knownMarketplacesPath,
|
||||
marketplaceDirectory,
|
||||
npmPackagePluginDirectory,
|
||||
npmPackageRootDirectory,
|
||||
pluginCacheDirectory,
|
||||
pluginsDirectory,
|
||||
readJsonFileSafe,
|
||||
readPluginVersion,
|
||||
writeJsonFileAtomic,
|
||||
} from '../utils/paths.js';
|
||||
import { detectInstalledIDEs } from './ide-detection.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration helpers (mirror installer/src/steps/install.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function registerMarketplace(): void {
|
||||
const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath());
|
||||
|
||||
knownMarketplaces['thedotmack'] = {
|
||||
source: {
|
||||
source: 'github',
|
||||
repo: 'thedotmack/claude-mem',
|
||||
},
|
||||
installLocation: marketplaceDirectory(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
autoUpdate: true,
|
||||
};
|
||||
|
||||
ensureDirectoryExists(pluginsDirectory());
|
||||
writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces);
|
||||
}
|
||||
|
||||
function registerPlugin(version: string): void {
|
||||
const installedPlugins = readJsonFileSafe(installedPluginsPath());
|
||||
|
||||
if (!installedPlugins.version) installedPlugins.version = 2;
|
||||
if (!installedPlugins.plugins) installedPlugins.plugins = {};
|
||||
|
||||
const cachePath = pluginCacheDirectory(version);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
installedPlugins.plugins['claude-mem@thedotmack'] = [
|
||||
{
|
||||
scope: 'user',
|
||||
installPath: cachePath,
|
||||
version,
|
||||
installedAt: now,
|
||||
lastUpdated: now,
|
||||
},
|
||||
];
|
||||
|
||||
writeJsonFileAtomic(installedPluginsPath(), installedPlugins);
|
||||
}
|
||||
|
||||
function enablePluginInClaudeSettings(): void {
|
||||
const settings = readJsonFileSafe(claudeSettingsPath());
|
||||
|
||||
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
||||
settings.enabledPlugins['claude-mem@thedotmack'] = true;
|
||||
|
||||
writeJsonFileAtomic(claudeSettingsPath(), settings);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IDE setup dispatcher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function setupIDEs(selectedIDEs: string[]): void {
|
||||
for (const ideId of selectedIDEs) {
|
||||
switch (ideId) {
|
||||
case 'claude-code':
|
||||
// Claude Code picks up the plugin via marketplace registration — nothing
|
||||
// else to do beyond what registerMarketplace / registerPlugin already did.
|
||||
p.log.success('Claude Code: plugin registered via marketplace.');
|
||||
break;
|
||||
|
||||
case 'cursor':
|
||||
p.log.info('Cursor: hook configuration available after first launch.');
|
||||
p.log.info(` Run: npx claude-mem cursor-setup (coming soon)`);
|
||||
break;
|
||||
|
||||
default: {
|
||||
const allIDEs = detectInstalledIDEs();
|
||||
const ide = allIDEs.find((i) => i.id === ideId);
|
||||
if (ide && !ide.supported) {
|
||||
p.log.warn(`Support for ${ide.label} coming soon.`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interactive IDE selection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function promptForIDESelection(): Promise<string[]> {
|
||||
const detectedIDEs = detectInstalledIDEs();
|
||||
const detected = detectedIDEs.filter((ide) => ide.detected);
|
||||
|
||||
if (detected.length === 0) {
|
||||
p.log.warn('No supported IDEs detected. Installing for Claude Code by default.');
|
||||
return ['claude-code'];
|
||||
}
|
||||
|
||||
const options = detected.map((ide) => ({
|
||||
value: ide.id,
|
||||
label: ide.label,
|
||||
hint: ide.supported ? ide.hint : 'coming soon',
|
||||
}));
|
||||
|
||||
const result = await p.multiselect({
|
||||
message: 'Which IDEs do you use?',
|
||||
options,
|
||||
initialValues: detected
|
||||
.filter((ide) => ide.supported)
|
||||
.map((ide) => ide.id),
|
||||
required: true,
|
||||
});
|
||||
|
||||
if (p.isCancel(result)) {
|
||||
p.cancel('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return result as string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core copy logic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function copyPluginToMarketplace(): void {
|
||||
const marketplaceDir = marketplaceDirectory();
|
||||
|
||||
ensureDirectoryExists(marketplaceDir);
|
||||
|
||||
// Copy the entire npm package (not just plugin/) so that package.json,
|
||||
// node_modules, and scripts are all present in the marketplace dir.
|
||||
const packageRoot = npmPackageRootDirectory();
|
||||
cpSync(packageRoot, marketplaceDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
filter: (source) => {
|
||||
// Skip .git and other unnecessary directories
|
||||
if (source.includes('.git') && !source.includes('.claude-plugin')) return false;
|
||||
if (source.endsWith('.tgz')) return false;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function copyPluginToCache(version: string): void {
|
||||
const sourcePluginDirectory = npmPackagePluginDirectory();
|
||||
const cachePath = pluginCacheDirectory(version);
|
||||
|
||||
ensureDirectoryExists(cachePath);
|
||||
cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// npm install in marketplace dir
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runNpmInstallInMarketplace(): void {
|
||||
const marketplaceDir = marketplaceDirectory();
|
||||
const packageJsonPath = join(marketplaceDir, 'package.json');
|
||||
|
||||
if (!existsSync(packageJsonPath)) return;
|
||||
|
||||
execSync('npm install --production', {
|
||||
cwd: marketplaceDir,
|
||||
stdio: 'pipe',
|
||||
...(IS_WINDOWS ? { shell: true as const } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trigger smart-install for Bun / uv
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runSmartInstall(): void {
|
||||
const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js');
|
||||
|
||||
if (!existsSync(smartInstallPath)) {
|
||||
p.log.warn('smart-install.js not found — skipping Bun/uv auto-install.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`node "${smartInstallPath}"`, {
|
||||
stdio: 'inherit',
|
||||
...(IS_WINDOWS ? { shell: true as const } : {}),
|
||||
});
|
||||
} catch {
|
||||
p.log.warn('smart-install encountered an issue. You may need to install Bun/uv manually.');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface InstallOptions {
|
||||
/** When provided, skip the interactive IDE multi-select and use this IDE. */
|
||||
ide?: string;
|
||||
}
|
||||
|
||||
export async function runInstallCommand(options: InstallOptions = {}): Promise<void> {
|
||||
const version = readPluginVersion();
|
||||
|
||||
p.intro(pc.bgCyan(pc.black(' claude-mem install ')));
|
||||
p.log.info(`Version: ${pc.cyan(version)}`);
|
||||
p.log.info(`Platform: ${process.platform} (${process.arch})`);
|
||||
|
||||
// Check for existing installation
|
||||
const marketplaceDir = marketplaceDirectory();
|
||||
const alreadyInstalled = existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'));
|
||||
|
||||
if (alreadyInstalled) {
|
||||
// Read existing version
|
||||
try {
|
||||
const existingPluginJson = JSON.parse(
|
||||
readFileSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'), 'utf-8'),
|
||||
);
|
||||
p.log.warn(`Existing installation detected (v${existingPluginJson.version ?? 'unknown'}).`);
|
||||
} catch {
|
||||
p.log.warn('Existing installation detected.');
|
||||
}
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
const shouldContinue = await p.confirm({
|
||||
message: 'Overwrite existing installation?',
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
||||
p.cancel('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IDE selection
|
||||
let selectedIDEs: string[];
|
||||
if (options.ide) {
|
||||
selectedIDEs = [options.ide];
|
||||
const allIDEs = detectInstalledIDEs();
|
||||
const match = allIDEs.find((i) => i.id === options.ide);
|
||||
if (match && !match.supported) {
|
||||
p.log.error(`Support for ${match.label} coming soon.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!match) {
|
||||
p.log.error(`Unknown IDE: ${options.ide}`);
|
||||
p.log.info(`Available IDEs: ${allIDEs.map((i) => i.id).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (process.stdin.isTTY) {
|
||||
selectedIDEs = await promptForIDESelection();
|
||||
} else {
|
||||
// Non-interactive: default to claude-code
|
||||
selectedIDEs = ['claude-code'];
|
||||
}
|
||||
|
||||
// Run tasks
|
||||
await p.tasks([
|
||||
{
|
||||
title: 'Copying plugin files',
|
||||
task: async (message) => {
|
||||
message('Copying to marketplace directory...');
|
||||
copyPluginToMarketplace();
|
||||
return `Plugin files copied ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Caching plugin version',
|
||||
task: async (message) => {
|
||||
message(`Caching v${version}...`);
|
||||
copyPluginToCache(version);
|
||||
return `Plugin cached (v${version}) ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Registering marketplace',
|
||||
task: async () => {
|
||||
registerMarketplace();
|
||||
return `Marketplace registered ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Registering plugin',
|
||||
task: async () => {
|
||||
registerPlugin(version);
|
||||
return `Plugin registered ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Enabling plugin in Claude settings',
|
||||
task: async () => {
|
||||
enablePluginInClaudeSettings();
|
||||
return `Plugin enabled ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Installing dependencies',
|
||||
task: async (message) => {
|
||||
message('Running npm install...');
|
||||
try {
|
||||
runNpmInstallInMarketplace();
|
||||
return `Dependencies installed ${pc.green('OK')}`;
|
||||
} catch {
|
||||
return `Dependencies may need manual install ${pc.yellow('!')}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Setting up Bun and uv',
|
||||
task: async (message) => {
|
||||
message('Running smart-install...');
|
||||
try {
|
||||
runSmartInstall();
|
||||
return `Runtime dependencies ready ${pc.green('OK')}`;
|
||||
} catch {
|
||||
return `Runtime setup may need attention ${pc.yellow('!')}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// IDE-specific setup
|
||||
setupIDEs(selectedIDEs);
|
||||
|
||||
// Summary
|
||||
const summaryLines = [
|
||||
`Version: ${pc.cyan(version)}`,
|
||||
`Plugin dir: ${pc.cyan(marketplaceDir)}`,
|
||||
`IDEs: ${pc.cyan(selectedIDEs.join(', '))}`,
|
||||
];
|
||||
|
||||
p.note(summaryLines.join('\n'), 'Installation Complete');
|
||||
|
||||
const nextSteps = [
|
||||
'Open Claude Code and start a conversation -- memory is automatic!',
|
||||
`View your memories: ${pc.underline('http://localhost:37777')}`,
|
||||
`Search past work: use ${pc.bold('/mem-search')} in Claude Code`,
|
||||
`Start worker: ${pc.bold('npx claude-mem start')}`,
|
||||
];
|
||||
|
||||
p.note(nextSteps.join('\n'), 'Next Steps');
|
||||
|
||||
p.outro(pc.green('claude-mem installed successfully!'));
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Uninstall command for `npx claude-mem uninstall`.
|
||||
*
|
||||
* Removes the plugin from the marketplace directory, cache, plugin
|
||||
* registrations, and Claude settings. Optionally cleans up IDE-specific
|
||||
* configurations.
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
*/
|
||||
import * as p from '@clack/prompts';
|
||||
import pc from 'picocolors';
|
||||
import { existsSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
claudeSettingsPath,
|
||||
installedPluginsPath,
|
||||
isPluginInstalled,
|
||||
knownMarketplacesPath,
|
||||
marketplaceDirectory,
|
||||
pluginsDirectory,
|
||||
readJsonFileSafe,
|
||||
writeJsonFileAtomic,
|
||||
} from '../utils/paths.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cleanup helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function removeMarketplaceDirectory(): boolean {
|
||||
const marketplaceDir = marketplaceDirectory();
|
||||
if (existsSync(marketplaceDir)) {
|
||||
rmSync(marketplaceDir, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeCacheDirectory(): boolean {
|
||||
const cacheBaseDirectory = join(pluginsDirectory(), 'cache', 'thedotmack');
|
||||
if (existsSync(cacheBaseDirectory)) {
|
||||
rmSync(cacheBaseDirectory, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeFromKnownMarketplaces(): void {
|
||||
const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath());
|
||||
if (knownMarketplaces['thedotmack']) {
|
||||
delete knownMarketplaces['thedotmack'];
|
||||
writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromInstalledPlugins(): void {
|
||||
const installedPlugins = readJsonFileSafe(installedPluginsPath());
|
||||
if (installedPlugins.plugins?.['claude-mem@thedotmack']) {
|
||||
delete installedPlugins.plugins['claude-mem@thedotmack'];
|
||||
writeJsonFileAtomic(installedPluginsPath(), installedPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromClaudeSettings(): void {
|
||||
const settings = readJsonFileSafe(claudeSettingsPath());
|
||||
if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) {
|
||||
delete settings.enabledPlugins['claude-mem@thedotmack'];
|
||||
writeJsonFileAtomic(claudeSettingsPath(), settings);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function runUninstallCommand(): Promise<void> {
|
||||
p.intro(pc.bgRed(pc.white(' claude-mem uninstall ')));
|
||||
|
||||
if (!isPluginInstalled()) {
|
||||
p.log.warn('claude-mem does not appear to be installed.');
|
||||
|
||||
// Still offer to clean up partial state
|
||||
if (process.stdin.isTTY) {
|
||||
const shouldCleanup = await p.confirm({
|
||||
message: 'Clean up any remaining registration data anyway?',
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
if (p.isCancel(shouldCleanup) || !shouldCleanup) {
|
||||
p.outro('Nothing to do.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
p.outro('Nothing to do.');
|
||||
return;
|
||||
}
|
||||
} else if (process.stdin.isTTY) {
|
||||
const shouldContinue = await p.confirm({
|
||||
message: 'Are you sure you want to uninstall claude-mem?',
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
||||
p.cancel('Uninstall cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the worker first (best-effort)
|
||||
try {
|
||||
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
|
||||
await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, {
|
||||
method: 'POST',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
p.log.info('Worker service stopped.');
|
||||
} catch {
|
||||
// Worker may not be running — that is fine
|
||||
}
|
||||
|
||||
await p.tasks([
|
||||
{
|
||||
title: 'Removing marketplace directory',
|
||||
task: async () => {
|
||||
const removed = removeMarketplaceDirectory();
|
||||
return removed
|
||||
? `Marketplace directory removed ${pc.green('OK')}`
|
||||
: `Marketplace directory not found ${pc.dim('skipped')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Removing cache directory',
|
||||
task: async () => {
|
||||
const removed = removeCacheDirectory();
|
||||
return removed
|
||||
? `Cache directory removed ${pc.green('OK')}`
|
||||
: `Cache directory not found ${pc.dim('skipped')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Removing marketplace registration',
|
||||
task: async () => {
|
||||
removeFromKnownMarketplaces();
|
||||
return `Marketplace registration removed ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Removing plugin registration',
|
||||
task: async () => {
|
||||
removeFromInstalledPlugins();
|
||||
return `Plugin registration removed ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Removing from Claude settings',
|
||||
task: async () => {
|
||||
removeFromClaudeSettings();
|
||||
return `Claude settings updated ${pc.green('OK')}`;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
p.note(
|
||||
[
|
||||
`Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`,
|
||||
'To remove it manually: rm -rf ~/.claude-mem',
|
||||
].join('\n'),
|
||||
'Note',
|
||||
);
|
||||
|
||||
p.outro(pc.green('claude-mem has been uninstalled.'));
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* NPX CLI entry point for claude-mem.
|
||||
*
|
||||
* Usage:
|
||||
* npx claude-mem → interactive install
|
||||
* npx claude-mem install → interactive install
|
||||
* npx claude-mem install --ide <id> → direct IDE setup
|
||||
* npx claude-mem update → update to latest version
|
||||
* npx claude-mem uninstall → remove plugin and IDE configs
|
||||
* npx claude-mem version → print version
|
||||
* npx claude-mem start → start worker service
|
||||
* npx claude-mem stop → stop worker service
|
||||
* npx claude-mem restart → restart worker service
|
||||
* npx claude-mem status → show worker status
|
||||
* npx claude-mem search <query> → search observations
|
||||
* npx claude-mem transcript watch → start transcript watcher
|
||||
*
|
||||
* This file is pure Node.js — Bun is NOT required for install commands.
|
||||
* Runtime commands (`start`, `stop`, etc.) delegate to Bun via the installed plugin.
|
||||
*/
|
||||
import pc from 'picocolors';
|
||||
import { readPluginVersion } from './utils/paths.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Argument parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0]?.toLowerCase() ?? '';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Help text
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function printHelp(): void {
|
||||
const version = readPluginVersion();
|
||||
|
||||
console.log(`
|
||||
${pc.bold('claude-mem')} v${version} — persistent memory for AI coding assistants
|
||||
|
||||
${pc.bold('Install Commands')} (no Bun required):
|
||||
${pc.cyan('npx claude-mem')} Interactive install
|
||||
${pc.cyan('npx claude-mem install')} Interactive install
|
||||
${pc.cyan('npx claude-mem install --ide <id>')} Install for specific IDE
|
||||
${pc.cyan('npx claude-mem update')} Update to latest version
|
||||
${pc.cyan('npx claude-mem uninstall')} Remove plugin and configs
|
||||
${pc.cyan('npx claude-mem version')} Print version
|
||||
|
||||
${pc.bold('Runtime Commands')} (requires Bun, delegates to installed plugin):
|
||||
${pc.cyan('npx claude-mem start')} Start worker service
|
||||
${pc.cyan('npx claude-mem stop')} Stop worker service
|
||||
${pc.cyan('npx claude-mem restart')} Restart worker service
|
||||
${pc.cyan('npx claude-mem status')} Show worker status
|
||||
${pc.cyan('npx claude-mem search <query>')} Search observations
|
||||
${pc.cyan('npx claude-mem transcript watch')} Start transcript watcher
|
||||
|
||||
${pc.bold('IDE Identifiers')}:
|
||||
claude-code, cursor, gemini-cli, opencode, openclaw,
|
||||
windsurf, codex-cli, copilot-cli, antigravity, goose,
|
||||
crush, roo-code, warp
|
||||
`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command routing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main(): Promise<void> {
|
||||
switch (command) {
|
||||
// -- No command: default to install ------------------------------------
|
||||
case '': {
|
||||
const { runInstallCommand } = await import('./commands/install.js');
|
||||
await runInstallCommand();
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Install -----------------------------------------------------------
|
||||
case 'install': {
|
||||
const ideIndex = args.indexOf('--ide');
|
||||
const ideValue = ideIndex !== -1 ? args[ideIndex + 1] : undefined;
|
||||
|
||||
const { runInstallCommand } = await import('./commands/install.js');
|
||||
await runInstallCommand({ ide: ideValue });
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Update (alias for install — overwrite with latest) ----------------
|
||||
case 'update':
|
||||
case 'upgrade': {
|
||||
const { runInstallCommand } = await import('./commands/install.js');
|
||||
await runInstallCommand();
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Uninstall ---------------------------------------------------------
|
||||
case 'uninstall':
|
||||
case 'remove': {
|
||||
const { runUninstallCommand } = await import('./commands/uninstall.js');
|
||||
await runUninstallCommand();
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Version -----------------------------------------------------------
|
||||
case 'version':
|
||||
case '--version':
|
||||
case '-v': {
|
||||
console.log(readPluginVersion());
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Help --------------------------------------------------------------
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h': {
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Runtime: start / stop / restart / status --------------------------
|
||||
case 'start': {
|
||||
const { runStartCommand } = await import('./commands/runtime.js');
|
||||
runStartCommand();
|
||||
break;
|
||||
}
|
||||
case 'stop': {
|
||||
const { runStopCommand } = await import('./commands/runtime.js');
|
||||
runStopCommand();
|
||||
break;
|
||||
}
|
||||
case 'restart': {
|
||||
const { runRestartCommand } = await import('./commands/runtime.js');
|
||||
runRestartCommand();
|
||||
break;
|
||||
}
|
||||
case 'status': {
|
||||
const { runStatusCommand } = await import('./commands/runtime.js');
|
||||
runStatusCommand();
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Search ------------------------------------------------------------
|
||||
case 'search': {
|
||||
const { runSearchCommand } = await import('./commands/runtime.js');
|
||||
await runSearchCommand(args.slice(1));
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Transcript --------------------------------------------------------
|
||||
case 'transcript': {
|
||||
const subCommand = args[1]?.toLowerCase();
|
||||
if (subCommand === 'watch') {
|
||||
const { runTranscriptWatchCommand } = await import('./commands/runtime.js');
|
||||
runTranscriptWatchCommand();
|
||||
} else {
|
||||
console.error(pc.red(`Unknown transcript subcommand: ${subCommand ?? '(none)'}`));
|
||||
console.error(`Usage: npx claude-mem transcript watch`);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// -- Unknown -----------------------------------------------------------
|
||||
default: {
|
||||
console.error(pc.red(`Unknown command: ${command}`));
|
||||
console.error(`Run ${pc.bold('npx claude-mem --help')} for usage information.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(pc.red('Fatal error:'), error.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Bun binary resolution utility.
|
||||
*
|
||||
* Extracted from `plugin/scripts/bun-runner.js` so that the NPX CLI
|
||||
* can locate Bun without duplicating the search logic.
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
*/
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { IS_WINDOWS } from './paths.js';
|
||||
|
||||
/**
|
||||
* Well-known locations where Bun might be installed, beyond PATH.
|
||||
* Order matches the search priority in bun-runner.js and smart-install.js.
|
||||
*/
|
||||
function bunCandidatePaths(): string[] {
|
||||
if (IS_WINDOWS) {
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun'),
|
||||
'/usr/local/bin/bun',
|
||||
'/opt/homebrew/bin/bun',
|
||||
'/home/linuxbrew/.linuxbrew/bin/bun',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to locate the Bun executable.
|
||||
*
|
||||
* 1. Check PATH via `which` / `where`.
|
||||
* 2. Probe well-known installation directories.
|
||||
*
|
||||
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
|
||||
* or `null` if Bun cannot be found.
|
||||
*/
|
||||
export function resolveBunBinaryPath(): string | null {
|
||||
// Try PATH first
|
||||
const whichCommand = IS_WINDOWS ? 'where' : 'which';
|
||||
const pathCheck = spawnSync(whichCommand, ['bun'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
|
||||
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
||||
return 'bun'; // Available in PATH — use short name
|
||||
}
|
||||
|
||||
// Probe known install locations
|
||||
for (const candidatePath of bunCandidatePaths()) {
|
||||
if (existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
|
||||
* if Bun is not available.
|
||||
*/
|
||||
export function getBunVersionString(): string | null {
|
||||
const bunPath = resolveBunBinaryPath();
|
||||
if (!bunPath) return null;
|
||||
|
||||
try {
|
||||
const result = spawnSync(bunPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Shared path utilities for the NPX CLI.
|
||||
*
|
||||
* All platform-specific path logic is centralized here so that every command
|
||||
* resolves directories in exactly the same way, regardless of OS.
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Platform detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Root of the Claude Code config directory. */
|
||||
export function claudeConfigDirectory(): string {
|
||||
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
||||
}
|
||||
|
||||
/** Marketplace install directory for thedotmack. */
|
||||
export function marketplaceDirectory(): string {
|
||||
return join(claudeConfigDirectory(), 'plugins', 'marketplaces', 'thedotmack');
|
||||
}
|
||||
|
||||
/** Top-level plugins directory. */
|
||||
export function pluginsDirectory(): string {
|
||||
return join(claudeConfigDirectory(), 'plugins');
|
||||
}
|
||||
|
||||
/** Path to `known_marketplaces.json`. */
|
||||
export function knownMarketplacesPath(): string {
|
||||
return join(pluginsDirectory(), 'known_marketplaces.json');
|
||||
}
|
||||
|
||||
/** Path to `installed_plugins.json`. */
|
||||
export function installedPluginsPath(): string {
|
||||
return join(pluginsDirectory(), 'installed_plugins.json');
|
||||
}
|
||||
|
||||
/** Path to `~/.claude/settings.json`. */
|
||||
export function claudeSettingsPath(): string {
|
||||
return join(claudeConfigDirectory(), 'settings.json');
|
||||
}
|
||||
|
||||
/** Plugin cache directory for a specific version. */
|
||||
export function pluginCacheDirectory(version: string): string {
|
||||
return join(pluginsDirectory(), 'cache', 'thedotmack', 'claude-mem', version);
|
||||
}
|
||||
|
||||
/** claude-mem data directory (default `~/.claude-mem`). */
|
||||
export function claudeMemDataDirectory(): string {
|
||||
return join(homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NPM package root (where the NPX package lives on disk)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve the root of the installed npm package.
|
||||
*
|
||||
* The CLI entry point lives at `<pkg>/dist/cli/index.js`. Walking up
|
||||
* from `import.meta.url` (or `__dirname` equivalent) we reach the
|
||||
* package root where `plugin/` can be found.
|
||||
*/
|
||||
export function npmPackageRootDirectory(): string {
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
// <pkg>/dist/npx-cli/utils/paths.js -> up 3 levels -> <pkg>
|
||||
return join(dirname(currentFilePath), '..', '..', '..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the `plugin/` directory bundled inside the npm package.
|
||||
*/
|
||||
export function npmPackagePluginDirectory(): string {
|
||||
return join(npmPackageRootDirectory(), 'plugin');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Version helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read the current plugin version from the npm package's
|
||||
* `plugin/.claude-plugin/plugin.json` (preferred) or from `package.json`.
|
||||
*/
|
||||
export function readPluginVersion(): string {
|
||||
// Try plugin.json first (authoritative for plugin version)
|
||||
const pluginJsonPath = join(npmPackagePluginDirectory(), '.claude-plugin', 'plugin.json');
|
||||
if (existsSync(pluginJsonPath)) {
|
||||
try {
|
||||
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
|
||||
if (pluginJson.version) return pluginJson.version;
|
||||
} catch {
|
||||
// Fall through to package.json
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to package.json at package root
|
||||
const packageJsonPath = join(npmPackageRootDirectory(), 'package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
if (packageJson.version) return packageJson.version;
|
||||
} catch {
|
||||
// Unable to read
|
||||
}
|
||||
}
|
||||
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Installation detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Returns true if the plugin appears to be installed in the marketplace dir. */
|
||||
export function isPluginInstalled(): boolean {
|
||||
const marketplaceDir = marketplaceDirectory();
|
||||
return existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON file helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ensureDirectoryExists(directoryPath: string): void {
|
||||
if (!existsSync(directoryPath)) {
|
||||
mkdirSync(directoryPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function readJsonFileSafe(filepath: string): any {
|
||||
if (!existsSync(filepath)) return {};
|
||||
try {
|
||||
return JSON.parse(readFileSync(filepath, 'utf-8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function writeJsonFileAtomic(filepath: string, data: any): void {
|
||||
ensureDirectoryExists(dirname(filepath));
|
||||
writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
Reference in New Issue
Block a user