hana-cli
Version:
HANA Developer Command Line Interface
266 lines (239 loc) • 10 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import { buildDocEpilogue } from '../utils/doc-linker.js'
import { fileURLToPath } from 'url'
import { dirname, join, resolve } from 'path'
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
import { homedir, platform } from 'os'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const ALL_CLIENT_IDS = ['claude-desktop', 'claude-code', 'cursor', 'windsurf', 'cline', 'vscode', 'continue', 'zed']
/**
* Client descriptor: declares config file path(s) per OS and the JSON key used for MCP servers.
* Most clients use { "mcpServers": { name: config } }.
* VS Code uses { "servers": { name: config } } in a dedicated mcp.json.
* Zed uses { "context_servers": { name: config } } inside settings.json.
*/
function getClientDescriptors(useGlobal) {
const home = homedir()
const os = platform()
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming')
function perOS(win, mac, linux) {
if (os === 'win32') return win
if (os === 'darwin') return mac
return linux
}
return {
'claude-desktop': {
label: 'Claude Desktop',
configKey: 'mcpServers',
path: perOS(
join(appData, 'Claude', 'claude_desktop_config.json'),
join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
join(home, '.config', 'Claude', 'claude_desktop_config.json')
),
},
'claude-code': {
label: useGlobal ? 'Claude Code (user)' : 'Claude Code (project)',
configKey: 'mcpServers',
path: useGlobal
? join(home, '.claude.json')
: join(process.cwd(), '.mcp.json'),
},
'cursor': {
label: 'Cursor',
configKey: 'mcpServers',
path: perOS(
join(appData, 'Cursor', 'User', 'globalStorage', 'cursor.mcp', 'settings.json'),
join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'cursor.mcp', 'settings.json'),
join(home, '.config', 'Cursor', 'User', 'globalStorage', 'cursor.mcp', 'settings.json')
),
},
'windsurf': {
label: 'Windsurf',
configKey: 'mcpServers',
path: join(home, '.codeium', 'windsurf', 'mcp_config.json'),
},
'cline': {
label: 'Cline (VS Code)',
configKey: 'mcpServers',
path: perOS(
join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json')
),
},
'vscode': {
label: useGlobal ? 'VS Code (user)' : 'VS Code (workspace)',
configKey: 'servers',
path: useGlobal
? perOS(
join(appData, 'Code', 'User', 'mcp.json'),
join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'),
join(home, '.config', 'Code', 'User', 'mcp.json')
)
: join(process.cwd(), '.vscode', 'mcp.json'),
},
'continue': {
label: 'Continue',
configKey: 'mcpServers',
path: join(home, '.continue', 'config.json'),
},
'zed': {
label: 'Zed',
configKey: 'context_servers',
path: perOS(
join(appData, 'Zed', 'settings.json'),
join(home, '.config', 'zed', 'settings.json'),
join(home, '.config', 'zed', 'settings.json')
),
},
}
}
export const command = 'mcpServerInstall'
export const aliases = ['mcp', 'mcpInstall', 'mcp-install']
export const describe = baseLite.bundle.getText("mcpServerInstall")
export const builder = (yargs) => yargs.options(baseLite.getBuilder({
client: {
alias: ['c'],
type: 'string',
choices: [...ALL_CLIENT_IDS, 'auto'],
default: 'auto',
describe: baseLite.bundle.getText("mcpServerInstallClient")
},
name: {
alias: ['n'],
type: 'string',
default: 'hana-cli',
describe: baseLite.bundle.getText("mcpServerInstallName")
},
dryRun: {
alias: ['dr'],
type: 'boolean',
default: false,
describe: baseLite.bundle.getText("mcpServerInstallDryRun")
},
global: {
alias: ['g'],
type: 'boolean',
default: false,
describe: baseLite.bundle.getText("mcpServerInstallGlobal")
}
}, false, false)).wrap(160).example(
'hana-cli mcpServerInstall --client claude-desktop',
baseLite.bundle.getText("mcpServerInstallExample1")
).example(
'hana-cli mcp --dry-run',
baseLite.bundle.getText("mcpServerInstallExample2")
).example(
'hana-cli mcp --client claude-code --global',
baseLite.bundle.getText("mcpServerInstallExample3")
).epilog(buildDocEpilogue('mcpServerInstall', 'developer-tools', ['mcpServerStatus']))
export async function handler(argv) {
const base = await import('../utils/base.js')
base.promptHandler(argv, mcpInstall, {
client: argv.client,
name: argv.name,
dryRun: argv.dryRun,
global: argv.global,
}, false)
}
function getMcpServerPath() {
return resolve(__dirname, '..', 'mcp-server', 'build', 'index.js')
}
function buildMcpConfig() {
return {
type: 'stdio',
command: process.execPath,
args: [getMcpServerPath()],
}
}
function getTargetClients(client, useGlobal) {
const descriptors = getClientDescriptors(useGlobal)
if (client === 'auto') {
return ALL_CLIENT_IDS.map(id => ({ id, ...descriptors[id] }))
}
const desc = descriptors[client]
return desc ? [{ id: client, ...desc }] : []
}
function mergeConfig(existingContent, serverName, mcpConfig, configKey) {
let config = {}
if (existingContent) {
try {
config = JSON.parse(existingContent)
} catch {
config = {}
}
}
if (!config[configKey]) config[configKey] = {}
config[configKey][serverName] = mcpConfig
return JSON.stringify(config, null, 2)
}
export async function mcpInstall(prompts) {
const base = await import('../utils/base.js')
base.debug('mcpInstall')
const colors = baseLite.colors
const client = prompts.client || 'auto'
const serverName = prompts.name || 'hana-cli'
const dryRun = prompts.dryRun || false
const useGlobal = prompts.global || false
const mcpServerPath = getMcpServerPath()
if (!existsSync(mcpServerPath)) {
console.error(colors.red(baseLite.bundle.getText("mcpServerInstallNotBuilt")))
console.error(colors.yellow(` cd mcp-server && npm run build`))
return base.end()
}
const mcpConfig = buildMcpConfig()
const targets = getTargetClients(client, useGlobal)
console.log(colors.bold(baseLite.bundle.getText("mcpServerInstallHeader")))
console.log()
console.log(` ${colors.cyan(baseLite.bundle.getText("mcpServerInstallServerPath"))} ${mcpServerPath}`)
console.log(` ${colors.cyan(baseLite.bundle.getText("mcpServerInstallServerName"))} ${serverName}`)
console.log()
if (dryRun) {
console.log(colors.yellow(baseLite.bundle.getText("mcpServerInstallDryRunHeader")))
console.log()
for (const target of targets) {
const preview = { [target.configKey]: { [serverName]: mcpConfig } }
console.log(` ${colors.cyan(target.label)} ${colors.dim(`(${target.configKey})`)}:`)
console.log(` ${baseLite.bundle.getText("mcpServerInstallConfigPath")} ${target.path}`)
console.log(` ${baseLite.bundle.getText("mcpServerInstallConfigExists")} ${existsSync(target.path) ? colors.green('yes') : colors.dim('no')}`)
console.log(colors.dim(JSON.stringify(preview, null, 2).split('\n').map(l => ' ' + l).join('\n')))
console.log()
}
return base.end()
}
let installedCount = 0
for (const target of targets) {
try {
let existingContent = null
if (existsSync(target.path)) {
existingContent = readFileSync(target.path, 'utf-8')
}
if (client === 'auto' && !existingContent) {
console.log(colors.dim(` ${target.label}: ${baseLite.bundle.getText("mcpServerInstallSkipped")}`))
continue
}
const configDir = dirname(target.path)
if (!existsSync(configDir)) {
mkdirSync(configDir, { recursive: true })
}
const newContent = mergeConfig(existingContent, serverName, mcpConfig, target.configKey)
writeFileSync(target.path, newContent, 'utf-8')
console.log(colors.green(` ${target.label}: ${baseLite.bundle.getText("mcpServerInstallSuccess")}`))
console.log(colors.dim(` ${target.path}`))
installedCount++
} catch (err) {
console.error(colors.red(` ${target.label}: ${baseLite.bundle.getText("mcpServerInstallFailed")} ${err.message}`))
}
}
console.log()
if (installedCount > 0) {
console.log(colors.green(baseLite.bundle.getText("mcpServerInstallComplete", [installedCount])))
console.log(colors.dim(baseLite.bundle.getText("mcpServerInstallRestart")))
} else {
console.log(colors.yellow(baseLite.bundle.getText("mcpServerInstallNone")))
console.log(colors.dim(baseLite.bundle.getText("mcpServerInstallNoneHint")))
}
return base.end()
}