ai-summon 0.0.1

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.
Files changed (61) hide show
  1. package/.claude/commands/speckit.analyze.md +184 -0
  2. package/.claude/commands/speckit.checklist.md +294 -0
  3. package/.claude/commands/speckit.clarify.md +177 -0
  4. package/.claude/commands/speckit.constitution.md +78 -0
  5. package/.claude/commands/speckit.implement.md +121 -0
  6. package/.claude/commands/speckit.plan.md +81 -0
  7. package/.claude/commands/speckit.specify.md +204 -0
  8. package/.claude/commands/speckit.tasks.md +108 -0
  9. package/.claude/settings.local.json +23 -0
  10. package/.prettierignore +5 -0
  11. package/.prettierrc.json +10 -0
  12. package/.specify/memory/constitution.md +72 -0
  13. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  14. package/.specify/scripts/bash/common.sh +113 -0
  15. package/.specify/scripts/bash/create-new-feature.sh +97 -0
  16. package/.specify/scripts/bash/setup-plan.sh +60 -0
  17. package/.specify/scripts/bash/update-agent-context.sh +738 -0
  18. package/.specify/templates/agent-file-template.md +28 -0
  19. package/.specify/templates/checklist-template.md +40 -0
  20. package/.specify/templates/plan-template.md +111 -0
  21. package/.specify/templates/spec-template.md +115 -0
  22. package/.specify/templates/tasks-template.md +250 -0
  23. package/CLAUDE.md +199 -0
  24. package/PRD.md +268 -0
  25. package/README.md +171 -0
  26. package/dist/ai-summon.d.ts +2 -0
  27. package/dist/ai-summon.js +73 -0
  28. package/dist/commands/ide/index.d.ts +3 -0
  29. package/dist/commands/ide/index.js +253 -0
  30. package/dist/commands/init.d.ts +4 -0
  31. package/dist/commands/init.js +55 -0
  32. package/dist/commands/url.d.ts +4 -0
  33. package/dist/commands/url.js +223 -0
  34. package/dist/types/index.d.ts +40 -0
  35. package/dist/types/index.js +1 -0
  36. package/dist/util.d.ts +16 -0
  37. package/dist/util.js +109 -0
  38. package/eslint.config.js +47 -0
  39. package/package.json +47 -0
  40. package/specs/001-cloud-login-feature/contracts/cloud-command.ts +82 -0
  41. package/specs/001-cloud-login-feature/contracts/config-service.ts +170 -0
  42. package/specs/001-cloud-login-feature/data-model.md +269 -0
  43. package/specs/001-cloud-login-feature/plan.md +91 -0
  44. package/specs/001-cloud-login-feature/quickstart.md +366 -0
  45. package/specs/001-cloud-login-feature/research.md +290 -0
  46. package/specs/001-cloud-login-feature/spec.md +195 -0
  47. package/specs/001-cloud-login-feature/tasks.md +235 -0
  48. package/specs/001-cloud-scp-command/contracts/cloud-scp-api.ts +402 -0
  49. package/specs/001-cloud-scp-command/data-model.md +424 -0
  50. package/specs/001-cloud-scp-command/plan.md +124 -0
  51. package/specs/001-cloud-scp-command/quickstart.md +536 -0
  52. package/specs/001-cloud-scp-command/research.md +345 -0
  53. package/specs/001-cloud-scp-command/spec.md +248 -0
  54. package/specs/001-cloud-scp-command/tasks.md +434 -0
  55. package/src/ai-summon.ts +88 -0
  56. package/src/commands/ide/index.ts +322 -0
  57. package/src/commands/init.ts +64 -0
  58. package/src/commands/url.ts +262 -0
  59. package/src/types/index.ts +49 -0
  60. package/src/util.ts +146 -0
  61. package/tsconfig.json +21 -0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { getPackageJson } from './util.js';
