@gaodes/pi-gitlab 0.3.0
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/.primecodex.json +19 -0
- package/.upstream.json +49 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/package.json +79 -0
- package/skills/gitlab-assistant/SKILL.md +28 -0
- package/skills/gitlab-issue/SKILL.md +41 -0
- package/skills/gitlab-mr/SKILL.md +49 -0
- package/skills/gitlab-pipeline/SKILL.md +36 -0
- package/skills/gitlab-release/SKILL.md +45 -0
- package/skills/gitlab-workflow/SKILL.md +39 -0
- package/src/commands/gitlab-doctor.ts +228 -0
- package/src/config/guard.ts +96 -0
- package/src/config/loader.ts +170 -0
- package/src/config/types.ts +57 -0
- package/src/events/resourcesDiscover.ts +27 -0
- package/src/index.ts +87 -0
- package/src/lib/confirm.ts +47 -0
- package/src/lib/env.ts +48 -0
- package/src/lib/errors.ts +156 -0
- package/src/lib/gitRemoteParse.ts +41 -0
- package/src/lib/glab.ts +34 -0
- package/src/lib/pagination.ts +3 -0
- package/src/lib/projectCache.ts +39 -0
- package/src/lib/projectFallback.ts +19 -0
- package/src/lib/redact.ts +24 -0
- package/src/lib/resolveProjectId.ts +37 -0
- package/src/lib/schemas.ts +16 -0
- package/src/tools/IMPLEMENTATION-NOTE-1B.md +33 -0
- package/src/tools/gitlab_api.ts +146 -0
- package/src/tools/gitlab_force_push_safe.ts +246 -0
- package/src/tools/gitlab_issue_close.ts +64 -0
- package/src/tools/gitlab_issue_create.ts +71 -0
- package/src/tools/gitlab_issue_list.ts +110 -0
- package/src/tools/gitlab_job_logs.ts +89 -0
- package/src/tools/gitlab_mr_bulk_approve.ts +109 -0
- package/src/tools/gitlab_mr_create.ts +83 -0
- package/src/tools/gitlab_mr_list.ts +108 -0
- package/src/tools/gitlab_mr_merge.ts +86 -0
- package/src/tools/gitlab_mr_view.ts +99 -0
- package/src/tools/gitlab_pipeline_run.ts +68 -0
- package/src/tools/gitlab_pipeline_status.ts +122 -0
- package/src/tools/gitlab_project_resolve.ts +59 -0
- package/src/tools/gitlab_release_create.ts +92 -0
- package/src/tools/gitlab_release_list.ts +97 -0
- package/src/tools/gitlab_release_view.ts +78 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /gitlab-doctor — diagnostics and interactive setup for pi-gitlab.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { checkSetup } from "../config/guard.js";
|
|
10
|
+
import { loadConfig, writeConfig } from "../config/loader.js";
|
|
11
|
+
import { getApiHost } from "../lib/env.js";
|
|
12
|
+
|
|
13
|
+
const MIN_GLAB_VERSION = "1.40.0";
|
|
14
|
+
|
|
15
|
+
interface Check {
|
|
16
|
+
label: string;
|
|
17
|
+
status: "pass" | "fail" | "warn" | "info";
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function versionSatisfies(installed: string, required: string): boolean {
|
|
22
|
+
const [majI = 0, minI = 0, patI = 0] = installed
|
|
23
|
+
.split(".")
|
|
24
|
+
.map((n) => Number(n) || 0);
|
|
25
|
+
const [majR = 0, minR = 0, patR = 0] = required
|
|
26
|
+
.split(".")
|
|
27
|
+
.map((n) => Number(n) || 0);
|
|
28
|
+
if (majI > majR) return true;
|
|
29
|
+
if (majI < majR) return false;
|
|
30
|
+
if (minI > minR) return true;
|
|
31
|
+
if (minI < minR) return false;
|
|
32
|
+
return patI >= patR;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runDoctor(pi: ExtensionAPI, cwd?: string): Promise<Check[]> {
|
|
36
|
+
const checks: Check[] = [];
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await pi.exec("glab", ["--version"]);
|
|
40
|
+
const versionMatch = stdout.match(/glab ([\d.]+)/);
|
|
41
|
+
if (!versionMatch) {
|
|
42
|
+
checks.push({
|
|
43
|
+
label: "glab CLI",
|
|
44
|
+
status: "fail",
|
|
45
|
+
detail: `Unexpected --version output: ${stdout.trim()}`,
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
const installed = versionMatch[1];
|
|
49
|
+
const ok = versionSatisfies(installed, MIN_GLAB_VERSION);
|
|
50
|
+
checks.push({
|
|
51
|
+
label: "glab CLI version",
|
|
52
|
+
status: ok ? "pass" : "fail",
|
|
53
|
+
detail: ok
|
|
54
|
+
? `glab ${installed} (meets minimum ${MIN_GLAB_VERSION})`
|
|
55
|
+
: `glab ${installed} — below minimum ${MIN_GLAB_VERSION}. Run: brew upgrade glab`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
checks.push({
|
|
60
|
+
label: "glab CLI",
|
|
61
|
+
status: "fail",
|
|
62
|
+
detail: "glab not found in PATH. Install from: https://gitlab.com/gitlab-org/cli#installation",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const { stdout, code } = await pi.exec("glab", ["auth", "status"]);
|
|
68
|
+
checks.push({
|
|
69
|
+
label: "glab auth",
|
|
70
|
+
status: code === 0 ? "pass" : "fail",
|
|
71
|
+
detail:
|
|
72
|
+
code === 0
|
|
73
|
+
? (stdout.trim().split("\n")[0] ?? "Authenticated")
|
|
74
|
+
: "Not authenticated. Run `glab auth login` to authenticate.",
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
checks.push({
|
|
78
|
+
label: "glab auth",
|
|
79
|
+
status: "fail",
|
|
80
|
+
detail: `glab auth status check failed: ${String(err)}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const apiHost = getApiHost();
|
|
85
|
+
try {
|
|
86
|
+
const { code, stderr } = await pi.exec("glab", [
|
|
87
|
+
"api",
|
|
88
|
+
"version",
|
|
89
|
+
"--hostname",
|
|
90
|
+
apiHost,
|
|
91
|
+
]);
|
|
92
|
+
checks.push({
|
|
93
|
+
label: `GitLab API (${apiHost})`,
|
|
94
|
+
status: code === 0 ? "pass" : "fail",
|
|
95
|
+
detail:
|
|
96
|
+
code === 0
|
|
97
|
+
? `API is reachable at ${apiHost}`
|
|
98
|
+
: `API request failed: ${stderr.trim()}`,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
checks.push({
|
|
102
|
+
label: `GitLab API (${apiHost})`,
|
|
103
|
+
status: "fail",
|
|
104
|
+
detail: `Could not reach ${apiHost}: ${String(err)}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const setupStatus = checkSetup(cwd);
|
|
109
|
+
const hasToken = !setupStatus.missingToken;
|
|
110
|
+
const hasExplicitConfig = !setupStatus.missingConfig;
|
|
111
|
+
|
|
112
|
+
checks.push({
|
|
113
|
+
label: "pi-gitlab token",
|
|
114
|
+
status: hasToken ? "pass" : "fail",
|
|
115
|
+
detail: hasToken
|
|
116
|
+
? `Token sourced from ${setupStatus.config.tokenEnv} environment variable`
|
|
117
|
+
: `No token found. Set ${setupStatus.config.tokenEnv} in your shell or .env.1pass.`,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
checks.push({
|
|
121
|
+
label: "pi-gitlab config key",
|
|
122
|
+
status: hasExplicitConfig ? "pass" : "fail",
|
|
123
|
+
detail: hasExplicitConfig
|
|
124
|
+
? "pi-gitlab configuration key found in prime-settings.json"
|
|
125
|
+
: "Missing pi-gitlab configuration key in prime-settings.json",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
checks.push({
|
|
129
|
+
label: "pi-gitlab ready",
|
|
130
|
+
status: setupStatus.ready ? "pass" : "fail",
|
|
131
|
+
detail: setupStatus.ready
|
|
132
|
+
? "All checks passed — pi-gitlab tools are available."
|
|
133
|
+
: "Configuration incomplete — tools remain blocked until setup completes.",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return checks;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runSetupWizard(ctx: ExtensionContext): Promise<boolean> {
|
|
140
|
+
const current = loadConfig(ctx.cwd);
|
|
141
|
+
const proceed = await ctx.ui.confirm(
|
|
142
|
+
"Run pi-gitlab setup wizard?",
|
|
143
|
+
"pi-gitlab tools are blocked until config + token are valid. Start guided setup now?",
|
|
144
|
+
);
|
|
145
|
+
if (!proceed) return false;
|
|
146
|
+
|
|
147
|
+
const hostname =
|
|
148
|
+
(await ctx.ui.input("GitLab hostname", current.hostname)) ?? current.hostname;
|
|
149
|
+
const sshHostname =
|
|
150
|
+
(await ctx.ui.input("GitLab SSH hostname", current.sshHostname)) ??
|
|
151
|
+
current.sshHostname;
|
|
152
|
+
const sshPortRaw =
|
|
153
|
+
(await ctx.ui.input("GitLab SSH port", String(current.sshPort))) ??
|
|
154
|
+
String(current.sshPort);
|
|
155
|
+
const tokenEnv =
|
|
156
|
+
(await ctx.ui.input("Token environment variable name", current.tokenEnv)) ??
|
|
157
|
+
current.tokenEnv;
|
|
158
|
+
const defaultProjectPathInput =
|
|
159
|
+
(await ctx.ui.input(
|
|
160
|
+
"Default project path (optional)",
|
|
161
|
+
current.defaultProjectPath ?? "",
|
|
162
|
+
)) ?? "";
|
|
163
|
+
|
|
164
|
+
const sshPort = Number(sshPortRaw);
|
|
165
|
+
if (!Number.isInteger(sshPort) || sshPort <= 0) {
|
|
166
|
+
ctx.ui.notify("Invalid SSH port. Setup cancelled.", "error");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const defaultProjectPath = defaultProjectPathInput.trim() || null;
|
|
171
|
+
|
|
172
|
+
const confirm = await ctx.ui.confirm(
|
|
173
|
+
"Save pi-gitlab configuration?",
|
|
174
|
+
`Host: ${hostname}\nSSH: ${sshHostname}:${sshPort}\nToken env: ${tokenEnv}\nDefault project: ${defaultProjectPath ?? "(none)"}`,
|
|
175
|
+
);
|
|
176
|
+
if (!confirm) return false;
|
|
177
|
+
|
|
178
|
+
writeConfig({
|
|
179
|
+
hostname,
|
|
180
|
+
sshHostname,
|
|
181
|
+
sshPort,
|
|
182
|
+
tokenEnv,
|
|
183
|
+
defaultProjectPath,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
ctx.ui.notify("Saved pi-gitlab configuration to global prime-settings.json", "info");
|
|
187
|
+
|
|
188
|
+
if (!process.env[tokenEnv] || process.env[tokenEnv]?.trim().length === 0) {
|
|
189
|
+
ctx.ui.notify(
|
|
190
|
+
`Token variable ${tokenEnv} is not set in the current environment. Set it (or load via .env.1pass) before using tools.`,
|
|
191
|
+
"warning",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function gitlabDoctorCommand(
|
|
199
|
+
_args: unknown,
|
|
200
|
+
ctx: ExtensionContext,
|
|
201
|
+
pi: ExtensionAPI,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
let checks = await runDoctor(pi, ctx.cwd);
|
|
204
|
+
let status = checkSetup(ctx.cwd);
|
|
205
|
+
|
|
206
|
+
if (!status.ready) {
|
|
207
|
+
const configured = await runSetupWizard(ctx);
|
|
208
|
+
if (configured) {
|
|
209
|
+
checks = await runDoctor(pi, ctx.cwd);
|
|
210
|
+
status = checkSetup(ctx.cwd);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lines: string[] = [];
|
|
215
|
+
for (const check of checks) {
|
|
216
|
+
const icon =
|
|
217
|
+
check.status === "pass" ? "✅" : check.status === "warn" ? "⚠️" : "❌";
|
|
218
|
+
lines.push(`${icon} **${check.label}**\n ${check.detail}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!status.ready) {
|
|
222
|
+
lines.push(
|
|
223
|
+
"\n⚠️ **Setup incomplete**\n Tools remain blocked until both token and `pi-gitlab` config are valid.",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ctx.ui.notify(lines.join("\n\n"), "info");
|
|
228
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadConfig } from "./loader.js";
|
|
5
|
+
import type { PiGitlabConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface SetupStatus {
|
|
8
|
+
ready: boolean;
|
|
9
|
+
missingToken: boolean;
|
|
10
|
+
missingConfig: boolean;
|
|
11
|
+
config: PiGitlabConfig;
|
|
12
|
+
issues: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check prerequisites for the pi-gitlab extension.
|
|
17
|
+
*
|
|
18
|
+
* Returns ready=true only when both PI_GITLAB_TOKEN is set AND the
|
|
19
|
+
* pi-gitlab key exists in prime-settings.json (global or project).
|
|
20
|
+
*
|
|
21
|
+
* The key alone is not enough; the extension intentionally blocks
|
|
22
|
+
* until the config is explicitly present. Auto-seeding remains
|
|
23
|
+
* available only inside explicit setup/doctor flows.
|
|
24
|
+
*
|
|
25
|
+
* If ready=false, the extension entry should block all tool usage
|
|
26
|
+
* and start the interactive setup wizard.
|
|
27
|
+
*/
|
|
28
|
+
export function checkSetup(cwd?: string): SetupStatus {
|
|
29
|
+
const config = loadConfig(cwd);
|
|
30
|
+
const issues: string[] = [];
|
|
31
|
+
|
|
32
|
+
const tokenValue = process.env[config.tokenEnv];
|
|
33
|
+
const missingToken = !tokenValue || tokenValue.trim().length === 0;
|
|
34
|
+
|
|
35
|
+
const hasExplicitConfig =
|
|
36
|
+
hasPiGitlabKeyInGlobal() || hasPiGitlabKeyInProject(cwd);
|
|
37
|
+
const missingConfig = !hasExplicitConfig;
|
|
38
|
+
|
|
39
|
+
if (missingToken) {
|
|
40
|
+
issues.push(
|
|
41
|
+
`Environment variable ${config.tokenEnv} is not set. ` +
|
|
42
|
+
`Set it or run the setup wizard.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (missingConfig) {
|
|
47
|
+
issues.push(
|
|
48
|
+
`No pi-gitlab configuration found in prime-settings.json. ` +
|
|
49
|
+
`Run the setup wizard to create one.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ready: !missingToken && !missingConfig,
|
|
55
|
+
missingToken,
|
|
56
|
+
missingConfig,
|
|
57
|
+
config,
|
|
58
|
+
issues,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasPiGitlabKeyInGlobal(): boolean {
|
|
63
|
+
try {
|
|
64
|
+
const path = join(homedir(), ".pi", "agent", "prime-settings.json");
|
|
65
|
+
if (!existsSync(path)) return false;
|
|
66
|
+
const raw = readFileSync(path, "utf-8");
|
|
67
|
+
const parsed: unknown = JSON.parse(raw);
|
|
68
|
+
return (
|
|
69
|
+
parsed !== null &&
|
|
70
|
+
typeof parsed === "object" &&
|
|
71
|
+
!Array.isArray(parsed) &&
|
|
72
|
+
"pi-gitlab" in parsed
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasPiGitlabKeyInProject(cwd?: string): boolean {
|
|
80
|
+
if (!cwd) return false;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const path = join(cwd, ".pi", "prime-settings.json");
|
|
84
|
+
if (!existsSync(path)) return false;
|
|
85
|
+
const raw = readFileSync(path, "utf-8");
|
|
86
|
+
const parsed: unknown = JSON.parse(raw);
|
|
87
|
+
return (
|
|
88
|
+
parsed !== null &&
|
|
89
|
+
typeof parsed === "object" &&
|
|
90
|
+
!Array.isArray(parsed) &&
|
|
91
|
+
"pi-gitlab" in parsed
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { PiGitlabConfig } from "./types.js";
|
|
5
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const GLOBAL_SETTINGS_PATH = join(
|
|
8
|
+
homedir(),
|
|
9
|
+
".pi",
|
|
10
|
+
"agent",
|
|
11
|
+
"prime-settings.json",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function cloneDefaults(): PiGitlabConfig {
|
|
15
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG)) as PiGitlabConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function applyOverlay(
|
|
19
|
+
base: PiGitlabConfig,
|
|
20
|
+
overlay: Record<string, unknown>,
|
|
21
|
+
): void {
|
|
22
|
+
if (typeof overlay.hostname === "string")
|
|
23
|
+
base.hostname = overlay.hostname as string;
|
|
24
|
+
if (typeof overlay.sshHostname === "string")
|
|
25
|
+
base.sshHostname = overlay.sshHostname as string;
|
|
26
|
+
if (typeof overlay.sshPort === "number")
|
|
27
|
+
base.sshPort = overlay.sshPort as number;
|
|
28
|
+
if (typeof overlay.apiBase === "string")
|
|
29
|
+
base.apiBase = overlay.apiBase as string;
|
|
30
|
+
if (overlay.tokenRef !== undefined)
|
|
31
|
+
base.tokenRef = overlay.tokenRef as string | null;
|
|
32
|
+
if (typeof overlay.tokenEnv === "string")
|
|
33
|
+
base.tokenEnv = overlay.tokenEnv as string;
|
|
34
|
+
if (
|
|
35
|
+
typeof overlay.defaultProjectId === "number" ||
|
|
36
|
+
overlay.defaultProjectId === null
|
|
37
|
+
)
|
|
38
|
+
base.defaultProjectId = overlay.defaultProjectId as number | null;
|
|
39
|
+
if (
|
|
40
|
+
typeof overlay.defaultProjectPath === "string" ||
|
|
41
|
+
overlay.defaultProjectPath === null
|
|
42
|
+
)
|
|
43
|
+
base.defaultProjectPath = overlay.defaultProjectPath as string | null;
|
|
44
|
+
if (overlay.render && typeof overlay.render === "object") {
|
|
45
|
+
const r = overlay.render as Record<string, unknown>;
|
|
46
|
+
if (typeof r.tableMaxRows === "number")
|
|
47
|
+
base.render.tableMaxRows = r.tableMaxRows as number;
|
|
48
|
+
if (typeof r.diffMaxLines === "number")
|
|
49
|
+
base.render.diffMaxLines = r.diffMaxLines as number;
|
|
50
|
+
if (typeof r.logTailLines === "number")
|
|
51
|
+
base.render.logTailLines = r.logTailLines as number;
|
|
52
|
+
}
|
|
53
|
+
if (overlay.safety && typeof overlay.safety === "object") {
|
|
54
|
+
const s = overlay.safety as Record<string, unknown>;
|
|
55
|
+
if (typeof s.requireConfirmForDelete === "boolean")
|
|
56
|
+
base.safety.requireConfirmForDelete =
|
|
57
|
+
s.requireConfirmForDelete as boolean;
|
|
58
|
+
if (typeof s.previewMutatingApiCalls === "boolean")
|
|
59
|
+
base.safety.previewMutatingApiCalls =
|
|
60
|
+
s.previewMutatingApiCalls as boolean;
|
|
61
|
+
if (typeof s.redactJobLogsByDefault === "boolean")
|
|
62
|
+
base.safety.redactJobLogsByDefault = s.redactJobLogsByDefault as boolean;
|
|
63
|
+
if (typeof s.forcePushReprotectAlways === "boolean")
|
|
64
|
+
base.safety.forcePushReprotectAlways =
|
|
65
|
+
s.forcePushReprotectAlways as boolean;
|
|
66
|
+
if (typeof s.minGlabVersion === "string")
|
|
67
|
+
base.safety.minGlabVersion = s.minGlabVersion as string;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadConfig(cwd?: string): PiGitlabConfig {
|
|
72
|
+
const base = cloneDefaults();
|
|
73
|
+
|
|
74
|
+
if (existsSync(GLOBAL_SETTINGS_PATH)) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = readFileSync(GLOBAL_SETTINGS_PATH, "utf-8");
|
|
77
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
78
|
+
if (parsed["pi-gitlab"] && typeof parsed["pi-gitlab"] === "object") {
|
|
79
|
+
applyOverlay(base, parsed["pi-gitlab"] as Record<string, unknown>);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// malformed file — keep defaults
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cwd) {
|
|
87
|
+
const projectSettings = join(cwd, ".pi", "prime-settings.json");
|
|
88
|
+
if (existsSync(projectSettings)) {
|
|
89
|
+
try {
|
|
90
|
+
const raw = readFileSync(projectSettings, "utf-8");
|
|
91
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
92
|
+
if (parsed["pi-gitlab"] && typeof parsed["pi-gitlab"] === "object") {
|
|
93
|
+
applyOverlay(base, parsed["pi-gitlab"] as Record<string, unknown>);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// malformed project overlay — ignored
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return base;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readGlobalSettings(): Record<string, unknown> {
|
|
105
|
+
if (!existsSync(GLOBAL_SETTINGS_PATH)) return {};
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(readFileSync(GLOBAL_SETTINGS_PATH, "utf-8")) as Record<
|
|
108
|
+
string,
|
|
109
|
+
unknown
|
|
110
|
+
>;
|
|
111
|
+
} catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeGlobalSettings(payload: Record<string, unknown>): void {
|
|
117
|
+
const dir = join(homedir(), ".pi", "agent");
|
|
118
|
+
mkdirSync(dir, { recursive: true });
|
|
119
|
+
writeFileSync(
|
|
120
|
+
GLOBAL_SETTINGS_PATH,
|
|
121
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
122
|
+
"utf-8",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function ensureConfig(): void {
|
|
127
|
+
const existing = readGlobalSettings();
|
|
128
|
+
if (existing["pi-gitlab"] !== undefined) return;
|
|
129
|
+
|
|
130
|
+
const defaults = cloneDefaults();
|
|
131
|
+
writeGlobalSettings({
|
|
132
|
+
...existing,
|
|
133
|
+
"pi-gitlab": {
|
|
134
|
+
hostname: defaults.hostname,
|
|
135
|
+
sshHostname: defaults.sshHostname,
|
|
136
|
+
sshPort: defaults.sshPort,
|
|
137
|
+
apiBase: defaults.apiBase,
|
|
138
|
+
tokenRef: defaults.tokenRef,
|
|
139
|
+
tokenEnv: defaults.tokenEnv,
|
|
140
|
+
defaultProjectId: defaults.defaultProjectId,
|
|
141
|
+
defaultProjectPath: defaults.defaultProjectPath,
|
|
142
|
+
render: { ...defaults.render },
|
|
143
|
+
safety: { ...defaults.safety },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function writeConfig(overrides: Partial<PiGitlabConfig>): PiGitlabConfig {
|
|
149
|
+
const existing = readGlobalSettings();
|
|
150
|
+
const base = cloneDefaults();
|
|
151
|
+
if (existing["pi-gitlab"] && typeof existing["pi-gitlab"] === "object") {
|
|
152
|
+
applyOverlay(base, existing["pi-gitlab"] as Record<string, unknown>);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const merged: PiGitlabConfig = {
|
|
156
|
+
...base,
|
|
157
|
+
...overrides,
|
|
158
|
+
render: { ...base.render, ...(overrides.render ?? {}) },
|
|
159
|
+
safety: { ...base.safety, ...(overrides.safety ?? {}) },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
writeGlobalSettings({
|
|
163
|
+
...existing,
|
|
164
|
+
"pi-gitlab": merged,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { GLOBAL_SETTINGS_PATH };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Config shape for the pi-gitlab key in prime-settings.json */
|
|
2
|
+
export interface PiGitlabConfig {
|
|
3
|
+
hostname: string;
|
|
4
|
+
sshHostname: string;
|
|
5
|
+
sshPort: number;
|
|
6
|
+
apiBase: string;
|
|
7
|
+
tokenRef: string | null;
|
|
8
|
+
tokenEnv: string;
|
|
9
|
+
defaultProjectId: number | null;
|
|
10
|
+
defaultProjectPath: string | null;
|
|
11
|
+
render: {
|
|
12
|
+
tableMaxRows: number;
|
|
13
|
+
diffMaxLines: number;
|
|
14
|
+
logTailLines: number;
|
|
15
|
+
};
|
|
16
|
+
safety: {
|
|
17
|
+
requireConfirmForDelete: boolean;
|
|
18
|
+
previewMutatingApiCalls: boolean;
|
|
19
|
+
redactJobLogsByDefault: boolean;
|
|
20
|
+
forcePushReprotectAlways: boolean;
|
|
21
|
+
minGlabVersion: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_CONFIG: PiGitlabConfig = {
|
|
26
|
+
hostname: "gitlab.elches.dev",
|
|
27
|
+
sshHostname: "gitlab-ssh.elches.dev",
|
|
28
|
+
sshPort: 2222,
|
|
29
|
+
apiBase: "https://gitlab.elches.dev/api/v4",
|
|
30
|
+
tokenRef: null,
|
|
31
|
+
tokenEnv: "PI_GITLAB_TOKEN",
|
|
32
|
+
defaultProjectId: null,
|
|
33
|
+
defaultProjectPath: null,
|
|
34
|
+
render: {
|
|
35
|
+
tableMaxRows: 25,
|
|
36
|
+
diffMaxLines: 400,
|
|
37
|
+
logTailLines: 200,
|
|
38
|
+
},
|
|
39
|
+
safety: {
|
|
40
|
+
requireConfirmForDelete: true,
|
|
41
|
+
previewMutatingApiCalls: true,
|
|
42
|
+
redactJobLogsByDefault: true,
|
|
43
|
+
forcePushReprotectAlways: true,
|
|
44
|
+
minGlabVersion: "1.40.0",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface ProjectCacheEntry {
|
|
49
|
+
id: number;
|
|
50
|
+
pathWithNamespace: string;
|
|
51
|
+
resolvedAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ProjectCache {
|
|
55
|
+
version: 1;
|
|
56
|
+
entries: Record<string, ProjectCacheEntry>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resources_discover handler for pi-gitlab.
|
|
3
|
+
*
|
|
4
|
+
* Registers the package's in-package skills directory so Pi's skill
|
|
5
|
+
* system discovers and exposes them.
|
|
6
|
+
*
|
|
7
|
+
* Skills live at `<package-root>/skills/` and are discovered from there.
|
|
8
|
+
* Each skill directory contains a SKILL.md with YAML frontmatter.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
/** Resolve the skills/ directory path relative to the package root. */
|
|
15
|
+
export function getSkillsDir(): string {
|
|
16
|
+
// src/events/resourcesDiscover.ts → package root
|
|
17
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const packageRoot = path.join(extensionDir, "..", "..");
|
|
19
|
+
return path.join(packageRoot, "skills");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Return value for the resources_discover event — exposes skillPaths to Pi. */
|
|
23
|
+
export function registerResourcesDiscover(): { skillPaths: string[] } {
|
|
24
|
+
return {
|
|
25
|
+
skillPaths: [getSkillsDir()],
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-gitlab — Pi extension for GitLab workflows via glab CLI.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 read-only tools:
|
|
5
|
+
* gitlab_project_resolve — resolve project path to numeric ID
|
|
6
|
+
* gitlab_mr_list — list merge requests
|
|
7
|
+
* gitlab_mr_view — view a single MR
|
|
8
|
+
* gitlab_issue_list — list issues
|
|
9
|
+
* gitlab_pipeline_status — show pipeline status
|
|
10
|
+
* gitlab_job_logs — fetch job logs
|
|
11
|
+
* gitlab_api — generic glab API wrapper
|
|
12
|
+
*
|
|
13
|
+
* Phase 2 mutating tools (require confirm:true):
|
|
14
|
+
* gitlab_mr_create — create a merge request
|
|
15
|
+
* gitlab_mr_merge — merge a merge request
|
|
16
|
+
* gitlab_issue_create — create an issue
|
|
17
|
+
* gitlab_issue_close — close an issue
|
|
18
|
+
* gitlab_pipeline_run — trigger a pipeline
|
|
19
|
+
*
|
|
20
|
+
* Phase 3 advanced tools:
|
|
21
|
+
* gitlab_release_list — list releases
|
|
22
|
+
* gitlab_release_view — view a single release
|
|
23
|
+
* gitlab_release_create — create a release (confirm:true)
|
|
24
|
+
* gitlab_mr_bulk_approve — bulk-approve MRs (confirm:true)
|
|
25
|
+
* gitlab_force_push_safe — safe force push with branch protection lifecycle (confirm:true)
|
|
26
|
+
*
|
|
27
|
+
* Commands:
|
|
28
|
+
* /gitlab-doctor — diagnostic check for glab, auth, API, and config
|
|
29
|
+
*
|
|
30
|
+
* Configuration lives in prime-settings.json key `pi-gitlab`.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import { gitlabDoctorCommand } from "./commands/gitlab-doctor.js";
|
|
35
|
+
import { registerResourcesDiscover } from "./events/resourcesDiscover.js";
|
|
36
|
+
import { registerGitlabApi } from "./tools/gitlab_api.js";
|
|
37
|
+
import { registerGitlabForcePushSafe } from "./tools/gitlab_force_push_safe.js";
|
|
38
|
+
import { registerGitlabIssueClose } from "./tools/gitlab_issue_close.js";
|
|
39
|
+
import { registerGitlabIssueCreate } from "./tools/gitlab_issue_create.js";
|
|
40
|
+
import { registerGitlabIssueList } from "./tools/gitlab_issue_list.js";
|
|
41
|
+
import { registerGitlabJobLogs } from "./tools/gitlab_job_logs.js";
|
|
42
|
+
import { registerGitlabMrBulkApprove } from "./tools/gitlab_mr_bulk_approve.js";
|
|
43
|
+
import { registerGitlabMrCreate } from "./tools/gitlab_mr_create.js";
|
|
44
|
+
import { registerGitlabMrList } from "./tools/gitlab_mr_list.js";
|
|
45
|
+
import { registerGitlabMrMerge } from "./tools/gitlab_mr_merge.js";
|
|
46
|
+
import { registerGitlabMrView } from "./tools/gitlab_mr_view.js";
|
|
47
|
+
import { registerGitlabPipelineRun } from "./tools/gitlab_pipeline_run.js";
|
|
48
|
+
import { registerGitlabPipelineStatus } from "./tools/gitlab_pipeline_status.js";
|
|
49
|
+
import { registerGitlabProjectResolve } from "./tools/gitlab_project_resolve.js";
|
|
50
|
+
import { registerGitlabReleaseCreate } from "./tools/gitlab_release_create.js";
|
|
51
|
+
import { registerGitlabReleaseList } from "./tools/gitlab_release_list.js";
|
|
52
|
+
import { registerGitlabReleaseView } from "./tools/gitlab_release_view.js";
|
|
53
|
+
|
|
54
|
+
export default function piGitlab(pi: ExtensionAPI) {
|
|
55
|
+
// Expose in-package skills
|
|
56
|
+
pi.on("resources_discover", () => registerResourcesDiscover());
|
|
57
|
+
|
|
58
|
+
// Diagnostic command
|
|
59
|
+
pi.registerCommand("gitlab-doctor", {
|
|
60
|
+
description:
|
|
61
|
+
"Run diagnostics for glab CLI, GitLab auth, API connectivity, and pi-gitlab config",
|
|
62
|
+
handler: (args, ctx) => gitlabDoctorCommand(args, ctx, pi),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Phase 1 read-only tools
|
|
66
|
+
registerGitlabProjectResolve(pi);
|
|
67
|
+
registerGitlabMrList(pi);
|
|
68
|
+
registerGitlabMrView(pi);
|
|
69
|
+
registerGitlabIssueList(pi);
|
|
70
|
+
registerGitlabPipelineStatus(pi);
|
|
71
|
+
registerGitlabJobLogs(pi);
|
|
72
|
+
registerGitlabApi(pi);
|
|
73
|
+
|
|
74
|
+
// Phase 2 mutating tools (all require confirm:true)
|
|
75
|
+
registerGitlabMrCreate(pi);
|
|
76
|
+
registerGitlabMrMerge(pi);
|
|
77
|
+
registerGitlabIssueCreate(pi);
|
|
78
|
+
registerGitlabIssueClose(pi);
|
|
79
|
+
registerGitlabPipelineRun(pi);
|
|
80
|
+
|
|
81
|
+
// Phase 3 advanced tools
|
|
82
|
+
registerGitlabReleaseList(pi);
|
|
83
|
+
registerGitlabReleaseView(pi);
|
|
84
|
+
registerGitlabReleaseCreate(pi);
|
|
85
|
+
registerGitlabMrBulkApprove(pi);
|
|
86
|
+
registerGitlabForcePushSafe(pi);
|
|
87
|
+
}
|