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:
Alex Newman
2026-02-26 23:45:00 -05:00
parent e2a230286d
commit 85eb796b18
8 changed files with 1320 additions and 0 deletions
+152
View File
@@ -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');
}