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 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
- └── ide.ts # IDE integration (cursor, surf)
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
- - `surf`: Open project in Windsurf editor with config-based project selection
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 `surf` commands require a configuration file at `~/hsh.config.json`:
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,2 @@
1
+ import 'zx/globals';
2
+ export declare const openIDE: (ideType: "cursor" | "claude", searchMode?: string) => Promise<void>;
@@ -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,4 @@
1
+ /**
2
+ * Sync MCP servers from ~/.mcp/servers.json to all projects in ~/.claude.json
3
+ */
4
+ export declare function syncMcpServers(): Promise<void>;
@@ -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
- .action(async () => {
61
- await openIDE('cursor');
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
- .action(async () => {
66
- await openIDE('claude');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsh19900502",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "index.js",
@@ -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
- .action(async () => {
74
- await openIDE('cursor');
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
- .action(async () => {
80
- await openIDE('claude');
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();
@@ -1,2 +0,0 @@
1
- import 'zx/globals';
2
- export declare const openIDE: (ideType: "cursor" | "claude") => Promise<void>;
@@ -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
- };
@@ -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
- };