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.
Files changed (27) hide show
  1. package/dist/resources/extensions/gsd/auto-post-unit.js +0 -18
  2. package/dist/resources/extensions/gsd/git-service.js +1 -8
  3. package/dist/resources/extensions/gsd/preferences-types.js +0 -1
  4. package/dist/resources/extensions/gsd/preferences-validation.js +0 -58
  5. package/dist/resources/extensions/gsd/preferences.js +0 -3
  6. package/package.json +1 -1
  7. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -18
  8. package/src/resources/extensions/gsd/git-service.ts +1 -13
  9. package/src/resources/extensions/gsd/preferences-types.ts +0 -4
  10. package/src/resources/extensions/gsd/preferences-validation.ts +0 -50
  11. package/src/resources/extensions/gsd/preferences.ts +0 -3
  12. package/dist/resources/extensions/github-sync/cli.js +0 -284
  13. package/dist/resources/extensions/github-sync/index.js +0 -73
  14. package/dist/resources/extensions/github-sync/mapping.js +0 -67
  15. package/dist/resources/extensions/github-sync/sync.js +0 -424
  16. package/dist/resources/extensions/github-sync/templates.js +0 -118
  17. package/dist/resources/extensions/github-sync/types.js +0 -7
  18. package/src/resources/extensions/github-sync/cli.ts +0 -364
  19. package/src/resources/extensions/github-sync/index.ts +0 -93
  20. package/src/resources/extensions/github-sync/mapping.ts +0 -81
  21. package/src/resources/extensions/github-sync/sync.ts +0 -556
  22. package/src/resources/extensions/github-sync/templates.ts +0 -183
  23. package/src/resources/extensions/github-sync/tests/cli.test.ts +0 -20
  24. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +0 -39
  25. package/src/resources/extensions/github-sync/tests/mapping.test.ts +0 -104
  26. package/src/resources/extensions/github-sync/tests/templates.test.ts +0 -110
  27. 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
- bodyParts.push(fileLines);
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
  }
@@ -65,7 +65,6 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
65
65
  "context_selection",
66
66
  "widget_mode",
67
67
  "reactive_execution",
68
- "github",
69
68
  ]);
70
69
  /** Canonical list of all dispatch unit types. */
71
70
  export const KNOWN_UNIT_TYPES = [
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.38.0-dev.361f5e3",
3
+ "version": "2.38.0-dev.8f5c161",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- bodyParts.push(fileLines);
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
- }