hsh19900502 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +39 -4
- package/README.md +158 -0
- package/dist/commands/ide/index.d.ts +2 -0
- package/dist/commands/ide/index.js +161 -0
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +79 -0
- package/dist/hsh.js +18 -5
- package/docs/FUZZY_SEARCH_FEATURE.md +214 -0
- package/package.json +1 -1
- package/src/commands/ide/index.ts +197 -0
- package/src/commands/mcp.ts +118 -0
- package/src/hsh.ts +20 -5
- package/dist/commands/ide.d.ts +0 -2
- package/dist/commands/ide.js +0 -104
- package/src/commands/ide.ts +0 -130
package/CLAUDE.md
CHANGED
|
@@ -47,7 +47,8 @@ src/
|
|
|
47
47
|
├── commands/
|
|
48
48
|
│ ├── git.ts # Git workflow commands (gcm, push, merge, mr, branchout)
|
|
49
49
|
│ ├── mono.ts # Monorepo management (init, cd)
|
|
50
|
-
│
|
|
50
|
+
│ ├── ide.ts # IDE integration (cursor, claude)
|
|
51
|
+
│ └── mcp.ts # MCP server synchronization
|
|
51
52
|
├── types/
|
|
52
53
|
│ └── index.ts # TypeScript type definitions
|
|
53
54
|
└── util.ts # Utility functions (package.json reading)
|
|
@@ -89,13 +90,20 @@ The CLI follows a modular command structure using Commander.js:
|
|
|
89
90
|
### IDE Integration
|
|
90
91
|
|
|
91
92
|
- `cursor`: Open project in Cursor editor with config-based project selection
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
### MCP Server Management
|
|
95
|
+
|
|
96
|
+
- `mcp sync`: Synchronize MCP server configurations from `~/.mcp/servers.json` to all projects in `~/.claude.json`
|
|
97
|
+
- This addresses the limitation that Claude Code cannot install MCP servers globally
|
|
98
|
+
- Automatically updates all project configurations with centralized MCP server definitions
|
|
99
|
+
- Idempotent operation - safe to run multiple times
|
|
100
|
+
- Useful for maintaining consistent MCP configurations across multiple projects
|
|
93
101
|
|
|
94
102
|
## Configuration Requirements
|
|
95
103
|
|
|
96
104
|
### IDE Configuration
|
|
97
105
|
|
|
98
|
-
The `cursor` and `
|
|
106
|
+
The `cursor` and `claude` commands require a configuration file at `~/hsh.config.json`:
|
|
99
107
|
|
|
100
108
|
```json
|
|
101
109
|
{
|
|
@@ -116,6 +124,34 @@ For monorepo commands, projects should have:
|
|
|
116
124
|
- `.hsh` marker file in the root (created by `mono init`)
|
|
117
125
|
- Directory structure: `<repo-name>/client/` and `<repo-name>/server/`
|
|
118
126
|
|
|
127
|
+
### MCP Server Configuration
|
|
128
|
+
|
|
129
|
+
MCP (Model Context Protocol) servers can be centrally managed:
|
|
130
|
+
|
|
131
|
+
- Create `~/.mcp/servers.json` with your global MCP server configurations
|
|
132
|
+
- Run `hsh mcp sync` to propagate configurations to all projects in `~/.claude.json`
|
|
133
|
+
- Each project's `mcpServers` field will be updated with the latest configuration
|
|
134
|
+
|
|
135
|
+
Example `~/.mcp/servers.json`:
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"chrome-devtools": {
|
|
139
|
+
"type": "stdio",
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["chrome-devtools-mcp@latest"],
|
|
142
|
+
"env": {}
|
|
143
|
+
},
|
|
144
|
+
"GitLab communication server": {
|
|
145
|
+
"command": "npx",
|
|
146
|
+
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
147
|
+
"env": {
|
|
148
|
+
"GITLAB_PERSONAL_ACCESS_TOKEN": "your-token",
|
|
149
|
+
"GITLAB_API_URL": "https://gitlab.example.com/api/v4"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
119
155
|
## External Dependencies
|
|
120
156
|
|
|
121
157
|
### Required CLI Tools
|
|
@@ -123,7 +159,6 @@ For monorepo commands, projects should have:
|
|
|
123
159
|
- **git**: For all git operations
|
|
124
160
|
- **glab**: GitLab CLI for merge request creation (used in `mr create`)
|
|
125
161
|
- **cursor**: Cursor editor executable (for `cursor` command)
|
|
126
|
-
- **surf**: Windsurf editor executable (for `surf` command)
|
|
127
162
|
|
|
128
163
|
### Shell Integration
|
|
129
164
|
|
package/README.md
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# hsh - CLI Workflow Automation Tool
|
|
2
|
+
|
|
3
|
+
A TypeScript-based CLI tool that provides Git workflow automation, IDE project management, and MCP server configuration synchronization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn install
|
|
9
|
+
yarn build:install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
### Git Workflow Automation
|
|
15
|
+
- `hsh gcm <message> [--push]` - Add all, commit with message, optionally push
|
|
16
|
+
- `hsh push` - Interactive branch selection for pushing to remote
|
|
17
|
+
- `hsh merge <branch>` - Safe merge with branch switching and pulling
|
|
18
|
+
- `hsh mr create` - Create merge requests with JIRA integration
|
|
19
|
+
- `hsh branchout <branch>` - Create new branch from master
|
|
20
|
+
|
|
21
|
+
### Monorepo Management
|
|
22
|
+
- `hsh mono init` - Initialize workspace with `.hsh` marker file
|
|
23
|
+
- `hsh mono cd <level> [--repo <name>]` - Navigate to repo directories (root/client/server)
|
|
24
|
+
|
|
25
|
+
### IDE Integration
|
|
26
|
+
- `hsh cursor` - Open project in Cursor editor with config-based project selection
|
|
27
|
+
- `hsh claude` - Open project in Claude Code editor with config-based project selection
|
|
28
|
+
|
|
29
|
+
### MCP Server Management
|
|
30
|
+
|
|
31
|
+
**NEW!** Solve the problem of having to configure MCP servers individually for each project.
|
|
32
|
+
|
|
33
|
+
#### The Problem
|
|
34
|
+
Claude Code doesn't support global MCP server installation. You have to configure MCP servers in each project's `.claude.json` file individually, which is tedious and error-prone.
|
|
35
|
+
|
|
36
|
+
#### The Solution
|
|
37
|
+
`hsh mcp sync` - Synchronize MCP server configurations from a central location to all your projects
|
|
38
|
+
|
|
39
|
+
#### How It Works
|
|
40
|
+
|
|
41
|
+
1. Create a central MCP configuration file at `~/.mcp/servers.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"chrome-devtools": {
|
|
46
|
+
"type": "stdio",
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["chrome-devtools-mcp@latest"],
|
|
49
|
+
"env": {}
|
|
50
|
+
},
|
|
51
|
+
"GitLab communication server": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
54
|
+
"env": {
|
|
55
|
+
"GITLAB_PERSONAL_ACCESS_TOKEN": "your-token",
|
|
56
|
+
"GITLAB_API_URL": "https://gitlab.example.com/api/v4"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"jira": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "@aashari/mcp-server-atlassian-jira"],
|
|
62
|
+
"env": {
|
|
63
|
+
"ATLASSIAN_SITE_NAME": "your-site",
|
|
64
|
+
"ATLASSIAN_USER_EMAIL": "your-email",
|
|
65
|
+
"ATLASSIAN_API_TOKEN": "your-token"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
2. Run the sync command:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
hsh mcp sync
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. The command will:
|
|
78
|
+
- Read all MCP server configurations from `~/.mcp/servers.json`
|
|
79
|
+
- Update the `mcpServers` field for all projects in `~/.claude.json`
|
|
80
|
+
- Show you which servers were synced and how many projects were updated
|
|
81
|
+
|
|
82
|
+
#### Features
|
|
83
|
+
- ✅ Idempotent - Safe to run multiple times
|
|
84
|
+
- ✅ Atomic updates - Updates all projects at once
|
|
85
|
+
- ✅ Clear feedback - Shows which servers and projects were updated
|
|
86
|
+
- ✅ Validation - Checks for file existence and valid JSON
|
|
87
|
+
|
|
88
|
+
#### Example Output
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
$ hsh mcp sync
|
|
92
|
+
✔ Successfully synced MCP servers to 21 projects
|
|
93
|
+
|
|
94
|
+
MCP Servers synced:
|
|
95
|
+
• chrome-devtools
|
|
96
|
+
• Playwright
|
|
97
|
+
• figma-mcp
|
|
98
|
+
• GitLab communication server
|
|
99
|
+
• jira
|
|
100
|
+
• confluence
|
|
101
|
+
|
|
102
|
+
Projects updated:
|
|
103
|
+
• /Users/you/project1
|
|
104
|
+
• /Users/you/project2
|
|
105
|
+
• /Users/you/project3
|
|
106
|
+
... and 18 more
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
### IDE Configuration
|
|
112
|
+
Create `~/hsh.config.json` for IDE project management:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"work": {
|
|
117
|
+
"project1": "/path/to/work/project1",
|
|
118
|
+
"project2": "/path/to/work/project2"
|
|
119
|
+
},
|
|
120
|
+
"personal": {
|
|
121
|
+
"project3": "/path/to/personal/project3"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Monorepo Setup
|
|
127
|
+
For monorepo commands, projects should have:
|
|
128
|
+
- `.hsh` marker file in the root (created by `hsh mono init`)
|
|
129
|
+
- Directory structure: `<repo-name>/client/` and `<repo-name>/server/`
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Install dependencies
|
|
135
|
+
yarn install
|
|
136
|
+
|
|
137
|
+
# Build the project
|
|
138
|
+
yarn build
|
|
139
|
+
|
|
140
|
+
# Build and install globally
|
|
141
|
+
yarn build:install
|
|
142
|
+
|
|
143
|
+
# Development mode
|
|
144
|
+
yarn dev
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Requirements
|
|
148
|
+
|
|
149
|
+
- Node.js with ES module support
|
|
150
|
+
- Git
|
|
151
|
+
- GitLab CLI (`glab`) for merge request features
|
|
152
|
+
- Cursor editor (for `cursor` command)
|
|
153
|
+
- Claude Code editor (for `claude` command)
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
158
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import 'zx/globals';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
5
|
+
import { readConfig } from '../../util.js';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
// Register autocomplete prompt
|
|
8
|
+
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
9
|
+
let reposConfig;
|
|
10
|
+
let currentCategory;
|
|
11
|
+
async function loadConfig() {
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
reposConfig = config.repos;
|
|
14
|
+
}
|
|
15
|
+
async function searchCategories(answers, input = '') {
|
|
16
|
+
if (!reposConfig) {
|
|
17
|
+
await loadConfig();
|
|
18
|
+
}
|
|
19
|
+
const categories = Object.keys(reposConfig);
|
|
20
|
+
if (!input) {
|
|
21
|
+
return categories;
|
|
22
|
+
}
|
|
23
|
+
return categories.filter(category => category.toLowerCase().includes(input.toLowerCase()));
|
|
24
|
+
}
|
|
25
|
+
async function searchProjects(answers, input = '') {
|
|
26
|
+
if (!reposConfig) {
|
|
27
|
+
await loadConfig();
|
|
28
|
+
}
|
|
29
|
+
// Get projects for the selected category
|
|
30
|
+
if (!currentCategory || !reposConfig[currentCategory]) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const projects = Object.entries(reposConfig[currentCategory])
|
|
34
|
+
.map(([name, path]) => ({
|
|
35
|
+
name,
|
|
36
|
+
path,
|
|
37
|
+
display: `${name} (${path})`
|
|
38
|
+
}));
|
|
39
|
+
if (!input) {
|
|
40
|
+
return projects.map(p => p.name);
|
|
41
|
+
}
|
|
42
|
+
const searchTerm = input.toLowerCase();
|
|
43
|
+
return projects
|
|
44
|
+
.filter(project => project.name.toLowerCase().includes(searchTerm) ||
|
|
45
|
+
project.path.toLowerCase().includes(searchTerm))
|
|
46
|
+
.map(p => p.name);
|
|
47
|
+
}
|
|
48
|
+
// Flatten all projects with category context for fuzzy search
|
|
49
|
+
function getAllProjects() {
|
|
50
|
+
if (!reposConfig)
|
|
51
|
+
return [];
|
|
52
|
+
return Object.entries(reposConfig).flatMap(([category, projects]) => Object.entries(projects).map(([name, path]) => ({
|
|
53
|
+
category,
|
|
54
|
+
name,
|
|
55
|
+
path,
|
|
56
|
+
display: `${name} (${category})`
|
|
57
|
+
})));
|
|
58
|
+
}
|
|
59
|
+
// Single-keyword fuzzy search across all projects
|
|
60
|
+
async function searchAllProjects(answers, input = '') {
|
|
61
|
+
const allProjects = getAllProjects();
|
|
62
|
+
if (!input) {
|
|
63
|
+
return allProjects.map(p => ({ name: p.display, value: p }));
|
|
64
|
+
}
|
|
65
|
+
// Split input by spaces and filter projects matching ALL keywords
|
|
66
|
+
const keywords = input.toLowerCase().trim().split(/\s+/);
|
|
67
|
+
return allProjects
|
|
68
|
+
.filter(project => {
|
|
69
|
+
const searchText = `${project.name} ${project.category} ${project.path}`.toLowerCase();
|
|
70
|
+
// Check if all keywords are present in the search text
|
|
71
|
+
return keywords.every(keyword => searchText.includes(keyword));
|
|
72
|
+
})
|
|
73
|
+
.map(p => ({ name: p.display, value: p }));
|
|
74
|
+
}
|
|
75
|
+
// Select project using fuzzy search across all categories
|
|
76
|
+
async function selectProjectWithFuzzySearch(searchMode) {
|
|
77
|
+
const answer = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'autocomplete',
|
|
80
|
+
name: 'project',
|
|
81
|
+
message: `Search and select a project${searchMode ? ` (filtering: ${searchMode})` : ''}:`,
|
|
82
|
+
source: searchAllProjects,
|
|
83
|
+
pageSize: 15,
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
path: answer.project.path,
|
|
88
|
+
name: answer.project.name
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Select project using two-step category then project selection
|
|
92
|
+
async function selectProjectWithTwoStep() {
|
|
93
|
+
const categoryAnswer = await inquirer.prompt([
|
|
94
|
+
{
|
|
95
|
+
type: 'autocomplete',
|
|
96
|
+
name: 'category',
|
|
97
|
+
message: 'Select a category:',
|
|
98
|
+
source: searchCategories,
|
|
99
|
+
pageSize: 10,
|
|
100
|
+
}
|
|
101
|
+
]);
|
|
102
|
+
currentCategory = categoryAnswer.category;
|
|
103
|
+
const projectAnswer = await inquirer.prompt([
|
|
104
|
+
{
|
|
105
|
+
type: 'autocomplete',
|
|
106
|
+
name: 'project',
|
|
107
|
+
message: 'Select a project:',
|
|
108
|
+
source: searchProjects,
|
|
109
|
+
pageSize: 10,
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
return {
|
|
113
|
+
path: reposConfig[currentCategory][projectAnswer.project],
|
|
114
|
+
name: projectAnswer.project
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Launch Claude IDE with proper TTY handling
|
|
118
|
+
async function launchClaude(projectPath) {
|
|
119
|
+
const claudeProcess = spawn('claude', [], {
|
|
120
|
+
cwd: projectPath,
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
shell: true,
|
|
123
|
+
});
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
claudeProcess.on('close', (code) => {
|
|
126
|
+
if (code === 0) {
|
|
127
|
+
resolve();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
reject(new Error(`Claude process exited with code ${code}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
claudeProcess.on('error', (error) => {
|
|
134
|
+
reject(error);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Launch Cursor or other IDE
|
|
139
|
+
async function launchIDE(ideType, projectPath, projectName) {
|
|
140
|
+
await $ `${ideType} ${projectPath}`;
|
|
141
|
+
console.log(chalk.green(`Opening ${projectName} in ${ideType}...`));
|
|
142
|
+
}
|
|
143
|
+
export const openIDE = async (ideType, searchMode) => {
|
|
144
|
+
try {
|
|
145
|
+
await loadConfig();
|
|
146
|
+
// Select project using appropriate method
|
|
147
|
+
const project = searchMode !== undefined
|
|
148
|
+
? await selectProjectWithFuzzySearch(searchMode)
|
|
149
|
+
: await selectProjectWithTwoStep();
|
|
150
|
+
// Launch IDE based on type
|
|
151
|
+
if (ideType === 'claude') {
|
|
152
|
+
await launchClaude(project.path);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
await launchIDE(ideType, project.path, project.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error(chalk.red(`Error opening project in ${ideType}:`, error));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
/**
|
|
7
|
+
* Sync MCP servers from ~/.mcp/servers.json to all projects in ~/.claude.json
|
|
8
|
+
*/
|
|
9
|
+
export async function syncMcpServers() {
|
|
10
|
+
const spinner = ora('Syncing MCP servers...').start();
|
|
11
|
+
try {
|
|
12
|
+
// Read MCP servers configuration
|
|
13
|
+
const mcpConfigPath = join(homedir(), '.mcp', 'servers.json');
|
|
14
|
+
const claudeConfigPath = join(homedir(), '.claude.json');
|
|
15
|
+
let mcpServers;
|
|
16
|
+
try {
|
|
17
|
+
const mcpConfigContent = readFileSync(mcpConfigPath, 'utf-8');
|
|
18
|
+
mcpServers = JSON.parse(mcpConfigContent);
|
|
19
|
+
spinner.text = `Found ${Object.keys(mcpServers).length} MCP servers`;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
spinner.fail(chalk.red(`Failed to read MCP servers from ${mcpConfigPath}`));
|
|
23
|
+
console.error(chalk.yellow(`Please ensure ${mcpConfigPath} exists and is valid JSON`));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Read Claude configuration
|
|
27
|
+
let claudeConfig;
|
|
28
|
+
try {
|
|
29
|
+
const claudeConfigContent = readFileSync(claudeConfigPath, 'utf-8');
|
|
30
|
+
claudeConfig = JSON.parse(claudeConfigContent);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
spinner.fail(chalk.red(`Failed to read Claude config from ${claudeConfigPath}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Check if there are any projects
|
|
37
|
+
if (!claudeConfig.projects || Object.keys(claudeConfig.projects).length === 0) {
|
|
38
|
+
spinner.warn(chalk.yellow('No projects found in ~/.claude.json'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Update each project with MCP servers
|
|
42
|
+
const projectPaths = Object.keys(claudeConfig.projects);
|
|
43
|
+
spinner.text = `Updating ${projectPaths.length} projects...`;
|
|
44
|
+
let updatedCount = 0;
|
|
45
|
+
for (const projectPath of projectPaths) {
|
|
46
|
+
const project = claudeConfig.projects[projectPath];
|
|
47
|
+
// Check if mcpServers need to be updated
|
|
48
|
+
const currentServers = JSON.stringify(project.mcpServers || {});
|
|
49
|
+
const newServers = JSON.stringify(mcpServers);
|
|
50
|
+
if (currentServers !== newServers) {
|
|
51
|
+
project.mcpServers = mcpServers;
|
|
52
|
+
updatedCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Write back to ~/.claude.json
|
|
56
|
+
if (updatedCount > 0) {
|
|
57
|
+
writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
58
|
+
spinner.succeed(chalk.green(`✓ Successfully synced MCP servers to ${updatedCount} project${updatedCount > 1 ? 's' : ''}`));
|
|
59
|
+
console.log(chalk.cyan('\nMCP Servers synced:'));
|
|
60
|
+
Object.keys(mcpServers).forEach(serverName => {
|
|
61
|
+
console.log(chalk.gray(` • ${serverName}`));
|
|
62
|
+
});
|
|
63
|
+
console.log(chalk.cyan('\nProjects updated:'));
|
|
64
|
+
projectPaths.slice(0, 5).forEach(path => {
|
|
65
|
+
console.log(chalk.gray(` • ${path}`));
|
|
66
|
+
});
|
|
67
|
+
if (projectPaths.length > 5) {
|
|
68
|
+
console.log(chalk.gray(` ... and ${projectPaths.length - 5} more`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
spinner.info(chalk.blue('All projects already have the latest MCP servers configuration'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
spinner.fail(chalk.red('Failed to sync MCP servers'));
|
|
77
|
+
console.error(chalk.red(error.message));
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/hsh.js
CHANGED
|
@@ -3,8 +3,9 @@ import { branchout, createMR, gcm, merge, push } from './commands/git.js';
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { initMonoRepo, monoCd } from './commands/mono.js';
|
|
5
5
|
import { getPackageJson } from './util.js';
|
|
6
|
-
import { openIDE } from './commands/ide.js';
|
|
6
|
+
import { openIDE } from './commands/ide/index.js';
|
|
7
7
|
import { cloudLogin, cloudScp } from './commands/cloud.js';
|
|
8
|
+
import { syncMcpServers } from './commands/mcp.js';
|
|
8
9
|
const packageJson = getPackageJson();
|
|
9
10
|
const program = new Command();
|
|
10
11
|
program.usage('<command> [options]');
|
|
@@ -57,13 +58,15 @@ mono.command('cd').description('cd into repo')
|
|
|
57
58
|
});
|
|
58
59
|
program.command('cursor')
|
|
59
60
|
.description('open project in Cursor')
|
|
60
|
-
.
|
|
61
|
-
|
|
61
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
62
|
+
.action(async (search) => {
|
|
63
|
+
await openIDE('cursor', search);
|
|
62
64
|
});
|
|
63
65
|
program.command('claude')
|
|
64
66
|
.description('open project in Claude')
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
68
|
+
.action(async (search) => {
|
|
69
|
+
await openIDE('claude', search);
|
|
67
70
|
});
|
|
68
71
|
// Cloud infrastructure management commands
|
|
69
72
|
const cloudCommand = program
|
|
@@ -90,4 +93,14 @@ cloudCommand
|
|
|
90
93
|
.action(async (localPath, remotePath, options) => {
|
|
91
94
|
await cloudScp(localPath, remotePath, options);
|
|
92
95
|
});
|
|
96
|
+
// MCP management commands
|
|
97
|
+
const mcpCommand = program
|
|
98
|
+
.command('mcp')
|
|
99
|
+
.description('MCP (Model Context Protocol) server management');
|
|
100
|
+
mcpCommand
|
|
101
|
+
.command('sync')
|
|
102
|
+
.description('Sync MCP servers from ~/.mcp/servers.json to all projects in ~/.claude.json')
|
|
103
|
+
.action(async () => {
|
|
104
|
+
await syncMcpServers();
|
|
105
|
+
});
|
|
93
106
|
program.parse();
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Fuzzy Search Feature for IDE Commands
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document describes the fuzzy search feature added to the `hsh cursor` and `hsh claude` commands, providing users with an optional single-step project search across all categories.
|
|
6
|
+
|
|
7
|
+
## Feature Description
|
|
8
|
+
|
|
9
|
+
The IDE commands now support two modes:
|
|
10
|
+
|
|
11
|
+
### 1. Original Two-Step Mode (Default)
|
|
12
|
+
When no argument is provided:
|
|
13
|
+
```bash
|
|
14
|
+
hsh cursor
|
|
15
|
+
hsh claude
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Behavior:**
|
|
19
|
+
- Step 1: Select a category from the configured categories
|
|
20
|
+
- Step 2: Select a project within the chosen category
|
|
21
|
+
- Maintains backward compatibility with existing workflows
|
|
22
|
+
|
|
23
|
+
### 2. Fuzzy Search Mode (New)
|
|
24
|
+
When a search argument is provided:
|
|
25
|
+
```bash
|
|
26
|
+
hsh cursor search # Opens fuzzy search with no initial filter
|
|
27
|
+
hsh cursor airwallex # Opens fuzzy search pre-filtered with "airwallex"
|
|
28
|
+
hsh claude impactful # Opens fuzzy search pre-filtered with "impactful"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Behavior:**
|
|
32
|
+
- Single-step selection across ALL projects from ALL categories
|
|
33
|
+
- Real-time fuzzy matching as you type
|
|
34
|
+
- Searches across project name, category, and path
|
|
35
|
+
- Display format: `project-name (category) - /path/to/project`
|
|
36
|
+
- Supports keyboard navigation with arrow keys
|
|
37
|
+
|
|
38
|
+
## Implementation Details
|
|
39
|
+
|
|
40
|
+
### Modified Files
|
|
41
|
+
|
|
42
|
+
#### 1. `src/hsh.ts`
|
|
43
|
+
Updated command definitions to accept optional search argument:
|
|
44
|
+
```typescript
|
|
45
|
+
program.command('cursor')
|
|
46
|
+
.description('open project in Cursor')
|
|
47
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
48
|
+
.action(async (search?: string) => {
|
|
49
|
+
await openIDE('cursor', search);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program.command('claude')
|
|
53
|
+
.description('open project in Claude')
|
|
54
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
55
|
+
.action(async (search?: string) => {
|
|
56
|
+
await openIDE('claude', search);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### 2. `src/commands/ide.ts`
|
|
61
|
+
|
|
62
|
+
**New Functions:**
|
|
63
|
+
|
|
64
|
+
1. `getAllProjects()`: Flattens all projects across categories
|
|
65
|
+
- Returns array of `{category, name, path, display}` objects
|
|
66
|
+
- Display format includes category context
|
|
67
|
+
|
|
68
|
+
2. `searchAllProjects()`: Fuzzy search handler for inquirer
|
|
69
|
+
- Single-keyword substring matching
|
|
70
|
+
- Searches across project name, category, and path
|
|
71
|
+
- Returns formatted choices for inquirer autocomplete
|
|
72
|
+
|
|
73
|
+
**Updated Function:**
|
|
74
|
+
|
|
75
|
+
3. `openIDE()`: Now accepts optional `searchMode` parameter
|
|
76
|
+
- When `searchMode !== undefined`: Uses single-step fuzzy search
|
|
77
|
+
- When `searchMode === undefined`: Uses original two-step selection
|
|
78
|
+
- Maintains all existing IDE launching logic
|
|
79
|
+
|
|
80
|
+
### Key Design Decisions
|
|
81
|
+
|
|
82
|
+
1. **Backward Compatibility**: Original behavior preserved when no argument provided
|
|
83
|
+
2. **Single-Keyword Search**: Simple substring matching for fast performance
|
|
84
|
+
3. **Context Display**: Shows category in results for disambiguation
|
|
85
|
+
4. **Larger Page Size**: Fuzzy search shows 15 items vs 10 for category selection
|
|
86
|
+
5. **Empty String Support**: Passing any argument (even empty string) enables fuzzy mode
|
|
87
|
+
|
|
88
|
+
## Usage Examples
|
|
89
|
+
|
|
90
|
+
### Example 1: Original Mode
|
|
91
|
+
```bash
|
|
92
|
+
$ hsh cursor
|
|
93
|
+
? Select a category: ›
|
|
94
|
+
❯ personal
|
|
95
|
+
work
|
|
96
|
+
experiments
|
|
97
|
+
|
|
98
|
+
$ hsh cursor
|
|
99
|
+
? Select a category: › work
|
|
100
|
+
? Select a project: ›
|
|
101
|
+
❯ awx-platform
|
|
102
|
+
awx-impactful
|
|
103
|
+
internal-tools
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Example 2: Fuzzy Search Mode (No Initial Filter)
|
|
107
|
+
```bash
|
|
108
|
+
$ hsh cursor search
|
|
109
|
+
? Search and select a project: ›
|
|
110
|
+
❯ awx-platform (work) - /Users/you/projects/awx-platform
|
|
111
|
+
awx-impactful (work) - /Users/you/projects/awx-impactful
|
|
112
|
+
personal-site (personal) - /Users/you/projects/personal-site
|
|
113
|
+
blog (personal) - /Users/you/projects/blog
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Example 3: Fuzzy Search Mode (With Initial Filter)
|
|
117
|
+
```bash
|
|
118
|
+
$ hsh cursor awx
|
|
119
|
+
? Search and select a project (filtering: awx): ›
|
|
120
|
+
❯ awx-platform (work) - /Users/you/projects/awx-platform
|
|
121
|
+
awx-impactful (work) - /Users/you/projects/awx-impactful
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
As user types "imp":
|
|
125
|
+
```bash
|
|
126
|
+
? Search and select a project (filtering: awx): › imp
|
|
127
|
+
❯ awx-impactful (work) - /Users/you/projects/awx-impactful
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Testing
|
|
131
|
+
|
|
132
|
+
### Verification Steps
|
|
133
|
+
|
|
134
|
+
1. **Build the project:**
|
|
135
|
+
```bash
|
|
136
|
+
yarn build
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
2. **Verify help output:**
|
|
140
|
+
```bash
|
|
141
|
+
hsh --help
|
|
142
|
+
```
|
|
143
|
+
Should show:
|
|
144
|
+
```
|
|
145
|
+
cursor [search] open project in Cursor
|
|
146
|
+
claude [search] open project in Claude
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
3. **Test original mode (no argument):**
|
|
150
|
+
```bash
|
|
151
|
+
hsh cursor
|
|
152
|
+
```
|
|
153
|
+
- Should show two-step category → project selection
|
|
154
|
+
- Original workflow should work identically
|
|
155
|
+
|
|
156
|
+
4. **Test fuzzy search mode (with 'search'):**
|
|
157
|
+
```bash
|
|
158
|
+
hsh cursor search
|
|
159
|
+
```
|
|
160
|
+
- Should show all projects from all categories
|
|
161
|
+
- Type a keyword and verify real-time filtering
|
|
162
|
+
- Verify display format includes category
|
|
163
|
+
|
|
164
|
+
5. **Test fuzzy search with initial keyword:**
|
|
165
|
+
```bash
|
|
166
|
+
hsh cursor <keyword>
|
|
167
|
+
```
|
|
168
|
+
- Should open with pre-filtered results
|
|
169
|
+
- Verify keyword appears in prompt message
|
|
170
|
+
- Verify can still type to refine search
|
|
171
|
+
|
|
172
|
+
6. **Test with claude command:**
|
|
173
|
+
```bash
|
|
174
|
+
hsh claude search
|
|
175
|
+
hsh claude <keyword>
|
|
176
|
+
```
|
|
177
|
+
- Same behavior as cursor command
|
|
178
|
+
|
|
179
|
+
### Expected Behavior
|
|
180
|
+
|
|
181
|
+
✅ **Original mode preserves existing UX**
|
|
182
|
+
✅ **Fuzzy search provides single-step selection**
|
|
183
|
+
✅ **Real-time filtering works as you type**
|
|
184
|
+
✅ **Search matches project name, category, and path**
|
|
185
|
+
✅ **Display shows category context for clarity**
|
|
186
|
+
✅ **Keyboard navigation works with arrow keys**
|
|
187
|
+
✅ **Enter key opens selected project in IDE**
|
|
188
|
+
|
|
189
|
+
## Configuration Requirements
|
|
190
|
+
|
|
191
|
+
Requires `~/hsh.config.json` with structure:
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"repos": {
|
|
195
|
+
"category1": {
|
|
196
|
+
"project1": "/path/to/project1",
|
|
197
|
+
"project2": "/path/to/project2"
|
|
198
|
+
},
|
|
199
|
+
"category2": {
|
|
200
|
+
"project3": "/path/to/project3"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Future Enhancements
|
|
207
|
+
|
|
208
|
+
Potential improvements for future versions:
|
|
209
|
+
- Multi-keyword matching (e.g., "awx imp" matches both keywords)
|
|
210
|
+
- Fuzzy scoring algorithm (e.g., fuse.js) for typo tolerance
|
|
211
|
+
- Recent projects tracking and prioritization
|
|
212
|
+
- Favorites/bookmarks system
|
|
213
|
+
- Project metadata display (last opened, git branch, etc.)
|
|
214
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import 'zx/globals';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
5
|
+
import { readConfig } from '../../util.js';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
|
|
8
|
+
interface ReposConfig {
|
|
9
|
+
[key: string]: {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Register autocomplete prompt
|
|
15
|
+
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
16
|
+
|
|
17
|
+
let reposConfig: ReposConfig;
|
|
18
|
+
let currentCategory: string;
|
|
19
|
+
|
|
20
|
+
async function loadConfig() {
|
|
21
|
+
const config = readConfig();
|
|
22
|
+
reposConfig = config.repos;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function searchCategories(answers: any, input = '') {
|
|
26
|
+
if (!reposConfig) {
|
|
27
|
+
await loadConfig();
|
|
28
|
+
}
|
|
29
|
+
const categories = Object.keys(reposConfig);
|
|
30
|
+
|
|
31
|
+
if (!input) {
|
|
32
|
+
return categories;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return categories.filter(category =>
|
|
36
|
+
category.toLowerCase().includes(input.toLowerCase())
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function searchProjects(answers: any, input = '') {
|
|
41
|
+
if (!reposConfig) {
|
|
42
|
+
await loadConfig();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get projects for the selected category
|
|
46
|
+
if (!currentCategory || !reposConfig[currentCategory]) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const projects = Object.entries(reposConfig[currentCategory])
|
|
51
|
+
.map(([name, path]) => ({
|
|
52
|
+
name,
|
|
53
|
+
path,
|
|
54
|
+
display: `${name} (${path})`
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
if (!input) {
|
|
58
|
+
return projects.map(p => p.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const searchTerm = input.toLowerCase();
|
|
62
|
+
return projects
|
|
63
|
+
.filter(project =>
|
|
64
|
+
project.name.toLowerCase().includes(searchTerm) ||
|
|
65
|
+
project.path.toLowerCase().includes(searchTerm)
|
|
66
|
+
)
|
|
67
|
+
.map(p => p.name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Flatten all projects with category context for fuzzy search
|
|
71
|
+
function getAllProjects(): Array<{category: string, name: string, path: string, display: string}> {
|
|
72
|
+
if (!reposConfig) return [];
|
|
73
|
+
|
|
74
|
+
return Object.entries(reposConfig).flatMap(([category, projects]) =>
|
|
75
|
+
Object.entries(projects).map(([name, path]) => ({
|
|
76
|
+
category,
|
|
77
|
+
name,
|
|
78
|
+
path,
|
|
79
|
+
display: `${name} (${category})`
|
|
80
|
+
}))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Single-keyword fuzzy search across all projects
|
|
85
|
+
async function searchAllProjects(answers: any, input = '') {
|
|
86
|
+
const allProjects = getAllProjects();
|
|
87
|
+
|
|
88
|
+
if (!input) {
|
|
89
|
+
return allProjects.map(p => ({ name: p.display, value: p }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Split input by spaces and filter projects matching ALL keywords
|
|
93
|
+
const keywords = input.toLowerCase().trim().split(/\s+/);
|
|
94
|
+
return allProjects
|
|
95
|
+
.filter(project => {
|
|
96
|
+
const searchText = `${project.name} ${project.category} ${project.path}`.toLowerCase();
|
|
97
|
+
// Check if all keywords are present in the search text
|
|
98
|
+
return keywords.every(keyword => searchText.includes(keyword));
|
|
99
|
+
})
|
|
100
|
+
.map(p => ({ name: p.display, value: p }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Select project using fuzzy search across all categories
|
|
104
|
+
async function selectProjectWithFuzzySearch(searchMode?: string): Promise<{path: string, name: string}> {
|
|
105
|
+
const answer = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'autocomplete',
|
|
108
|
+
name: 'project',
|
|
109
|
+
message: `Search and select a project${searchMode ? ` (filtering: ${searchMode})` : ''}:`,
|
|
110
|
+
source: searchAllProjects,
|
|
111
|
+
pageSize: 15,
|
|
112
|
+
}
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
path: answer.project.path,
|
|
117
|
+
name: answer.project.name
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Select project using two-step category then project selection
|
|
122
|
+
async function selectProjectWithTwoStep(): Promise<{path: string, name: string}> {
|
|
123
|
+
const categoryAnswer = await inquirer.prompt([
|
|
124
|
+
{
|
|
125
|
+
type: 'autocomplete',
|
|
126
|
+
name: 'category',
|
|
127
|
+
message: 'Select a category:',
|
|
128
|
+
source: searchCategories,
|
|
129
|
+
pageSize: 10,
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
currentCategory = categoryAnswer.category;
|
|
134
|
+
|
|
135
|
+
const projectAnswer = await inquirer.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'autocomplete',
|
|
138
|
+
name: 'project',
|
|
139
|
+
message: 'Select a project:',
|
|
140
|
+
source: searchProjects,
|
|
141
|
+
pageSize: 10,
|
|
142
|
+
}
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
path: reposConfig[currentCategory][projectAnswer.project],
|
|
147
|
+
name: projectAnswer.project
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Launch Claude IDE with proper TTY handling
|
|
152
|
+
async function launchClaude(projectPath: string): Promise<void> {
|
|
153
|
+
const claudeProcess = spawn('claude', [], {
|
|
154
|
+
cwd: projectPath,
|
|
155
|
+
stdio: 'inherit',
|
|
156
|
+
shell: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await new Promise<void>((resolve, reject) => {
|
|
160
|
+
claudeProcess.on('close', (code) => {
|
|
161
|
+
if (code === 0) {
|
|
162
|
+
resolve();
|
|
163
|
+
} else {
|
|
164
|
+
reject(new Error(`Claude process exited with code ${code}`));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
claudeProcess.on('error', (error) => {
|
|
168
|
+
reject(error);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Launch Cursor or other IDE
|
|
174
|
+
async function launchIDE(ideType: string, projectPath: string, projectName: string): Promise<void> {
|
|
175
|
+
await $`${ideType} ${projectPath}`;
|
|
176
|
+
console.log(chalk.green(`Opening ${projectName} in ${ideType}...`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const openIDE = async (ideType: 'cursor' | 'claude', searchMode?: string) => {
|
|
180
|
+
try {
|
|
181
|
+
await loadConfig();
|
|
182
|
+
|
|
183
|
+
// Select project using appropriate method
|
|
184
|
+
const project = searchMode !== undefined
|
|
185
|
+
? await selectProjectWithFuzzySearch(searchMode)
|
|
186
|
+
: await selectProjectWithTwoStep();
|
|
187
|
+
|
|
188
|
+
// Launch IDE based on type
|
|
189
|
+
if (ideType === 'claude') {
|
|
190
|
+
await launchClaude(project.path);
|
|
191
|
+
} else {
|
|
192
|
+
await launchIDE(ideType, project.path, project.name);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(chalk.red(`Error opening project in ${ideType}:`, error));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { $ } from 'zx';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
interface McpServerConfig {
|
|
9
|
+
[serverName: string]: {
|
|
10
|
+
type?: string;
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ClaudeConfig {
|
|
18
|
+
projects: {
|
|
19
|
+
[projectPath: string]: {
|
|
20
|
+
mcpServers?: McpServerConfig;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sync MCP servers from ~/.mcp/servers.json to all projects in ~/.claude.json
|
|
29
|
+
*/
|
|
30
|
+
export async function syncMcpServers() {
|
|
31
|
+
const spinner = ora('Syncing MCP servers...').start();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Read MCP servers configuration
|
|
35
|
+
const mcpConfigPath = join(homedir(), '.mcp', 'servers.json');
|
|
36
|
+
const claudeConfigPath = join(homedir(), '.claude.json');
|
|
37
|
+
|
|
38
|
+
let mcpServers: McpServerConfig;
|
|
39
|
+
try {
|
|
40
|
+
const mcpConfigContent = readFileSync(mcpConfigPath, 'utf-8');
|
|
41
|
+
mcpServers = JSON.parse(mcpConfigContent);
|
|
42
|
+
spinner.text = `Found ${Object.keys(mcpServers).length} MCP servers`;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
spinner.fail(chalk.red(`Failed to read MCP servers from ${mcpConfigPath}`));
|
|
45
|
+
console.error(chalk.yellow(`Please ensure ${mcpConfigPath} exists and is valid JSON`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Read Claude configuration
|
|
50
|
+
let claudeConfig: ClaudeConfig;
|
|
51
|
+
try {
|
|
52
|
+
const claudeConfigContent = readFileSync(claudeConfigPath, 'utf-8');
|
|
53
|
+
claudeConfig = JSON.parse(claudeConfigContent);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
spinner.fail(chalk.red(`Failed to read Claude config from ${claudeConfigPath}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if there are any projects
|
|
60
|
+
if (!claudeConfig.projects || Object.keys(claudeConfig.projects).length === 0) {
|
|
61
|
+
spinner.warn(chalk.yellow('No projects found in ~/.claude.json'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Update each project with MCP servers
|
|
66
|
+
const projectPaths = Object.keys(claudeConfig.projects);
|
|
67
|
+
spinner.text = `Updating ${projectPaths.length} projects...`;
|
|
68
|
+
|
|
69
|
+
let updatedCount = 0;
|
|
70
|
+
for (const projectPath of projectPaths) {
|
|
71
|
+
const project = claudeConfig.projects[projectPath];
|
|
72
|
+
|
|
73
|
+
// Check if mcpServers need to be updated
|
|
74
|
+
const currentServers = JSON.stringify(project.mcpServers || {});
|
|
75
|
+
const newServers = JSON.stringify(mcpServers);
|
|
76
|
+
|
|
77
|
+
if (currentServers !== newServers) {
|
|
78
|
+
project.mcpServers = mcpServers;
|
|
79
|
+
updatedCount++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write back to ~/.claude.json
|
|
84
|
+
if (updatedCount > 0) {
|
|
85
|
+
writeFileSync(
|
|
86
|
+
claudeConfigPath,
|
|
87
|
+
JSON.stringify(claudeConfig, null, 2),
|
|
88
|
+
'utf-8'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
spinner.succeed(
|
|
92
|
+
chalk.green(
|
|
93
|
+
`✓ Successfully synced MCP servers to ${updatedCount} project${updatedCount > 1 ? 's' : ''}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
console.log(chalk.cyan('\nMCP Servers synced:'));
|
|
98
|
+
Object.keys(mcpServers).forEach(serverName => {
|
|
99
|
+
console.log(chalk.gray(` • ${serverName}`));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(chalk.cyan('\nProjects updated:'));
|
|
103
|
+
projectPaths.slice(0, 5).forEach(path => {
|
|
104
|
+
console.log(chalk.gray(` • ${path}`));
|
|
105
|
+
});
|
|
106
|
+
if (projectPaths.length > 5) {
|
|
107
|
+
console.log(chalk.gray(` ... and ${projectPaths.length - 5} more`));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
spinner.info(chalk.blue('All projects already have the latest MCP servers configuration'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
spinner.fail(chalk.red('Failed to sync MCP servers'));
|
|
115
|
+
console.error(chalk.red(error.message));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
package/src/hsh.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { initMonoRepo, monoCd } from './commands/mono.js';
|
|
5
5
|
import { RepoDirLevel } from './types/index.js';
|
|
6
6
|
import { getPackageJson } from './util.js';
|
|
7
|
-
import { openIDE } from './commands/ide.js';
|
|
7
|
+
import { openIDE } from './commands/ide/index.js';
|
|
8
8
|
import { cloudLogin, cloudScp } from './commands/cloud.js';
|
|
9
|
+
import { syncMcpServers } from './commands/mcp.js';
|
|
9
10
|
|
|
10
11
|
const packageJson = getPackageJson();
|
|
11
12
|
|
|
@@ -70,14 +71,16 @@ mono.command('cd').description('cd into repo')
|
|
|
70
71
|
|
|
71
72
|
program.command('cursor')
|
|
72
73
|
.description('open project in Cursor')
|
|
73
|
-
.
|
|
74
|
-
|
|
74
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
75
|
+
.action(async (search?: string) => {
|
|
76
|
+
await openIDE('cursor', search);
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
program.command('claude')
|
|
78
80
|
.description('open project in Claude')
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
82
|
+
.action(async (search?: string) => {
|
|
83
|
+
await openIDE('claude', search);
|
|
81
84
|
});
|
|
82
85
|
|
|
83
86
|
// Cloud infrastructure management commands
|
|
@@ -108,4 +111,16 @@ cloudCommand
|
|
|
108
111
|
await cloudScp(localPath, remotePath, options);
|
|
109
112
|
});
|
|
110
113
|
|
|
114
|
+
// MCP management commands
|
|
115
|
+
const mcpCommand = program
|
|
116
|
+
.command('mcp')
|
|
117
|
+
.description('MCP (Model Context Protocol) server management');
|
|
118
|
+
|
|
119
|
+
mcpCommand
|
|
120
|
+
.command('sync')
|
|
121
|
+
.description('Sync MCP servers from ~/.mcp/servers.json to all projects in ~/.claude.json')
|
|
122
|
+
.action(async () => {
|
|
123
|
+
await syncMcpServers();
|
|
124
|
+
});
|
|
125
|
+
|
|
111
126
|
program.parse();
|
package/dist/commands/ide.d.ts
DELETED
package/dist/commands/ide.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
|
-
import 'zx/globals';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
5
|
-
import { readConfig } from '../util.js';
|
|
6
|
-
import { spawn } from 'child_process';
|
|
7
|
-
// Register autocomplete prompt
|
|
8
|
-
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
9
|
-
let reposConfig;
|
|
10
|
-
let currentCategory;
|
|
11
|
-
async function loadConfig() {
|
|
12
|
-
const config = readConfig();
|
|
13
|
-
reposConfig = config.repos;
|
|
14
|
-
}
|
|
15
|
-
async function searchCategories(answers, input = '') {
|
|
16
|
-
if (!reposConfig) {
|
|
17
|
-
await loadConfig();
|
|
18
|
-
}
|
|
19
|
-
const categories = Object.keys(reposConfig);
|
|
20
|
-
if (!input) {
|
|
21
|
-
return categories;
|
|
22
|
-
}
|
|
23
|
-
return categories.filter(category => category.toLowerCase().includes(input.toLowerCase()));
|
|
24
|
-
}
|
|
25
|
-
async function searchProjects(answers, input = '') {
|
|
26
|
-
if (!reposConfig) {
|
|
27
|
-
await loadConfig();
|
|
28
|
-
}
|
|
29
|
-
// Get projects for the selected category
|
|
30
|
-
if (!currentCategory || !reposConfig[currentCategory]) {
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
const projects = Object.entries(reposConfig[currentCategory])
|
|
34
|
-
.map(([name, path]) => ({
|
|
35
|
-
name,
|
|
36
|
-
path,
|
|
37
|
-
display: `${name} (${path})`
|
|
38
|
-
}));
|
|
39
|
-
if (!input) {
|
|
40
|
-
return projects.map(p => p.name);
|
|
41
|
-
}
|
|
42
|
-
const searchTerm = input.toLowerCase();
|
|
43
|
-
return projects
|
|
44
|
-
.filter(project => project.name.toLowerCase().includes(searchTerm) ||
|
|
45
|
-
project.path.toLowerCase().includes(searchTerm))
|
|
46
|
-
.map(p => p.name);
|
|
47
|
-
}
|
|
48
|
-
export const openIDE = async (ideType) => {
|
|
49
|
-
try {
|
|
50
|
-
await loadConfig();
|
|
51
|
-
// First level selection with search
|
|
52
|
-
const firstLevelAnswer = await inquirer.prompt([
|
|
53
|
-
{
|
|
54
|
-
type: 'autocomplete',
|
|
55
|
-
name: 'category',
|
|
56
|
-
message: 'Select a category:',
|
|
57
|
-
source: searchCategories,
|
|
58
|
-
pageSize: 10,
|
|
59
|
-
}
|
|
60
|
-
]);
|
|
61
|
-
currentCategory = firstLevelAnswer.category;
|
|
62
|
-
// Second level selection with search
|
|
63
|
-
const secondLevelAnswer = await inquirer.prompt([
|
|
64
|
-
{
|
|
65
|
-
type: 'autocomplete',
|
|
66
|
-
name: 'project',
|
|
67
|
-
message: 'Select a project:',
|
|
68
|
-
source: searchProjects,
|
|
69
|
-
pageSize: 10,
|
|
70
|
-
}
|
|
71
|
-
]);
|
|
72
|
-
const selectedProject = secondLevelAnswer.project;
|
|
73
|
-
const projectPath = reposConfig[currentCategory][selectedProject];
|
|
74
|
-
if (ideType === 'claude') {
|
|
75
|
-
// Use spawn with inherited stdio to support interactive TTY
|
|
76
|
-
const claudeProcess = spawn('claude', [], {
|
|
77
|
-
cwd: projectPath,
|
|
78
|
-
stdio: 'inherit', // Inherit stdin, stdout, stderr from parent process
|
|
79
|
-
shell: true,
|
|
80
|
-
});
|
|
81
|
-
// Wait for the process to exit
|
|
82
|
-
await new Promise((resolve, reject) => {
|
|
83
|
-
claudeProcess.on('close', (code) => {
|
|
84
|
-
if (code === 0) {
|
|
85
|
-
resolve();
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
reject(new Error(`Claude process exited with code ${code}`));
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
claudeProcess.on('error', (error) => {
|
|
92
|
-
reject(error);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
// Open project in IDE
|
|
98
|
-
await $ `${ideType} ${projectPath}`;
|
|
99
|
-
console.log(chalk.green(`Opening ${selectedProject} in ${ideType}...`));
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
console.error(chalk.red(`Error opening project in ${ideType}:`, error));
|
|
103
|
-
}
|
|
104
|
-
};
|
package/src/commands/ide.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
|
-
import 'zx/globals';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
|
5
|
-
import { readConfig } from '../util.js';
|
|
6
|
-
import { spawn } from 'child_process';
|
|
7
|
-
|
|
8
|
-
interface ReposConfig {
|
|
9
|
-
[key: string]: {
|
|
10
|
-
[key: string]: string;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Register autocomplete prompt
|
|
15
|
-
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
|
|
16
|
-
|
|
17
|
-
let reposConfig: ReposConfig;
|
|
18
|
-
let currentCategory: string;
|
|
19
|
-
|
|
20
|
-
async function loadConfig() {
|
|
21
|
-
const config = readConfig();
|
|
22
|
-
reposConfig = config.repos;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function searchCategories(answers: any, input = '') {
|
|
26
|
-
if (!reposConfig) {
|
|
27
|
-
await loadConfig();
|
|
28
|
-
}
|
|
29
|
-
const categories = Object.keys(reposConfig);
|
|
30
|
-
|
|
31
|
-
if (!input) {
|
|
32
|
-
return categories;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return categories.filter(category =>
|
|
36
|
-
category.toLowerCase().includes(input.toLowerCase())
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function searchProjects(answers: any, input = '') {
|
|
41
|
-
if (!reposConfig) {
|
|
42
|
-
await loadConfig();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Get projects for the selected category
|
|
46
|
-
if (!currentCategory || !reposConfig[currentCategory]) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const projects = Object.entries(reposConfig[currentCategory])
|
|
51
|
-
.map(([name, path]) => ({
|
|
52
|
-
name,
|
|
53
|
-
path,
|
|
54
|
-
display: `${name} (${path})`
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
if (!input) {
|
|
58
|
-
return projects.map(p => p.name);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const searchTerm = input.toLowerCase();
|
|
62
|
-
return projects
|
|
63
|
-
.filter(project =>
|
|
64
|
-
project.name.toLowerCase().includes(searchTerm) ||
|
|
65
|
-
project.path.toLowerCase().includes(searchTerm)
|
|
66
|
-
)
|
|
67
|
-
.map(p => p.name);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const openIDE = async (ideType: 'cursor' | 'claude') => {
|
|
71
|
-
try {
|
|
72
|
-
await loadConfig();
|
|
73
|
-
|
|
74
|
-
// First level selection with search
|
|
75
|
-
const firstLevelAnswer = await inquirer.prompt([
|
|
76
|
-
{
|
|
77
|
-
type: 'autocomplete',
|
|
78
|
-
name: 'category',
|
|
79
|
-
message: 'Select a category:',
|
|
80
|
-
source: searchCategories,
|
|
81
|
-
pageSize: 10,
|
|
82
|
-
}
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
currentCategory = firstLevelAnswer.category;
|
|
86
|
-
|
|
87
|
-
// Second level selection with search
|
|
88
|
-
const secondLevelAnswer = await inquirer.prompt([
|
|
89
|
-
{
|
|
90
|
-
type: 'autocomplete',
|
|
91
|
-
name: 'project',
|
|
92
|
-
message: 'Select a project:',
|
|
93
|
-
source: searchProjects,
|
|
94
|
-
pageSize: 10,
|
|
95
|
-
}
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
const selectedProject = secondLevelAnswer.project;
|
|
99
|
-
const projectPath = reposConfig[currentCategory][selectedProject];
|
|
100
|
-
|
|
101
|
-
if(ideType === 'claude') {
|
|
102
|
-
// Use spawn with inherited stdio to support interactive TTY
|
|
103
|
-
const claudeProcess = spawn('claude', [], {
|
|
104
|
-
cwd: projectPath,
|
|
105
|
-
stdio: 'inherit', // Inherit stdin, stdout, stderr from parent process
|
|
106
|
-
shell: true,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Wait for the process to exit
|
|
110
|
-
await new Promise<void>((resolve, reject) => {
|
|
111
|
-
claudeProcess.on('close', (code) => {
|
|
112
|
-
if (code === 0) {
|
|
113
|
-
resolve();
|
|
114
|
-
} else {
|
|
115
|
-
reject(new Error(`Claude process exited with code ${code}`));
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
claudeProcess.on('error', (error) => {
|
|
119
|
-
reject(error);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
// Open project in IDE
|
|
125
|
-
await $`${ideType} ${projectPath}`;
|
|
126
|
-
console.log(chalk.green(`Opening ${selectedProject} in ${ideType}...`));
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error(chalk.red(`Error opening project in ${ideType}:`, error));
|
|
129
|
-
}
|
|
130
|
-
};
|