specweave 0.8.19 ā 0.8.20
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/dist/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
- package/dist/cli/helpers/issue-tracker/ado.js +17 -5
- package/dist/cli/helpers/issue-tracker/ado.js.map +1 -1
- package/dist/cli/helpers/issue-tracker/types.d.ts +3 -0
- package/dist/cli/helpers/issue-tracker/types.d.ts.map +1 -1
- package/dist/cli/helpers/issue-tracker/types.js.map +1 -1
- package/dist/core/credentials-manager.d.ts +3 -0
- package/dist/core/credentials-manager.d.ts.map +1 -1
- package/dist/core/credentials-manager.js +69 -9
- package/dist/core/credentials-manager.js.map +1 -1
- package/dist/integrations/ado/ado-client.d.ts +4 -0
- package/dist/integrations/ado/ado-client.d.ts.map +1 -1
- package/dist/integrations/ado/ado-client.js +18 -0
- package/dist/integrations/ado/ado-client.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave-ado/lib/project-selector.d.ts +42 -0
- package/plugins/specweave-ado/lib/project-selector.d.ts.map +1 -0
- package/plugins/specweave-ado/lib/project-selector.js +211 -0
- package/plugins/specweave-ado/lib/project-selector.js.map +1 -0
- package/plugins/specweave-ado/lib/project-selector.ts +317 -0
- package/plugins/specweave-github/lib/github-client.ts +28 -0
- package/plugins/specweave-github/lib/repo-selector.ts +329 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Repository Selector with Pagination
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Fetches all GitHub repos via gh CLI
|
|
6
|
+
* - Interactive multi-select with search
|
|
7
|
+
* - Manual repo entry (comma-separated, format: owner/repo)
|
|
8
|
+
* - Handles large repo lists (50+ repos)
|
|
9
|
+
* - Validates repo names
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import inquirer from 'inquirer';
|
|
13
|
+
import { GitHubClient } from './github-client.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface RepoSelectionResult {
|
|
20
|
+
selectedRepos: string[]; // Format: "owner/repo"
|
|
21
|
+
method: 'interactive' | 'manual' | 'all';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RepoSelectorOptions {
|
|
25
|
+
/** Allow manual entry of repo names */
|
|
26
|
+
allowManualEntry?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Allow "Select All" option */
|
|
29
|
+
allowSelectAll?: boolean;
|
|
30
|
+
|
|
31
|
+
/** Pre-select these repos (owner/repo format) */
|
|
32
|
+
preSelected?: string[];
|
|
33
|
+
|
|
34
|
+
/** Minimum repos to select (0 = optional) */
|
|
35
|
+
minSelection?: number;
|
|
36
|
+
|
|
37
|
+
/** Maximum repos to select (undefined = unlimited) */
|
|
38
|
+
maxSelection?: number;
|
|
39
|
+
|
|
40
|
+
/** Page size for pagination */
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
|
|
43
|
+
/** Filter by owner/org */
|
|
44
|
+
owner?: string;
|
|
45
|
+
|
|
46
|
+
/** Maximum number of repos to fetch (default: 100) */
|
|
47
|
+
limit?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// GitHub Repository Fetching
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch all GitHub repositories via gh CLI
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchAllGitHubRepos(
|
|
58
|
+
owner?: string,
|
|
59
|
+
limit: number = 100
|
|
60
|
+
): Promise<Array<{owner: string, name: string, fullName: string}>> {
|
|
61
|
+
console.log('š Fetching GitHub repositories...');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const repos = await GitHubClient.getRepositories(owner, limit);
|
|
65
|
+
|
|
66
|
+
console.log(`ā
Found ${repos.length} GitHub repositories\n`);
|
|
67
|
+
|
|
68
|
+
return repos;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('ā Failed to fetch GitHub repositories:', (error as Error).message);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Interactive Repository Selection
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Interactive repository selector with search and pagination
|
|
81
|
+
*/
|
|
82
|
+
export async function selectGitHubRepos(
|
|
83
|
+
options: RepoSelectorOptions = {}
|
|
84
|
+
): Promise<RepoSelectionResult> {
|
|
85
|
+
const {
|
|
86
|
+
allowManualEntry = true,
|
|
87
|
+
allowSelectAll = true,
|
|
88
|
+
preSelected = [],
|
|
89
|
+
minSelection = 1,
|
|
90
|
+
maxSelection,
|
|
91
|
+
pageSize = 15,
|
|
92
|
+
owner,
|
|
93
|
+
limit = 100,
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
96
|
+
// Fetch all repositories
|
|
97
|
+
const allRepos = await fetchAllGitHubRepos(owner, limit);
|
|
98
|
+
|
|
99
|
+
if (allRepos.length === 0) {
|
|
100
|
+
console.log('ā ļø No GitHub repositories found');
|
|
101
|
+
return {
|
|
102
|
+
selectedRepos: [],
|
|
103
|
+
method: 'interactive',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show repository overview
|
|
108
|
+
console.log('š Available GitHub Repositories:\n');
|
|
109
|
+
console.log(` Total: ${allRepos.length} repositories\n`);
|
|
110
|
+
|
|
111
|
+
// Decide selection method
|
|
112
|
+
const { selectionMethod } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'list',
|
|
115
|
+
name: 'selectionMethod',
|
|
116
|
+
message: 'How would you like to select repositories?',
|
|
117
|
+
choices: [
|
|
118
|
+
{
|
|
119
|
+
name: `š Interactive (browse and select from ${allRepos.length} repositories)`,
|
|
120
|
+
value: 'interactive',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'āļø Manual entry (type repository names)',
|
|
124
|
+
value: 'manual',
|
|
125
|
+
},
|
|
126
|
+
...(allowSelectAll
|
|
127
|
+
? [
|
|
128
|
+
{
|
|
129
|
+
name: `⨠Select all (${allRepos.length} repositories)`,
|
|
130
|
+
value: 'all',
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
: []),
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
if (selectionMethod === 'all') {
|
|
139
|
+
return {
|
|
140
|
+
selectedRepos: allRepos.map((r) => r.fullName),
|
|
141
|
+
method: 'all',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (selectionMethod === 'manual') {
|
|
146
|
+
return await manualRepoEntry(allRepos, minSelection, maxSelection);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Interactive selection
|
|
150
|
+
return await interactiveRepoSelection(
|
|
151
|
+
allRepos,
|
|
152
|
+
preSelected,
|
|
153
|
+
minSelection,
|
|
154
|
+
maxSelection,
|
|
155
|
+
pageSize
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Interactive repository selection with checkbox
|
|
161
|
+
*/
|
|
162
|
+
async function interactiveRepoSelection(
|
|
163
|
+
allRepos: Array<{owner: string, name: string, fullName: string}>,
|
|
164
|
+
preSelected: string[],
|
|
165
|
+
minSelection: number,
|
|
166
|
+
maxSelection: number | undefined,
|
|
167
|
+
pageSize: number
|
|
168
|
+
): Promise<RepoSelectionResult> {
|
|
169
|
+
console.log('š” Use <space> to select, <a> to toggle all, <i> to invert\n');
|
|
170
|
+
|
|
171
|
+
const choices = allRepos.map((r) => ({
|
|
172
|
+
name: formatRepoChoice(r),
|
|
173
|
+
value: r.fullName,
|
|
174
|
+
checked: preSelected.includes(r.fullName),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
// Add manual entry option at the end
|
|
178
|
+
choices.push(
|
|
179
|
+
new inquirer.Separator(),
|
|
180
|
+
{
|
|
181
|
+
name: 'āļø Enter repository names manually instead',
|
|
182
|
+
value: '__MANUAL__',
|
|
183
|
+
checked: false,
|
|
184
|
+
} as any
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const { selectedRepos } = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'checkbox',
|
|
190
|
+
name: 'selectedRepos',
|
|
191
|
+
message: `Select GitHub repositories (${minSelection}${maxSelection ? `-${maxSelection}` : '+'} required):`,
|
|
192
|
+
choices,
|
|
193
|
+
pageSize,
|
|
194
|
+
loop: false,
|
|
195
|
+
validate: (selected: string[]) => {
|
|
196
|
+
const actualSelected = selected.filter((k) => k !== '__MANUAL__');
|
|
197
|
+
|
|
198
|
+
if (actualSelected.length < minSelection) {
|
|
199
|
+
return `Please select at least ${minSelection} repository(ies)`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (maxSelection && actualSelected.length > maxSelection) {
|
|
203
|
+
return `Please select at most ${maxSelection} repository(ies)`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
// Check if user chose manual entry
|
|
212
|
+
if (selectedRepos.includes('__MANUAL__')) {
|
|
213
|
+
return await manualRepoEntry(allRepos, minSelection, maxSelection);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(`\nā
Selected ${selectedRepos.length} repositories: ${selectedRepos.join(', ')}\n`);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
selectedRepos,
|
|
220
|
+
method: 'interactive',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Manual repository entry (comma-separated, format: owner/repo)
|
|
226
|
+
*/
|
|
227
|
+
async function manualRepoEntry(
|
|
228
|
+
allRepos: Array<{owner: string, name: string, fullName: string}>,
|
|
229
|
+
minSelection: number,
|
|
230
|
+
maxSelection: number | undefined
|
|
231
|
+
): Promise<RepoSelectionResult> {
|
|
232
|
+
console.log('\nš Enter repository names manually\n');
|
|
233
|
+
console.log('š” Format: Comma-separated owner/repo format (e.g., octocat/Hello-World,owner/repo2)\n');
|
|
234
|
+
|
|
235
|
+
if (allRepos.length > 0) {
|
|
236
|
+
console.log('Available repositories:');
|
|
237
|
+
console.log(
|
|
238
|
+
allRepos
|
|
239
|
+
.map((r) => r.fullName)
|
|
240
|
+
.join(', ')
|
|
241
|
+
.substring(0, 100) + (allRepos.length > 20 ? '...' : '')
|
|
242
|
+
);
|
|
243
|
+
console.log('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { manualRepos } = await inquirer.prompt([
|
|
247
|
+
{
|
|
248
|
+
type: 'input',
|
|
249
|
+
name: 'manualRepos',
|
|
250
|
+
message: 'Repository names:',
|
|
251
|
+
validate: (input: string) => {
|
|
252
|
+
if (!input.trim()) {
|
|
253
|
+
return 'Please enter at least one repository name';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const repos = input
|
|
257
|
+
.split(',')
|
|
258
|
+
.map((r) => r.trim())
|
|
259
|
+
.filter((r) => r.length > 0);
|
|
260
|
+
|
|
261
|
+
if (repos.length < minSelection) {
|
|
262
|
+
return `Please enter at least ${minSelection} repository name(s)`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (maxSelection && repos.length > maxSelection) {
|
|
266
|
+
return `Please enter at most ${maxSelection} repository name(s)`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Validate format (owner/repo)
|
|
270
|
+
const invalidRepos = repos.filter((r) => !/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(r));
|
|
271
|
+
if (invalidRepos.length > 0) {
|
|
272
|
+
return `Invalid repository format: ${invalidRepos.join(', ')}. Use owner/repo format (e.g., octocat/Hello-World).`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
const selectedRepos = manualRepos
|
|
281
|
+
.split(',')
|
|
282
|
+
.map((r: string) => r.trim())
|
|
283
|
+
.filter((r: string) => r.length > 0);
|
|
284
|
+
|
|
285
|
+
// Warn about unknown repositories
|
|
286
|
+
const knownRepos = allRepos.map((r) => r.fullName);
|
|
287
|
+
const unknownRepos = selectedRepos.filter((r) => !knownRepos.includes(r));
|
|
288
|
+
|
|
289
|
+
if (unknownRepos.length > 0) {
|
|
290
|
+
console.log(`\nā ļø Unknown repository names (will be used anyway): ${unknownRepos.join(', ')}`);
|
|
291
|
+
console.log(' Make sure these repositories exist and you have access to them.\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(`ā
Selected ${selectedRepos.length} repositories: ${selectedRepos.join(', ')}\n`);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
selectedRepos,
|
|
298
|
+
method: 'manual',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Helpers
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format repository choice for display
|
|
308
|
+
*/
|
|
309
|
+
function formatRepoChoice(repo: {owner: string, name: string, fullName: string}): string {
|
|
310
|
+
return `${repo.fullName.padEnd(40)} (${repo.owner})`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Quick repo selector - select single repository
|
|
315
|
+
*/
|
|
316
|
+
export async function selectSingleGitHubRepo(
|
|
317
|
+
message: string = 'Select GitHub repository:',
|
|
318
|
+
owner?: string
|
|
319
|
+
): Promise<string> {
|
|
320
|
+
const result = await selectGitHubRepos({
|
|
321
|
+
allowManualEntry: true,
|
|
322
|
+
allowSelectAll: false,
|
|
323
|
+
minSelection: 1,
|
|
324
|
+
maxSelection: 1,
|
|
325
|
+
owner,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return result.selectedRepos[0];
|
|
329
|
+
}
|