feat: abstract GitLab/GitHub CLI usage (#10)
- Move resolveRepoPath to lib/utils.ts - Update all tools to use createProvider() from lib/providers/ - Remove direct imports from lib/gitlab.ts - Mark lib/gitlab.ts as deprecated - All tools now work with both GitHub (gh CLI) and GitLab (glab CLI) - Provider auto-detected from git remote URL
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
|
* @deprecated This module is deprecated and kept only for reference.
|
||||||
|
* Use lib/providers/index.ts with createProvider() for GitLab/GitHub abstraction.
|
||||||
|
*
|
||||||
* GitLab wrapper using glab CLI.
|
* GitLab wrapper using glab CLI.
|
||||||
* Handles label transitions, issue fetching, and MR verification.
|
* Handles label transitions, issue fetching, and MR verification.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { ToolContext } from "../types.js";
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
||||||
import { resolveRepoPath } from "../gitlab.js";
|
import { resolveRepoPath } from "../utils.js";
|
||||||
import { createProvider } from "../providers/index.js";
|
import { createProvider } from "../providers/index.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
import { DEV_TIERS, QA_TIERS } from "../tiers.js";
|
import { DEV_TIERS, QA_TIERS } from "../tiers.js";
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
import { readProjects, getProject } from "../projects.js";
|
import { readProjects, getProject } from "../projects.js";
|
||||||
import { listIssuesByLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
import { type StateLabel } from "../issue-provider.js";
|
||||||
|
import { createProvider } from "../providers/index.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
import { detectContext, generateGuardrails } from "../context-guard.js";
|
import { detectContext, generateGuardrails } from "../context-guard.js";
|
||||||
|
import { resolveRepoPath } from "../utils.js";
|
||||||
|
|
||||||
export function createQueueStatusTool(api: OpenClawPluginApi) {
|
export function createQueueStatusTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: ToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
@@ -61,7 +63,6 @@ export function createQueueStatusTool(api: OpenClawPluginApi) {
|
|||||||
? [groupId]
|
? [groupId]
|
||||||
: Object.keys(data.projects);
|
: Object.keys(data.projects);
|
||||||
|
|
||||||
const glabPath = (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined;
|
|
||||||
const projects: Array<Record<string, unknown>> = [];
|
const projects: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
for (const pid of projectIds) {
|
for (const pid of projectIds) {
|
||||||
@@ -69,15 +70,19 @@ export function createQueueStatusTool(api: OpenClawPluginApi) {
|
|||||||
if (!project) continue;
|
if (!project) continue;
|
||||||
|
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = { glabPath, repoPath };
|
const { provider } = createProvider({
|
||||||
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined,
|
||||||
|
ghPath: (api.pluginConfig as Record<string, unknown>)?.ghPath as string | undefined,
|
||||||
|
repoPath,
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch queue counts from GitLab
|
// Fetch queue counts from issue tracker
|
||||||
const queueLabels: StateLabel[] = ["To Improve", "To Test", "To Do"];
|
const queueLabels: StateLabel[] = ["To Improve", "To Test", "To Do"];
|
||||||
const queue: Record<string, Array<{ id: number; title: string }>> = {};
|
const queue: Record<string, Array<{ id: number; title: string }>> = {};
|
||||||
|
|
||||||
for (const label of queueLabels) {
|
for (const label of queueLabels) {
|
||||||
try {
|
try {
|
||||||
const issues = await listIssuesByLabel(label, glabOpts);
|
const issues = await provider.listIssuesByLabel(label);
|
||||||
queue[label] = issues.map((i) => ({ id: i.iid, title: i.title }));
|
queue[label] = issues.map((i) => ({ id: i.iid, title: i.title }));
|
||||||
} catch {
|
} catch {
|
||||||
queue[label] = [];
|
queue[label] = [];
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
import { readProjects, updateWorker, getSessionForModel } from "../projects.js";
|
import { readProjects, updateWorker, getSessionForModel } from "../projects.js";
|
||||||
import { transitionLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
import { type StateLabel } from "../issue-provider.js";
|
||||||
|
import { createProvider } from "../providers/index.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
|
import { resolveRepoPath } from "../utils.js";
|
||||||
|
|
||||||
export function createSessionHealthTool(api: OpenClawPluginApi) {
|
export function createSessionHealthTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: ToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
@@ -41,14 +43,17 @@ export function createSessionHealthTool(api: OpenClawPluginApi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await readProjects(workspaceDir);
|
const data = await readProjects(workspaceDir);
|
||||||
const glabPath = (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined;
|
|
||||||
|
|
||||||
const issues: Array<Record<string, unknown>> = [];
|
const issues: Array<Record<string, unknown>> = [];
|
||||||
let fixesApplied = 0;
|
let fixesApplied = 0;
|
||||||
|
|
||||||
for (const [groupId, project] of Object.entries(data.projects)) {
|
for (const [groupId, project] of Object.entries(data.projects)) {
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = { glabPath, repoPath };
|
const { provider } = createProvider({
|
||||||
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined,
|
||||||
|
ghPath: (api.pluginConfig as Record<string, unknown>)?.ghPath as string | undefined,
|
||||||
|
repoPath,
|
||||||
|
});
|
||||||
|
|
||||||
for (const role of ["dev", "qa"] as const) {
|
for (const role of ["dev", "qa"] as const) {
|
||||||
const worker = project[role];
|
const worker = project[role];
|
||||||
@@ -98,13 +103,13 @@ export function createSessionHealthTool(api: OpenClawPluginApi) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (autoFix) {
|
if (autoFix) {
|
||||||
// Revert GitLab label
|
// Revert issue label
|
||||||
const revertLabel: StateLabel = role === "dev" ? "To Do" : "To Test";
|
const revertLabel: StateLabel = role === "dev" ? "To Do" : "To Test";
|
||||||
const currentLabel: StateLabel = role === "dev" ? "Doing" : "Testing";
|
const currentLabel: StateLabel = role === "dev" ? "Doing" : "Testing";
|
||||||
try {
|
try {
|
||||||
if (worker.issueId) {
|
if (worker.issueId) {
|
||||||
const primaryIssueId = Number(worker.issueId.split(",")[0]);
|
const primaryIssueId = Number(worker.issueId.split(",")[0]);
|
||||||
await transitionLabel(primaryIssueId, currentLabel, revertLabel, glabOpts);
|
await provider.transitionLabel(primaryIssueId, currentLabel, revertLabel);
|
||||||
issue.labelReverted = `${currentLabel} → ${revertLabel}`;
|
issue.labelReverted = `${currentLabel} → ${revertLabel}`;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -14,14 +14,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
import { dispatchTask } from "../dispatch.js";
|
import { dispatchTask } from "../dispatch.js";
|
||||||
import {
|
import { type StateLabel } from "../issue-provider.js";
|
||||||
closeIssue,
|
import { createProvider } from "../providers/index.js";
|
||||||
getIssue,
|
|
||||||
reopenIssue,
|
|
||||||
resolveRepoPath,
|
|
||||||
transitionLabel,
|
|
||||||
type StateLabel,
|
|
||||||
} from "../gitlab.js";
|
|
||||||
import {
|
import {
|
||||||
deactivateWorker,
|
deactivateWorker,
|
||||||
getProject,
|
getProject,
|
||||||
@@ -30,6 +24,7 @@ import {
|
|||||||
readProjects,
|
readProjects,
|
||||||
} from "../projects.js";
|
} from "../projects.js";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
|
import { resolveRepoPath } from "../utils.js";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -111,12 +106,15 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = {
|
const { provider } = createProvider({
|
||||||
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
||||||
| string
|
| string
|
||||||
| undefined,
|
| undefined,
|
||||||
|
ghPath: (api.pluginConfig as Record<string, unknown>)?.ghPath as
|
||||||
|
| string
|
||||||
|
| undefined,
|
||||||
repoPath,
|
repoPath,
|
||||||
};
|
});
|
||||||
|
|
||||||
const output: Record<string, unknown> = {
|
const output: Record<string, unknown> = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -140,7 +138,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deactivateWorker(workspaceDir, groupId, "dev");
|
await deactivateWorker(workspaceDir, groupId, "dev");
|
||||||
await transitionLabel(issueId, "Doing", "To Test", glabOpts);
|
await provider.transitionLabel(issueId, "Doing", "To Test");
|
||||||
|
|
||||||
output.labelTransition = "Doing → To Test";
|
output.labelTransition = "Doing → To Test";
|
||||||
output.announcement = `✅ DEV done #${issueId}${summary ? ` — ${summary}` : ""}. Moved to QA queue.`;
|
output.announcement = `✅ DEV done #${issueId}${summary ? ` — ${summary}` : ""}. Moved to QA queue.`;
|
||||||
@@ -150,7 +148,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
const pluginConfig = api.pluginConfig as
|
const pluginConfig = api.pluginConfig as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined;
|
| undefined;
|
||||||
const issue = await getIssue(issueId, glabOpts);
|
const issue = await provider.getIssue(issueId);
|
||||||
const chainResult = await dispatchTask({
|
const chainResult = await dispatchTask({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
agentId: ctx.agentId,
|
agentId: ctx.agentId,
|
||||||
@@ -165,11 +163,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
fromLabel: "To Test",
|
fromLabel: "To Test",
|
||||||
toLabel: "Testing",
|
toLabel: "Testing",
|
||||||
transitionLabel: (id, from, to) =>
|
transitionLabel: (id, from, to) =>
|
||||||
transitionLabel(
|
provider.transitionLabel(
|
||||||
id,
|
id,
|
||||||
from as StateLabel,
|
from as StateLabel,
|
||||||
to as StateLabel,
|
to as StateLabel,
|
||||||
glabOpts,
|
|
||||||
),
|
),
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
});
|
});
|
||||||
@@ -194,8 +191,8 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
// === QA PASS ===
|
// === QA PASS ===
|
||||||
if (role === "qa" && result === "pass") {
|
if (role === "qa" && result === "pass") {
|
||||||
await deactivateWorker(workspaceDir, groupId, "qa");
|
await deactivateWorker(workspaceDir, groupId, "qa");
|
||||||
await transitionLabel(issueId, "Testing", "Done", glabOpts);
|
await provider.transitionLabel(issueId, "Testing", "Done");
|
||||||
await closeIssue(issueId, glabOpts);
|
await provider.closeIssue(issueId);
|
||||||
|
|
||||||
output.labelTransition = "Testing → Done";
|
output.labelTransition = "Testing → Done";
|
||||||
output.issueClosed = true;
|
output.issueClosed = true;
|
||||||
@@ -205,8 +202,8 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
// === QA FAIL ===
|
// === QA FAIL ===
|
||||||
if (role === "qa" && result === "fail") {
|
if (role === "qa" && result === "fail") {
|
||||||
await deactivateWorker(workspaceDir, groupId, "qa");
|
await deactivateWorker(workspaceDir, groupId, "qa");
|
||||||
await transitionLabel(issueId, "Testing", "To Improve", glabOpts);
|
await provider.transitionLabel(issueId, "Testing", "To Improve");
|
||||||
await reopenIssue(issueId, glabOpts);
|
await provider.reopenIssue(issueId);
|
||||||
|
|
||||||
const devWorker = getWorker(project, "dev");
|
const devWorker = getWorker(project, "dev");
|
||||||
const devModel = devWorker.model;
|
const devModel = devWorker.model;
|
||||||
@@ -225,7 +222,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
const pluginConfig = api.pluginConfig as
|
const pluginConfig = api.pluginConfig as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined;
|
| undefined;
|
||||||
const issue = await getIssue(issueId, glabOpts);
|
const issue = await provider.getIssue(issueId);
|
||||||
const chainResult = await dispatchTask({
|
const chainResult = await dispatchTask({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
agentId: ctx.agentId,
|
agentId: ctx.agentId,
|
||||||
@@ -240,11 +237,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
fromLabel: "To Improve",
|
fromLabel: "To Improve",
|
||||||
toLabel: "Doing",
|
toLabel: "Doing",
|
||||||
transitionLabel: (id, from, to) =>
|
transitionLabel: (id, from, to) =>
|
||||||
transitionLabel(
|
provider.transitionLabel(
|
||||||
id,
|
id,
|
||||||
from as StateLabel,
|
from as StateLabel,
|
||||||
to as StateLabel,
|
to as StateLabel,
|
||||||
glabOpts,
|
|
||||||
),
|
),
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
});
|
});
|
||||||
@@ -269,7 +265,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
// === QA REFINE ===
|
// === QA REFINE ===
|
||||||
if (role === "qa" && result === "refine") {
|
if (role === "qa" && result === "refine") {
|
||||||
await deactivateWorker(workspaceDir, groupId, "qa");
|
await deactivateWorker(workspaceDir, groupId, "qa");
|
||||||
await transitionLabel(issueId, "Testing", "Refining", glabOpts);
|
await provider.transitionLabel(issueId, "Testing", "Refining");
|
||||||
|
|
||||||
output.labelTransition = "Testing → Refining";
|
output.labelTransition = "Testing → Refining";
|
||||||
output.announcement = `🤔 QA REFINE #${issueId}${summary ? ` — ${summary}` : ""}. Awaiting human decision.`;
|
output.announcement = `🤔 QA REFINE #${issueId}${summary ? ` — ${summary}` : ""}. Awaiting human decision.`;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
import { readProjects } from "../projects.js";
|
import { readProjects } from "../projects.js";
|
||||||
import { resolveRepoPath } from "../gitlab.js";
|
import { resolveRepoPath } from "../utils.js";
|
||||||
import { createProvider } from "../providers/index.js";
|
import { createProvider } from "../providers/index.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
import type { StateLabel } from "../issue-provider.js";
|
import type { StateLabel } from "../issue-provider.js";
|
||||||
|
|||||||
@@ -11,17 +11,13 @@
|
|||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import { dispatchTask } from "../dispatch.js";
|
import { dispatchTask } from "../dispatch.js";
|
||||||
import {
|
import { type StateLabel } from "../issue-provider.js";
|
||||||
getCurrentStateLabel,
|
import { createProvider } from "../providers/index.js";
|
||||||
getIssue,
|
|
||||||
resolveRepoPath,
|
|
||||||
transitionLabel,
|
|
||||||
type StateLabel,
|
|
||||||
} from "../gitlab.js";
|
|
||||||
import { selectModel } from "../model-selector.js";
|
import { selectModel } from "../model-selector.js";
|
||||||
import { getProject, getWorker, readProjects } from "../projects.js";
|
import { getProject, getWorker, readProjects } from "../projects.js";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
import { detectContext, generateGuardrails } from "../context-guard.js";
|
import { detectContext, generateGuardrails } from "../context-guard.js";
|
||||||
|
import { resolveRepoPath } from "../utils.js";
|
||||||
|
|
||||||
export function createTaskPickupTool(api: OpenClawPluginApi) {
|
export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: ToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
@@ -101,15 +97,18 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
// 3. Fetch issue and verify state
|
// 3. Fetch issue and verify state
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = {
|
const { provider } = createProvider({
|
||||||
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
||||||
| string
|
| string
|
||||||
| undefined,
|
| undefined,
|
||||||
|
ghPath: (api.pluginConfig as Record<string, unknown>)?.ghPath as
|
||||||
|
| string
|
||||||
|
| undefined,
|
||||||
repoPath,
|
repoPath,
|
||||||
};
|
});
|
||||||
|
|
||||||
const issue = await getIssue(issueId, glabOpts);
|
const issue = await provider.getIssue(issueId);
|
||||||
const currentLabel = getCurrentStateLabel(issue);
|
const currentLabel = provider.getCurrentStateLabel(issue);
|
||||||
|
|
||||||
const validLabelsForDev: StateLabel[] = ["To Do", "To Improve"];
|
const validLabelsForDev: StateLabel[] = ["To Do", "To Improve"];
|
||||||
const validLabelsForQa: StateLabel[] = ["To Test"];
|
const validLabelsForQa: StateLabel[] = ["To Test"];
|
||||||
@@ -160,7 +159,7 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
|||||||
fromLabel: currentLabel,
|
fromLabel: currentLabel,
|
||||||
toLabel: targetLabel,
|
toLabel: targetLabel,
|
||||||
transitionLabel: (id, from, to) =>
|
transitionLabel: (id, from, to) =>
|
||||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
provider.transitionLabel(id, from as StateLabel, to as StateLabel),
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
lib/utils.ts
Normal file
13
lib/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Shared utilities for DevClaw.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the repo path from projects.json repo field (handles ~/).
|
||||||
|
*/
|
||||||
|
export function resolveRepoPath(repoField: string): string {
|
||||||
|
if (repoField.startsWith("~/")) {
|
||||||
|
return repoField.replace("~", process.env.HOME ?? "/home/lauren");
|
||||||
|
}
|
||||||
|
return repoField;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user