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