specweave 1.0.255 → 1.0.256

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 (92) hide show
  1. package/CLAUDE.md +24 -24
  2. package/README.md +138 -203
  3. package/dist/src/core/ac-checkbox-formatter.d.ts +24 -0
  4. package/dist/src/core/ac-checkbox-formatter.d.ts.map +1 -0
  5. package/dist/src/core/ac-checkbox-formatter.js +35 -0
  6. package/dist/src/core/ac-checkbox-formatter.js.map +1 -0
  7. package/dist/src/core/ac-progress-sync.d.ts +116 -0
  8. package/dist/src/core/ac-progress-sync.d.ts.map +1 -0
  9. package/dist/src/core/ac-progress-sync.js +272 -0
  10. package/dist/src/core/ac-progress-sync.js.map +1 -0
  11. package/dist/src/core/fabric/registry-schema.d.ts +79 -0
  12. package/dist/src/core/fabric/registry-schema.d.ts.map +1 -0
  13. package/dist/src/core/fabric/registry-schema.js +6 -0
  14. package/dist/src/core/fabric/registry-schema.js.map +1 -0
  15. package/dist/src/core/fabric/security-scanner.d.ts +12 -0
  16. package/dist/src/core/fabric/security-scanner.d.ts.map +1 -0
  17. package/dist/src/core/fabric/security-scanner.js +219 -0
  18. package/dist/src/core/fabric/security-scanner.js.map +1 -0
  19. package/dist/src/core/types/sync-profile.d.ts +44 -0
  20. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  21. package/dist/src/core/types/sync-profile.js.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +4 -4
  24. package/plugins/{specweave-github/hooks/github-ac-sync-handler.sh → specweave/hooks/v2/handlers/ac-sync-dispatcher.sh} +96 -92
  25. package/plugins/specweave/skills/architect/SKILL.md +1 -1
  26. package/plugins/specweave/skills/auto/SKILL.md +1 -1
  27. package/plugins/specweave/skills/cancel-auto/SKILL.md +1 -1
  28. package/plugins/specweave/skills/code-simplifier/SKILL.md +1 -1
  29. package/plugins/specweave/skills/do/SKILL.md +1 -1
  30. package/plugins/specweave/skills/docs/SKILL.md +1 -1
  31. package/plugins/specweave/skills/docs-updater/SKILL.md +1 -1
  32. package/plugins/specweave/skills/done/SKILL.md +13 -70
  33. package/plugins/specweave/skills/framework/SKILL.md +1 -1
  34. package/plugins/specweave/skills/grill/SKILL.md +1 -1
  35. package/plugins/specweave/skills/increment/SKILL.md +1 -1
  36. package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
  37. package/plugins/specweave/skills/lsp/SKILL.md +1 -1
  38. package/plugins/specweave/skills/pm/SKILL.md +1 -1
  39. package/plugins/specweave/skills/progress/SKILL.md +1 -1
  40. package/plugins/specweave/skills/save/SKILL.md +1 -1
  41. package/plugins/specweave/skills/security/SKILL.md +1 -1
  42. package/plugins/specweave/skills/security-patterns/SKILL.md +1 -1
  43. package/plugins/specweave/skills/tdd-cycle/SKILL.md +1 -1
  44. package/plugins/specweave/skills/tdd-green/SKILL.md +1 -1
  45. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  46. package/plugins/specweave/skills/tdd-red/SKILL.md +1 -1
  47. package/plugins/specweave/skills/validate/SKILL.md +1 -1
  48. package/plugins/specweave-github/commands/sync.md +1 -22
  49. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +0 -205
  50. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +0 -1
  51. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -685
  52. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +0 -1
  53. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  54. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  55. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -28
  56. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  57. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +0 -21
  58. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +0 -1
  59. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -471
  60. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +0 -1
  61. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +0 -53
  62. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +0 -1
  63. package/dist/plugins/specweave-github/lib/github-status-sync.js +0 -120
  64. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +0 -1
  65. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  66. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  67. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -297
  68. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  69. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +0 -94
  70. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +0 -1
  71. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +0 -385
  72. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +0 -1
  73. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -611
  74. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +0 -909
  75. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.js +0 -1
  76. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  77. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  78. package/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -17
  79. package/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  80. package/plugins/specweave-github/lib/cli-sync-increment-changes.ts +0 -33
  81. package/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -474
  82. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +0 -616
  83. package/plugins/specweave-github/lib/github-status-sync.js +0 -107
  84. package/plugins/specweave-github/lib/github-status-sync.ts +0 -163
  85. package/plugins/specweave-github/lib/github-sync-increment-changes.d.js +0 -0
  86. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  87. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  88. package/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -253
  89. package/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  90. package/plugins/specweave-github/lib/github-sync-increment-changes.ts +0 -391
  91. package/plugins/specweave-github/lib/increment-issue-builder.js +0 -402
  92. package/plugins/specweave-github/lib/increment-issue-builder.ts +0 -520
