hsh19900502 1.0.22 → 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 +2 -4
- package/dist/commands/ide/index.d.ts +2 -0
- package/dist/commands/ide/index.js +161 -0
- package/dist/hsh.js +7 -5
- package/docs/FUZZY_SEARCH_FEATURE.md +214 -0
- package/package.json +1 -1
- package/src/commands/ide/index.ts +197 -0
- package/src/hsh.ts +7 -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,7 @@ 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,
|
|
50
|
+
│ ├── ide.ts # IDE integration (cursor, claude)
|
|
51
51
|
│ └── mcp.ts # MCP server synchronization
|
|
52
52
|
├── types/
|
|
53
53
|
│ └── index.ts # TypeScript type definitions
|
|
@@ -90,7 +90,6 @@ The CLI follows a modular command structure using Commander.js:
|
|
|
90
90
|
### IDE Integration
|
|
91
91
|
|
|
92
92
|
- `cursor`: Open project in Cursor editor with config-based project selection
|
|
93
|
-
- `surf`: Open project in Windsurf editor with config-based project selection
|
|
94
93
|
|
|
95
94
|
### MCP Server Management
|
|
96
95
|
|
|
@@ -104,7 +103,7 @@ The CLI follows a modular command structure using Commander.js:
|
|
|
104
103
|
|
|
105
104
|
### IDE Configuration
|
|
106
105
|
|
|
107
|
-
The `cursor` and `
|
|
106
|
+
The `cursor` and `claude` commands require a configuration file at `~/hsh.config.json`:
|
|
108
107
|
|
|
109
108
|
```json
|
|
110
109
|
{
|
|
@@ -160,7 +159,6 @@ Example `~/.mcp/servers.json`:
|
|
|
160
159
|
- **git**: For all git operations
|
|
161
160
|
- **glab**: GitLab CLI for merge request creation (used in `mr create`)
|
|
162
161
|
- **cursor**: Cursor editor executable (for `cursor` command)
|
|
163
|
-
- **surf**: Windsurf editor executable (for `surf` command)
|
|
164
162
|
|
|
165
163
|
### Shell Integration
|
|
166
164
|
|
|
@@ -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
|
+
};
|
package/dist/hsh.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
8
|
import { syncMcpServers } from './commands/mcp.js';
|
|
9
9
|
const packageJson = getPackageJson();
|
|
@@ -58,13 +58,15 @@ mono.command('cd').description('cd into repo')
|
|
|
58
58
|
});
|
|
59
59
|
program.command('cursor')
|
|
60
60
|
.description('open project in Cursor')
|
|
61
|
-
.
|
|
62
|
-
|
|
61
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
62
|
+
.action(async (search) => {
|
|
63
|
+
await openIDE('cursor', search);
|
|
63
64
|
});
|
|
64
65
|
program.command('claude')
|
|
65
66
|
.description('open project in Claude')
|
|
66
|
-
.
|
|
67
|
-
|
|
67
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
68
|
+
.action(async (search) => {
|
|
69
|
+
await openIDE('claude', search);
|
|
68
70
|
});
|
|
69
71
|
// Cloud infrastructure management commands
|
|
70
72
|
const cloudCommand = program
|
|
@@ -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
|
+
};
|
package/src/hsh.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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
9
|
import { syncMcpServers } from './commands/mcp.js';
|
|
10
10
|
|
|
@@ -71,14 +71,16 @@ mono.command('cd').description('cd into repo')
|
|
|
71
71
|
|
|
72
72
|
program.command('cursor')
|
|
73
73
|
.description('open project in Cursor')
|
|
74
|
-
.
|
|
75
|
-
|
|
74
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
75
|
+
.action(async (search?: string) => {
|
|
76
|
+
await openIDE('cursor', search);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
program.command('claude')
|
|
79
80
|
.description('open project in Claude')
|
|
80
|
-
.
|
|
81
|
-
|
|
81
|
+
.argument('[search]', 'optional search keyword for fuzzy search')
|
|
82
|
+
.action(async (search?: string) => {
|
|
83
|
+
await openIDE('claude', search);
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
// Cloud infrastructure management commands
|
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
|
-
};
|