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 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, surf)
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 `surf` commands require a configuration file at `~/hsh.config.json`:
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,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
+ };
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
- .action(async () => {
62
- await openIDE('cursor');
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
- .action(async () => {
67
- await openIDE('claude');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsh19900502",
3
- "version": "1.0.22",
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
+ };
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
- .action(async () => {
75
- await openIDE('cursor');
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
- .action(async () => {
81
- await openIDE('claude');
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
@@ -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
- };