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