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.
- package/.claude/commands/speckit.analyze.md +184 -0
- package/.claude/commands/speckit.checklist.md +294 -0
- package/.claude/commands/speckit.clarify.md +177 -0
- package/.claude/commands/speckit.constitution.md +78 -0
- package/.claude/commands/speckit.implement.md +121 -0
- package/.claude/commands/speckit.plan.md +81 -0
- package/.claude/commands/speckit.specify.md +204 -0
- package/.claude/commands/speckit.tasks.md +108 -0
- package/.claude/settings.local.json +23 -0
- package/.prettierignore +5 -0
- package/.prettierrc.json +10 -0
- package/.specify/memory/constitution.md +72 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +113 -0
- package/.specify/scripts/bash/create-new-feature.sh +97 -0
- package/.specify/scripts/bash/setup-plan.sh +60 -0
- package/.specify/scripts/bash/update-agent-context.sh +738 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +111 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +250 -0
- package/CLAUDE.md +199 -0
- package/PRD.md +268 -0
- package/README.md +171 -0
- package/dist/ai-summon.d.ts +2 -0
- package/dist/ai-summon.js +73 -0
- package/dist/commands/ide/index.d.ts +3 -0
- package/dist/commands/ide/index.js +253 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +55 -0
- package/dist/commands/url.d.ts +4 -0
- package/dist/commands/url.js +223 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.js +1 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.js +109 -0
- package/eslint.config.js +47 -0
- package/package.json +47 -0
- package/specs/001-cloud-login-feature/contracts/cloud-command.ts +82 -0
- package/specs/001-cloud-login-feature/contracts/config-service.ts +170 -0
- package/specs/001-cloud-login-feature/data-model.md +269 -0
- package/specs/001-cloud-login-feature/plan.md +91 -0
- package/specs/001-cloud-login-feature/quickstart.md +366 -0
- package/specs/001-cloud-login-feature/research.md +290 -0
- package/specs/001-cloud-login-feature/spec.md +195 -0
- package/specs/001-cloud-login-feature/tasks.md +235 -0
- package/specs/001-cloud-scp-command/contracts/cloud-scp-api.ts +402 -0
- package/specs/001-cloud-scp-command/data-model.md +424 -0
- package/specs/001-cloud-scp-command/plan.md +124 -0
- package/specs/001-cloud-scp-command/quickstart.md +536 -0
- package/specs/001-cloud-scp-command/research.md +345 -0
- package/specs/001-cloud-scp-command/spec.md +248 -0
- package/specs/001-cloud-scp-command/tasks.md +434 -0
- package/src/ai-summon.ts +88 -0
- package/src/commands/ide/index.ts +322 -0
- package/src/commands/init.ts +64 -0
- package/src/commands/url.ts +262 -0
- package/src/types/index.ts +49 -0
- package/src/util.ts +146 -0
- 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,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,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[];
|