specweave 1.0.351 → 1.0.352
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/bin/specweave.js +9 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts +5 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js +61 -23
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +3 -2
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +2 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +25 -9
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +14 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +10 -7
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +7 -5
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +13 -3
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +24 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +36 -20
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +32 -22
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +144 -8
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +8 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.js +94 -24
- package/dist/plugins/specweave-github/lib/github-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.js +29 -9
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts +59 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js +159 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts +45 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js +92 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +13 -28
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +2 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +19 -7
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts +47 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js +110 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js +77 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +17 -2
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +103 -33
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +4 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +19 -6
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts +29 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js +73 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts +15 -2
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js +121 -33
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js.map +1 -1
- package/dist/src/cli/commands/sync-progress.d.ts +6 -0
- package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-progress.js +37 -0
- package/dist/src/cli/commands/sync-progress.js.map +1 -1
- package/dist/src/cli/commands/sync-task.d.ts +16 -0
- package/dist/src/cli/commands/sync-task.d.ts.map +1 -0
- package/dist/src/cli/commands/sync-task.js +42 -0
- package/dist/src/cli/commands/sync-task.js.map +1 -0
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts +9 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js +26 -8
- package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +26 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +33 -11
- package/dist/src/core/increment/status-commands.js.map +1 -1
- package/dist/src/locales/de/cli.json +245 -77
- package/dist/src/locales/es/cli.json +238 -3
- package/dist/src/locales/fr/cli.json +254 -86
- package/dist/src/locales/ja/cli.json +246 -78
- package/dist/src/locales/ko/cli.json +246 -78
- package/dist/src/locales/pt/cli.json +245 -77
- package/dist/src/locales/zh/cli.json +251 -83
- package/dist/src/sync/github-reconciler.d.ts +30 -0
- package/dist/src/sync/github-reconciler.d.ts.map +1 -1
- package/dist/src/sync/github-reconciler.js +235 -1
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +31 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +26 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +30 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +235 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/skills/team-lead/agents/security.md +0 -1
- package/plugins/specweave-ado/hooks/post-task-completion.sh +2 -2
- package/plugins/specweave-ado/lib/ado-client-v2.js +51 -21
- package/plugins/specweave-ado/lib/ado-client-v2.ts +62 -23
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +4 -4
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +4 -3
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.js +54 -12
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +88 -18
- package/plugins/specweave-ado/lib/ado-profile-resolver.js +1 -1
- package/plugins/specweave-ado/lib/ado-profile-resolver.ts +3 -1
- package/plugins/specweave-ado/lib/ado-spec-sync.js +22 -9
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +27 -9
- package/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/plugins/specweave-ado/lib/conflict-resolver.ts +17 -1
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +11 -1
- package/plugins/specweave-ado/lib/per-us-sync.js +8 -1
- package/plugins/specweave-ado/lib/per-us-sync.ts +17 -2
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +28 -2
- package/plugins/specweave-github/hooks/post-task-completion.sh +6 -3
- package/plugins/specweave-github/lib/enhanced-github-sync.js +35 -6
- package/plugins/specweave-github/lib/github-board-resolver.js +4 -4
- package/plugins/specweave-github/lib/github-board-resolver.ts +4 -4
- package/plugins/specweave-github/lib/github-client-v2.js +6 -6
- package/plugins/specweave-github/lib/github-client-v2.ts +11 -7
- package/plugins/specweave-github/lib/github-client.js +5 -4
- package/plugins/specweave-github/lib/github-client.ts +7 -5
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +17 -3
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +16 -3
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +20 -11
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +42 -20
- package/plugins/specweave-github/lib/github-graphql-client.js +29 -20
- package/plugins/specweave-github/lib/github-graphql-client.ts +34 -22
- package/plugins/specweave-github/lib/github-hierarchical-sync.js +2 -2
- package/plugins/specweave-github/lib/github-hierarchical-sync.ts +2 -2
- package/plugins/specweave-github/lib/github-multi-project-sync.js +23 -7
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +26 -8
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +110 -5
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +135 -9
- package/plugins/specweave-github/lib/github-spec-sync.js +85 -24
- package/plugins/specweave-github/lib/github-spec-sync.ts +100 -26
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +3 -1
- package/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +43 -0
- package/plugins/specweave-github/lib/per-us-sync.js +26 -11
- package/plugins/specweave-github/lib/per-us-sync.ts +29 -11
- package/plugins/specweave-jira/hooks/post-task-completion.sh +2 -1
- package/plugins/specweave-jira/lib/content-format-adapter.js +116 -0
- package/plugins/specweave-jira/lib/content-format-adapter.ts +189 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +21 -5
- package/plugins/specweave-jira/lib/jira-deployment-detector.js +63 -0
- package/plugins/specweave-jira/lib/jira-deployment-detector.ts +113 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +12 -29
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +13 -27
- package/plugins/specweave-jira/lib/jira-epic-sync.js +15 -5
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +22 -7
- package/plugins/specweave-jira/lib/jira-field-discovery.js +76 -0
- package/plugins/specweave-jira/lib/jira-field-discovery.ts +139 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.js +10 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +11 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +19 -9
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +25 -14
- package/plugins/specweave-jira/lib/jira-paginated-search.js +55 -0
- package/plugins/specweave-jira/lib/jira-paginated-search.ts +108 -0
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.ts +5 -3
- package/plugins/specweave-jira/lib/jira-spec-sync.js +102 -31
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +123 -45
- package/plugins/specweave-jira/lib/jira-status-sync.js +18 -5
- package/plugins/specweave-jira/lib/jira-status-sync.ts +21 -6
- package/plugins/specweave-jira/lib/metadata-paths.js +38 -0
- package/plugins/specweave-jira/lib/metadata-paths.ts +73 -0
- package/plugins/specweave-jira/lib/reorganization-detector.js +101 -23
- package/plugins/specweave-jira/lib/reorganization-detector.ts +125 -35
- package/plugins/specweave-jira/scripts/refresh-cache.js +1 -1
- package/plugins/specweave-jira/scripts/refresh-cache.ts +2 -2
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +3 -5
|
@@ -27,9 +27,10 @@ async function syncSpecWithEnhancedContent(options) {
|
|
|
27
27
|
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
28
28
|
console.log(`\u{1F4CB} Mapped ${Object.keys(mapping.userStoryMappings).length} user stories to tasks`);
|
|
29
29
|
}
|
|
30
|
-
const
|
|
30
|
+
const defaultBranch = (owner && repo) ? await getDefaultBranch(owner, repo) : null;
|
|
31
|
+
const taskMapping = buildTaskMapping(mapping.increments, owner, repo, defaultBranch);
|
|
31
32
|
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
32
|
-
const sourceLinks = buildSourceLinks(mapping.increments[0]?.id, owner, repo);
|
|
33
|
+
const sourceLinks = buildSourceLinks(mapping.increments[0]?.id, owner, repo, defaultBranch);
|
|
33
34
|
const enhancedSpec = {
|
|
34
35
|
...baseSpec,
|
|
35
36
|
summary: baseSpec.description,
|
|
@@ -153,7 +154,33 @@ async function findSpecWeaveRoot(specPath) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
// Cache for default branch per repo per session
|
|
158
|
+
const defaultBranchCache = new Map();
|
|
159
|
+
|
|
160
|
+
async function getDefaultBranch(owner, repo) {
|
|
161
|
+
const cacheKey = `${owner}/${repo}`;
|
|
162
|
+
if (defaultBranchCache.has(cacheKey)) {
|
|
163
|
+
return defaultBranchCache.get(cacheKey);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const { execFileSync } = await import("child_process");
|
|
167
|
+
const result = execFileSync("gh", [
|
|
168
|
+
"repo", "view", `${owner}/${repo}`,
|
|
169
|
+
"--json", "defaultBranchRef",
|
|
170
|
+
"--jq", ".defaultBranchRef.name"
|
|
171
|
+
], { encoding: "utf-8", timeout: 10000 }).trim();
|
|
172
|
+
if (result) {
|
|
173
|
+
defaultBranchCache.set(cacheKey, result);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// On failure, return null — callers omit branch segment
|
|
178
|
+
}
|
|
179
|
+
defaultBranchCache.set(cacheKey, null);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildTaskMapping(increments, owner, repo, defaultBranch) {
|
|
157
184
|
if (increments.length === 0) return void 0;
|
|
158
185
|
const firstIncrement = increments[0];
|
|
159
186
|
const tasks = firstIncrement.tasks.map((task) => ({
|
|
@@ -162,10 +189,11 @@ function buildTaskMapping(increments, owner, repo) {
|
|
|
162
189
|
userStories: task.userStories,
|
|
163
190
|
githubIssue: task.githubIssue
|
|
164
191
|
}));
|
|
192
|
+
const branchSegment = defaultBranch ? `/blob/${defaultBranch}` : "";
|
|
165
193
|
return {
|
|
166
194
|
incrementId: firstIncrement.id,
|
|
167
195
|
tasks,
|
|
168
|
-
tasksUrl: `https://github.com/${owner}/${repo}
|
|
196
|
+
tasksUrl: `https://github.com/${owner}/${repo}${branchSegment}/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
169
197
|
};
|
|
170
198
|
}
|
|
171
199
|
async function findArchitectureDocs(rootDir, specId) {
|
|
@@ -198,9 +226,10 @@ async function findArchitectureDocs(rootDir, specId) {
|
|
|
198
226
|
}
|
|
199
227
|
return docs;
|
|
200
228
|
}
|
|
201
|
-
function buildSourceLinks(incrementId, owner, repo) {
|
|
229
|
+
function buildSourceLinks(incrementId, owner, repo, defaultBranch) {
|
|
202
230
|
if (!incrementId) return void 0;
|
|
203
|
-
const
|
|
231
|
+
const branchSegment = defaultBranch ? `/blob/${defaultBranch}` : "";
|
|
232
|
+
const baseUrl = `https://github.com/${owner}/${repo}${branchSegment}/.specweave`;
|
|
204
233
|
return {
|
|
205
234
|
spec: `${baseUrl}/docs/internal/specs/default/spec-${incrementId.replace(/^\d+-/, "")}.md`,
|
|
206
235
|
plan: `${baseUrl}/increments/${incrementId}/plan.md`,
|
|
@@ -15,9 +15,9 @@ async function fetchBoardsForRepo(owner, repo) {
|
|
|
15
15
|
"-H",
|
|
16
16
|
"Accept: application/vnd.github+json"
|
|
17
17
|
], { env: getGhEnv() });
|
|
18
|
-
if (result.
|
|
18
|
+
if (result.exitCode !== 0) {
|
|
19
19
|
console.error(`\u274C Failed to fetch boards for ${owner}/${repo}:`, result.stderr);
|
|
20
|
-
throw new Error(`GitHub API error
|
|
20
|
+
throw new Error(`GitHub API error (exit code ${result.exitCode}): ${result.stderr}`);
|
|
21
21
|
}
|
|
22
22
|
const boards = result.stdout.trim().split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
23
23
|
console.log(`\u2705 Found ${boards.length} board(s) for repo ${owner}/${repo}`);
|
|
@@ -38,9 +38,9 @@ async function fetchBoardsForOrg(org) {
|
|
|
38
38
|
"-H",
|
|
39
39
|
"Accept: application/vnd.github+json"
|
|
40
40
|
], { env: getGhEnv() });
|
|
41
|
-
if (result.
|
|
41
|
+
if (result.exitCode !== 0) {
|
|
42
42
|
console.error(`\u274C Failed to fetch boards for org ${org}:`, result.stderr);
|
|
43
|
-
throw new Error(`GitHub API error
|
|
43
|
+
throw new Error(`GitHub API error (exit code ${result.exitCode}): ${result.stderr}`);
|
|
44
44
|
}
|
|
45
45
|
const boards = result.stdout.trim().split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
46
46
|
console.log(`\u2705 Found ${boards.length} board(s) for org ${org}`);
|
|
@@ -54,9 +54,9 @@ export async function fetchBoardsForRepo(
|
|
|
54
54
|
'Accept: application/vnd.github+json',
|
|
55
55
|
], { env: getGhEnv() });
|
|
56
56
|
|
|
57
|
-
if (result.
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
58
|
console.error(`❌ Failed to fetch boards for ${owner}/${repo}:`, result.stderr);
|
|
59
|
-
throw new Error(`GitHub API error
|
|
59
|
+
throw new Error(`GitHub API error (exit code ${result.exitCode}): ${result.stderr}`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Parse JSONL (one JSON object per line)
|
|
@@ -98,9 +98,9 @@ export async function fetchBoardsForOrg(
|
|
|
98
98
|
'Accept: application/vnd.github+json',
|
|
99
99
|
], { env: getGhEnv() });
|
|
100
100
|
|
|
101
|
-
if (result.
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
102
|
console.error(`❌ Failed to fetch boards for org ${org}:`, result.stderr);
|
|
103
|
-
throw new Error(`GitHub API error
|
|
103
|
+
throw new Error(`GitHub API error (exit code ${result.exitCode}): ${result.stderr}`);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Parse JSONL
|
|
@@ -103,13 +103,14 @@ class GitHubClientV2 {
|
|
|
103
103
|
/**
|
|
104
104
|
* Create or get existing milestone
|
|
105
105
|
*/
|
|
106
|
-
async createOrGetMilestone(title, description, daysFromNow
|
|
106
|
+
async createOrGetMilestone(title, description, daysFromNow) {
|
|
107
|
+
const dueDays = daysFromNow ?? 2;
|
|
107
108
|
const existing = await this.getMilestoneByTitle(title);
|
|
108
109
|
if (existing) {
|
|
109
110
|
return existing;
|
|
110
111
|
}
|
|
111
112
|
const dueDate = /* @__PURE__ */ new Date();
|
|
112
|
-
dueDate.setDate(dueDate.getDate() +
|
|
113
|
+
dueDate.setDate(dueDate.getDate() + dueDays);
|
|
113
114
|
const dueDateISO = dueDate.toISOString();
|
|
114
115
|
const args = [
|
|
115
116
|
"api",
|
|
@@ -136,7 +137,7 @@ class GitHubClientV2 {
|
|
|
136
137
|
async getMilestoneByTitle(title) {
|
|
137
138
|
const result = await execFileNoThrow("gh", [
|
|
138
139
|
"api",
|
|
139
|
-
`repos/${this.fullRepo}/milestones`,
|
|
140
|
+
`repos/${this.fullRepo}/milestones?per_page=100&state=all`,
|
|
140
141
|
"--jq",
|
|
141
142
|
`.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}`
|
|
142
143
|
], { env: this.getGhEnv() });
|
|
@@ -477,10 +478,9 @@ ${body}`;
|
|
|
477
478
|
async getLastComment(issueNumber) {
|
|
478
479
|
const result = await execFileNoThrow("gh", [
|
|
479
480
|
"api",
|
|
480
|
-
`repos/${this.fullRepo}/issues/${issueNumber}/comments`,
|
|
481
|
+
`repos/${this.fullRepo}/issues/${issueNumber}/comments?sort=created&direction=desc&per_page=1`,
|
|
481
482
|
"--jq",
|
|
482
|
-
".[
|
|
483
|
-
// Get last comment only
|
|
483
|
+
".[0] | {body: .body, author: .user.login}"
|
|
484
484
|
], { env: this.getGhEnv() });
|
|
485
485
|
if (result.exitCode !== 0) {
|
|
486
486
|
return null;
|
|
@@ -148,17 +148,20 @@ export class GitHubClientV2 {
|
|
|
148
148
|
async createOrGetMilestone(
|
|
149
149
|
title: string,
|
|
150
150
|
description?: string,
|
|
151
|
-
daysFromNow
|
|
151
|
+
daysFromNow?: number
|
|
152
152
|
): Promise<GitHubMilestone> {
|
|
153
|
+
// Use configured value or default to 2 days
|
|
154
|
+
const dueDays = daysFromNow ?? 2;
|
|
155
|
+
|
|
153
156
|
// Check if milestone already exists
|
|
154
157
|
const existing = await this.getMilestoneByTitle(title);
|
|
155
158
|
if (existing) {
|
|
156
159
|
return existing;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
// Calculate due date
|
|
162
|
+
// Calculate due date from creation timestamp
|
|
160
163
|
const dueDate = new Date();
|
|
161
|
-
dueDate.setDate(dueDate.getDate() +
|
|
164
|
+
dueDate.setDate(dueDate.getDate() + dueDays);
|
|
162
165
|
const dueDateISO = dueDate.toISOString();
|
|
163
166
|
|
|
164
167
|
// Build API request
|
|
@@ -194,7 +197,7 @@ export class GitHubClientV2 {
|
|
|
194
197
|
): Promise<GitHubMilestone | null> {
|
|
195
198
|
const result = await execFileNoThrow('gh', [
|
|
196
199
|
'api',
|
|
197
|
-
`repos/${this.fullRepo}/milestones`,
|
|
200
|
+
`repos/${this.fullRepo}/milestones?per_page=100&state=all`,
|
|
198
201
|
'--jq',
|
|
199
202
|
`.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}`,
|
|
200
203
|
], { env: this.getGhEnv() });
|
|
@@ -607,12 +610,13 @@ export class GitHubClientV2 {
|
|
|
607
610
|
* Returns the most recent comment body, or null if no comments exist
|
|
608
611
|
*/
|
|
609
612
|
async getLastComment(issueNumber: number): Promise<{body: string; author: string} | null> {
|
|
610
|
-
//
|
|
613
|
+
// Query the last comment directly using per_page=1 + page from last page
|
|
614
|
+
// sort=created&direction=desc gives newest first, per_page=1 returns just one
|
|
611
615
|
const result = await execFileNoThrow('gh', [
|
|
612
616
|
'api',
|
|
613
|
-
`repos/${this.fullRepo}/issues/${issueNumber}/comments`,
|
|
617
|
+
`repos/${this.fullRepo}/issues/${issueNumber}/comments?sort=created&direction=desc&per_page=1`,
|
|
614
618
|
'--jq',
|
|
615
|
-
'.[
|
|
619
|
+
'.[0] | {body: .body, author: .user.login}',
|
|
616
620
|
], { env: this.getGhEnv() });
|
|
617
621
|
|
|
618
622
|
if (result.exitCode !== 0) {
|
|
@@ -40,15 +40,16 @@ class GitHubClient {
|
|
|
40
40
|
*
|
|
41
41
|
* @param title Milestone title
|
|
42
42
|
* @param description Milestone description
|
|
43
|
-
* @param daysFromNow Days until milestone due (default: 2
|
|
43
|
+
* @param daysFromNow Days until milestone due (configurable via github.milestoneDueDays, default: 2)
|
|
44
44
|
*/
|
|
45
|
-
async createOrGetMilestone(title, description, daysFromNow
|
|
45
|
+
async createOrGetMilestone(title, description, daysFromNow) {
|
|
46
|
+
const dueDays = daysFromNow ?? 2;
|
|
46
47
|
const existing = await this.getMilestoneByTitle(title);
|
|
47
48
|
if (existing) {
|
|
48
49
|
return existing;
|
|
49
50
|
}
|
|
50
51
|
const dueDate = /* @__PURE__ */ new Date();
|
|
51
|
-
dueDate.setDate(dueDate.getDate() +
|
|
52
|
+
dueDate.setDate(dueDate.getDate() + dueDays);
|
|
52
53
|
const dueDateISO = dueDate.toISOString();
|
|
53
54
|
const cmd = `gh api repos/${this.repo}/milestones -f title="${title}" ${description ? `-f description="${description}"` : ""} -f due_on="${dueDateISO}" --jq '{number: .number, title: .title, description: .description, state: .state, due_on: .due_on}'`;
|
|
54
55
|
try {
|
|
@@ -63,7 +64,7 @@ class GitHubClient {
|
|
|
63
64
|
*/
|
|
64
65
|
async getMilestoneByTitle(title) {
|
|
65
66
|
try {
|
|
66
|
-
const cmd = `gh api repos/${this.repo}/milestones --jq '.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}'`;
|
|
67
|
+
const cmd = `gh api "repos/${this.repo}/milestones?per_page=100&state=all" --jq '.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}'`;
|
|
67
68
|
const output = execSync(cmd, { encoding: "utf-8" }).trim();
|
|
68
69
|
return output ? JSON.parse(output) : null;
|
|
69
70
|
} catch {
|
|
@@ -54,22 +54,24 @@ export class GitHubClient {
|
|
|
54
54
|
*
|
|
55
55
|
* @param title Milestone title
|
|
56
56
|
* @param description Milestone description
|
|
57
|
-
* @param daysFromNow Days until milestone due (default: 2
|
|
57
|
+
* @param daysFromNow Days until milestone due (configurable via github.milestoneDueDays, default: 2)
|
|
58
58
|
*/
|
|
59
59
|
async createOrGetMilestone(
|
|
60
60
|
title: string,
|
|
61
61
|
description?: string,
|
|
62
|
-
daysFromNow
|
|
62
|
+
daysFromNow?: number
|
|
63
63
|
): Promise<GitHubMilestone> {
|
|
64
|
+
// Use configured value or default to 2 days
|
|
65
|
+
const dueDays = daysFromNow ?? 2;
|
|
64
66
|
// Check if milestone already exists
|
|
65
67
|
const existing = await this.getMilestoneByTitle(title);
|
|
66
68
|
if (existing) {
|
|
67
69
|
return existing;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
// Calculate due date
|
|
72
|
+
// Calculate due date from creation timestamp
|
|
71
73
|
const dueDate = new Date();
|
|
72
|
-
dueDate.setDate(dueDate.getDate() +
|
|
74
|
+
dueDate.setDate(dueDate.getDate() + dueDays);
|
|
73
75
|
const dueDateISO = dueDate.toISOString();
|
|
74
76
|
|
|
75
77
|
// Create new milestone with due date
|
|
@@ -88,7 +90,7 @@ export class GitHubClient {
|
|
|
88
90
|
*/
|
|
89
91
|
private async getMilestoneByTitle(title: string): Promise<GitHubMilestone | null> {
|
|
90
92
|
try {
|
|
91
|
-
const cmd = `gh api repos/${this.repo}/milestones --jq '.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}'`;
|
|
93
|
+
const cmd = `gh api "repos/${this.repo}/milestones?per_page=100&state=all" --jq '.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}'`;
|
|
92
94
|
const output = execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
93
95
|
return output ? JSON.parse(output) : null;
|
|
94
96
|
} catch {
|
|
@@ -134,6 +134,22 @@ async function updateIssue(issueNumber, title, body, repo, env) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
async function appendToIssueBody(issueNumber, repo, section, env) {
|
|
137
|
+
const viewRes = await execFileNoThrow("gh", [
|
|
138
|
+
"issue",
|
|
139
|
+
"view",
|
|
140
|
+
String(issueNumber),
|
|
141
|
+
"--repo",
|
|
142
|
+
repo,
|
|
143
|
+
"--json",
|
|
144
|
+
"body",
|
|
145
|
+
"-q",
|
|
146
|
+
".body"
|
|
147
|
+
], { env });
|
|
148
|
+
const existingBody = viewRes.success ? (viewRes.stdout || "").trim() : "";
|
|
149
|
+
const combinedBody = existingBody ? `${existingBody}
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
${section}` : section;
|
|
137
153
|
const res = await execFileNoThrow("gh", [
|
|
138
154
|
"issue",
|
|
139
155
|
"edit",
|
|
@@ -141,9 +157,7 @@ async function appendToIssueBody(issueNumber, repo, section, env) {
|
|
|
141
157
|
"--repo",
|
|
142
158
|
repo,
|
|
143
159
|
"--body",
|
|
144
|
-
|
|
145
|
-
"--json",
|
|
146
|
-
"number,url"
|
|
160
|
+
combinedBody
|
|
147
161
|
], { env });
|
|
148
162
|
if (!res.success) {
|
|
149
163
|
throw new Error(`Cross-ref update failed: ${res.stderr}`);
|
|
@@ -226,12 +226,25 @@ async function appendToIssueBody(
|
|
|
226
226
|
section: string,
|
|
227
227
|
env: NodeJS.ProcessEnv,
|
|
228
228
|
): Promise<void> {
|
|
229
|
-
//
|
|
229
|
+
// Read existing body first to avoid overwriting it
|
|
230
|
+
const viewRes = await execFileNoThrow('gh', [
|
|
231
|
+
'issue', 'view', String(issueNumber),
|
|
232
|
+
'--repo', repo,
|
|
233
|
+
'--json', 'body',
|
|
234
|
+
'-q', '.body',
|
|
235
|
+
], { env });
|
|
236
|
+
|
|
237
|
+
const existingBody = viewRes.success ? (viewRes.stdout || '').trim() : '';
|
|
238
|
+
|
|
239
|
+
// Concatenate: if existing body is empty, use section directly (no leading separator)
|
|
240
|
+
const combinedBody = existingBody
|
|
241
|
+
? `${existingBody}\n\n---\n${section}`
|
|
242
|
+
: section;
|
|
243
|
+
|
|
230
244
|
const res = await execFileNoThrow('gh', [
|
|
231
245
|
'issue', 'edit', String(issueNumber),
|
|
232
246
|
'--repo', repo,
|
|
233
|
-
'--body',
|
|
234
|
-
'--json', 'number,url',
|
|
247
|
+
'--body', combinedBody,
|
|
235
248
|
], { env });
|
|
236
249
|
|
|
237
250
|
if (!res.success) {
|
|
@@ -3,19 +3,20 @@ import { existsSync, readFileSync } from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { GitHubFeatureSync } from "./github-feature-sync.js";
|
|
5
5
|
import { GitHubClientV2 } from "./github-client-v2.js";
|
|
6
|
-
async function
|
|
7
|
-
const projectRoot = process.cwd();
|
|
6
|
+
async function resolveGitHubConfig(options) {
|
|
7
|
+
const projectRoot = options?.projectRoot || process.cwd();
|
|
8
8
|
const configPath = path.join(projectRoot, ".specweave/config.json");
|
|
9
|
-
let owner = process.env.GITHUB_OWNER || "";
|
|
10
|
-
let repo = process.env.GITHUB_REPO || "";
|
|
11
|
-
const token = process.env.GITHUB_TOKEN || "";
|
|
12
|
-
if (existsSync(configPath)) {
|
|
9
|
+
let owner = options?.cliOwner || process.env.GITHUB_OWNER || "";
|
|
10
|
+
let repo = options?.cliRepo || process.env.GITHUB_REPO || "";
|
|
11
|
+
const token = options?.cliToken || process.env.GITHUB_TOKEN || "";
|
|
12
|
+
if ((!owner || !repo) && existsSync(configPath)) {
|
|
13
13
|
try {
|
|
14
14
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
15
|
-
if (config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
15
|
+
if (!owner && !repo && config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
16
16
|
owner = config.sync.github.owner;
|
|
17
17
|
repo = config.sync.github.repo;
|
|
18
|
-
}
|
|
18
|
+
}
|
|
19
|
+
if (!owner && !repo && config.multiProject?.enabled && config.multiProject?.activeProject) {
|
|
19
20
|
const activeProject = config.multiProject.activeProject;
|
|
20
21
|
const projectConfig = config.multiProject.projects?.[activeProject];
|
|
21
22
|
if (projectConfig?.externalTools?.github?.repository) {
|
|
@@ -25,13 +26,15 @@ async function loadGitHubConfig() {
|
|
|
25
26
|
repo = parts[1];
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
}
|
|
29
|
+
}
|
|
30
|
+
if (!owner && !repo && config.sync?.defaultProfile && config.sync?.profiles) {
|
|
29
31
|
const profile = config.sync.profiles[config.sync.defaultProfile];
|
|
30
32
|
if (profile?.config?.owner && profile?.config?.repo) {
|
|
31
33
|
owner = profile.config.owner;
|
|
32
34
|
repo = profile.config.repo;
|
|
33
35
|
}
|
|
34
|
-
}
|
|
36
|
+
}
|
|
37
|
+
if (!owner && !repo && config.sync?.profiles) {
|
|
35
38
|
const profileNames = Object.keys(config.sync.profiles);
|
|
36
39
|
for (const name of profileNames) {
|
|
37
40
|
const profile = config.sync.profiles[name];
|
|
@@ -74,6 +77,9 @@ async function loadGitHubConfig() {
|
|
|
74
77
|
}
|
|
75
78
|
return { owner, repo, token };
|
|
76
79
|
}
|
|
80
|
+
async function loadGitHubConfig() {
|
|
81
|
+
return resolveGitHubConfig();
|
|
82
|
+
}
|
|
77
83
|
async function main() {
|
|
78
84
|
const args = process.argv.slice(2);
|
|
79
85
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
@@ -83,7 +89,7 @@ async function main() {
|
|
|
83
89
|
console.log(" feature-id Feature ID (e.g., FS-062)");
|
|
84
90
|
console.log("");
|
|
85
91
|
console.log("Environment:");
|
|
86
|
-
console.log(" GITHUB_TOKEN Required - GitHub
|
|
92
|
+
console.log(" GITHUB_TOKEN Required - GitHub PAT with repo scope");
|
|
87
93
|
console.log("");
|
|
88
94
|
console.log("Example:");
|
|
89
95
|
console.log(" GITHUB_TOKEN=ghp_xxx node github-feature-sync-cli.js FS-062");
|
|
@@ -149,3 +155,6 @@ main().catch((error) => {
|
|
|
149
155
|
console.error("Fatal error:", error);
|
|
150
156
|
process.exit(1);
|
|
151
157
|
});
|
|
158
|
+
export {
|
|
159
|
+
resolveGitHubConfig
|
|
160
|
+
};
|
|
@@ -29,26 +29,46 @@ interface GitHubConfig {
|
|
|
29
29
|
token: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Resolve GitHub config (owner/repo) with a single canonical precedence order.
|
|
34
|
+
*
|
|
35
|
+
* Resolution order (documented for both CLI and hooks):
|
|
36
|
+
* 1. CLI flags / env vars: GITHUB_OWNER, GITHUB_REPO
|
|
37
|
+
* 2. .specweave/config.json — sync.github.owner/repo
|
|
38
|
+
* 3. .specweave/config.json — multiProject.projects[active].externalTools.github
|
|
39
|
+
* 4. .specweave/config.json — sync.profiles[defaultProfile].config
|
|
40
|
+
* 5. .specweave/config.json — first GitHub profile (fallback)
|
|
41
|
+
* 6. Git remote detection (git remote get-url origin)
|
|
42
|
+
*
|
|
43
|
+
* This is the SINGLE source of truth for owner/repo config.
|
|
44
|
+
* Shell hooks should read the same config.json paths in the same order.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveGitHubConfig(options?: {
|
|
47
|
+
cliOwner?: string;
|
|
48
|
+
cliRepo?: string;
|
|
49
|
+
cliToken?: string;
|
|
50
|
+
projectRoot?: string;
|
|
51
|
+
}): Promise<{ owner: string; repo: string; token: string } | null> {
|
|
52
|
+
const projectRoot = options?.projectRoot || process.cwd();
|
|
34
53
|
const configPath = path.join(projectRoot, '.specweave/config.json');
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
55
|
+
// Step 1: CLI flags / env vars (highest precedence)
|
|
56
|
+
let owner = options?.cliOwner || process.env.GITHUB_OWNER || '';
|
|
57
|
+
let repo = options?.cliRepo || process.env.GITHUB_REPO || '';
|
|
58
|
+
const token = options?.cliToken || process.env.GITHUB_TOKEN || '';
|
|
39
59
|
|
|
40
|
-
//
|
|
41
|
-
if (existsSync(configPath)) {
|
|
60
|
+
// Step 2-5: config.json methods (only if not already resolved)
|
|
61
|
+
if ((!owner || !repo) && existsSync(configPath)) {
|
|
42
62
|
try {
|
|
43
63
|
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
44
64
|
|
|
45
|
-
// Method
|
|
46
|
-
if (config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
65
|
+
// Method 2: sync.github
|
|
66
|
+
if (!owner && !repo && config.sync?.github?.owner && config.sync?.github?.repo) {
|
|
47
67
|
owner = config.sync.github.owner;
|
|
48
68
|
repo = config.sync.github.repo;
|
|
49
69
|
}
|
|
50
|
-
// Method
|
|
51
|
-
|
|
70
|
+
// Method 3: multiProject.projects[activeProject].externalTools.github
|
|
71
|
+
if (!owner && !repo && config.multiProject?.enabled && config.multiProject?.activeProject) {
|
|
52
72
|
const activeProject = config.multiProject.activeProject;
|
|
53
73
|
const projectConfig = config.multiProject.projects?.[activeProject];
|
|
54
74
|
if (projectConfig?.externalTools?.github?.repository) {
|
|
@@ -59,17 +79,16 @@ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
81
|
}
|
|
62
|
-
// Method
|
|
63
|
-
|
|
82
|
+
// Method 4: sync.profiles[defaultProfile]
|
|
83
|
+
if (!owner && !repo && config.sync?.defaultProfile && config.sync?.profiles) {
|
|
64
84
|
const profile = config.sync.profiles[config.sync.defaultProfile];
|
|
65
85
|
if (profile?.config?.owner && profile?.config?.repo) {
|
|
66
86
|
owner = profile.config.owner;
|
|
67
87
|
repo = profile.config.repo;
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
|
-
// Method
|
|
71
|
-
|
|
72
|
-
else if (config.sync?.profiles) {
|
|
90
|
+
// Method 5: First GitHub profile if no defaultProfile is set
|
|
91
|
+
if (!owner && !repo && config.sync?.profiles) {
|
|
73
92
|
const profileNames = Object.keys(config.sync.profiles);
|
|
74
93
|
for (const name of profileNames) {
|
|
75
94
|
const profile = config.sync.profiles[name];
|
|
@@ -86,7 +105,7 @@ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
|
86
105
|
}
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
//
|
|
108
|
+
// Step 6: Git remote detection (lowest precedence)
|
|
90
109
|
if (!owner || !repo) {
|
|
91
110
|
try {
|
|
92
111
|
const { execSync } = await import('child_process');
|
|
@@ -95,14 +114,13 @@ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
|
95
114
|
cwd: projectRoot
|
|
96
115
|
}).trim();
|
|
97
116
|
|
|
98
|
-
// Parse GitHub URL (HTTPS or SSH)
|
|
99
117
|
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
100
118
|
if (match) {
|
|
101
119
|
owner = owner || match[1];
|
|
102
120
|
repo = repo || match[2];
|
|
103
121
|
}
|
|
104
122
|
} catch {
|
|
105
|
-
// Git detection failed
|
|
123
|
+
// Git detection failed
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
|
|
@@ -121,6 +139,10 @@ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
|
121
139
|
return { owner, repo, token };
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
143
|
+
return resolveGitHubConfig();
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
async function main() {
|
|
125
147
|
const args = process.argv.slice(2);
|
|
126
148
|
|
|
@@ -131,7 +153,7 @@ async function main() {
|
|
|
131
153
|
console.log(' feature-id Feature ID (e.g., FS-062)');
|
|
132
154
|
console.log('');
|
|
133
155
|
console.log('Environment:');
|
|
134
|
-
console.log(' GITHUB_TOKEN Required - GitHub
|
|
156
|
+
console.log(' GITHUB_TOKEN Required - GitHub PAT with repo scope');
|
|
135
157
|
console.log('');
|
|
136
158
|
console.log('Example:');
|
|
137
159
|
console.log(' GITHUB_TOKEN=ghp_xxx node github-feature-sync-cli.js FS-062');
|
|
@@ -8,13 +8,13 @@ class GitHubGraphQLClient {
|
|
|
8
8
|
* Tries user first, falls back to organization.
|
|
9
9
|
*/
|
|
10
10
|
async getOwnerNodeId(login) {
|
|
11
|
-
const userQuery = `query { user(login:
|
|
12
|
-
const userResult = await this.executeGraphQL(userQuery);
|
|
11
|
+
const userQuery = `query($login: String!) { user(login: $login) { id } }`;
|
|
12
|
+
const userResult = await this.executeGraphQL(userQuery, { login });
|
|
13
13
|
if (userResult.data?.user && userResult.data.user.id) {
|
|
14
14
|
return userResult.data.user.id;
|
|
15
15
|
}
|
|
16
|
-
const orgQuery = `query { organization(login:
|
|
17
|
-
const orgResult = await this.executeGraphQL(orgQuery);
|
|
16
|
+
const orgQuery = `query($login: String!) { organization(login: $login) { id } }`;
|
|
17
|
+
const orgResult = await this.executeGraphQL(orgQuery, { login });
|
|
18
18
|
if (orgResult.data?.organization && orgResult.data.organization.id) {
|
|
19
19
|
return orgResult.data.organization.id;
|
|
20
20
|
}
|
|
@@ -24,12 +24,12 @@ class GitHubGraphQLClient {
|
|
|
24
24
|
* Create a new GitHub Projects V2 board.
|
|
25
25
|
*/
|
|
26
26
|
async createProjectV2(ownerId, title) {
|
|
27
|
-
const query = `mutation {
|
|
28
|
-
createProjectV2(input: { ownerId:
|
|
27
|
+
const query = `mutation($ownerId: ID!, $title: String!) {
|
|
28
|
+
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
29
29
|
projectV2 { id number }
|
|
30
30
|
}
|
|
31
31
|
}`;
|
|
32
|
-
const result = await this.executeGraphQL(query);
|
|
32
|
+
const result = await this.executeGraphQL(query, { ownerId, title });
|
|
33
33
|
const project = result.data?.createProjectV2?.projectV2;
|
|
34
34
|
return { id: project.id, number: project.number };
|
|
35
35
|
}
|
|
@@ -38,12 +38,12 @@ class GitHubGraphQLClient {
|
|
|
38
38
|
* Returns the project item ID. Idempotent.
|
|
39
39
|
*/
|
|
40
40
|
async addProjectV2Item(projectId, contentId) {
|
|
41
|
-
const query = `mutation {
|
|
42
|
-
addProjectV2ItemById(input: { projectId:
|
|
41
|
+
const query = `mutation($projectId: ID!, $contentId: ID!) {
|
|
42
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
43
43
|
item { id }
|
|
44
44
|
}
|
|
45
45
|
}`;
|
|
46
|
-
const result = await this.executeGraphQL(query);
|
|
46
|
+
const result = await this.executeGraphQL(query, { projectId, contentId });
|
|
47
47
|
const item = result.data?.addProjectV2ItemById?.item;
|
|
48
48
|
return item.id;
|
|
49
49
|
}
|
|
@@ -51,24 +51,29 @@ class GitHubGraphQLClient {
|
|
|
51
51
|
* Update a field value on a Projects V2 item.
|
|
52
52
|
*/
|
|
53
53
|
async updateItemFieldValue(projectId, itemId, fieldId, value) {
|
|
54
|
-
const query = `mutation {
|
|
54
|
+
const query = `mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
55
55
|
updateProjectV2ItemFieldValue(input: {
|
|
56
|
-
projectId:
|
|
57
|
-
itemId:
|
|
58
|
-
fieldId:
|
|
59
|
-
value: { singleSelectOptionId:
|
|
56
|
+
projectId: $projectId,
|
|
57
|
+
itemId: $itemId,
|
|
58
|
+
fieldId: $fieldId,
|
|
59
|
+
value: { singleSelectOptionId: $optionId }
|
|
60
60
|
}) {
|
|
61
61
|
projectV2Item { id }
|
|
62
62
|
}
|
|
63
63
|
}`;
|
|
64
|
-
await this.executeGraphQL(query
|
|
64
|
+
await this.executeGraphQL(query, {
|
|
65
|
+
projectId,
|
|
66
|
+
itemId,
|
|
67
|
+
fieldId,
|
|
68
|
+
optionId: value.singleSelectOptionId
|
|
69
|
+
});
|
|
65
70
|
}
|
|
66
71
|
/**
|
|
67
72
|
* Get field definitions for a Projects V2 board.
|
|
68
73
|
*/
|
|
69
74
|
async getProjectFields(projectId) {
|
|
70
|
-
const query = `query {
|
|
71
|
-
node(id:
|
|
75
|
+
const query = `query($nodeId: ID!) {
|
|
76
|
+
node(id: $nodeId) {
|
|
72
77
|
... on ProjectV2 {
|
|
73
78
|
fields(first: 50) {
|
|
74
79
|
nodes {
|
|
@@ -82,7 +87,7 @@ class GitHubGraphQLClient {
|
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
}`;
|
|
85
|
-
const result = await this.executeGraphQL(query);
|
|
90
|
+
const result = await this.executeGraphQL(query, { nodeId: projectId });
|
|
86
91
|
const fields = result.data?.node?.fields?.nodes || [];
|
|
87
92
|
return fields.map((f) => {
|
|
88
93
|
const field = { id: f.id, name: f.name };
|
|
@@ -94,10 +99,14 @@ class GitHubGraphQLClient {
|
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
96
101
|
* Execute a GraphQL query via `gh api graphql`.
|
|
102
|
+
* All variables are passed via `-F` flags to prevent injection.
|
|
97
103
|
*/
|
|
98
|
-
executeGraphQL(query) {
|
|
104
|
+
executeGraphQL(query, variables = {}) {
|
|
99
105
|
return new Promise((resolve, reject) => {
|
|
100
106
|
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
107
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
108
|
+
args.push("-F", `${key}=${value}`);
|
|
109
|
+
}
|
|
101
110
|
const opts = {};
|
|
102
111
|
if (this.token) {
|
|
103
112
|
opts.env = { ...process.env, GH_TOKEN: this.token };
|