@@ -1,616 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * GitHub Increment Sync CLI
4
- *
5
- * For brownfield projects without living docs structure.
6
- * Creates GitHub issues directly from increment spec.md with CORRECT format.
7
- *
8
- * CORRECT FORMAT (SpecWeave Universal Hierarchy):
9
- * - Feature (FS-XXX) → GitHub Milestone
10
- * - User Story (US-XXX) → GitHub Issue with [FS-XXX][US-YYY] Title
11
- * - Tasks (T-XXX) → Checkboxes in User Story issue
12
- * - ACs → Checkboxes in User Story issue
13
- *
14
- * Usage:
15
- * node github-increment-sync-cli.js <increment-id>
16
- * node github-increment-sync-cli.js 0002-thumbnail-optimizer-mvp
17
- *
18
- * @see CLAUDE.md (GitHub Issue Format rules)
19
- */
20
-
21
- import { existsSync, readFileSync } from 'fs';
22
- import * as fs from 'fs/promises';
23
- import * as path from 'path';
24
- import { IncrementIssueBuilder, UserStory, Task, IncrementData } from './increment-issue-builder.js';
25
- import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
26
- import { getGitHubAuthFromProject } from '../../../src/utils/auth-helpers.js';
27
-
28
- /**
29
- * Get environment object with GH_TOKEN for gh CLI commands.
30
- */
31
- function getGhEnv(): NodeJS.ProcessEnv {
32
- const { token } = getGitHubAuthFromProject(process.cwd());
33
- return token
34
- ? { ...process.env, GH_TOKEN: token }
35
- : process.env;
36
- }
37
-
38
- interface GitHubConfig {
39
- owner: string;
40
- repo: string;
41
- token: string;
42
- }
43
-
44
- interface SyncResult {
45
- milestoneNumber: number;
46
- milestoneUrl: string;
47
- issues: Array<{
48
- userStoryId: string;
49
- issueNumber: number;
50
- issueUrl: string;
51
- title: string;
52
- }>;
53
- }
54
-
55
- async function loadGitHubConfig(): Promise<GitHubConfig | null> {
56
- const projectRoot = process.cwd();
57
- const configPath = path.join(projectRoot, '.specweave/config.json');
58
-
59
- let owner = process.env.GITHUB_OWNER || '';
60
- let repo = process.env.GITHUB_REPO || '';
61
- const token = process.env.GITHUB_TOKEN || '';
62
-
63
- // Try to load from config.json
64
- if (existsSync(configPath)) {
65
- try {
66
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
67
-
68
- // Method 1: sync.github
69
- if (config.sync?.github?.owner && config.sync?.github?.repo) {
70
- owner = config.sync.github.owner;
71
- repo = config.sync.github.repo;
72
- }
73
- // Method 2: multiProject.projects[activeProject].externalTools.github
74
- else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
75
- const activeProject = config.multiProject.activeProject;
76
- const projectConfig = config.multiProject.projects?.[activeProject];
77
- if (projectConfig?.externalTools?.github?.repository) {
78
- const parts = projectConfig.externalTools.github.repository.split('/');
79
- if (parts.length === 2) {
80
- owner = parts[0];
81
- repo = parts[1];
82
- }
83
- }
84
- }
85
- // Method 3: sync.profiles[defaultProfile]
86
- else if (config.sync?.defaultProfile && config.sync?.profiles) {
87
- const profile = config.sync.profiles[config.sync.defaultProfile];
88
- if (profile?.config?.owner && profile?.config?.repo) {
89
- owner = profile.config.owner;
90
- repo = profile.config.repo;
91
- }
92
- }
93
- } catch (error) {
94
- console.error('āš ļø Failed to parse config.json:', error);
95
- }
96
- }
97
-
98
- // Fallback: detect from git remote
99
- if (!owner || !repo) {
100
- const result = await execFileNoThrow('git', ['remote', 'get-url', 'origin']);
101
- if (result.exitCode === 0 && result.stdout) {
102
- const remoteUrl = result.stdout.trim();
103
- const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
104
- if (match) {
105
- owner = owner || match[1];
106
- repo = repo || match[2];
107
- }
108
- }
109
- }
110
-
111
- if (!token) {
112
- console.error('āŒ GITHUB_TOKEN not set');
113
- console.error(' Set it in .env file or export GITHUB_TOKEN=ghp_xxx');
114
- return null;
115
- }
116
-
117
- if (!owner || !repo) {
118
- console.error('āŒ Could not detect GitHub owner/repo');
119
- console.error(' Set sync.github.owner and sync.github.repo in .specweave/config.json');
120
- return null;
121
- }
122
-
123
- return { owner, repo, token };
124
- }
125
-
126
- /**
127
- * Find increment folder by ID (supports partial matching)
128
- */
129
- async function findIncrementFolder(incrementId: string): Promise<string | null> {
130
- const projectRoot = process.cwd();
131
- const incrementsDir = path.join(projectRoot, '.specweave/increments');
132
-
133
- if (!existsSync(incrementsDir)) {
134
- return null;
135
- }
136
-
137
- const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
138
-
139
- for (const entry of entries) {
140
- if (!entry.isDirectory()) continue;
141
- if (entry.name.startsWith('_')) continue; // Skip _archive, _backlog
142
-
143
- // Exact match
144
- if (entry.name === incrementId) {
145
- return path.join(incrementsDir, entry.name);
146
- }
147
-
148
- // Partial match (e.g., "0063" matches "0063-fix-external-import")
149
- if (entry.name.startsWith(incrementId + '-')) {
150
- return path.join(incrementsDir, entry.name);
151
- }
152
- }
153
-
154
- return null;
155
- }
156
-
157
- /**
158
- * Load existing GitHub links from metadata.json
159
- */
160
- function loadExistingGitHubLinks(incrementPath: string): {
161
- milestone?: number;
162
- userStoryIssues: Record<string, number>;
163
- } {
164
- const metadataPath = path.join(incrementPath, 'metadata.json');
165
-
166
- if (!existsSync(metadataPath)) {
167
- return { userStoryIssues: {} };
168
- }
169
-
170
- try {
171
- const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
172
-
173
- return {
174
- milestone: metadata.github?.milestone,
175
- userStoryIssues: metadata.github?.userStoryIssues || {}
176
- };
177
- } catch {
178
- return { userStoryIssues: {} };
179
- }
180
- }
181
-
182
- /**
183
- * Update increment metadata with GitHub links
184
- */
185
- async function updateIncrementMetadata(
186
- incrementPath: string,
187
- milestoneNumber: number,
188
- userStoryIssues: Record<string, number>
189
- ): Promise<void> {
190
- const metadataPath = path.join(incrementPath, 'metadata.json');
191
-
192
- let metadata: Record<string, unknown> = {};
193
-
194
- if (existsSync(metadataPath)) {
195
- try {
196
- metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
197
- } catch {
198
- // Start fresh
199
- }
200
- }
201
-
202
- // Update github section
203
- metadata.github = {
204
- milestone: milestoneNumber,
205
- userStoryIssues,
206
- lastSync: new Date().toISOString()
207
- };
208
-
209
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
210
- }
211
-
212
- /**
213
- * Create or get GitHub milestone for the feature
214
- */
215
- async function createOrGetMilestone(
216
- owner: string,
217
- repo: string,
218
- featureId: string,
219
- title: string,
220
- existingMilestone?: number
221
- ): Promise<{ number: number; url: string }> {
222
- // If we have an existing milestone, verify it exists
223
- if (existingMilestone) {
224
- const verifyResult = await execFileNoThrow('gh', [
225
- 'api', `repos/${owner}/${repo}/milestones/${existingMilestone}`,
226
- '--jq', '.number'
227
- ], { env: getGhEnv() });
228
- if (verifyResult.exitCode === 0) {
229
- return {
230
- number: existingMilestone,
231
- url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
232
- };
233
- }
234
- }
235
-
236
- // Search for existing milestone by title
237
- const searchResult = await execFileNoThrow('gh', [
238
- 'api', `repos/${owner}/${repo}/milestones`,
239
- '--jq', `.[] | select(.title | contains("${featureId}")) | .number`
240
- ], { env: getGhEnv() });
241
-
242
- if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
243
- const milestoneNumber = parseInt(searchResult.stdout.trim().split('\n')[0], 10);
244
- return {
245
- number: milestoneNumber,
246
- url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
247
- };
248
- }
249
-
250
- // Create new milestone
251
- const milestoneTitle = `[${featureId}] ${title}`;
252
- const createResult = await execFileNoThrow('gh', [
253
- 'api', `repos/${owner}/${repo}/milestones`,
254
- '-X', 'POST',
255
- '-f', `title=${milestoneTitle}`,
256
- '-f', `description=Feature milestone for ${featureId}`,
257
- '--jq', '.number'
258
- ], { env: getGhEnv() });
259
-
260
- if (createResult.exitCode !== 0) {
261
- throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
262
- }
263
-
264
- const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
265
- return {
266
- number: milestoneNumber,
267
- url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
268
- };
269
- }
270
-
271
- /**
272
- * Build issue body for a single user story
273
- */
274
- function buildUserStoryIssueBody(
275
- story: UserStory,
276
- tasks: Task[],
277
- incrementData: IncrementData,
278
- githubRepo: string
279
- ): string {
280
- const incrementId = incrementData.frontmatter.increment;
281
- let body = '';
282
-
283
- // āŒ REMOVED: Metadata header (Feature, Status, Priority)
284
- // WHY: GitHub has NATIVE fields for this (labels, milestones)
285
- // Body should contain ONLY actual work content (ACs, tasks, user story)
286
- // See: .specweave/docs/internal/troubleshooting/CRITICAL-remove-metadata-header-from-github-issues.md
287
-
288
- // Progress section (consistency with user-story-issue-builder.ts)
289
- const completedACs = story.acceptanceCriteria.filter(ac => ac.completed).length;
290
- const totalACs = story.acceptanceCriteria.length;
291
- const storyTasks = tasks.filter(t =>
292
- t.userStories.includes(story.id) ||
293
- t.userStories.some(us => us.includes(story.id.replace('US-', '')))
294
- );
295
- const completedTasks = storyTasks.filter(t => t.completed).length;
296
- const totalTasks = storyTasks.length;
297
- const overallPercentage = (totalACs + totalTasks) > 0
298
- ? Math.round(((completedACs + completedTasks) / (totalACs + totalTasks)) * 100)
299
- : 0;
300
-
301
- body += `## Progress\n\n`;
302
- body += `**Acceptance Criteria**: ${completedACs}/${totalACs} (${totalACs > 0 ? Math.round((completedACs / totalACs) * 100) : 0}%)\n`;
303
- body += `**Tasks**: ${completedTasks}/${totalTasks} (${totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0}%)\n`;
304
- body += `**Overall**: ${overallPercentage}%\n\n`;
305
-
306
- // Progress bar
307
- const filledBlocks = Math.floor(overallPercentage / 5);
308
- const emptyBlocks = 20 - filledBlocks;
309
- body += `${'ā–ˆ'.repeat(filledBlocks)}${'ā–‘'.repeat(emptyBlocks)} ${overallPercentage}%\n\n`;
310
- body += `---\n\n`;
311
-
312
- // User Story description
313
- body += `## User Story\n\n`;
314
- if (story.asA && story.iWant && story.soThat) {
315
- body += `**As a** ${story.asA}\n`;
316
- body += `**I want** ${story.iWant}\n`;
317
- body += `**So that** ${story.soThat}\n\n`;
318
- } else {
319
- body += `${story.title}\n\n`;
320
- }
321
-
322
- body += `---\n\n`;
323
-
324
- // Acceptance Criteria
325
- body += `## Acceptance Criteria\n\n`;
326
- if (story.acceptanceCriteria.length > 0) {
327
- const completed = story.acceptanceCriteria.filter(ac => ac.completed).length;
328
- const total = story.acceptanceCriteria.length;
329
- const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
330
- body += `Progress: ${completed}/${total} criteria met (${percentage}%)\n\n`;
331
-
332
- for (const ac of story.acceptanceCriteria) {
333
- const checkbox = ac.completed ? '[x]' : '[ ]';
334
- body += `- ${checkbox} **${ac.id}**: ${ac.description}\n`;
335
- }
336
- body += '\n';
337
- } else {
338
- body += `*No acceptance criteria defined*\n\n`;
339
- }
340
-
341
- body += `---\n\n`;
342
-
343
- // Tasks for this user story (already calculated above for Progress section)
344
- if (storyTasks.length > 0) {
345
- body += `## Implementation Tasks\n\n`;
346
- const completedTasks = storyTasks.filter(t => t.completed).length;
347
- const totalTasks = storyTasks.length;
348
- const taskPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
349
- body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)\n\n`;
350
-
351
- for (const task of storyTasks) {
352
- const checkbox = task.completed ? '[x]' : '[ ]';
353
- body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
354
- }
355
- body += '\n';
356
-
357
- body += `---\n\n`;
358
- }
359
-
360
- // Link to increment
361
- body += `## SpecWeave Increment\n\n`;
362
- body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})\n\n`;
363
-
364
- body += `---\n\n`;
365
- body += `šŸ¤– Auto-synced by SpecWeave`;
366
-
367
- return body;
368
- }
369
-
370
- /**
371
- * Create or update GitHub issue for a user story
372
- */
373
- async function syncUserStoryIssue(
374
- owner: string,
375
- repo: string,
376
- featureId: string,
377
- story: UserStory,
378
- tasks: Task[],
379
- incrementData: IncrementData,
380
- milestoneNumber: number,
381
- existingIssueNumber?: number
382
- ): Promise<{ number: number; url: string }> {
383
- // CORRECT FORMAT: [FS-XXX][US-YYY] User Story Title
384
- const title = `[${featureId}][${story.id}] ${story.title}`;
385
- const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
386
-
387
- // Labels
388
- const labels = ['specweave', 'user-story'];
389
- const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || 'p2';
390
- labels.push(priority);
391
-
392
- if (existingIssueNumber) {
393
- // Update existing issue
394
- const updateResult = await execFileNoThrow('gh', [
395
- 'issue', 'edit',
396
- String(existingIssueNumber),
397
- '--repo', `${owner}/${repo}`,
398
- '--title', title,
399
- '--body', body
400
- ], { env: getGhEnv() });
401
-
402
- if (updateResult.exitCode !== 0) {
403
- throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
404
- }
405
-
406
- return {
407
- number: existingIssueNumber,
408
- url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
409
- };
410
- }
411
-
412
- // Create new issue
413
- const createArgs = [
414
- 'issue', 'create',
415
- '--repo', `${owner}/${repo}`,
416
- '--title', title,
417
- '--body', body,
418
- '--milestone', String(milestoneNumber)
419
- ];
420
-
421
- if (labels.length > 0) {
422
- createArgs.push('--label', labels.join(','));
423
- }
424
-
425
- const createResult = await execFileNoThrow('gh', createArgs, { env: getGhEnv() });
426
-
427
- if (createResult.exitCode !== 0) {
428
- throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
429
- }
430
-
431
- // Parse issue URL from output
432
- const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
433
- if (!urlMatch) {
434
- throw new Error('Could not parse issue URL from gh output');
435
- }
436
-
437
- return {
438
- number: parseInt(urlMatch[1], 10),
439
- url: urlMatch[0]
440
- };
441
- }
442
-
443
- async function main() {
444
- const args = process.argv.slice(2);
445
-
446
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
447
- console.log('Usage: node github-increment-sync-cli.js <increment-id> [options]');
448
- console.log('');
449
- console.log('Creates GitHub issues with CORRECT format:');
450
- console.log(' - Milestone: [FS-XXX] Feature Title');
451
- console.log(' - Issues: [FS-XXX][US-YYY] User Story Title (one per US)');
452
- console.log('');
453
- console.log('Arguments:');
454
- console.log(' increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)');
455
- console.log('');
456
- console.log('Options:');
457
- console.log(' --dry-run Preview issues without creating');
458
- console.log('');
459
- console.log('Environment:');
460
- console.log(' GITHUB_TOKEN Required - GitHub personal access token');
461
- console.log('');
462
- console.log('Example:');
463
- console.log(' GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002');
464
- process.exit(args.length === 0 ? 1 : 0);
465
- }
466
-
467
- const incrementId = args[0];
468
- const dryRun = args.includes('--dry-run');
469
-
470
- console.log(`\nšŸ™ GitHub Increment Sync CLI (Per-User-Story Mode)`);
471
- console.log(` Increment: ${incrementId}`);
472
-
473
- // Find increment folder
474
- const incrementPath = await findIncrementFolder(incrementId);
475
- if (!incrementPath) {
476
- console.error(`āŒ Increment not found: ${incrementId}`);
477
- console.error(' Check: ls .specweave/increments/');
478
- process.exit(1);
479
- }
480
-
481
- const fullIncrementId = path.basename(incrementPath);
482
- console.log(` Found: ${fullIncrementId}`);
483
-
484
- // For dry-run, we can skip config validation
485
- let config: GitHubConfig | null = null;
486
- if (!dryRun) {
487
- config = await loadGitHubConfig();
488
- if (!config) {
489
- process.exit(1);
490
- }
491
- console.log(` Repository: ${config.owner}/${config.repo}`);
492
- } else {
493
- config = await loadGitHubConfig().catch((): null => null);
494
- if (config) {
495
- console.log(` Repository: ${config.owner}/${config.repo}`);
496
- } else {
497
- console.log(` Repository: (not detected - dry run mode)`);
498
- }
499
- }
500
-
501
- // Parse increment
502
- const projectRoot = process.cwd();
503
- const builder = new IncrementIssueBuilder(incrementPath, projectRoot);
504
-
505
- try {
506
- console.log(`\nšŸ”„ Parsing increment spec.md...`);
507
- const incrementData = await builder.parse();
508
-
509
- const featureId = incrementData.frontmatter.feature_id ||
510
- `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, '0') || 'UNKNOWN'}`;
511
-
512
- console.log(` šŸ“¦ Feature: ${featureId}`);
513
- console.log(` šŸ“¦ Title: ${incrementData.title}`);
514
- console.log(` šŸ“ User Stories: ${incrementData.userStories.length}`);
515
-
516
- const totalACs = incrementData.userStories.reduce(
517
- (sum, us) => sum + us.acceptanceCriteria.length, 0
518
- );
519
- console.log(` āœ“ Acceptance Criteria: ${totalACs}`);
520
- console.log(` šŸ”§ Tasks: ${incrementData.tasks.length}`);
521
-
522
- if (incrementData.userStories.length === 0) {
523
- console.error(`\nāŒ No user stories found in spec.md`);
524
- console.error(' Ensure spec.md has ### US-XXX: Title sections');
525
- process.exit(1);
526
- }
527
-
528
- // Preview issues
529
- console.log(`\nšŸ“‹ Issues to Create/Update:`);
530
- console.log(` šŸŽÆ Milestone: [${featureId}] ${incrementData.title}`);
531
- for (const story of incrementData.userStories) {
532
- const storyTasks = incrementData.tasks.filter(t =>
533
- t.userStories.includes(story.id) ||
534
- t.userStories.some(us => us.includes(story.id.replace('US-', '')))
535
- );
536
- console.log(` šŸ“ [${featureId}][${story.id}] ${story.title}`);
537
- console.log(` └─ ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
538
- }
539
-
540
- if (dryRun) {
541
- console.log(`\nšŸ“„ Sample Issue Body (${incrementData.userStories[0].id}):\n`);
542
- const sampleBody = buildUserStoryIssueBody(
543
- incrementData.userStories[0],
544
- incrementData.tasks,
545
- incrementData,
546
- config ? `${config.owner}/${config.repo}` : 'owner/repo'
547
- );
548
- console.log(sampleBody);
549
- console.log(`\nāœ… Dry run complete (no issues created)`);
550
- process.exit(0);
551
- }
552
-
553
- // After dry-run check, config is guaranteed to be non-null
554
- if (!config) {
555
- console.error('āŒ GitHub config not available');
556
- process.exit(1);
557
- }
558
-
559
- // Load existing links
560
- const existingLinks = loadExistingGitHubLinks(incrementPath);
561
-
562
- // Create or get milestone
563
- console.log(`\nšŸŽÆ Creating/updating milestone...`);
564
- const milestone = await createOrGetMilestone(
565
- config.owner,
566
- config.repo,
567
- featureId,
568
- incrementData.title,
569
- existingLinks.milestone
570
- );
571
- console.log(` āœ… Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
572
-
573
- // Create/update issues for each user story
574
- console.log(`\nšŸ“ Creating/updating user story issues...`);
575
- const userStoryIssues: Record<string, number> = {};
576
-
577
- for (const story of incrementData.userStories) {
578
- const existingIssue = existingLinks.userStoryIssues[story.id];
579
-
580
- const issue = await syncUserStoryIssue(
581
- config.owner,
582
- config.repo,
583
- featureId,
584
- story,
585
- incrementData.tasks,
586
- incrementData,
587
- milestone.number,
588
- existingIssue
589
- );
590
-
591
- userStoryIssues[story.id] = issue.number;
592
-
593
- const action = existingIssue ? 'ā™»ļø Updated' : 'āœ… Created';
594
- console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
595
- }
596
-
597
- // Update metadata
598
- await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
599
- console.log(`\nšŸ“ Metadata updated`);
600
-
601
- console.log(`\nāœ… Sync complete!`);
602
- console.log(` šŸŽÆ Milestone: ${milestone.url}`);
603
- console.log(` šŸ“ Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
604
-
605
- process.exit(0);
606
- } catch (error) {
607
- console.error(`\nāŒ Sync failed:`, error);
608
- process.exit(1);
609
- }
610
- }
611
-
612
- // Run CLI
613
- main().catch(error => {
614
- console.error('Fatal error:', error);
615
- process.exit(1);
616
- });