gsd-pi 2.38.0-dev.361f5e3 → 2.38.0-dev.8f5c161
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/resources/extensions/gsd/auto-post-unit.js +0 -18
- package/dist/resources/extensions/gsd/git-service.js +1 -8
- package/dist/resources/extensions/gsd/preferences-types.js +0 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +0 -58
- package/dist/resources/extensions/gsd/preferences.js +0 -3
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +0 -18
- package/src/resources/extensions/gsd/git-service.ts +1 -13
- package/src/resources/extensions/gsd/preferences-types.ts +0 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +0 -50
- package/src/resources/extensions/gsd/preferences.ts +0 -3
- package/dist/resources/extensions/github-sync/cli.js +0 -284
- package/dist/resources/extensions/github-sync/index.js +0 -73
- package/dist/resources/extensions/github-sync/mapping.js +0 -67
- package/dist/resources/extensions/github-sync/sync.js +0 -424
- package/dist/resources/extensions/github-sync/templates.js +0 -118
- package/dist/resources/extensions/github-sync/types.js +0 -7
- package/src/resources/extensions/github-sync/cli.ts +0 -364
- package/src/resources/extensions/github-sync/index.ts +0 -93
- package/src/resources/extensions/github-sync/mapping.ts +0 -81
- package/src/resources/extensions/github-sync/sync.ts +0 -556
- package/src/resources/extensions/github-sync/templates.ts +0 -183
- package/src/resources/extensions/github-sync/tests/cli.test.ts +0 -20
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +0 -39
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +0 -104
- package/src/resources/extensions/github-sync/tests/templates.test.ts +0 -110
- package/src/resources/extensions/github-sync/types.ts +0 -47
|
@@ -72,21 +72,11 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
72
72
|
const summaryContent = await loadFile(summaryPath);
|
|
73
73
|
if (summaryContent) {
|
|
74
74
|
const summary = parseSummary(summaryContent);
|
|
75
|
-
// Look up GitHub issue number for commit linking
|
|
76
|
-
let ghIssueNumber;
|
|
77
|
-
try {
|
|
78
|
-
const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
|
|
79
|
-
ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// GitHub sync not available — skip
|
|
83
|
-
}
|
|
84
75
|
taskContext = {
|
|
85
76
|
taskId: `${sid}/${tid}`,
|
|
86
77
|
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
87
78
|
oneLiner: summary.oneLiner || undefined,
|
|
88
79
|
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
89
|
-
issueNumber: ghIssueNumber,
|
|
90
80
|
};
|
|
91
81
|
}
|
|
92
82
|
}
|
|
@@ -104,14 +94,6 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
104
94
|
catch (e) {
|
|
105
95
|
debugLog("postUnit", { phase: "auto-commit", error: String(e) });
|
|
106
96
|
}
|
|
107
|
-
// GitHub sync (non-blocking, opt-in)
|
|
108
|
-
try {
|
|
109
|
-
const { runGitHubSync } = await import("../github-sync/sync.js");
|
|
110
|
-
await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
debugLog("postUnit", { phase: "github-sync", error: String(e) });
|
|
114
|
-
}
|
|
115
97
|
// Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
|
|
116
98
|
if (!opts?.skipDoctor)
|
|
117
99
|
try {
|
|
@@ -36,19 +36,12 @@ export function buildTaskCommitMessage(ctx) {
|
|
|
36
36
|
: description;
|
|
37
37
|
const subject = `${type}(${scope}): ${truncated}`;
|
|
38
38
|
// Build body with key files if available
|
|
39
|
-
const bodyParts = [];
|
|
40
39
|
if (ctx.keyFiles && ctx.keyFiles.length > 0) {
|
|
41
40
|
const fileLines = ctx.keyFiles
|
|
42
41
|
.slice(0, 8) // cap at 8 files to keep commit concise
|
|
43
42
|
.map(f => `- ${f}`)
|
|
44
43
|
.join("\n");
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
if (ctx.issueNumber) {
|
|
48
|
-
bodyParts.push(`Resolves #${ctx.issueNumber}`);
|
|
49
|
-
}
|
|
50
|
-
if (bodyParts.length > 0) {
|
|
51
|
-
return `${subject}\n\n${bodyParts.join("\n\n")}`;
|
|
44
|
+
return `${subject}\n\n${fileLines}`;
|
|
52
45
|
}
|
|
53
46
|
return subject;
|
|
54
47
|
}
|
|
@@ -717,63 +717,5 @@ export function validatePreferences(preferences) {
|
|
|
717
717
|
errors.push(`context_selection must be one of: full, smart`);
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
|
-
// ─── GitHub Sync ────────────────────────────────────────────────────────
|
|
721
|
-
if (preferences.github !== undefined) {
|
|
722
|
-
if (typeof preferences.github === "object" && preferences.github !== null) {
|
|
723
|
-
const gh = preferences.github;
|
|
724
|
-
const validGh = {};
|
|
725
|
-
if (gh.enabled !== undefined) {
|
|
726
|
-
if (typeof gh.enabled === "boolean")
|
|
727
|
-
validGh.enabled = gh.enabled;
|
|
728
|
-
else
|
|
729
|
-
errors.push("github.enabled must be a boolean");
|
|
730
|
-
}
|
|
731
|
-
if (gh.repo !== undefined) {
|
|
732
|
-
if (typeof gh.repo === "string" && gh.repo.includes("/"))
|
|
733
|
-
validGh.repo = gh.repo;
|
|
734
|
-
else
|
|
735
|
-
errors.push('github.repo must be a string in "owner/repo" format');
|
|
736
|
-
}
|
|
737
|
-
if (gh.project !== undefined) {
|
|
738
|
-
const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
|
|
739
|
-
if (Number.isFinite(p) && p > 0)
|
|
740
|
-
validGh.project = Math.floor(p);
|
|
741
|
-
else
|
|
742
|
-
errors.push("github.project must be a positive number");
|
|
743
|
-
}
|
|
744
|
-
if (gh.labels !== undefined) {
|
|
745
|
-
if (Array.isArray(gh.labels) && gh.labels.every((l) => typeof l === "string")) {
|
|
746
|
-
validGh.labels = gh.labels;
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
errors.push("github.labels must be an array of strings");
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (gh.auto_link_commits !== undefined) {
|
|
753
|
-
if (typeof gh.auto_link_commits === "boolean")
|
|
754
|
-
validGh.auto_link_commits = gh.auto_link_commits;
|
|
755
|
-
else
|
|
756
|
-
errors.push("github.auto_link_commits must be a boolean");
|
|
757
|
-
}
|
|
758
|
-
if (gh.slice_prs !== undefined) {
|
|
759
|
-
if (typeof gh.slice_prs === "boolean")
|
|
760
|
-
validGh.slice_prs = gh.slice_prs;
|
|
761
|
-
else
|
|
762
|
-
errors.push("github.slice_prs must be a boolean");
|
|
763
|
-
}
|
|
764
|
-
const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
|
|
765
|
-
for (const key of Object.keys(gh)) {
|
|
766
|
-
if (!knownGhKeys.has(key)) {
|
|
767
|
-
warnings.push(`unknown github key "${key}" — ignored`);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (Object.keys(validGh).length > 0) {
|
|
771
|
-
validated.github = validGh;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
errors.push("github must be an object");
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
720
|
return { preferences: validated, errors, warnings };
|
|
779
721
|
}
|
|
@@ -203,9 +203,6 @@ function mergePreferences(base, override) {
|
|
|
203
203
|
context_selection: override.context_selection ?? base.context_selection,
|
|
204
204
|
auto_visualize: override.auto_visualize ?? base.auto_visualize,
|
|
205
205
|
auto_report: override.auto_report ?? base.auto_report,
|
|
206
|
-
github: (base.github || override.github)
|
|
207
|
-
? { ...(base.github ?? {}), ...(override.github ?? {}) }
|
|
208
|
-
: undefined,
|
|
209
206
|
};
|
|
210
207
|
}
|
|
211
208
|
function mergeStringLists(base, override) {
|
package/package.json
CHANGED
|
@@ -121,21 +121,11 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
121
121
|
const summaryContent = await loadFile(summaryPath);
|
|
122
122
|
if (summaryContent) {
|
|
123
123
|
const summary = parseSummary(summaryContent);
|
|
124
|
-
// Look up GitHub issue number for commit linking
|
|
125
|
-
let ghIssueNumber: number | undefined;
|
|
126
|
-
try {
|
|
127
|
-
const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
|
|
128
|
-
ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
|
|
129
|
-
} catch {
|
|
130
|
-
// GitHub sync not available — skip
|
|
131
|
-
}
|
|
132
|
-
|
|
133
124
|
taskContext = {
|
|
134
125
|
taskId: `${sid}/${tid}`,
|
|
135
126
|
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
136
127
|
oneLiner: summary.oneLiner || undefined,
|
|
137
128
|
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
138
|
-
issueNumber: ghIssueNumber,
|
|
139
129
|
};
|
|
140
130
|
}
|
|
141
131
|
} catch (e) {
|
|
@@ -153,14 +143,6 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
153
143
|
debugLog("postUnit", { phase: "auto-commit", error: String(e) });
|
|
154
144
|
}
|
|
155
145
|
|
|
156
|
-
// GitHub sync (non-blocking, opt-in)
|
|
157
|
-
try {
|
|
158
|
-
const { runGitHubSync } = await import("../github-sync/sync.js");
|
|
159
|
-
await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
160
|
-
} catch (e) {
|
|
161
|
-
debugLog("postUnit", { phase: "github-sync", error: String(e) });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
146
|
// Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
|
|
165
147
|
if (!opts?.skipDoctor) try {
|
|
166
148
|
const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
|
@@ -95,8 +95,6 @@ export interface TaskCommitContext {
|
|
|
95
95
|
oneLiner?: string;
|
|
96
96
|
/** Files modified by this task (from task summary frontmatter) */
|
|
97
97
|
keyFiles?: string[];
|
|
98
|
-
/** GitHub issue number — appends "Resolves #N" trailer when set. */
|
|
99
|
-
issueNumber?: number;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
/**
|
|
@@ -120,22 +118,12 @@ export function buildTaskCommitMessage(ctx: TaskCommitContext): string {
|
|
|
120
118
|
const subject = `${type}(${scope}): ${truncated}`;
|
|
121
119
|
|
|
122
120
|
// Build body with key files if available
|
|
123
|
-
const bodyParts: string[] = [];
|
|
124
|
-
|
|
125
121
|
if (ctx.keyFiles && ctx.keyFiles.length > 0) {
|
|
126
122
|
const fileLines = ctx.keyFiles
|
|
127
123
|
.slice(0, 8) // cap at 8 files to keep commit concise
|
|
128
124
|
.map(f => `- ${f}`)
|
|
129
125
|
.join("\n");
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (ctx.issueNumber) {
|
|
134
|
-
bodyParts.push(`Resolves #${ctx.issueNumber}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (bodyParts.length > 0) {
|
|
138
|
-
return `${subject}\n\n${bodyParts.join("\n\n")}`;
|
|
126
|
+
return `${subject}\n\n${fileLines}`;
|
|
139
127
|
}
|
|
140
128
|
|
|
141
129
|
return subject;
|
|
@@ -20,7 +20,6 @@ import type {
|
|
|
20
20
|
ReactiveExecutionConfig,
|
|
21
21
|
} from "./types.js";
|
|
22
22
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
23
|
-
import type { GitHubSyncConfig } from "../github-sync/types.js";
|
|
24
23
|
|
|
25
24
|
// ─── Workflow Modes ──────────────────────────────────────────────────────────
|
|
26
25
|
|
|
@@ -87,7 +86,6 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
87
86
|
"context_selection",
|
|
88
87
|
"widget_mode",
|
|
89
88
|
"reactive_execution",
|
|
90
|
-
"github",
|
|
91
89
|
]);
|
|
92
90
|
|
|
93
91
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -217,8 +215,6 @@ export interface GSDPreferences {
|
|
|
217
215
|
widget_mode?: "full" | "small" | "min" | "off";
|
|
218
216
|
/** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
|
|
219
217
|
reactive_execution?: ReactiveExecutionConfig;
|
|
220
|
-
/** GitHub sync configuration. Opt-in: syncs GSD events to GitHub Issues, Milestones, and PRs. */
|
|
221
|
-
github?: GitHubSyncConfig;
|
|
222
218
|
}
|
|
223
219
|
|
|
224
220
|
export interface LoadedGSDPreferences {
|
|
@@ -696,55 +696,5 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
696
696
|
}
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
-
// ─── GitHub Sync ────────────────────────────────────────────────────────
|
|
700
|
-
if (preferences.github !== undefined) {
|
|
701
|
-
if (typeof preferences.github === "object" && preferences.github !== null) {
|
|
702
|
-
const gh = preferences.github as unknown as Record<string, unknown>;
|
|
703
|
-
const validGh: Record<string, unknown> = {};
|
|
704
|
-
|
|
705
|
-
if (gh.enabled !== undefined) {
|
|
706
|
-
if (typeof gh.enabled === "boolean") validGh.enabled = gh.enabled;
|
|
707
|
-
else errors.push("github.enabled must be a boolean");
|
|
708
|
-
}
|
|
709
|
-
if (gh.repo !== undefined) {
|
|
710
|
-
if (typeof gh.repo === "string" && gh.repo.includes("/")) validGh.repo = gh.repo;
|
|
711
|
-
else errors.push('github.repo must be a string in "owner/repo" format');
|
|
712
|
-
}
|
|
713
|
-
if (gh.project !== undefined) {
|
|
714
|
-
const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
|
|
715
|
-
if (Number.isFinite(p) && p > 0) validGh.project = Math.floor(p);
|
|
716
|
-
else errors.push("github.project must be a positive number");
|
|
717
|
-
}
|
|
718
|
-
if (gh.labels !== undefined) {
|
|
719
|
-
if (Array.isArray(gh.labels) && gh.labels.every((l: unknown) => typeof l === "string")) {
|
|
720
|
-
validGh.labels = gh.labels;
|
|
721
|
-
} else {
|
|
722
|
-
errors.push("github.labels must be an array of strings");
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (gh.auto_link_commits !== undefined) {
|
|
726
|
-
if (typeof gh.auto_link_commits === "boolean") validGh.auto_link_commits = gh.auto_link_commits;
|
|
727
|
-
else errors.push("github.auto_link_commits must be a boolean");
|
|
728
|
-
}
|
|
729
|
-
if (gh.slice_prs !== undefined) {
|
|
730
|
-
if (typeof gh.slice_prs === "boolean") validGh.slice_prs = gh.slice_prs;
|
|
731
|
-
else errors.push("github.slice_prs must be a boolean");
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
|
|
735
|
-
for (const key of Object.keys(gh)) {
|
|
736
|
-
if (!knownGhKeys.has(key)) {
|
|
737
|
-
warnings.push(`unknown github key "${key}" — ignored`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (Object.keys(validGh).length > 0) {
|
|
742
|
-
validated.github = validGh as unknown as import("../github-sync/types.js").GitHubSyncConfig;
|
|
743
|
-
}
|
|
744
|
-
} else {
|
|
745
|
-
errors.push("github must be an object");
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
699
|
return { preferences: validated, errors, warnings };
|
|
750
700
|
}
|
|
@@ -271,9 +271,6 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
271
271
|
context_selection: override.context_selection ?? base.context_selection,
|
|
272
272
|
auto_visualize: override.auto_visualize ?? base.auto_visualize,
|
|
273
273
|
auto_report: override.auto_report ?? base.auto_report,
|
|
274
|
-
github: (base.github || override.github)
|
|
275
|
-
? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
|
|
276
|
-
: undefined,
|
|
277
274
|
};
|
|
278
275
|
}
|
|
279
276
|
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin wrapper around the `gh` CLI.
|
|
3
|
-
*
|
|
4
|
-
* Every public function returns `GhResult<T>` — never throws.
|
|
5
|
-
* Uses `execFileSync` (not `execSync`) for safety.
|
|
6
|
-
*/
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
function ok(data) {
|
|
9
|
-
return { ok: true, data };
|
|
10
|
-
}
|
|
11
|
-
function fail(error) {
|
|
12
|
-
return { ok: false, error };
|
|
13
|
-
}
|
|
14
|
-
// ─── gh Availability ────────────────────────────────────────────────────────
|
|
15
|
-
let _ghAvailable = null;
|
|
16
|
-
export function ghIsAvailable() {
|
|
17
|
-
if (_ghAvailable !== null)
|
|
18
|
-
return _ghAvailable;
|
|
19
|
-
try {
|
|
20
|
-
execFileSync("gh", ["--version"], {
|
|
21
|
-
encoding: "utf-8",
|
|
22
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
-
timeout: 5_000,
|
|
24
|
-
});
|
|
25
|
-
_ghAvailable = true;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
_ghAvailable = false;
|
|
29
|
-
}
|
|
30
|
-
return _ghAvailable;
|
|
31
|
-
}
|
|
32
|
-
/** Reset cached availability (for testing). */
|
|
33
|
-
export function _resetGhCache() {
|
|
34
|
-
_ghAvailable = null;
|
|
35
|
-
}
|
|
36
|
-
// ─── Rate Limit Check ───────────────────────────────────────────────────────
|
|
37
|
-
let _rateLimitCheckedAt = 0;
|
|
38
|
-
let _rateLimitOk = true;
|
|
39
|
-
const RATE_LIMIT_CHECK_INTERVAL_MS = 300_000; // 5 minutes
|
|
40
|
-
export function ghHasRateLimit(cwd) {
|
|
41
|
-
const now = Date.now();
|
|
42
|
-
if (now - _rateLimitCheckedAt < RATE_LIMIT_CHECK_INTERVAL_MS)
|
|
43
|
-
return _rateLimitOk;
|
|
44
|
-
_rateLimitCheckedAt = now;
|
|
45
|
-
try {
|
|
46
|
-
const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".rate.remaining"], {
|
|
47
|
-
cwd,
|
|
48
|
-
encoding: "utf-8",
|
|
49
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
50
|
-
timeout: 10_000,
|
|
51
|
-
}).trim();
|
|
52
|
-
const remaining = parseInt(raw, 10);
|
|
53
|
-
_rateLimitOk = Number.isFinite(remaining) && remaining >= 100;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Can't check — assume OK so we don't silently disable sync
|
|
57
|
-
_rateLimitOk = true;
|
|
58
|
-
}
|
|
59
|
-
return _rateLimitOk;
|
|
60
|
-
}
|
|
61
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
62
|
-
const GH_TIMEOUT = 15_000;
|
|
63
|
-
const MAX_BODY_LENGTH = 65_000;
|
|
64
|
-
function truncateBody(body) {
|
|
65
|
-
if (body.length <= MAX_BODY_LENGTH)
|
|
66
|
-
return body;
|
|
67
|
-
return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n*Body truncated (exceeded 65K characters)*";
|
|
68
|
-
}
|
|
69
|
-
function runGh(args, cwd) {
|
|
70
|
-
try {
|
|
71
|
-
const stdout = execFileSync("gh", args, {
|
|
72
|
-
cwd,
|
|
73
|
-
encoding: "utf-8",
|
|
74
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
-
timeout: GH_TIMEOUT,
|
|
76
|
-
}).trim();
|
|
77
|
-
return ok(stdout);
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
-
return fail(msg);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function runGhJson(args, cwd) {
|
|
85
|
-
const result = runGh(args, cwd);
|
|
86
|
-
if (!result.ok)
|
|
87
|
-
return fail(result.error);
|
|
88
|
-
try {
|
|
89
|
-
return ok(JSON.parse(result.data));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return fail(`Failed to parse JSON: ${result.data}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// ─── Repo Detection ─────────────────────────────────────────────────────────
|
|
96
|
-
export function ghDetectRepo(cwd) {
|
|
97
|
-
const result = runGh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], cwd);
|
|
98
|
-
if (!result.ok)
|
|
99
|
-
return fail(result.error);
|
|
100
|
-
const repo = result.data.trim();
|
|
101
|
-
if (!repo || !repo.includes("/"))
|
|
102
|
-
return fail("Could not detect repo");
|
|
103
|
-
return ok(repo);
|
|
104
|
-
}
|
|
105
|
-
export function ghCreateIssue(cwd, opts) {
|
|
106
|
-
const args = [
|
|
107
|
-
"issue", "create",
|
|
108
|
-
"--repo", opts.repo,
|
|
109
|
-
"--title", opts.title,
|
|
110
|
-
"--body", truncateBody(opts.body),
|
|
111
|
-
];
|
|
112
|
-
if (opts.labels?.length) {
|
|
113
|
-
args.push("--label", opts.labels.join(","));
|
|
114
|
-
}
|
|
115
|
-
if (opts.milestone) {
|
|
116
|
-
args.push("--milestone", String(opts.milestone));
|
|
117
|
-
}
|
|
118
|
-
const result = runGh(args, cwd);
|
|
119
|
-
if (!result.ok)
|
|
120
|
-
return fail(result.error);
|
|
121
|
-
// gh issue create returns the URL; extract issue number
|
|
122
|
-
const match = result.data.match(/\/issues\/(\d+)/);
|
|
123
|
-
if (!match)
|
|
124
|
-
return fail(`Could not parse issue number from: ${result.data}`);
|
|
125
|
-
const issueNumber = parseInt(match[1], 10);
|
|
126
|
-
// If parent specified, add as sub-issue via GraphQL
|
|
127
|
-
if (opts.parentIssue) {
|
|
128
|
-
ghAddSubIssue(cwd, opts.repo, opts.parentIssue, issueNumber);
|
|
129
|
-
}
|
|
130
|
-
return ok(issueNumber);
|
|
131
|
-
}
|
|
132
|
-
export function ghCloseIssue(cwd, repo, issueNumber, comment) {
|
|
133
|
-
if (comment) {
|
|
134
|
-
ghAddComment(cwd, repo, issueNumber, comment);
|
|
135
|
-
}
|
|
136
|
-
const result = runGh(["issue", "close", String(issueNumber), "--repo", repo], cwd);
|
|
137
|
-
if (!result.ok)
|
|
138
|
-
return fail(result.error);
|
|
139
|
-
return ok(undefined);
|
|
140
|
-
}
|
|
141
|
-
export function ghAddComment(cwd, repo, issueNumber, body) {
|
|
142
|
-
const result = runGh(["issue", "comment", String(issueNumber), "--repo", repo, "--body", truncateBody(body)], cwd);
|
|
143
|
-
if (!result.ok)
|
|
144
|
-
return fail(result.error);
|
|
145
|
-
return ok(undefined);
|
|
146
|
-
}
|
|
147
|
-
// ─── Sub-Issues (GraphQL) ───────────────────────────────────────────────────
|
|
148
|
-
function ghAddSubIssue(cwd, repo, parentNumber, childNumber) {
|
|
149
|
-
// Get node IDs for both issues
|
|
150
|
-
const parentResult = runGhJson(["api", `repos/${repo}/issues/${parentNumber}`, "--jq", "{id: .node_id}"], cwd);
|
|
151
|
-
const childResult = runGhJson(["api", `repos/${repo}/issues/${childNumber}`, "--jq", "{id: .node_id}"], cwd);
|
|
152
|
-
if (!parentResult.ok || !childResult.ok) {
|
|
153
|
-
return fail("Could not resolve issue node IDs for sub-issue linking");
|
|
154
|
-
}
|
|
155
|
-
const mutation = `mutation { addSubIssue(input: { issueId: "${parentResult.data.id}", subIssueId: "${childResult.data.id}" }) { issue { id } } }`;
|
|
156
|
-
return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
|
|
157
|
-
}
|
|
158
|
-
// ─── Milestones ─────────────────────────────────────────────────────────────
|
|
159
|
-
export function ghCreateMilestone(cwd, repo, title, description) {
|
|
160
|
-
const result = runGhJson([
|
|
161
|
-
"api", `repos/${repo}/milestones`,
|
|
162
|
-
"-X", "POST",
|
|
163
|
-
"-f", `title=${title}`,
|
|
164
|
-
"-f", `description=${truncateBody(description)}`,
|
|
165
|
-
"-f", "state=open",
|
|
166
|
-
"--jq", "{number: .number}",
|
|
167
|
-
], cwd);
|
|
168
|
-
if (!result.ok)
|
|
169
|
-
return fail(result.error);
|
|
170
|
-
return ok(result.data.number);
|
|
171
|
-
}
|
|
172
|
-
export function ghCloseMilestone(cwd, repo, milestoneNumber) {
|
|
173
|
-
const result = runGh([
|
|
174
|
-
"api", `repos/${repo}/milestones/${milestoneNumber}`,
|
|
175
|
-
"-X", "PATCH",
|
|
176
|
-
"-f", "state=closed",
|
|
177
|
-
], cwd);
|
|
178
|
-
if (!result.ok)
|
|
179
|
-
return fail(result.error);
|
|
180
|
-
return ok(undefined);
|
|
181
|
-
}
|
|
182
|
-
export function ghCreatePR(cwd, opts) {
|
|
183
|
-
const args = [
|
|
184
|
-
"pr", "create",
|
|
185
|
-
"--repo", opts.repo,
|
|
186
|
-
"--base", opts.base,
|
|
187
|
-
"--head", opts.head,
|
|
188
|
-
"--title", opts.title,
|
|
189
|
-
"--body", truncateBody(opts.body),
|
|
190
|
-
];
|
|
191
|
-
if (opts.draft)
|
|
192
|
-
args.push("--draft");
|
|
193
|
-
const result = runGh(args, cwd);
|
|
194
|
-
if (!result.ok)
|
|
195
|
-
return fail(result.error);
|
|
196
|
-
const match = result.data.match(/\/pull\/(\d+)/);
|
|
197
|
-
if (!match)
|
|
198
|
-
return fail(`Could not parse PR number from: ${result.data}`);
|
|
199
|
-
return ok(parseInt(match[1], 10));
|
|
200
|
-
}
|
|
201
|
-
export function ghMarkPRReady(cwd, repo, prNumber) {
|
|
202
|
-
const result = runGh(["pr", "ready", String(prNumber), "--repo", repo], cwd);
|
|
203
|
-
if (!result.ok)
|
|
204
|
-
return fail(result.error);
|
|
205
|
-
return ok(undefined);
|
|
206
|
-
}
|
|
207
|
-
export function ghMergePR(cwd, repo, prNumber, strategy = "squash") {
|
|
208
|
-
const args = [
|
|
209
|
-
"pr", "merge", String(prNumber),
|
|
210
|
-
"--repo", repo,
|
|
211
|
-
strategy === "squash" ? "--squash" : "--merge",
|
|
212
|
-
"--delete-branch",
|
|
213
|
-
];
|
|
214
|
-
const result = runGh(args, cwd);
|
|
215
|
-
if (!result.ok)
|
|
216
|
-
return fail(result.error);
|
|
217
|
-
return ok(undefined);
|
|
218
|
-
}
|
|
219
|
-
// ─── Projects v2 ────────────────────────────────────────────────────────────
|
|
220
|
-
export function ghAddToProject(cwd, repo, projectNumber, issueNumber) {
|
|
221
|
-
// Get the issue's node ID first
|
|
222
|
-
const issueResult = runGhJson(["api", `repos/${repo}/issues/${issueNumber}`, "--jq", "{id: .node_id}"], cwd);
|
|
223
|
-
if (!issueResult.ok)
|
|
224
|
-
return fail(issueResult.error);
|
|
225
|
-
// Get the project's node ID
|
|
226
|
-
const [owner] = repo.split("/");
|
|
227
|
-
const projectResult = runGhJson([
|
|
228
|
-
"api", "graphql",
|
|
229
|
-
"-f", `query=query { user(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
|
|
230
|
-
"--jq", ".data.user.projectV2.id",
|
|
231
|
-
], cwd);
|
|
232
|
-
// Try org if user fails
|
|
233
|
-
let projectId;
|
|
234
|
-
if (projectResult.ok && projectResult.data?.id) {
|
|
235
|
-
projectId = projectResult.data.id;
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
const orgResult = runGhJson([
|
|
239
|
-
"api", "graphql",
|
|
240
|
-
"-f", `query=query { organization(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
|
|
241
|
-
"--jq", ".data.organization.projectV2.id",
|
|
242
|
-
], cwd);
|
|
243
|
-
if (orgResult.ok)
|
|
244
|
-
projectId = orgResult.data?.id;
|
|
245
|
-
}
|
|
246
|
-
if (!projectId)
|
|
247
|
-
return fail("Could not find project");
|
|
248
|
-
const mutation = `mutation { addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${issueResult.data.id}" }) { item { id } } }`;
|
|
249
|
-
return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd);
|
|
250
|
-
}
|
|
251
|
-
// ─── Branch Operations ──────────────────────────────────────────────────────
|
|
252
|
-
export function ghPushBranch(cwd, branch, setUpstream = true) {
|
|
253
|
-
const args = ["git", "push"];
|
|
254
|
-
if (setUpstream)
|
|
255
|
-
args.push("-u", "origin", branch);
|
|
256
|
-
else
|
|
257
|
-
args.push("origin", branch);
|
|
258
|
-
try {
|
|
259
|
-
execFileSync(args[0], args.slice(1), {
|
|
260
|
-
cwd,
|
|
261
|
-
encoding: "utf-8",
|
|
262
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
263
|
-
timeout: 30_000,
|
|
264
|
-
});
|
|
265
|
-
return ok(undefined);
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
export function ghCreateBranch(cwd, branch, from) {
|
|
272
|
-
try {
|
|
273
|
-
execFileSync("git", ["branch", branch, from], {
|
|
274
|
-
cwd,
|
|
275
|
-
encoding: "utf-8",
|
|
276
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
277
|
-
timeout: 10_000,
|
|
278
|
-
});
|
|
279
|
-
return ok(undefined);
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Sync extension for GSD.
|
|
3
|
-
*
|
|
4
|
-
* Opt-in extension that syncs GSD lifecycle events to GitHub:
|
|
5
|
-
* milestones → GH Milestones + tracking issues, slices → draft PRs,
|
|
6
|
-
* tasks → sub-issues with auto-close on commit.
|
|
7
|
-
*
|
|
8
|
-
* Integration happens via a single dynamic import in auto-post-unit.ts.
|
|
9
|
-
* This index registers a `/github-sync` command for manual bootstrap
|
|
10
|
-
* and status display.
|
|
11
|
-
*/
|
|
12
|
-
import { bootstrapSync } from "./sync.js";
|
|
13
|
-
import { loadSyncMapping } from "./mapping.js";
|
|
14
|
-
import { ghIsAvailable } from "./cli.js";
|
|
15
|
-
export default function (pi) {
|
|
16
|
-
pi.registerCommand("github-sync", {
|
|
17
|
-
description: "Bootstrap GitHub sync or show sync status",
|
|
18
|
-
handler: async (args, ctx) => {
|
|
19
|
-
const subcommand = args.trim().toLowerCase();
|
|
20
|
-
if (subcommand === "status") {
|
|
21
|
-
await showStatus(ctx);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (subcommand === "bootstrap" || subcommand === "") {
|
|
25
|
-
await runBootstrap(ctx);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
ctx.ui.notify("Usage: /github-sync [bootstrap|status]", "info");
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
async function showStatus(ctx) {
|
|
33
|
-
if (!ghIsAvailable()) {
|
|
34
|
-
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const mapping = loadSyncMapping(ctx.cwd);
|
|
38
|
-
if (!mapping) {
|
|
39
|
-
ctx.ui.notify("GitHub sync: No sync mapping found. Run `/github-sync bootstrap` to initialize.", "info");
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const milestoneCount = Object.keys(mapping.milestones).length;
|
|
43
|
-
const sliceCount = Object.keys(mapping.slices).length;
|
|
44
|
-
const taskCount = Object.keys(mapping.tasks).length;
|
|
45
|
-
const openMilestones = Object.values(mapping.milestones).filter(m => m.state === "open").length;
|
|
46
|
-
const openSlices = Object.values(mapping.slices).filter(s => s.state === "open").length;
|
|
47
|
-
const openTasks = Object.values(mapping.tasks).filter(t => t.state === "open").length;
|
|
48
|
-
ctx.ui.notify([
|
|
49
|
-
`GitHub sync: repo=${mapping.repo}`,
|
|
50
|
-
` Milestones: ${milestoneCount} (${openMilestones} open)`,
|
|
51
|
-
` Slices: ${sliceCount} (${openSlices} open)`,
|
|
52
|
-
` Tasks: ${taskCount} (${openTasks} open)`,
|
|
53
|
-
].join("\n"), "info");
|
|
54
|
-
}
|
|
55
|
-
async function runBootstrap(ctx) {
|
|
56
|
-
if (!ghIsAvailable()) {
|
|
57
|
-
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
ctx.ui.notify("GitHub sync: bootstrapping...", "info");
|
|
61
|
-
try {
|
|
62
|
-
const counts = await bootstrapSync(ctx.cwd);
|
|
63
|
-
if (counts.milestones === 0 && counts.slices === 0 && counts.tasks === 0) {
|
|
64
|
-
ctx.ui.notify("GitHub sync: everything already synced (or no milestones found).", "info");
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
ctx.ui.notify(`GitHub sync: created ${counts.milestones} milestone(s), ${counts.slices} slice(s), ${counts.tasks} task(s).`, "info");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
ctx.ui.notify(`GitHub sync bootstrap failed: ${err}`, "error");
|
|
72
|
-
}
|
|
73
|
-
}
|