gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc
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/README.md +15 -11
- package/dist/app-paths.js +1 -1
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +636 -594
- package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
- package/dist/resources/extensions/gsd/auto-start.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +4 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +48 -9
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +149 -38
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
- package/dist/resources/extensions/gsd/preferences.js +22 -11
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +42 -23
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +4 -1
- package/dist/resources/extensions/remote-questions/store.js +4 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +526 -545
- package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
- package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
- package/src/resources/extensions/gsd/auto-start.ts +11 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +5 -3
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +51 -11
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +177 -44
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
- package/src/resources/extensions/gsd/preferences.ts +25 -11
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +39 -21
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/types.ts +18 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +5 -1
- package/src/resources/extensions/remote-questions/store.ts +5 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/shared/frontmatter.ts +1 -1
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
// ─── Result Type ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface GhResult<T> {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
data?: T;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ok<T>(data: T): GhResult<T> {
|
|
19
|
+
return { ok: true, data };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail<T>(error: string): GhResult<T> {
|
|
23
|
+
return { ok: false, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── gh Availability ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let _ghAvailable: boolean | null = null;
|
|
29
|
+
|
|
30
|
+
export function ghIsAvailable(): boolean {
|
|
31
|
+
if (_ghAvailable !== null) return _ghAvailable;
|
|
32
|
+
try {
|
|
33
|
+
execFileSync("gh", ["--version"], {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
timeout: 5_000,
|
|
37
|
+
});
|
|
38
|
+
_ghAvailable = true;
|
|
39
|
+
} catch {
|
|
40
|
+
_ghAvailable = false;
|
|
41
|
+
}
|
|
42
|
+
return _ghAvailable;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reset cached availability (for testing). */
|
|
46
|
+
export function _resetGhCache(): void {
|
|
47
|
+
_ghAvailable = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Rate Limit Check ───────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
let _rateLimitCheckedAt = 0;
|
|
53
|
+
let _rateLimitOk = true;
|
|
54
|
+
const RATE_LIMIT_CHECK_INTERVAL_MS = 300_000; // 5 minutes
|
|
55
|
+
|
|
56
|
+
export function ghHasRateLimit(cwd: string): boolean {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (now - _rateLimitCheckedAt < RATE_LIMIT_CHECK_INTERVAL_MS) return _rateLimitOk;
|
|
59
|
+
_rateLimitCheckedAt = now;
|
|
60
|
+
try {
|
|
61
|
+
const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".rate.remaining"], {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
65
|
+
timeout: 10_000,
|
|
66
|
+
}).trim();
|
|
67
|
+
const remaining = parseInt(raw, 10);
|
|
68
|
+
_rateLimitOk = Number.isFinite(remaining) && remaining >= 100;
|
|
69
|
+
} catch {
|
|
70
|
+
// Can't check — assume OK so we don't silently disable sync
|
|
71
|
+
_rateLimitOk = true;
|
|
72
|
+
}
|
|
73
|
+
return _rateLimitOk;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const GH_TIMEOUT = 15_000;
|
|
79
|
+
const MAX_BODY_LENGTH = 65_000;
|
|
80
|
+
|
|
81
|
+
function truncateBody(body: string): string {
|
|
82
|
+
if (body.length <= MAX_BODY_LENGTH) return body;
|
|
83
|
+
return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n*Body truncated (exceeded 65K characters)*";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runGh(args: string[], cwd: string): GhResult<string> {
|
|
87
|
+
try {
|
|
88
|
+
const stdout = execFileSync("gh", args, {
|
|
89
|
+
cwd,
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
92
|
+
timeout: GH_TIMEOUT,
|
|
93
|
+
}).trim();
|
|
94
|
+
return ok(stdout);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
return fail(msg);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runGhJson<T>(args: string[], cwd: string): GhResult<T> {
|
|
102
|
+
const result = runGh(args, cwd);
|
|
103
|
+
if (!result.ok) return fail(result.error!);
|
|
104
|
+
try {
|
|
105
|
+
return ok(JSON.parse(result.data!) as T);
|
|
106
|
+
} catch {
|
|
107
|
+
return fail(`Failed to parse JSON: ${result.data}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Repo Detection ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export function ghDetectRepo(cwd: string): GhResult<string> {
|
|
114
|
+
const result = runGh(
|
|
115
|
+
["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
|
|
116
|
+
cwd,
|
|
117
|
+
);
|
|
118
|
+
if (!result.ok) return fail(result.error!);
|
|
119
|
+
const repo = result.data!.trim();
|
|
120
|
+
if (!repo || !repo.includes("/")) return fail("Could not detect repo");
|
|
121
|
+
return ok(repo);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Issues ─────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export interface CreateIssueOpts {
|
|
127
|
+
repo: string;
|
|
128
|
+
title: string;
|
|
129
|
+
body: string;
|
|
130
|
+
labels?: string[];
|
|
131
|
+
milestone?: number;
|
|
132
|
+
parentIssue?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ghCreateIssue(cwd: string, opts: CreateIssueOpts): GhResult<number> {
|
|
136
|
+
const args = [
|
|
137
|
+
"issue", "create",
|
|
138
|
+
"--repo", opts.repo,
|
|
139
|
+
"--title", opts.title,
|
|
140
|
+
"--body", truncateBody(opts.body),
|
|
141
|
+
];
|
|
142
|
+
if (opts.labels?.length) {
|
|
143
|
+
args.push("--label", opts.labels.join(","));
|
|
144
|
+
}
|
|
145
|
+
if (opts.milestone) {
|
|
146
|
+
args.push("--milestone", String(opts.milestone));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = runGh(args, cwd);
|
|
150
|
+
if (!result.ok) return fail(result.error!);
|
|
151
|
+
|
|
152
|
+
// gh issue create returns the URL; extract issue number
|
|
153
|
+
const match = result.data!.match(/\/issues\/(\d+)/);
|
|
154
|
+
if (!match) return fail(`Could not parse issue number from: ${result.data}`);
|
|
155
|
+
const issueNumber = parseInt(match[1], 10);
|
|
156
|
+
|
|
157
|
+
// If parent specified, add as sub-issue via GraphQL
|
|
158
|
+
if (opts.parentIssue) {
|
|
159
|
+
ghAddSubIssue(cwd, opts.repo, opts.parentIssue, issueNumber);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return ok(issueNumber);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function ghCloseIssue(cwd: string, repo: string, issueNumber: number, comment?: string): GhResult<void> {
|
|
166
|
+
if (comment) {
|
|
167
|
+
ghAddComment(cwd, repo, issueNumber, comment);
|
|
168
|
+
}
|
|
169
|
+
const result = runGh(
|
|
170
|
+
["issue", "close", String(issueNumber), "--repo", repo],
|
|
171
|
+
cwd,
|
|
172
|
+
);
|
|
173
|
+
if (!result.ok) return fail(result.error!);
|
|
174
|
+
return ok(undefined);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function ghAddComment(cwd: string, repo: string, issueNumber: number, body: string): GhResult<void> {
|
|
178
|
+
const result = runGh(
|
|
179
|
+
["issue", "comment", String(issueNumber), "--repo", repo, "--body", truncateBody(body)],
|
|
180
|
+
cwd,
|
|
181
|
+
);
|
|
182
|
+
if (!result.ok) return fail(result.error!);
|
|
183
|
+
return ok(undefined);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Sub-Issues (GraphQL) ───────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function ghAddSubIssue(cwd: string, repo: string, parentNumber: number, childNumber: number): GhResult<void> {
|
|
189
|
+
// Get node IDs for both issues
|
|
190
|
+
const parentResult = runGhJson<{ id: string }>(
|
|
191
|
+
["api", `repos/${repo}/issues/${parentNumber}`, "--jq", "{id: .node_id}"],
|
|
192
|
+
cwd,
|
|
193
|
+
);
|
|
194
|
+
const childResult = runGhJson<{ id: string }>(
|
|
195
|
+
["api", `repos/${repo}/issues/${childNumber}`, "--jq", "{id: .node_id}"],
|
|
196
|
+
cwd,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (!parentResult.ok || !childResult.ok) {
|
|
200
|
+
return fail("Could not resolve issue node IDs for sub-issue linking");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const mutation = `mutation { addSubIssue(input: { issueId: "${parentResult.data!.id}", subIssueId: "${childResult.data!.id}" }) { issue { id } } }`;
|
|
204
|
+
return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd) as GhResult<void>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Milestones ─────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
export function ghCreateMilestone(cwd: string, repo: string, title: string, description: string): GhResult<number> {
|
|
210
|
+
const result = runGhJson<{ number: number }>(
|
|
211
|
+
[
|
|
212
|
+
"api", `repos/${repo}/milestones`,
|
|
213
|
+
"-X", "POST",
|
|
214
|
+
"-f", `title=${title}`,
|
|
215
|
+
"-f", `description=${truncateBody(description)}`,
|
|
216
|
+
"-f", "state=open",
|
|
217
|
+
"--jq", "{number: .number}",
|
|
218
|
+
],
|
|
219
|
+
cwd,
|
|
220
|
+
);
|
|
221
|
+
if (!result.ok) return fail(result.error!);
|
|
222
|
+
return ok(result.data!.number);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ghCloseMilestone(cwd: string, repo: string, milestoneNumber: number): GhResult<void> {
|
|
226
|
+
const result = runGh(
|
|
227
|
+
[
|
|
228
|
+
"api", `repos/${repo}/milestones/${milestoneNumber}`,
|
|
229
|
+
"-X", "PATCH",
|
|
230
|
+
"-f", "state=closed",
|
|
231
|
+
],
|
|
232
|
+
cwd,
|
|
233
|
+
);
|
|
234
|
+
if (!result.ok) return fail(result.error!);
|
|
235
|
+
return ok(undefined);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Pull Requests ──────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export interface CreatePROpts {
|
|
241
|
+
repo: string;
|
|
242
|
+
base: string;
|
|
243
|
+
head: string;
|
|
244
|
+
title: string;
|
|
245
|
+
body: string;
|
|
246
|
+
draft?: boolean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function ghCreatePR(cwd: string, opts: CreatePROpts): GhResult<number> {
|
|
250
|
+
const args = [
|
|
251
|
+
"pr", "create",
|
|
252
|
+
"--repo", opts.repo,
|
|
253
|
+
"--base", opts.base,
|
|
254
|
+
"--head", opts.head,
|
|
255
|
+
"--title", opts.title,
|
|
256
|
+
"--body", truncateBody(opts.body),
|
|
257
|
+
];
|
|
258
|
+
if (opts.draft) args.push("--draft");
|
|
259
|
+
|
|
260
|
+
const result = runGh(args, cwd);
|
|
261
|
+
if (!result.ok) return fail(result.error!);
|
|
262
|
+
|
|
263
|
+
const match = result.data!.match(/\/pull\/(\d+)/);
|
|
264
|
+
if (!match) return fail(`Could not parse PR number from: ${result.data}`);
|
|
265
|
+
return ok(parseInt(match[1], 10));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function ghMarkPRReady(cwd: string, repo: string, prNumber: number): GhResult<void> {
|
|
269
|
+
const result = runGh(
|
|
270
|
+
["pr", "ready", String(prNumber), "--repo", repo],
|
|
271
|
+
cwd,
|
|
272
|
+
);
|
|
273
|
+
if (!result.ok) return fail(result.error!);
|
|
274
|
+
return ok(undefined);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function ghMergePR(cwd: string, repo: string, prNumber: number, strategy: "squash" | "merge" = "squash"): GhResult<void> {
|
|
278
|
+
const args = [
|
|
279
|
+
"pr", "merge", String(prNumber),
|
|
280
|
+
"--repo", repo,
|
|
281
|
+
strategy === "squash" ? "--squash" : "--merge",
|
|
282
|
+
"--delete-branch",
|
|
283
|
+
];
|
|
284
|
+
const result = runGh(args, cwd);
|
|
285
|
+
if (!result.ok) return fail(result.error!);
|
|
286
|
+
return ok(undefined);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Projects v2 ────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
export function ghAddToProject(cwd: string, repo: string, projectNumber: number, issueNumber: number): GhResult<void> {
|
|
292
|
+
// Get the issue's node ID first
|
|
293
|
+
const issueResult = runGhJson<{ id: string }>(
|
|
294
|
+
["api", `repos/${repo}/issues/${issueNumber}`, "--jq", "{id: .node_id}"],
|
|
295
|
+
cwd,
|
|
296
|
+
);
|
|
297
|
+
if (!issueResult.ok) return fail(issueResult.error!);
|
|
298
|
+
|
|
299
|
+
// Get the project's node ID
|
|
300
|
+
const [owner] = repo.split("/");
|
|
301
|
+
const projectResult = runGhJson<{ id: string }>(
|
|
302
|
+
[
|
|
303
|
+
"api", "graphql",
|
|
304
|
+
"-f", `query=query { user(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
|
|
305
|
+
"--jq", ".data.user.projectV2.id",
|
|
306
|
+
],
|
|
307
|
+
cwd,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Try org if user fails
|
|
311
|
+
let projectId: string | undefined;
|
|
312
|
+
if (projectResult.ok && projectResult.data?.id) {
|
|
313
|
+
projectId = projectResult.data.id;
|
|
314
|
+
} else {
|
|
315
|
+
const orgResult = runGhJson<{ id: string }>(
|
|
316
|
+
[
|
|
317
|
+
"api", "graphql",
|
|
318
|
+
"-f", `query=query { organization(login: "${owner}") { projectV2(number: ${projectNumber}) { id } } }`,
|
|
319
|
+
"--jq", ".data.organization.projectV2.id",
|
|
320
|
+
],
|
|
321
|
+
cwd,
|
|
322
|
+
);
|
|
323
|
+
if (orgResult.ok) projectId = orgResult.data?.id;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!projectId) return fail("Could not find project");
|
|
327
|
+
|
|
328
|
+
const mutation = `mutation { addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${issueResult.data!.id}" }) { item { id } } }`;
|
|
329
|
+
return runGh(["api", "graphql", "-f", `query=${mutation}`], cwd) as GhResult<void>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Branch Operations ──────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
export function ghPushBranch(cwd: string, branch: string, setUpstream = true): GhResult<void> {
|
|
335
|
+
const args = ["git", "push"];
|
|
336
|
+
if (setUpstream) args.push("-u", "origin", branch);
|
|
337
|
+
else args.push("origin", branch);
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
execFileSync(args[0], args.slice(1), {
|
|
341
|
+
cwd,
|
|
342
|
+
encoding: "utf-8",
|
|
343
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
344
|
+
timeout: 30_000,
|
|
345
|
+
});
|
|
346
|
+
return ok(undefined);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function ghCreateBranch(cwd: string, branch: string, from: string): GhResult<void> {
|
|
353
|
+
try {
|
|
354
|
+
execFileSync("git", ["branch", branch, from], {
|
|
355
|
+
cwd,
|
|
356
|
+
encoding: "utf-8",
|
|
357
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
358
|
+
timeout: 10_000,
|
|
359
|
+
});
|
|
360
|
+
return ok(undefined);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
13
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
14
|
+
import { bootstrapSync } from "./sync.js";
|
|
15
|
+
import { loadSyncMapping } from "./mapping.js";
|
|
16
|
+
import { ghIsAvailable } from "./cli.js";
|
|
17
|
+
|
|
18
|
+
export default function (pi: ExtensionAPI) {
|
|
19
|
+
pi.registerCommand("github-sync", {
|
|
20
|
+
description: "Bootstrap GitHub sync or show sync status",
|
|
21
|
+
handler: async (args: string, ctx) => {
|
|
22
|
+
const subcommand = args.trim().toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (subcommand === "status") {
|
|
25
|
+
await showStatus(ctx);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (subcommand === "bootstrap" || subcommand === "") {
|
|
30
|
+
await runBootstrap(ctx);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ctx.ui.notify(
|
|
35
|
+
"Usage: /github-sync [bootstrap|status]",
|
|
36
|
+
"info",
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function showStatus(ctx: import("@gsd/pi-coding-agent").ExtensionCommandContext) {
|
|
43
|
+
if (!ghIsAvailable()) {
|
|
44
|
+
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const mapping = loadSyncMapping(ctx.cwd);
|
|
49
|
+
if (!mapping) {
|
|
50
|
+
ctx.ui.notify("GitHub sync: No sync mapping found. Run `/github-sync bootstrap` to initialize.", "info");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const milestoneCount = Object.keys(mapping.milestones).length;
|
|
55
|
+
const sliceCount = Object.keys(mapping.slices).length;
|
|
56
|
+
const taskCount = Object.keys(mapping.tasks).length;
|
|
57
|
+
const openMilestones = Object.values(mapping.milestones).filter(m => m.state === "open").length;
|
|
58
|
+
const openSlices = Object.values(mapping.slices).filter(s => s.state === "open").length;
|
|
59
|
+
const openTasks = Object.values(mapping.tasks).filter(t => t.state === "open").length;
|
|
60
|
+
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
[
|
|
63
|
+
`GitHub sync: repo=${mapping.repo}`,
|
|
64
|
+
` Milestones: ${milestoneCount} (${openMilestones} open)`,
|
|
65
|
+
` Slices: ${sliceCount} (${openSlices} open)`,
|
|
66
|
+
` Tasks: ${taskCount} (${openTasks} open)`,
|
|
67
|
+
].join("\n"),
|
|
68
|
+
"info",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runBootstrap(ctx: import("@gsd/pi-coding-agent").ExtensionCommandContext) {
|
|
73
|
+
if (!ghIsAvailable()) {
|
|
74
|
+
ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ctx.ui.notify("GitHub sync: bootstrapping...", "info");
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const counts = await bootstrapSync(ctx.cwd);
|
|
82
|
+
if (counts.milestones === 0 && counts.slices === 0 && counts.tasks === 0) {
|
|
83
|
+
ctx.ui.notify("GitHub sync: everything already synced (or no milestones found).", "info");
|
|
84
|
+
} else {
|
|
85
|
+
ctx.ui.notify(
|
|
86
|
+
`GitHub sync: created ${counts.milestones} milestone(s), ${counts.slices} slice(s), ${counts.tasks} task(s).`,
|
|
87
|
+
"info",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
ctx.ui.notify(`GitHub sync bootstrap failed: ${err}`, "error");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for the GitHub sync mapping.
|
|
3
|
+
*
|
|
4
|
+
* The mapping lives at `.gsd/github-sync.json` and tracks which GSD
|
|
5
|
+
* entities have been synced to which GitHub entities (issues, PRs,
|
|
6
|
+
* milestones) along with their numbers and sync timestamps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { atomicWriteSync } from "../gsd/atomic-write.js";
|
|
12
|
+
import type { SyncMapping, MilestoneSyncRecord, SliceSyncRecord, SyncEntityRecord } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const MAPPING_FILENAME = "github-sync.json";
|
|
15
|
+
|
|
16
|
+
function mappingPath(basePath: string): string {
|
|
17
|
+
return join(basePath, ".gsd", MAPPING_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Load / Save ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export function loadSyncMapping(basePath: string): SyncMapping | null {
|
|
23
|
+
const path = mappingPath(basePath);
|
|
24
|
+
if (!existsSync(path)) return null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(path, "utf-8");
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (parsed?.version !== 1) return null;
|
|
29
|
+
return parsed as SyncMapping;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveSyncMapping(basePath: string, mapping: SyncMapping): void {
|
|
36
|
+
const path = mappingPath(basePath);
|
|
37
|
+
atomicWriteSync(path, JSON.stringify(mapping, null, 2) + "\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createEmptyMapping(repo: string): SyncMapping {
|
|
41
|
+
return {
|
|
42
|
+
version: 1,
|
|
43
|
+
repo,
|
|
44
|
+
milestones: {},
|
|
45
|
+
slices: {},
|
|
46
|
+
tasks: {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Accessors ──────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export function getMilestoneRecord(mapping: SyncMapping, mid: string): MilestoneSyncRecord | null {
|
|
53
|
+
return mapping.milestones[mid] ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getSliceRecord(mapping: SyncMapping, mid: string, sid: string): SliceSyncRecord | null {
|
|
57
|
+
return mapping.slices[`${mid}/${sid}`] ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getTaskRecord(mapping: SyncMapping, mid: string, sid: string, tid: string): SyncEntityRecord | null {
|
|
61
|
+
return mapping.tasks[`${mid}/${sid}/${tid}`] ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getTaskIssueNumber(mapping: SyncMapping, mid: string, sid: string, tid: string): number | null {
|
|
65
|
+
const record = getTaskRecord(mapping, mid, sid, tid);
|
|
66
|
+
return record?.issueNumber ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Mutators ───────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function setMilestoneRecord(mapping: SyncMapping, mid: string, record: MilestoneSyncRecord): void {
|
|
72
|
+
mapping.milestones[mid] = record;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function setSliceRecord(mapping: SyncMapping, mid: string, sid: string, record: SliceSyncRecord): void {
|
|
76
|
+
mapping.slices[`${mid}/${sid}`] = record;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function setTaskRecord(mapping: SyncMapping, mid: string, sid: string, tid: string, record: SyncEntityRecord): void {
|
|
80
|
+
mapping.tasks[`${mid}/${sid}/${tid}`] = record;
|
|
81
|
+
}
|