specweave 0.28.19 → 0.28.22
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/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +8 -6
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +230 -165
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +2 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js +25 -5
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +7 -3
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +193 -23
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
- package/dist/src/cli/helpers/init/language-selection.js +1 -1
- package/dist/src/cli/helpers/init/language-selection.js.map +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
- package/dist/src/config/types.d.ts +6 -6
- package/dist/src/core/background/index.d.ts +11 -0
- package/dist/src/core/background/index.d.ts.map +1 -0
- package/dist/src/core/background/index.js +11 -0
- package/dist/src/core/background/index.js.map +1 -0
- package/dist/src/core/background/job-manager.d.ts +65 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -0
- package/dist/src/core/background/job-manager.js +192 -0
- package/dist/src/core/background/job-manager.js.map +1 -0
- package/dist/src/core/background/types.d.ts +59 -0
- package/dist/src/core/background/types.d.ts.map +1 -0
- package/dist/src/core/background/types.js +8 -0
- package/dist/src/core/background/types.js.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.js +6 -36
- package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
- package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
- package/dist/src/core/repo-structure/repo-initializer.js +260 -0
- package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/types/spec-metadata.d.ts +2 -0
- package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
- package/dist/src/importers/import-coordinator.d.ts +20 -0
- package/dist/src/importers/import-coordinator.d.ts.map +1 -1
- package/dist/src/importers/import-coordinator.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +2 -2
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-jobs.md +160 -0
- package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
- package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
- package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +6 -0
- package/plugins/specweave-github/lib/github-increment-sync-cli.js +268 -155
- package/plugins/specweave-github/lib/github-increment-sync-cli.ts +313 -209
- package/plugins/specweave-github/lib/github-status-sync.js +37 -1
- package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
- package/plugins/specweave-github/lib/increment-issue-builder.js +26 -5
- package/plugins/specweave-github/lib/increment-issue-builder.ts +36 -5
- package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
- package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
- package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
- package/plugins/specweave-release/commands/specweave-release-npm.md +187 -8
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +9 -0
|
@@ -79,32 +79,205 @@ async function findIncrementFolder(incrementId) {
|
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`repo:${owner}/${repo}`,
|
|
87
|
-
`"[${featureId}]" in:title`,
|
|
88
|
-
"is:open",
|
|
89
|
-
"--json",
|
|
90
|
-
"number,title",
|
|
91
|
-
"--limit",
|
|
92
|
-
"5"
|
|
93
|
-
]);
|
|
94
|
-
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
95
|
-
return null;
|
|
82
|
+
function loadExistingGitHubLinks(incrementPath) {
|
|
83
|
+
const metadataPath = path.join(incrementPath, "metadata.json");
|
|
84
|
+
if (!existsSync(metadataPath)) {
|
|
85
|
+
return { userStoryIssues: {} };
|
|
96
86
|
}
|
|
97
87
|
try {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
89
|
+
return {
|
|
90
|
+
milestone: metadata.github?.milestone,
|
|
91
|
+
userStoryIssues: metadata.github?.userStoryIssues || {}
|
|
92
|
+
};
|
|
102
93
|
} catch {
|
|
94
|
+
return { userStoryIssues: {} };
|
|
103
95
|
}
|
|
104
|
-
return null;
|
|
105
96
|
}
|
|
106
|
-
async function
|
|
107
|
-
const
|
|
97
|
+
async function updateIncrementMetadata(incrementPath, milestoneNumber, userStoryIssues) {
|
|
98
|
+
const metadataPath = path.join(incrementPath, "metadata.json");
|
|
99
|
+
let metadata = {};
|
|
100
|
+
if (existsSync(metadataPath)) {
|
|
101
|
+
try {
|
|
102
|
+
metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
metadata.github = {
|
|
107
|
+
milestone: milestoneNumber,
|
|
108
|
+
userStoryIssues,
|
|
109
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString()
|
|
110
|
+
};
|
|
111
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
|
|
112
|
+
}
|
|
113
|
+
async function createOrGetMilestone(owner, repo, featureId, title, existingMilestone) {
|
|
114
|
+
if (existingMilestone) {
|
|
115
|
+
const verifyResult = await execFileNoThrow("gh", [
|
|
116
|
+
"api",
|
|
117
|
+
`repos/${owner}/${repo}/milestones/${existingMilestone}`,
|
|
118
|
+
"--jq",
|
|
119
|
+
".number"
|
|
120
|
+
]);
|
|
121
|
+
if (verifyResult.exitCode === 0) {
|
|
122
|
+
return {
|
|
123
|
+
number: existingMilestone,
|
|
124
|
+
url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const searchResult = await execFileNoThrow("gh", [
|
|
129
|
+
"api",
|
|
130
|
+
`repos/${owner}/${repo}/milestones`,
|
|
131
|
+
"--jq",
|
|
132
|
+
`.[] | select(.title | contains("${featureId}")) | .number`
|
|
133
|
+
]);
|
|
134
|
+
if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
|
|
135
|
+
const milestoneNumber2 = parseInt(searchResult.stdout.trim().split("\n")[0], 10);
|
|
136
|
+
return {
|
|
137
|
+
number: milestoneNumber2,
|
|
138
|
+
url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber2}`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const milestoneTitle = `[${featureId}] ${title}`;
|
|
142
|
+
const createResult = await execFileNoThrow("gh", [
|
|
143
|
+
"api",
|
|
144
|
+
`repos/${owner}/${repo}/milestones`,
|
|
145
|
+
"-X",
|
|
146
|
+
"POST",
|
|
147
|
+
"-f",
|
|
148
|
+
`title=${milestoneTitle}`,
|
|
149
|
+
"-f",
|
|
150
|
+
`description=Feature milestone for ${featureId}`,
|
|
151
|
+
"--jq",
|
|
152
|
+
".number"
|
|
153
|
+
]);
|
|
154
|
+
if (createResult.exitCode !== 0) {
|
|
155
|
+
throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
|
|
156
|
+
}
|
|
157
|
+
const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
|
|
158
|
+
return {
|
|
159
|
+
number: milestoneNumber,
|
|
160
|
+
url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function buildUserStoryIssueBody(story, tasks, incrementData, githubRepo) {
|
|
164
|
+
const incrementId = incrementData.frontmatter.increment;
|
|
165
|
+
let body = "";
|
|
166
|
+
body += `**Feature**: ${incrementData.frontmatter.feature_id || "N/A"}
|
|
167
|
+
`;
|
|
168
|
+
body += `**Status**: ${story.acceptanceCriteria.every((ac) => ac.completed) ? "complete" : "in-progress"}
|
|
169
|
+
`;
|
|
170
|
+
body += `**Priority**: ${story.priority || incrementData.frontmatter.priority || "P2"}
|
|
171
|
+
`;
|
|
172
|
+
body += `
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
`;
|
|
176
|
+
body += `## User Story
|
|
177
|
+
|
|
178
|
+
`;
|
|
179
|
+
if (story.asA && story.iWant && story.soThat) {
|
|
180
|
+
body += `**As a** ${story.asA}
|
|
181
|
+
`;
|
|
182
|
+
body += `**I want** ${story.iWant}
|
|
183
|
+
`;
|
|
184
|
+
body += `**So that** ${story.soThat}
|
|
185
|
+
|
|
186
|
+
`;
|
|
187
|
+
} else {
|
|
188
|
+
body += `${story.title}
|
|
189
|
+
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
body += `---
|
|
193
|
+
|
|
194
|
+
`;
|
|
195
|
+
body += `## Acceptance Criteria
|
|
196
|
+
|
|
197
|
+
`;
|
|
198
|
+
if (story.acceptanceCriteria.length > 0) {
|
|
199
|
+
const completed = story.acceptanceCriteria.filter((ac) => ac.completed).length;
|
|
200
|
+
const total = story.acceptanceCriteria.length;
|
|
201
|
+
const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
202
|
+
body += `Progress: ${completed}/${total} criteria met (${percentage}%)
|
|
203
|
+
|
|
204
|
+
`;
|
|
205
|
+
for (const ac of story.acceptanceCriteria) {
|
|
206
|
+
const checkbox = ac.completed ? "[x]" : "[ ]";
|
|
207
|
+
body += `- ${checkbox} **${ac.id}**: ${ac.description}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
body += "\n";
|
|
211
|
+
} else {
|
|
212
|
+
body += `*No acceptance criteria defined*
|
|
213
|
+
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
body += `---
|
|
217
|
+
|
|
218
|
+
`;
|
|
219
|
+
const storyTasks = tasks.filter(
|
|
220
|
+
(t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
|
|
221
|
+
);
|
|
222
|
+
if (storyTasks.length > 0) {
|
|
223
|
+
body += `## Implementation Tasks
|
|
224
|
+
|
|
225
|
+
`;
|
|
226
|
+
const completedTasks = storyTasks.filter((t) => t.completed).length;
|
|
227
|
+
const totalTasks = storyTasks.length;
|
|
228
|
+
const taskPercentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0;
|
|
229
|
+
body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)
|
|
230
|
+
|
|
231
|
+
`;
|
|
232
|
+
for (const task of storyTasks) {
|
|
233
|
+
const checkbox = task.completed ? "[x]" : "[ ]";
|
|
234
|
+
body += `- ${checkbox} **${task.id}**: ${task.title}
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
body += "\n";
|
|
238
|
+
body += `---
|
|
239
|
+
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
body += `## SpecWeave Increment
|
|
243
|
+
|
|
244
|
+
`;
|
|
245
|
+
body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})
|
|
246
|
+
|
|
247
|
+
`;
|
|
248
|
+
body += `---
|
|
249
|
+
|
|
250
|
+
`;
|
|
251
|
+
body += `\u{1F916} Auto-synced by SpecWeave`;
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
async function syncUserStoryIssue(owner, repo, featureId, story, tasks, incrementData, milestoneNumber, existingIssueNumber) {
|
|
255
|
+
const title = `[${featureId}][${story.id}] ${story.title}`;
|
|
256
|
+
const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
|
|
257
|
+
const labels = ["specweave", "user-story"];
|
|
258
|
+
const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || "p2";
|
|
259
|
+
labels.push(priority);
|
|
260
|
+
if (existingIssueNumber) {
|
|
261
|
+
const updateResult = await execFileNoThrow("gh", [
|
|
262
|
+
"issue",
|
|
263
|
+
"edit",
|
|
264
|
+
String(existingIssueNumber),
|
|
265
|
+
"--repo",
|
|
266
|
+
`${owner}/${repo}`,
|
|
267
|
+
"--title",
|
|
268
|
+
title,
|
|
269
|
+
"--body",
|
|
270
|
+
body
|
|
271
|
+
]);
|
|
272
|
+
if (updateResult.exitCode !== 0) {
|
|
273
|
+
throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
number: existingIssueNumber,
|
|
277
|
+
url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const createArgs = [
|
|
108
281
|
"issue",
|
|
109
282
|
"create",
|
|
110
283
|
"--repo",
|
|
@@ -112,16 +285,18 @@ async function createGitHubIssue(owner, repo, title, body, labels) {
|
|
|
112
285
|
"--title",
|
|
113
286
|
title,
|
|
114
287
|
"--body",
|
|
115
|
-
body
|
|
288
|
+
body,
|
|
289
|
+
"--milestone",
|
|
290
|
+
String(milestoneNumber)
|
|
116
291
|
];
|
|
117
292
|
if (labels.length > 0) {
|
|
118
|
-
|
|
293
|
+
createArgs.push("--label", labels.join(","));
|
|
119
294
|
}
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
throw new Error(`Failed to create issue: ${
|
|
295
|
+
const createResult = await execFileNoThrow("gh", createArgs);
|
|
296
|
+
if (createResult.exitCode !== 0) {
|
|
297
|
+
throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
|
|
123
298
|
}
|
|
124
|
-
const urlMatch =
|
|
299
|
+
const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
125
300
|
if (!urlMatch) {
|
|
126
301
|
throw new Error("Could not parse issue URL from gh output");
|
|
127
302
|
}
|
|
@@ -130,84 +305,32 @@ async function createGitHubIssue(owner, repo, title, body, labels) {
|
|
|
130
305
|
url: urlMatch[0]
|
|
131
306
|
};
|
|
132
307
|
}
|
|
133
|
-
async function updateGitHubIssue(owner, repo, issueNumber, body) {
|
|
134
|
-
const result = await execFileNoThrow("gh", [
|
|
135
|
-
"issue",
|
|
136
|
-
"edit",
|
|
137
|
-
String(issueNumber),
|
|
138
|
-
"--repo",
|
|
139
|
-
`${owner}/${repo}`,
|
|
140
|
-
"--body",
|
|
141
|
-
body
|
|
142
|
-
]);
|
|
143
|
-
if (result.exitCode !== 0) {
|
|
144
|
-
throw new Error(`Failed to update issue: ${result.stderr || result.stdout}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function loadExistingGitHubLink(incrementPath) {
|
|
148
|
-
const metadataPath = path.join(incrementPath, "metadata.json");
|
|
149
|
-
if (!existsSync(metadataPath)) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
154
|
-
if (metadata.github?.issue) {
|
|
155
|
-
return {
|
|
156
|
-
issue: metadata.github.issue,
|
|
157
|
-
url: metadata.github.url
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (metadata.sync?.issueNumber) {
|
|
161
|
-
return {
|
|
162
|
-
issue: metadata.sync.issueNumber,
|
|
163
|
-
url: metadata.sync.issueUrl
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
} catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function updateIncrementMetadata(incrementPath, issueNumber, issueUrl) {
|
|
172
|
-
const metadataPath = path.join(incrementPath, "metadata.json");
|
|
173
|
-
let metadata = {};
|
|
174
|
-
if (existsSync(metadataPath)) {
|
|
175
|
-
try {
|
|
176
|
-
metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
metadata.github = {
|
|
181
|
-
issue: issueNumber,
|
|
182
|
-
url: issueUrl,
|
|
183
|
-
lastSync: (/* @__PURE__ */ new Date()).toISOString()
|
|
184
|
-
};
|
|
185
|
-
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
|
|
186
|
-
}
|
|
187
308
|
async function main() {
|
|
188
309
|
const args = process.argv.slice(2);
|
|
189
310
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
190
311
|
console.log("Usage: node github-increment-sync-cli.js <increment-id> [options]");
|
|
191
312
|
console.log("");
|
|
313
|
+
console.log("Creates GitHub issues with CORRECT format:");
|
|
314
|
+
console.log(" - Milestone: [FS-XXX] Feature Title");
|
|
315
|
+
console.log(" - Issues: [FS-XXX][US-YYY] User Story Title (one per US)");
|
|
316
|
+
console.log("");
|
|
192
317
|
console.log("Arguments:");
|
|
193
|
-
console.log(" increment-id Increment ID (e.g.,
|
|
318
|
+
console.log(" increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)");
|
|
194
319
|
console.log("");
|
|
195
320
|
console.log("Options:");
|
|
196
|
-
console.log(" --
|
|
197
|
-
console.log(" --dry-run Preview issue without creating");
|
|
321
|
+
console.log(" --dry-run Preview issues without creating");
|
|
198
322
|
console.log("");
|
|
199
323
|
console.log("Environment:");
|
|
200
324
|
console.log(" GITHUB_TOKEN Required - GitHub personal access token");
|
|
201
325
|
console.log("");
|
|
202
326
|
console.log("Example:");
|
|
203
|
-
console.log(" GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js
|
|
327
|
+
console.log(" GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002");
|
|
204
328
|
process.exit(args.length === 0 ? 1 : 0);
|
|
205
329
|
}
|
|
206
330
|
const incrementId = args[0];
|
|
207
|
-
const force = args.includes("--force");
|
|
208
331
|
const dryRun = args.includes("--dry-run");
|
|
209
332
|
console.log(`
|
|
210
|
-
\u{1F419} GitHub Increment Sync CLI`);
|
|
333
|
+
\u{1F419} GitHub Increment Sync CLI (Per-User-Story Mode)`);
|
|
211
334
|
console.log(` Increment: ${incrementId}`);
|
|
212
335
|
const incrementPath = await findIncrementFolder(incrementId);
|
|
213
336
|
if (!incrementPath) {
|
|
@@ -238,6 +361,8 @@ async function main() {
|
|
|
238
361
|
console.log(`
|
|
239
362
|
\u{1F504} Parsing increment spec.md...`);
|
|
240
363
|
const incrementData = await builder.parse();
|
|
364
|
+
const featureId = incrementData.frontmatter.feature_id || `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, "0") || "UNKNOWN"}`;
|
|
365
|
+
console.log(` \u{1F4E6} Feature: ${featureId}`);
|
|
241
366
|
console.log(` \u{1F4E6} Title: ${incrementData.title}`);
|
|
242
367
|
console.log(` \u{1F4DD} User Stories: ${incrementData.userStories.length}`);
|
|
243
368
|
const totalACs = incrementData.userStories.reduce(
|
|
@@ -245,91 +370,79 @@ async function main() {
|
|
|
245
370
|
0
|
|
246
371
|
);
|
|
247
372
|
console.log(` \u2713 Acceptance Criteria: ${totalACs}`);
|
|
248
|
-
|
|
249
|
-
|
|
373
|
+
console.log(` \u{1F527} Tasks: ${incrementData.tasks.length}`);
|
|
374
|
+
if (incrementData.userStories.length === 0) {
|
|
375
|
+
console.error(`
|
|
376
|
+
\u274C No user stories found in spec.md`);
|
|
377
|
+
console.error(" Ensure spec.md has ### US-XXX: Title sections");
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
250
380
|
console.log(`
|
|
251
|
-
\u{1F4CB}
|
|
252
|
-
console.log(`
|
|
253
|
-
|
|
381
|
+
\u{1F4CB} Issues to Create/Update:`);
|
|
382
|
+
console.log(` \u{1F3AF} Milestone: [${featureId}] ${incrementData.title}`);
|
|
383
|
+
for (const story of incrementData.userStories) {
|
|
384
|
+
const storyTasks = incrementData.tasks.filter(
|
|
385
|
+
(t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
|
|
386
|
+
);
|
|
387
|
+
console.log(` \u{1F4DD} [${featureId}][${story.id}] ${story.title}`);
|
|
388
|
+
console.log(` \u2514\u2500 ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
|
|
389
|
+
}
|
|
254
390
|
if (dryRun) {
|
|
255
391
|
console.log(`
|
|
256
|
-
\u{1F4C4} Issue Body
|
|
392
|
+
\u{1F4C4} Sample Issue Body (${incrementData.userStories[0].id}):
|
|
257
393
|
`);
|
|
258
|
-
|
|
394
|
+
const sampleBody = buildUserStoryIssueBody(
|
|
395
|
+
incrementData.userStories[0],
|
|
396
|
+
incrementData.tasks,
|
|
397
|
+
incrementData,
|
|
398
|
+
config ? `${config.owner}/${config.repo}` : "owner/repo"
|
|
399
|
+
);
|
|
400
|
+
console.log(sampleBody);
|
|
259
401
|
console.log(`
|
|
260
|
-
\u2705 Dry run complete (no
|
|
402
|
+
\u2705 Dry run complete (no issues created)`);
|
|
261
403
|
process.exit(0);
|
|
262
404
|
}
|
|
263
405
|
if (!config) {
|
|
264
406
|
console.error("\u274C GitHub config not available");
|
|
265
407
|
process.exit(1);
|
|
266
408
|
}
|
|
267
|
-
const
|
|
268
|
-
if (metadataLink) {
|
|
269
|
-
console.log(`
|
|
270
|
-
\u{1F4CE} Found existing issue link in metadata: #${metadataLink.issue}`);
|
|
271
|
-
console.log(`\u{1F504} Updating issue #${metadataLink.issue} with new format...`);
|
|
272
|
-
await updateGitHubIssue(config.owner, config.repo, metadataLink.issue, issue.body);
|
|
273
|
-
console.log(` \u2705 Body updated with User Stories and ACs`);
|
|
274
|
-
const updateTitleResult = await execFileNoThrow("gh", [
|
|
275
|
-
"issue",
|
|
276
|
-
"edit",
|
|
277
|
-
String(metadataLink.issue),
|
|
278
|
-
"--repo",
|
|
279
|
-
`${config.owner}/${config.repo}`,
|
|
280
|
-
"--title",
|
|
281
|
-
issue.title
|
|
282
|
-
]);
|
|
283
|
-
if (updateTitleResult.exitCode === 0) {
|
|
284
|
-
console.log(` \u2705 Title updated to: ${issue.title}`);
|
|
285
|
-
} else {
|
|
286
|
-
console.log(` \u26A0\uFE0F Could not update title (may need permissions)`);
|
|
287
|
-
}
|
|
288
|
-
await updateIncrementMetadata(
|
|
289
|
-
incrementPath,
|
|
290
|
-
metadataLink.issue,
|
|
291
|
-
`https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`
|
|
292
|
-
);
|
|
293
|
-
console.log(`
|
|
294
|
-
\u2705 Sync complete!`);
|
|
295
|
-
console.log(` \u{1F517} https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`);
|
|
296
|
-
process.exit(0);
|
|
297
|
-
}
|
|
298
|
-
const featureId = incrementData.frontmatter.feature_id || `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, "0") || "UNKNOWN"}`;
|
|
299
|
-
console.log(`
|
|
300
|
-
\u{1F50D} Searching GitHub for existing issue [${featureId}]...`);
|
|
301
|
-
const existingIssue = await findExistingIssue(config.owner, config.repo, featureId);
|
|
302
|
-
if (existingIssue) {
|
|
303
|
-
console.log(` Found existing issue: #${existingIssue}`);
|
|
304
|
-
console.log(`\u{1F504} Updating issue #${existingIssue}...`);
|
|
305
|
-
await updateGitHubIssue(config.owner, config.repo, existingIssue, issue.body);
|
|
306
|
-
console.log(` \u2705 Issue #${existingIssue} updated`);
|
|
307
|
-
await updateIncrementMetadata(
|
|
308
|
-
incrementPath,
|
|
309
|
-
existingIssue,
|
|
310
|
-
`https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`
|
|
311
|
-
);
|
|
312
|
-
console.log(`
|
|
313
|
-
\u2705 Sync complete!`);
|
|
314
|
-
console.log(` \u{1F517} https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`);
|
|
315
|
-
process.exit(0);
|
|
316
|
-
}
|
|
317
|
-
console.log(` No existing issue found`);
|
|
409
|
+
const existingLinks = loadExistingGitHubLinks(incrementPath);
|
|
318
410
|
console.log(`
|
|
319
|
-
\u{
|
|
320
|
-
const
|
|
411
|
+
\u{1F3AF} Creating/updating milestone...`);
|
|
412
|
+
const milestone = await createOrGetMilestone(
|
|
321
413
|
config.owner,
|
|
322
414
|
config.repo,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
415
|
+
featureId,
|
|
416
|
+
incrementData.title,
|
|
417
|
+
existingLinks.milestone
|
|
326
418
|
);
|
|
327
|
-
console.log(` \u2705
|
|
328
|
-
|
|
329
|
-
|
|
419
|
+
console.log(` \u2705 Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
|
|
420
|
+
console.log(`
|
|
421
|
+
\u{1F4DD} Creating/updating user story issues...`);
|
|
422
|
+
const userStoryIssues = {};
|
|
423
|
+
for (const story of incrementData.userStories) {
|
|
424
|
+
const existingIssue = existingLinks.userStoryIssues[story.id];
|
|
425
|
+
const issue = await syncUserStoryIssue(
|
|
426
|
+
config.owner,
|
|
427
|
+
config.repo,
|
|
428
|
+
featureId,
|
|
429
|
+
story,
|
|
430
|
+
incrementData.tasks,
|
|
431
|
+
incrementData,
|
|
432
|
+
milestone.number,
|
|
433
|
+
existingIssue
|
|
434
|
+
);
|
|
435
|
+
userStoryIssues[story.id] = issue.number;
|
|
436
|
+
const action = existingIssue ? "\u267B\uFE0F Updated" : "\u2705 Created";
|
|
437
|
+
console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
|
|
438
|
+
}
|
|
439
|
+
await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
|
|
440
|
+
console.log(`
|
|
441
|
+
\u{1F4DD} Metadata updated`);
|
|
330
442
|
console.log(`
|
|
331
443
|
\u2705 Sync complete!`);
|
|
332
|
-
console.log(` \u{
|
|
444
|
+
console.log(` \u{1F3AF} Milestone: ${milestone.url}`);
|
|
445
|
+
console.log(` \u{1F4DD} Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
|
|
333
446
|
process.exit(0);
|
|
334
447
|
} catch (error) {
|
|
335
448
|
console.error(`
|