4
+ import { openIDE, refreshIdeReposCache } from './commands/ide/index.js';
5
+ import { addUrl, openUrlGroup, removeUrl, searchAndOpenUrl } from './commands/url.js';
6
+ import { initConfig } from './commands/init.js';
7
+ const packageJson = getPackageJson();
8
+ const program = new Command();
9
+ program.usage('<command> [options]');
10
+ program.version(packageJson.version);
11
+ program
12
+ .command('init')
13
+ .description('initialize ~/.ai/config.json (prompts for workingDirectory)')
14
+ .option('-w, --working-directory <path>', 'set workingDirectory without prompting')
15
+ .option('-f, --force', 'overwrite existing config without confirmation')
16
+ .action(async (options) => {
17
+ await initConfig({ workingDirectory: options.workingDirectory, force: options.force });
18
+ });
19
+ const cursor = program.command('cursor').description('open project in Cursor');
20
+ cursor
21
+ .argument('[search]', 'optional search keyword for fuzzy search')
22
+ .action(async (search) => {
23
+ await openIDE('cursor', search);
24
+ });
25
+ cursor
26
+ .command('refresh')
27
+ .description('refresh cached auto-discovered repositories (workingDirectory mode)')
28
+ .action(async () => {
29
+ await refreshIdeReposCache();
30
+ });
31
+ const claude = program.command('claude').description('open project in Claude');
32
+ claude
33
+ .argument('[search]', 'optional search keyword for fuzzy search')
34
+ .action(async (search) => {
35
+ await openIDE('claude', search);
36
+ });
37
+ claude
38
+ .command('refresh')
39
+ .description('refresh cached auto-discovered repositories (workingDirectory mode)')
40
+ .action(async () => {
41
+ await refreshIdeReposCache();
42
+ });
43
+ // URL management commands
44
+ const urlCommand = program.command('url').description('URL bookmark management');
45
+ urlCommand
46
+ .command('add')
47
+ .description('Add a new URL bookmark')
48
+ .argument('<name>', 'Name of the URL bookmark')
49
+ .argument('<url>', 'URL to bookmark')
50
+ .action(async (name, url) => {
51
+ await addUrl(name, url);
52
+ });
53
+ urlCommand
54
+ .command('remove')
55
+ .description('Remove a URL bookmark')
56
+ .argument('[name]', 'Name of the URL bookmark to remove (optional, will enter search mode if not provided)')
57
+ .action(async (name) => {
58
+ await removeUrl(name);
59
+ });
60
+ urlCommand
61
+ .command('search')
62
+ .description('Search and open a URL bookmark in Chrome')
63
+ .option('--suppress', 'Auto-dismiss popups by simulating Enter key after 1s')
64
+ .action(async (options) => {
65
+ await searchAndOpenUrl(options.suppress);
66
+ });
67
+ urlCommand
68
+ .command('group')
69
+ .description('Select and open a URL group in a new Chrome window')
70
+ .action(async () => {
71
+ await openUrlGroup();
72
+ });
73
+ program.parse();
@@ -0,0 +1,3 @@
1
+ import 'zx/globals';
2
+ export declare const openIDE: (ideType: "cursor" | "claude", searchMode?: string) => Promise<void>;
3
+ export declare const refreshIdeReposCache: () => Promise<void>;
@@ -0,0 +1,253 @@
1
+ import inquirer from 'inquirer';
2
+ import 'zx/globals';
3
+ import chalk from 'chalk';
4
+ import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
5
+ import { readConfig, findGitRepositories, readIdeReposCache, writeIdeReposCache, } from '../../util.js';
6
+ import { spawn } from 'child_process';
7
+ import { existsSync } from 'fs';
8
+ // Register autocomplete prompt
9
+ inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
10
+ let reposConfig;
11
+ let currentCategory;
12
+ let workingDirectory;
13
+ let autoDiscoveredRepos = [];
14
+ async function loadConfig(options) {
15
+ const config = readConfig();
16
+ // Check if workingDirectory is configured
17
+ if (config.workingDirectory) {
18
+ workingDirectory = config.workingDirectory;
19
+ // Validate that workingDirectory exists
20
+ if (!existsSync(workingDirectory)) {
21
+ throw new Error(`Working directory does not exist: ${workingDirectory}`);
22
+ }
23
+ if (options?.refreshReposCache) {
24
+ // Force refresh: rescan and overwrite cache
25
+ autoDiscoveredRepos = findGitRepositories(workingDirectory);
26
+ writeIdeReposCache(workingDirectory, autoDiscoveredRepos);
27
+ }
28
+ else {
29
+ // Default: use cache if possible (fast path)
30
+ const cached = readIdeReposCache(workingDirectory);
31
+ if (cached) {
32
+ autoDiscoveredRepos = cached;
33
+ }
34
+ else {
35
+ // Warm the cache on first run
36
+ autoDiscoveredRepos = findGitRepositories(workingDirectory);
37
+ writeIdeReposCache(workingDirectory, autoDiscoveredRepos);
38
+ }
39
+ }
40
+ // Sort alphabetically by name for better UX
41
+ autoDiscoveredRepos.sort((a, b) => a.name.localeCompare(b.name));
42
+ }
43
+ else {
44
+ // Use manual configuration (existing behavior)
45
+ reposConfig = config.repos;
46
+ }
47
+ }
48
+ async function searchCategories(_answers, input = '') {
49
+ if (!reposConfig) {
50
+ await loadConfig();
51
+ }
52
+ const categories = Object.keys(reposConfig);
53
+ if (!input) {
54
+ return categories;
55
+ }
56
+ return categories.filter((category) => category.toLowerCase().includes(input.toLowerCase()));
57
+ }
58
+ async function searchProjects(_answers, input = '') {
59
+ if (!reposConfig) {
60
+ await loadConfig();
61
+ }
62
+ // Get projects for the selected category
63
+ if (!currentCategory || !reposConfig[currentCategory]) {
64
+ return [];
65
+ }
66
+ const projects = Object.entries(reposConfig[currentCategory]).map(([name, path]) => ({
67
+ name,
68
+ path,
69
+ display: `${name} (${path})`,
70
+ }));
71
+ if (!input) {
72
+ return projects.map((p) => p.name);
73
+ }
74
+ const searchTerm = input.toLowerCase();
75
+ return projects
76
+ .filter((project) => project.name.toLowerCase().includes(searchTerm) ||
77
+ project.path.toLowerCase().includes(searchTerm))
78
+ .map((p) => p.name);
79
+ }
80
+ // Flatten all projects with category context for fuzzy search
81
+ function getAllProjects() {
82
+ // Auto-discovery mode: use discovered Git repos
83
+ if (workingDirectory) {
84
+ return autoDiscoveredRepos.map((repo) => ({
85
+ category: repo.topLevelFolder,
86
+ name: repo.name,
87
+ path: repo.path,
88
+ display: `${repo.name} (${repo.path})`,
89
+ }));
90
+ }
91
+ // Manual configuration mode
92
+ if (!reposConfig)
93
+ return [];
94
+ return Object.entries(reposConfig).flatMap(([category, projects]) => Object.entries(projects).map(([name, path]) => ({
95
+ category,
96
+ name,
97
+ path,
98
+ display: `${name} (${category})`,
99
+ })));
100
+ }
101
+ // Single-keyword fuzzy search across all projects
102
+ async function searchAllProjects(_answers, input = '') {
103
+ const allProjects = getAllProjects();
104
+ // Filter projects based on search input
105
+ let filteredProjects = allProjects;
106
+ if (input) {
107
+ const keywords = input.toLowerCase().trim().split(/\s+/);
108
+ filteredProjects = allProjects.filter((project) => {
109
+ const searchText = `${project.name} ${project.category} ${project.path}`.toLowerCase();
110
+ return keywords.every((keyword) => searchText.includes(keyword));
111
+ });
112
+ }
113
+ // If auto-discovery mode, add category separators
114
+ if (workingDirectory) {
115
+ return formatProjectsWithSeparators(filteredProjects);
116
+ }
117
+ // Manual mode: simple display
118
+ return filteredProjects.map((p) => ({ name: p.display, value: p }));
119
+ }
120
+ // Format projects with top-level folder separators (for auto-discovery mode)
121
+ function formatProjectsWithSeparators(projects) {
122
+ // Group projects by category (top-level folder)
123
+ const grouped = new Map();
124
+ projects.forEach((project) => {
125
+ const category = project.category;
126
+ if (!grouped.has(category)) {
127
+ grouped.set(category, []);
128
+ }
129
+ grouped.get(category).push(project);
130
+ });
131
+ // Sort categories alphabetically, but put root (/) last
132
+ const sortedCategories = Array.from(grouped.keys()).sort((a, b) => {
133
+ if (a === '/')
134
+ return 1;
135
+ if (b === '/')
136
+ return 1;
137
+ return a.localeCompare(b);
138
+ });
139
+ // Build result with separators
140
+ const result = [];
141
+ sortedCategories.forEach((category) => {
142
+ const categoryProjects = grouped.get(category);
143
+ // Add category separator
144
+ const categoryLabel = category === '/' ? '/ (root)' : category;
145
+ result.push({
146
+ name: `--------- ${categoryLabel} ---------`,
147
+ disabled: 'separator',
148
+ });
149
+ // Add projects in this category
150
+ categoryProjects.forEach((p) => {
151
+ result.push({
152
+ name: ` ${p.display}`,
153
+ value: p,
154
+ });
155
+ });
156
+ });
157
+ return result;
158
+ }
159
+ // Select project using fuzzy search across all categories
160
+ async function selectProjectWithFuzzySearch(searchMode) {
161
+ const answer = await inquirer.prompt([
162
+ {
163
+ type: 'autocomplete',
164
+ name: 'project',
165
+ message: `Search and select a project${searchMode ? ` (filtering: ${searchMode})` : ''}:`,
166
+ source: searchAllProjects,
167
+ pageSize: 15,
168
+ },
169
+ ]);
170
+ return {
171
+ path: answer.project.path,
172
+ name: answer.project.name,
173
+ };
174
+ }
175
+ // Select project using two-step category then project selection
176
+ async function selectProjectWithTwoStep() {
177
+ const categoryAnswer = await inquirer.prompt([
178
+ {
179
+ type: 'autocomplete',
180
+ name: 'category',
181
+ message: 'Select a category:',
182
+ source: searchCategories,
183
+ pageSize: 10,
184
+ },
185
+ ]);
186
+ currentCategory = categoryAnswer.category;
187
+ const projectAnswer = await inquirer.prompt([
188
+ {
189
+ type: 'autocomplete',
190
+ name: 'project',
191
+ message: 'Select a project:',
192
+ source: searchProjects,
193
+ pageSize: 10,
194
+ },
195
+ ]);
196
+ return {
197
+ path: reposConfig[currentCategory][projectAnswer.project],
198
+ name: projectAnswer.project,
199
+ };
200
+ }
201
+ // Launch Claude IDE with proper TTY handling
202
+ async function launchClaude(projectPath) {
203
+ const claudeProcess = spawn('claude', [], {
204
+ cwd: projectPath,
205
+ stdio: 'inherit',
206
+ shell: true,
207
+ });
208
+ await new Promise((resolve, reject) => {
209
+ claudeProcess.on('close', (code) => {
210
+ if (code === 0) {
211
+ resolve();
212
+ }
213
+ else {
214
+ reject(new Error(`Claude process exited with code ${code}`));
215
+ }
216
+ });
217
+ claudeProcess.on('error', (error) => {
218
+ reject(error);
219
+ });
220
+ });
221
+ }
222
+ // Launch Cursor or other IDE
223
+ async function launchIDE(ideType, projectPath, projectName) {
224
+ await $ `${ideType} ${projectPath}`;
225
+ console.log(chalk.green(`Opening ${projectName} in ${ideType}...`));
226
+ }
227
+ export const openIDE = async (ideType, searchMode) => {
228
+ try {
229
+ await loadConfig();
230
+ // In auto-discovery mode, always use fuzzy search (no categories)
231
+ const project = workingDirectory || searchMode !== undefined
232
+ ? await selectProjectWithFuzzySearch(searchMode)
233
+ : await selectProjectWithTwoStep();
234
+ // Launch IDE based on type
235
+ if (ideType === 'claude') {
236
+ await launchClaude(project.path);
237
+ }
238
+ else {
239
+ await launchIDE(ideType, project.path, project.name);
240
+ }
241
+ }
242
+ catch (error) {
243
+ console.error(chalk.red(`Error opening project in ${ideType}:`, error));
244
+ }
245
+ };
246
+ export const refreshIdeReposCache = async () => {
247
+ await loadConfig({ refreshReposCache: true });
248
+ if (!workingDirectory) {
249
+ console.log(chalk.yellow('No workingDirectory configured; nothing to refresh.'));
250
+ return;
251
+ }
252
+ console.log(chalk.green('IDE repository cache refreshed.'));
253
+ };
@@ -0,0 +1,4 @@
1
+ export declare function initConfig(options?: {
2
+ workingDirectory?: string;
3
+ force?: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { existsSync, writeFileSync, statSync } from 'fs';
4
+ import { getConfigPath } from '../util.js';
5
+ export async function initConfig(options) {
6
+ const configPath = getConfigPath();
7
+ if (existsSync(configPath) && !options?.force) {
8
+ const { overwrite } = await inquirer.prompt([
9
+ {
10
+ type: 'confirm',
11
+ name: 'overwrite',
12
+ message: `Config already exists at ${configPath}. Overwrite?`,
13
+ default: false,
14
+ },
15
+ ]);
16
+ if (!overwrite) {
17
+ console.log(chalk.yellow('Init cancelled.'));
18
+ return;
19
+ }
20
+ }
21
+ const workingDirectory = options?.workingDirectory ??
22
+ (await inquirer.prompt([
23
+ {
24
+ type: 'input',
25
+ name: 'workingDirectory',
26
+ message: 'workingDirectory (a folder containing your git repos):',
27
+ default: process.cwd(),
28
+ validate: (input) => {
29
+ const value = input.trim();
30
+ if (!value)
31
+ return 'workingDirectory is required';
32
+ if (!existsSync(value))
33
+ return `Path does not exist: ${value}`;
34
+ try {
35
+ if (!statSync(value).isDirectory())
36
+ return `Not a directory: ${value}`;
37
+ }
38
+ catch {
39
+ return `Cannot access path: ${value}`;
40
+ }
41
+ return true;
42
+ },
43
+ filter: (input) => input.trim(),
44
+ },
45
+ ])).workingDirectory;
46
+ const payload = {
47
+ workingDirectory,
48
+ repos: {},
49
+ yiren: {},
50
+ urls: {},
51
+ urlGroups: {},
52
+ };
53
+ writeFileSync(configPath, JSON.stringify(payload, null, 2), 'utf-8');
54
+ console.log(chalk.green(`✅ Wrote config: ${configPath}`));
55
+ }
@@ -0,0 +1,4 @@
1
+ export declare function addUrl(name: string, url: string): Promise<void>;
2
+ export declare function removeUrl(name?: string): Promise<void>;
3
+ export declare function searchAndOpenUrl(suppress?: boolean): Promise<void>;
4
+ export declare function openUrlGroup(): Promise<void>;
@@ -0,0 +1,223 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
4
+ import { getConfigPath, readConfig } from '../util.js';
5
+ import { writeFileSync } from 'fs';
6
+ import { $ } from 'zx';
7
+ // Register autocomplete prompt
8
+ inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
9
+ // Read and write config helpers
10
+ function readFullConfig() {
11
+ const config = readConfig();
12
+ if (!config.urls) {
13
+ config.urls = {};
14
+ }
15
+ return config;
16
+ }
17
+ function writeConfig(config) {
18
+ // Sort URLs by domain before writing
19
+ if (config.urls) {
20
+ const sortedUrls = {};
21
+ // Create entries with extracted domains for sorting
22
+ const entries = Object.entries(config.urls).map(([name, url]) => {
23
+ // Extract domain from URL
24
+ let domain = '';
25
+ try {
26
+ const urlObj = new URL(url);
27
+ domain = urlObj.hostname;
28
+ }
29
+ catch {
30
+ // If URL parsing fails, use the URL string itself for sorting
31
+ domain = url;
32
+ }
33
+ return { name, url, domain };
34
+ });
35
+ // Sort by domain, then by name
36
+ entries.sort((a, b) => {
37
+ const domainCompare = a.domain.localeCompare(b.domain);
38
+ if (domainCompare !== 0)
39
+ return domainCompare;
40
+ return a.name.localeCompare(b.name);
41
+ });
42
+ // Rebuild the urls object with sorted entries
43
+ for (const entry of entries) {
44
+ sortedUrls[entry.name] = entry.url;
45
+ }
46
+ config.urls = sortedUrls;
47
+ }
48
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), 'utf-8');
49
+ }
50
+ // Add a new URL
51
+ export async function addUrl(name, url) {
52
+ try {
53
+ const config = readFullConfig();
54
+ if (config.urls[name]) {
55
+ console.log(chalk.yellow(`⚠️ URL with name "${name}" already exists: ${config.urls[name]}`));
56
+ const answer = await inquirer.prompt([
57
+ {
58
+ type: 'confirm',
59
+ name: 'overwrite',
60
+ message: 'Do you want to overwrite it?',
61
+ default: false,
62
+ },
63
+ ]);
64
+ if (!answer.overwrite) {
65
+ console.log(chalk.blue('ℹ️ Operation cancelled.'));
66
+ return;
67
+ }
68
+ }
69
+ config.urls[name] = url;
70
+ writeConfig(config);
71
+ console.log(chalk.green(`✅ Added URL: ${name} → ${url}`));
72
+ }
73
+ catch (error) {
74
+ console.error(chalk.red('Error adding URL:'), error);
75
+ }
76
+ }
77
+ // Search URLs and return selected entry
78
+ async function searchUrls(_answers, input = '') {
79
+ const config = readFullConfig();
80
+ const urls = config.urls || {};
81
+ const entries = Object.entries(urls).map(([name, url]) => ({
82
+ name,
83
+ url,
84
+ display: `${name} - ${url}`,
85
+ }));
86
+ if (!input) {
87
+ return entries.map((entry) => ({
88
+ name: entry.display,
89
+ value: { name: entry.name, url: entry.url },
90
+ }));
91
+ }
92
+ // Multi-keyword search: split by spaces and match all keywords
93
+ const keywords = input.toLowerCase().trim().split(/\s+/);
94
+ return entries
95
+ .filter((entry) => {
96
+ const searchText = `${entry.name} ${entry.url}`.toLowerCase();
97
+ return keywords.every((keyword) => searchText.includes(keyword));
98
+ })
99
+ .map((entry) => ({
100
+ name: entry.display,
101
+ value: { name: entry.name, url: entry.url },
102
+ }));
103
+ }
104
+ // Remove a URL
105
+ export async function removeUrl(name) {
106
+ try {
107
+ const config = readFullConfig();
108
+ if (!config.urls || Object.keys(config.urls).length === 0) {
109
+ console.log(chalk.yellow('⚠️ No URLs found in configuration.'));
110
+ return;
111
+ }
112
+ let targetName;
113
+ if (name) {
114
+ // Direct removal with name provided
115
+ if (!config.urls[name]) {
116
+ console.log(chalk.yellow(`⚠️ URL with name "${name}" not found.`));
117
+ return;
118
+ }
119
+ targetName = name;
120
+ }
121
+ else {
122
+ // Search mode
123
+ const answer = await inquirer.prompt([
124
+ {
125
+ type: 'autocomplete',
126
+ name: 'selected',
127
+ message: 'Search and select a URL to remove:',
128
+ source: searchUrls,
129
+ pageSize: 15,
130
+ },
131
+ ]);
132
+ targetName = answer.selected.name;
133
+ }
134
+ const removedUrl = config.urls[targetName];
135
+ delete config.urls[targetName];
136
+ writeConfig(config);
137
+ console.log(chalk.green(`✅ Removed URL: ${targetName} - ${removedUrl}`));
138
+ }
139
+ catch (error) {
140
+ console.error(chalk.red('Error removing URL:'), error);
141
+ }
142
+ }
143
+ // Search and open URL in Chrome
144
+ export async function searchAndOpenUrl(suppress) {
145
+ try {
146
+ const config = readFullConfig();
147
+ if (!config.urls || Object.keys(config.urls).length === 0) {
148
+ console.log(chalk.yellow('⚠️ No URLs found in configuration.'));
149
+ return;
150
+ }
151
+ const answer = await inquirer.prompt([
152
+ {
153
+ type: 'autocomplete',
154
+ name: 'selected',
155
+ message: 'Search and select a URL to open:',
156
+ source: searchUrls,
157
+ pageSize: 15,
158
+ },
159
+ ]);
160
+ const selectedUrl = answer.selected.url;
161
+ console.log(chalk.blue(`🌐 Opening ${answer.selected.name}: ${selectedUrl}`));
162
+ // Open URL in Chrome (works on macOS, Linux, and Windows)
163
+ await $ `open -a "Google Chrome" ${selectedUrl}`;
164
+ console.log(chalk.green('✅ URL opened in Chrome'));
165
+ // Auto-dismiss popups if --suppress flag is enabled
166
+ if (suppress) {
167
+ console.log(chalk.blue('⏳ Waiting 1s before dismissing popup...'));
168
+ // Wait 1 second
169
+ await new Promise((resolve) => setTimeout(resolve, 1000));
170
+ // Use osascript to simulate Enter key press on macOS
171
+ await $ `osascript -e 'tell application "System Events" to keystroke return'`;
172
+ // Wait 1 second
173
+ await new Promise((resolve) => setTimeout(resolve, 1000));
174
+ // Use osascript to simulate Enter key press on macOS
175
+ await $ `osascript -e 'tell application "System Events" to keystroke return'`;
176
+ console.log(chalk.green('✅ Popup dismissed with Enter key'));
177
+ }
178
+ }
179
+ catch (error) {
180
+ console.error(chalk.red('Error opening URL:'), error);
181
+ }
182
+ }
183
+ // Open URL group in a new Chrome window
184
+ export async function openUrlGroup() {
185
+ try {
186
+ const config = readFullConfig();
187
+ if (!config.urlGroups || Object.keys(config.urlGroups).length === 0) {
188
+ console.log(chalk.yellow('⚠️ No URL groups found in configuration.'));
189
+ return;
190
+ }
191
+ const groupNames = Object.keys(config.urlGroups);
192
+ const answer = await inquirer.prompt([
193
+ {
194
+ type: 'list',
195
+ name: 'selectedGroup',
196
+ message: 'Select a URL group to open:',
197
+ choices: groupNames,
198
+ },
199
+ ]);
200
+ const selectedGroupName = answer.selectedGroup;
201
+ const urls = config.urlGroups[selectedGroupName];
202
+ if (!urls || urls.length === 0) {
203
+ console.log(chalk.yellow(`⚠️ No URLs found in group "${selectedGroupName}".`));
204
+ return;
205
+ }
206
+ console.log(chalk.blue(`🌐 Opening ${urls.length} URLs from group "${selectedGroupName}"...`));
207
+ // Open all URLs in a new Chrome window
208
+ // First URL opens in a new window
209
+ await $ `open -na "Google Chrome" --args --new-window ${urls[0]}`;
210
+ // Add a small delay to ensure Chrome window is ready
211
+ await new Promise((resolve) => setTimeout(resolve, 500));
212
+ // Open remaining URLs as tabs in the same window
213
+ for (let i = 1; i < urls.length; i++) {
214
+ await $ `open -a "Google Chrome" ${urls[i]}`;
215
+ // Small delay between opening tabs to ensure they all load properly
216
+ await new Promise((resolve) => setTimeout(resolve, 100));
217
+ }
218
+ console.log(chalk.green(`✅ Opened ${urls.length} URLs in Chrome`));
219
+ }
220
+ catch (error) {
221
+ console.error(chalk.red('Error opening URL group:'), error);
222
+ }
223
+ }
@@ -0,0 +1,40 @@
1
+ export type RepoDirLevel = 'root' | 'client' | 'server';
2
+ export interface CloudConfig {
3
+ ip: string;
4
+ privateKeyFile: string;
5
+ }
6
+ export interface ServiceConfig {
7
+ dev: CloudConfig;
8
+ staging: CloudConfig;
9
+ prod: CloudConfig;
10
+ }
11
+ export interface YirenConfig {
12
+ [serviceName: string]: ServiceConfig;
13
+ }
14
+ export interface HshConfig {
15
+ workingDirectory?: string;
16
+ repos: {
17
+ [groupName: string]: {
18
+ [repoName: string]: string;
19
+ };
20
+ };
21
+ yiren: YirenConfig;
22
+ urls?: {
23
+ [name: string]: string;
24
+ };
25
+ urlGroups?: {
26
+ [groupName: string]: string[];
27
+ };
28
+ }
29
+ export type Environment = 'dev' | 'staging' | 'prod';
30
+ export interface ScpOptions {
31
+ env?: Environment;
32
+ service?: string;
33
+ recursive?: boolean;
34
+ }
35
+ export interface PathValidationResult {
36
+ exists: boolean;
37
+ isDirectory: boolean;
38
+ isFile: boolean;
39
+ path: string;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/util.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { HshConfig } from './types/index.js';
2
+ export declare const getPackageJson: () => any;
3
+ export declare const getConfigPath: () => string;
4
+ export declare const readConfig: () => HshConfig;
5
+ /**
6
+ * Recursively scan directory for Git repositories
7
+ * Returns list of { name, path, topLevelFolder } objects
8
+ */
9
+ export interface GitRepository {
10
+ name: string;
11
+ path: string;
12
+ topLevelFolder: string;
13
+ }
14
+ export declare const readIdeReposCache: (workingDirectory: string) => GitRepository[] | null;
15
+ export declare const writeIdeReposCache: (workingDirectory: string, repos: GitRepository[]) => void;
16
+ export declare const findGitRepositories: (workingDirectory: string) => GitRepository[];