@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,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { glab } from "../lib/glab.js";
|
|
8
|
+
import { requireConfirm } from "../lib/confirm.js";
|
|
9
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
10
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
11
|
+
import { OptionalProject } from "../lib/schemas.js";
|
|
12
|
+
|
|
13
|
+
export function registerGitlabMrCreate(pi: ExtensionAPI) {
|
|
14
|
+
pi.registerTool({
|
|
15
|
+
name: "gitlab_mr_create",
|
|
16
|
+
label: "Create Merge Request",
|
|
17
|
+
description: "Create a new merge request. Requires confirmation.",
|
|
18
|
+
parameters: Type.Object(
|
|
19
|
+
{
|
|
20
|
+
project: OptionalProject,
|
|
21
|
+
sourceBranch: Type.String({ description: "Source branch name" }),
|
|
22
|
+
targetBranch: Type.String({ description: "Target branch name" }),
|
|
23
|
+
title: Type.String({ description: "MR title" }),
|
|
24
|
+
description: Type.Optional(
|
|
25
|
+
Type.String({ description: "MR description (markdown supported)" }),
|
|
26
|
+
),
|
|
27
|
+
draft: Type.Optional(Type.Boolean({ default: false })),
|
|
28
|
+
confirm: Type.Optional(Type.Boolean({ default: false })),
|
|
29
|
+
dryRun: Type.Optional(Type.Boolean({ default: false })),
|
|
30
|
+
},
|
|
31
|
+
{ additionalProperties: false },
|
|
32
|
+
),
|
|
33
|
+
async execute(
|
|
34
|
+
_toolCallId,
|
|
35
|
+
params,
|
|
36
|
+
_signal,
|
|
37
|
+
_onUpdate,
|
|
38
|
+
ctx: ExtensionContext,
|
|
39
|
+
) {
|
|
40
|
+
try {
|
|
41
|
+
requireSetup(ctx.cwd);
|
|
42
|
+
} catch {
|
|
43
|
+
return setupRequiredResult();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const projectPath = await resolveProject(params.project, ctx.cwd);
|
|
47
|
+
const projectId = await resolveProjectId(projectPath);
|
|
48
|
+
|
|
49
|
+
const preview = `Create MR **${params.title}**\n${params.sourceBranch} → ${params.targetBranch}\nProject: \`${projectPath}\``;
|
|
50
|
+
const blocked = requireConfirm(preview, {
|
|
51
|
+
confirm: params.confirm,
|
|
52
|
+
dryRun: params.dryRun,
|
|
53
|
+
});
|
|
54
|
+
if (blocked) return blocked;
|
|
55
|
+
|
|
56
|
+
const body: Record<string, unknown> = {
|
|
57
|
+
source_branch: params.sourceBranch,
|
|
58
|
+
target_branch: params.targetBranch,
|
|
59
|
+
title: params.draft ? `Draft: ${params.title}` : params.title,
|
|
60
|
+
};
|
|
61
|
+
if (params.description) body.description = params.description;
|
|
62
|
+
|
|
63
|
+
const result = await glab([
|
|
64
|
+
"api",
|
|
65
|
+
"-X",
|
|
66
|
+
"POST",
|
|
67
|
+
`projects/${projectId}/merge_requests`,
|
|
68
|
+
...Object.entries(body).flatMap(([k, v]) => ["-f", `${k}=${v}`]),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const mr = result as Record<string, unknown>;
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `✅ MR created: !${mr.iid} — ${mr.title}\n${mr.web_url ?? ""}`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
details: { success: true, mr },
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { glab } from "../lib/glab.js";
|
|
8
|
+
import { limitRows } from "../lib/pagination.js";
|
|
9
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
10
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
11
|
+
import { MaxRows, OptionalProject } from "../lib/schemas.js";
|
|
12
|
+
|
|
13
|
+
interface MR {
|
|
14
|
+
iid: number;
|
|
15
|
+
title: string;
|
|
16
|
+
author?: { name?: string };
|
|
17
|
+
state: string;
|
|
18
|
+
pipeline?: { status?: string };
|
|
19
|
+
target_branch?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerGitlabMrList(pi: ExtensionAPI) {
|
|
23
|
+
pi.registerTool({
|
|
24
|
+
name: "gitlab_mr_list",
|
|
25
|
+
label: "List Merge Requests",
|
|
26
|
+
description:
|
|
27
|
+
"List merge requests with optional filters and transparent pagination.",
|
|
28
|
+
parameters: Type.Object(
|
|
29
|
+
{
|
|
30
|
+
project: OptionalProject,
|
|
31
|
+
state: Type.Optional(
|
|
32
|
+
Type.Union(
|
|
33
|
+
[
|
|
34
|
+
Type.Literal("opened"),
|
|
35
|
+
Type.Literal("closed"),
|
|
36
|
+
Type.Literal("merged"),
|
|
37
|
+
Type.Literal("all"),
|
|
38
|
+
],
|
|
39
|
+
{ default: "opened" },
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
author: Type.Optional(Type.String()),
|
|
43
|
+
assignee: Type.Optional(Type.String()),
|
|
44
|
+
reviewer: Type.Optional(Type.String()),
|
|
45
|
+
labels: Type.Optional(Type.Array(Type.String())),
|
|
46
|
+
targetBranch: Type.Optional(Type.String()),
|
|
47
|
+
sourceBranch: Type.Optional(Type.String()),
|
|
48
|
+
maxRows: MaxRows,
|
|
49
|
+
},
|
|
50
|
+
{ additionalProperties: false },
|
|
51
|
+
),
|
|
52
|
+
async execute(
|
|
53
|
+
_toolCallId,
|
|
54
|
+
params,
|
|
55
|
+
_signal,
|
|
56
|
+
_onUpdate,
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
requireSetup(ctx.cwd);
|
|
61
|
+
} catch {
|
|
62
|
+
return setupRequiredResult();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const projectPath = await resolveProject(params.project, ctx.cwd);
|
|
66
|
+
const projectId = await resolveProjectId(projectPath);
|
|
67
|
+
|
|
68
|
+
const query = new URLSearchParams();
|
|
69
|
+
query.set("state", params.state ?? "opened");
|
|
70
|
+
if (params.author) query.set("author_username", params.author);
|
|
71
|
+
if (params.assignee) query.set("assignee_username", params.assignee);
|
|
72
|
+
if (params.reviewer) query.set("reviewer_username", params.reviewer);
|
|
73
|
+
if (params.labels?.length) query.set("labels", params.labels.join(","));
|
|
74
|
+
if (params.targetBranch) query.set("target_branch", params.targetBranch);
|
|
75
|
+
if (params.sourceBranch) query.set("source_branch", params.sourceBranch);
|
|
76
|
+
query.set("per_page", "100");
|
|
77
|
+
|
|
78
|
+
const mrs = (await glab([
|
|
79
|
+
"api",
|
|
80
|
+
"--paginate",
|
|
81
|
+
`projects/${projectId}/merge_requests?${query.toString()}`,
|
|
82
|
+
])) as MR[];
|
|
83
|
+
|
|
84
|
+
const limited = limitRows(mrs, params.maxRows ?? 25);
|
|
85
|
+
if (limited.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: "No merge requests found." }],
|
|
88
|
+
details: { success: true, count: 0, mrs: [] },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const lines = [
|
|
93
|
+
"| IID | Title | Author | State | Pipeline | Target |",
|
|
94
|
+
"|---|---|---|---|---|---|",
|
|
95
|
+
];
|
|
96
|
+
for (const mr of limited) {
|
|
97
|
+
lines.push(
|
|
98
|
+
`| ${mr.iid} | ${mr.title} | ${mr.author?.name ?? "-"} | ${mr.state} | ${mr.pipeline?.status ?? "-"} | ${mr.target_branch ?? "-"} |`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
104
|
+
details: { success: true, count: limited.length, mrs: limited },
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { glab } from "../lib/glab.js";
|
|
8
|
+
import { requireConfirm } from "../lib/confirm.js";
|
|
9
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
10
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
11
|
+
import { OptionalProject } from "../lib/schemas.js";
|
|
12
|
+
|
|
13
|
+
export function registerGitlabMrMerge(pi: ExtensionAPI) {
|
|
14
|
+
pi.registerTool({
|
|
15
|
+
name: "gitlab_mr_merge",
|
|
16
|
+
label: "Merge Merge Request",
|
|
17
|
+
description: "Merge an open merge request. Requires confirmation.",
|
|
18
|
+
parameters: Type.Object(
|
|
19
|
+
{
|
|
20
|
+
project: OptionalProject,
|
|
21
|
+
mrId: Type.Number({ description: "MR IID" }),
|
|
22
|
+
squash: Type.Optional(Type.Boolean({ default: false })),
|
|
23
|
+
removeSourceBranch: Type.Optional(
|
|
24
|
+
Type.Boolean({ default: false }),
|
|
25
|
+
),
|
|
26
|
+
confirm: Type.Optional(Type.Boolean({ default: false })),
|
|
27
|
+
dryRun: Type.Optional(Type.Boolean({ default: false })),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
async execute(
|
|
32
|
+
_toolCallId,
|
|
33
|
+
params,
|
|
34
|
+
_signal,
|
|
35
|
+
_onUpdate,
|
|
36
|
+
ctx: ExtensionContext,
|
|
37
|
+
) {
|
|
38
|
+
try {
|
|
39
|
+
requireSetup(ctx.cwd);
|
|
40
|
+
} catch {
|
|
41
|
+
return setupRequiredResult();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const projectPath = await resolveProject(params.project, ctx.cwd);
|
|
45
|
+
const projectId = await resolveProjectId(projectPath);
|
|
46
|
+
|
|
47
|
+
// Fetch MR details for the preview
|
|
48
|
+
const mr = (await glab([
|
|
49
|
+
"api",
|
|
50
|
+
`projects/${projectId}/merge_requests/${params.mrId}`,
|
|
51
|
+
])) as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
const preview = `Merge MR !${params.mrId}: **${mr.title}**\n${mr.source_branch} → ${mr.target_branch}\nProject: \`${projectPath}\`${params.squash ? "\n(squash)" : ""}${params.removeSourceBranch ? "\n(remove source branch)" : ""}`;
|
|
54
|
+
const blocked = requireConfirm(preview, {
|
|
55
|
+
confirm: params.confirm,
|
|
56
|
+
dryRun: params.dryRun,
|
|
57
|
+
});
|
|
58
|
+
if (blocked) return blocked;
|
|
59
|
+
|
|
60
|
+
const body: Record<string, unknown> = {};
|
|
61
|
+
if (params.squash !== undefined) body.squash = params.squash;
|
|
62
|
+
if (params.removeSourceBranch !== undefined)
|
|
63
|
+
body.should_remove_source_branch = params.removeSourceBranch;
|
|
64
|
+
|
|
65
|
+
const result = await glab([
|
|
66
|
+
"api",
|
|
67
|
+
"-X",
|
|
68
|
+
"PUT",
|
|
69
|
+
`projects/${projectId}/merge_requests/${params.mrId}/merge`,
|
|
70
|
+
...(Object.keys(body).length > 0
|
|
71
|
+
? Object.entries(body).flatMap(([k, v]) => ["-f", `${k}=${v}`])
|
|
72
|
+
: []),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `✅ MR !${params.mrId} merged\n${(result as Record<string, unknown>).web_url ?? ""}`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
details: { success: true, mr: result },
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { glab } from "../lib/glab.js";
|
|
8
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
9
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
10
|
+
import { OptionalProject } from "../lib/schemas.js";
|
|
11
|
+
|
|
12
|
+
export function registerGitlabMrView(pi: ExtensionAPI) {
|
|
13
|
+
pi.registerTool({
|
|
14
|
+
name: "gitlab_mr_view",
|
|
15
|
+
label: "View Merge Request",
|
|
16
|
+
description:
|
|
17
|
+
"View a merge request with metadata, pipeline status, discussions, and optional diff.",
|
|
18
|
+
parameters: Type.Object(
|
|
19
|
+
{
|
|
20
|
+
project: OptionalProject,
|
|
21
|
+
mrId: Type.Number({ description: "MR IID (project-relative number)." }),
|
|
22
|
+
includeDiff: Type.Optional(Type.Boolean({ default: false })),
|
|
23
|
+
includeDiscussions: Type.Optional(Type.Boolean({ default: true })),
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
),
|
|
27
|
+
async execute(
|
|
28
|
+
_toolCallId,
|
|
29
|
+
params,
|
|
30
|
+
_signal,
|
|
31
|
+
_onUpdate,
|
|
32
|
+
ctx: ExtensionContext,
|
|
33
|
+
) {
|
|
34
|
+
try {
|
|
35
|
+
requireSetup(ctx.cwd);
|
|
36
|
+
} catch {
|
|
37
|
+
return setupRequiredResult();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const projectPath = await resolveProject(params.project, ctx.cwd);
|
|
41
|
+
const projectId = await resolveProjectId(projectPath);
|
|
42
|
+
|
|
43
|
+
const mr = (await glab([
|
|
44
|
+
"api",
|
|
45
|
+
`projects/${projectId}/merge_requests/${params.mrId}`,
|
|
46
|
+
])) as Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
parts.push(`## MR !${params.mrId}: ${String(mr.title ?? "")}`);
|
|
50
|
+
parts.push(`State: ${String(mr.state ?? "unknown")}`);
|
|
51
|
+
parts.push(`Author: ${(mr.author as { name?: string })?.name ?? "-"}`);
|
|
52
|
+
parts.push(
|
|
53
|
+
`Source → Target: ${String(mr.source_branch ?? "?")} → ${String(mr.target_branch ?? "?")}`,
|
|
54
|
+
);
|
|
55
|
+
parts.push(`URL: ${String(mr.web_url ?? "-")}`);
|
|
56
|
+
|
|
57
|
+
if (params.includeDiscussions) {
|
|
58
|
+
try {
|
|
59
|
+
const discussions = (await glab([
|
|
60
|
+
"api",
|
|
61
|
+
`projects/${projectId}/merge_requests/${params.mrId}/discussions`,
|
|
62
|
+
])) as Array<Record<string, unknown>>;
|
|
63
|
+
parts.push(`\nDiscussions: ${discussions.length}`);
|
|
64
|
+
} catch {
|
|
65
|
+
parts.push("\nDiscussions: unavailable");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (params.includeDiff) {
|
|
70
|
+
try {
|
|
71
|
+
const diff = (await glab([
|
|
72
|
+
"api",
|
|
73
|
+
`projects/${projectId}/merge_requests/${params.mrId}/changes`,
|
|
74
|
+
])) as {
|
|
75
|
+
changes?: Array<{
|
|
76
|
+
old_path: string;
|
|
77
|
+
new_path: string;
|
|
78
|
+
diff: string;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
parts.push("\n### Diff");
|
|
82
|
+
for (const c of diff.changes ?? []) {
|
|
83
|
+
parts.push(`\n**${c.old_path}**`);
|
|
84
|
+
parts.push("```diff");
|
|
85
|
+
parts.push(c.diff.slice(0, 2000));
|
|
86
|
+
parts.push("```");
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
parts.push("\nDiff: unavailable");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
95
|
+
details: { success: true, mr },
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
4
|
+
import { glab } from "../lib/glab.js";
|
|
5
|
+
import { requireConfirm } from "../lib/confirm.js";
|
|
6
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
7
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
8
|
+
import { OptionalProject } from "../lib/schemas.js";
|
|
9
|
+
|
|
10
|
+
export function registerGitlabPipelineRun(pi: ExtensionAPI) {
|
|
11
|
+
pi.registerTool({
|
|
12
|
+
name: "gitlab_pipeline_run",
|
|
13
|
+
label: "Run Pipeline",
|
|
14
|
+
description: "Trigger a new pipeline for a branch. Requires confirmation.",
|
|
15
|
+
parameters: Type.Object(
|
|
16
|
+
{
|
|
17
|
+
project: OptionalProject,
|
|
18
|
+
ref: Type.String({ description: "Branch or tag to run pipeline for" }),
|
|
19
|
+
variables: Type.Optional(Type.Object({}, { additionalProperties: true })),
|
|
20
|
+
confirm: Type.Optional(Type.Boolean({ default: false })),
|
|
21
|
+
dryRun: Type.Optional(Type.Boolean({ default: false })),
|
|
22
|
+
},
|
|
23
|
+
{ additionalProperties: false },
|
|
24
|
+
),
|
|
25
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
|
|
26
|
+
try {
|
|
27
|
+
requireSetup(ctx.cwd);
|
|
28
|
+
} catch {
|
|
29
|
+
return setupRequiredResult();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cwd = ctx.cwd;
|
|
33
|
+
const projectPath = await resolveProject(params.project, cwd);
|
|
34
|
+
const projectId = await resolveProjectId(projectPath);
|
|
35
|
+
|
|
36
|
+
const preview = `Trigger pipeline for \`${params.ref}\`\nProject: \`${projectPath}\``;
|
|
37
|
+
const blocked = requireConfirm(preview, { confirm: params.confirm, dryRun: params.dryRun });
|
|
38
|
+
if (blocked) return blocked;
|
|
39
|
+
|
|
40
|
+
const body: Record<string, unknown> = { ref: params.ref };
|
|
41
|
+
if (params.variables && Object.keys(params.variables).length > 0) {
|
|
42
|
+
body.variables = Object.entries(params.variables).map(([key, value]) => ({
|
|
43
|
+
key,
|
|
44
|
+
value: String(value),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await glab([
|
|
49
|
+
"api",
|
|
50
|
+
"-X",
|
|
51
|
+
"POST",
|
|
52
|
+
`projects/${projectId}/pipeline`,
|
|
53
|
+
...Object.entries(body).flatMap(([k, v]) => ["-f", `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`]),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const pipeline = result as Record<string, unknown>;
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `✅ Pipeline triggered: #${pipeline.id}\nStatus: ${pipeline.status}\n${pipeline.web_url ?? ""}`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
details: { success: true, pipeline },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { glab } from "../lib/glab.js";
|
|
8
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
9
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
10
|
+
import { OptionalProject } from "../lib/schemas.js";
|
|
11
|
+
|
|
12
|
+
interface Pipeline {
|
|
13
|
+
id: number;
|
|
14
|
+
status: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
sha: string;
|
|
17
|
+
web_url?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Job {
|
|
21
|
+
id: number;
|
|
22
|
+
name: string;
|
|
23
|
+
status: string;
|
|
24
|
+
stage: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerGitlabPipelineStatus(pi: ExtensionAPI) {
|
|
28
|
+
pi.registerTool({
|
|
29
|
+
name: "gitlab_pipeline_status",
|
|
30
|
+
label: "Pipeline Status",
|
|
31
|
+
description:
|
|
32
|
+
"Check pipeline status for a branch, commit SHA, or pipeline ID.",
|
|
33
|
+
parameters: Type.Object(
|
|
34
|
+
{
|
|
35
|
+
project: OptionalProject,
|
|
36
|
+
ref: Type.Optional(
|
|
37
|
+
Type.String({ description: "Branch name or commit SHA." }),
|
|
38
|
+
),
|
|
39
|
+
pipelineId: Type.Optional(Type.Number()),
|
|
40
|
+
includeJobs: Type.Optional(Type.Boolean({ default: false })),
|
|
41
|
+
},
|
|
42
|
+
{ additionalProperties: false },
|
|
43
|
+
),
|
|
44
|
+
async execute(
|
|
45
|
+
_toolCallId,
|
|
46
|
+
params,
|
|
47
|
+
_signal,
|
|
48
|
+
_onUpdate,
|
|
49
|
+
ctx: ExtensionContext,
|
|
50
|
+
) {
|
|
51
|
+
try {
|
|
52
|
+
requireSetup(ctx.cwd);
|
|
53
|
+
} catch {
|
|
54
|
+
return setupRequiredResult();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const projectPath = await resolveProject(params.project, ctx.cwd);
|
|
58
|
+
const projectId = await resolveProjectId(projectPath);
|
|
59
|
+
|
|
60
|
+
let pipeline: Pipeline;
|
|
61
|
+
|
|
62
|
+
if (params.pipelineId) {
|
|
63
|
+
pipeline = (await glab([
|
|
64
|
+
"api",
|
|
65
|
+
`projects/${projectId}/pipelines/${params.pipelineId}`,
|
|
66
|
+
])) as Pipeline;
|
|
67
|
+
} else if (params.ref) {
|
|
68
|
+
const pipelines = (await glab([
|
|
69
|
+
"api",
|
|
70
|
+
`projects/${projectId}/pipelines?ref=${encodeURIComponent(params.ref)}&per_page=1`,
|
|
71
|
+
])) as Pipeline[];
|
|
72
|
+
if (!pipelines.length) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: `No pipeline found for ref \`${params.ref}\`.`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
details: { success: true, found: false },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
pipeline = pipelines[0];
|
|
84
|
+
} else {
|
|
85
|
+
const pipelines = (await glab([
|
|
86
|
+
"api",
|
|
87
|
+
`projects/${projectId}/pipelines?per_page=1`,
|
|
88
|
+
])) as Pipeline[];
|
|
89
|
+
if (!pipelines.length) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "No recent pipelines found." }],
|
|
92
|
+
details: { success: true, found: false },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
pipeline = pipelines[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parts = [
|
|
99
|
+
`Pipeline **#${pipeline.id}** — ${pipeline.status}`,
|
|
100
|
+
`Ref: \`${pipeline.ref}\``,
|
|
101
|
+
`SHA: \`${pipeline.sha}\``,
|
|
102
|
+
`URL: ${pipeline.web_url ?? "-"}`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (params.includeJobs) {
|
|
106
|
+
const jobs = (await glab([
|
|
107
|
+
"api",
|
|
108
|
+
`projects/${projectId}/pipelines/${pipeline.id}/jobs?per_page=100`,
|
|
109
|
+
])) as Job[];
|
|
110
|
+
parts.push("\n| Job | Stage | Status |", "|---|---|---|");
|
|
111
|
+
for (const job of jobs) {
|
|
112
|
+
parts.push(`| ${job.name} | ${job.stage} | ${job.status} |`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
118
|
+
details: { success: true, pipeline, jobs: params.includeJobs },
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { requireSetup, setupRequiredResult } from "../lib/errors.js";
|
|
7
|
+
import { resolveProject } from "../lib/projectFallback.js";
|
|
8
|
+
import { resolveProjectId } from "../lib/resolveProjectId.js";
|
|
9
|
+
|
|
10
|
+
export function registerGitlabProjectResolve(pi: ExtensionAPI) {
|
|
11
|
+
pi.registerTool({
|
|
12
|
+
name: "gitlab_project_resolve",
|
|
13
|
+
label: "Resolve GitLab Project",
|
|
14
|
+
description:
|
|
15
|
+
"Resolve a GitLab project path (namespace/path) to a numeric project ID. Searches with pagination, exact-matches path_with_namespace, verifies the ID, then caches.",
|
|
16
|
+
parameters: Type.Object(
|
|
17
|
+
{
|
|
18
|
+
project: Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
description:
|
|
21
|
+
"Project path or numeric ID. Optional — falls back to CWD git remote, then settings default.",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
force: Type.Optional(
|
|
25
|
+
Type.Boolean({
|
|
26
|
+
default: false,
|
|
27
|
+
description: "Bypass cache and re-resolve.",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{ additionalProperties: false },
|
|
32
|
+
),
|
|
33
|
+
async execute(
|
|
34
|
+
_toolCallId,
|
|
35
|
+
params,
|
|
36
|
+
_signal,
|
|
37
|
+
_onUpdate,
|
|
38
|
+
ctx: ExtensionContext,
|
|
39
|
+
) {
|
|
40
|
+
try {
|
|
41
|
+
requireSetup(ctx.cwd);
|
|
42
|
+
} catch {
|
|
43
|
+
return setupRequiredResult();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const project = await resolveProject(params.project, ctx.cwd);
|
|
47
|
+
const id = await resolveProjectId(project, params.force);
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `Project \`${project}\` → ID **${id}**`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: { success: true, project, id },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|