refactor: remove deprecated GitLab wrapper module and related code

This commit is contained in:
Lauren ten Hoor
2026-02-09 22:43:01 +08:00
parent 4e10b171c1
commit 9bc0513198
6 changed files with 23 additions and 202 deletions

View File

@@ -1,183 +0,0 @@
/**
* @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.
* Handles label transitions, issue fetching, and MR verification.
*/
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
// State labels — each issue has exactly ONE at a time
const STATE_LABELS = [
"Planning",
"To Do",
"Doing",
"To Test",
"Testing",
"Done",
"To Improve",
"Refining",
] as const;
export type StateLabel = (typeof STATE_LABELS)[number];
type GlabOptions = {
repoPath: string;
};
async function glab(
args: string[],
opts: GlabOptions,
): Promise<string> {
const { stdout } = await execFileAsync("glab", args, {
cwd: opts.repoPath,
timeout: 30_000,
});
return stdout.trim();
}
export type GitLabIssue = {
iid: number;
title: string;
description: string;
labels: string[];
state: string;
web_url: string;
};
/**
* Fetch a single issue by ID.
*/
export async function getIssue(
issueId: number,
opts: GlabOptions,
): Promise<GitLabIssue> {
const raw = await glab(
["issue", "view", String(issueId), "--output", "json"],
opts,
);
return JSON.parse(raw) as GitLabIssue;
}
/**
* List issues with a specific label.
*/
export async function listIssuesByLabel(
label: StateLabel,
opts: GlabOptions,
): Promise<GitLabIssue[]> {
try {
const raw = await glab(
["issue", "list", "--label", label, "--output", "json"],
opts,
);
return JSON.parse(raw) as GitLabIssue[];
} catch {
// glab returns error when no issues found
return [];
}
}
/**
* Transition an issue from one state label to another.
* Uses --unlabel + --label to ensure only one state label at a time.
*/
export async function transitionLabel(
issueId: number,
from: StateLabel,
to: StateLabel,
opts: GlabOptions,
): Promise<void> {
await glab(
[
"issue",
"update",
String(issueId),
"--unlabel",
from,
"--label",
to,
],
opts,
);
}
/**
* Close an issue.
*/
export async function closeIssue(
issueId: number,
opts: GlabOptions,
): Promise<void> {
await glab(["issue", "close", String(issueId)], opts);
}
/**
* Reopen an issue.
*/
export async function reopenIssue(
issueId: number,
opts: GlabOptions,
): Promise<void> {
await glab(["issue", "reopen", String(issueId)], opts);
}
/**
* Check if the current state label on an issue matches expected.
*/
export function hasStateLabel(
issue: GitLabIssue,
expected: StateLabel,
): boolean {
return issue.labels.includes(expected);
}
/**
* Get the current state label of an issue (first match from STATE_LABELS).
*/
export function getCurrentStateLabel(
issue: GitLabIssue,
): StateLabel | null {
for (const label of STATE_LABELS) {
if (issue.labels.includes(label)) {
return label;
}
}
return null;
}
/**
* Check if any merged MR exists for a specific issue.
*/
export async function hasMergedMR(
issueId: number,
opts: GlabOptions,
): Promise<boolean> {
try {
const raw = await glab(
["mr", "list", "--output", "json", "--state", "merged"],
opts,
);
const mrs = JSON.parse(raw) as Array<{ title: string; description: string }>;
const pattern = `#${issueId}`;
return mrs.some(
(mr) =>
mr.title.includes(pattern) || (mr.description ?? "").includes(pattern),
);
} catch {
return false;
}
}
/**
* 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;
}

View File

@@ -4,6 +4,7 @@
*/ */
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { homedir } from "node:os";
import { TIER_MIGRATION } from "./tiers.js"; import { TIER_MIGRATION } from "./tiers.js";
export type WorkerState = { export type WorkerState = {
@@ -229,3 +230,14 @@ export async function deactivateWorker(
issueId: null, issueId: null,
}); });
} }
/**
* Resolve repo path from projects.json repo field (handles ~/ expansion).
* Uses os.homedir() for cross-platform home directory resolution.
*/
export function resolveRepoPath(repoField: string): string {
if (repoField.startsWith("~/")) {
return repoField.replace("~", homedir());
}
return repoField;
}

View File

@@ -11,7 +11,7 @@ import { execFileSync } from "node:child_process";
import type { TaskManager } from "./task-manager.js"; import type { TaskManager } from "./task-manager.js";
import { GitLabProvider } from "./gitlab.js"; import { GitLabProvider } from "./gitlab.js";
import { GitHubProvider } from "./github.js"; import { GitHubProvider } from "./github.js";
import { resolveRepoPath } from "../utils.js"; import { resolveRepoPath } from "../projects.js";
export type ProviderOptions = { export type ProviderOptions = {
provider?: "gitlab" | "github"; provider?: "gitlab" | "github";
@@ -24,7 +24,9 @@ function detectProvider(repoPath: string): "gitlab" | "github" {
const url = execFileSync("git", ["remote", "get-url", "origin"], { const url = execFileSync("git", ["remote", "get-url", "origin"], {
cwd: repoPath, cwd: repoPath,
timeout: 5_000, timeout: 5_000,
}).toString().trim(); })
.toString()
.trim();
if (url.includes("github.com")) return "github"; if (url.includes("github.com")) return "github";
return "gitlab"; return "gitlab";
@@ -39,9 +41,12 @@ export type ProviderWithType = {
}; };
export function createProvider(opts: ProviderOptions): ProviderWithType { export function createProvider(opts: ProviderOptions): ProviderWithType {
const repoPath = opts.repoPath ?? (opts.repo ? resolveRepoPath(opts.repo) : null); const repoPath =
opts.repoPath ?? (opts.repo ? resolveRepoPath(opts.repo) : null);
if (!repoPath) { if (!repoPath) {
throw new Error("Either repoPath or repo must be provided to createProvider"); throw new Error(
"Either repoPath or repo must be provided to createProvider",
);
} }
const type = opts.provider ?? detectProvider(repoPath); const type = opts.provider ?? detectProvider(repoPath);

View File

@@ -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 "../utils.js"; import { resolveRepoPath } from "../projects.js";
import { createProvider } from "../task-managers/index.js"; import { createProvider } from "../task-managers/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";

View File

@@ -16,7 +16,7 @@ import { log as auditLog } from "../audit.js";
import { dispatchTask } from "../dispatch.js"; import { dispatchTask } from "../dispatch.js";
import { type StateLabel } from "../task-managers/task-manager.js"; import { type StateLabel } from "../task-managers/task-manager.js";
import { createProvider } from "../task-managers/index.js"; import { createProvider } from "../task-managers/index.js";
import { resolveRepoPath } from "../utils.js"; import { resolveRepoPath } from "../projects.js";
import { import {
deactivateWorker, deactivateWorker,
getProject, getProject,

View File

@@ -1,13 +0,0 @@
/**
* 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;
}