chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

@@ -84,8 +84,7 @@ export type GmailRunOptions = {
tailscaleTarget?: string;
};
const DEFAULT_GMAIL_TOPIC_IAM_MEMBER =
"serviceAccount:gmail-api-push@system.gserviceaccount.com";
const DEFAULT_GMAIL_TOPIC_IAM_MEMBER = "serviceAccount:gmail-api-push@system.gserviceaccount.com";
export async function runGmailSetup(opts: GmailSetupOptions) {
await ensureDependency("gcloud", ["--cask", "gcloud-cli"]);
@@ -103,20 +102,15 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
const baseConfig = configSnapshot.config;
const hooksPath = normalizeHooksPath(baseConfig.hooks?.path);
const hookToken =
opts.hookToken ?? baseConfig.hooks?.token ?? generateHookToken();
const pushToken =
opts.pushToken ?? baseConfig.hooks?.gmail?.pushToken ?? generateHookToken();
const hookToken = opts.hookToken ?? baseConfig.hooks?.token ?? generateHookToken();
const pushToken = opts.pushToken ?? baseConfig.hooks?.gmail?.pushToken ?? generateHookToken();
const topicInput =
opts.topic ?? baseConfig.hooks?.gmail?.topic ?? DEFAULT_GMAIL_TOPIC;
const topicInput = opts.topic ?? baseConfig.hooks?.gmail?.topic ?? DEFAULT_GMAIL_TOPIC;
const parsedTopic = parseTopicPath(topicInput);
const topicName = parsedTopic?.topicName ?? topicInput;
const projectId =
opts.project ??
parsedTopic?.projectId ??
(await resolveProjectIdFromGogCredentials());
opts.project ?? parsedTopic?.projectId ?? (await resolveProjectIdFromGogCredentials());
// Gmail watch requires the Pub/Sub topic to live in the OAuth client project.
if (!projectId) {
throw new Error(
@@ -139,28 +133,23 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
const configuredTailscaleTarget =
opts.tailscaleTarget ?? baseConfig.hooks?.gmail?.tailscale?.target;
const normalizedServePath =
typeof configuredServePath === "string" &&
configuredServePath.trim().length > 0
typeof configuredServePath === "string" && configuredServePath.trim().length > 0
? normalizeServePath(configuredServePath)
: DEFAULT_GMAIL_SERVE_PATH;
const normalizedTailscaleTarget =
typeof configuredTailscaleTarget === "string" &&
configuredTailscaleTarget.trim().length > 0
typeof configuredTailscaleTarget === "string" && configuredTailscaleTarget.trim().length > 0
? configuredTailscaleTarget.trim()
: undefined;
const includeBody = opts.includeBody ?? true;
const maxBytes = opts.maxBytes ?? DEFAULT_GMAIL_MAX_BYTES;
const renewEveryMinutes =
opts.renewEveryMinutes ?? DEFAULT_GMAIL_RENEW_MINUTES;
const renewEveryMinutes = opts.renewEveryMinutes ?? DEFAULT_GMAIL_RENEW_MINUTES;
const tailscaleMode = opts.tailscale ?? "funnel";
// Tailscale strips the path before proxying; keep a public path while gog
// listens on "/" whenever Tailscale is enabled.
const servePath = normalizeServePath(
tailscaleMode !== "off" && !normalizedTailscaleTarget
? "/"
: normalizedServePath,
tailscaleMode !== "off" && !normalizedTailscaleTarget ? "/" : normalizedServePath,
);
const tailscalePath = normalizeServePath(
opts.tailscalePath ??
@@ -256,9 +245,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
const validated = validateConfigObject(nextConfig);
if (!validated.ok) {
throw new Error(
`Config validation failed: ${validated.issues[0]?.message ?? "invalid"}`,
);
throw new Error(`Config validation failed: ${validated.issues[0]?.message ?? "invalid"}`);
}
await writeConfigFile(validated.config);

View File

@@ -33,9 +33,7 @@ describe("resolvePythonExecutablePath", () => {
process.env.PATH = `${shimDir}${path.delimiter}/usr/bin`;
const { resolvePythonExecutablePath } = await import(
"./gmail-setup-utils.js"
);
const { resolvePythonExecutablePath } = await import("./gmail-setup-utils.js");
const resolved = await resolvePythonExecutablePath();
expect(resolved).toBe(realPython);

View File

@@ -40,16 +40,9 @@ function formatCommandResult(command: string, result: SpawnResult): string {
return lines.join("\n");
}
function formatJsonParseFailure(
command: string,
result: SpawnResult,
err: unknown,
): string {
function formatJsonParseFailure(command: string, result: SpawnResult, err: unknown): string {
const reason = err instanceof Error ? err.message : String(err);
return `${command} returned invalid JSON: ${reason}\n${formatCommandResult(
command,
result,
)}`;
return `${command} returned invalid JSON: ${reason}\n${formatCommandResult(command, result)}`;
}
function formatCommand(command: string, args: string[]): string {
@@ -81,8 +74,7 @@ function ensurePathIncludes(dirPath: string, position: "append" | "prepend") {
const pathEnv = process.env.PATH ?? "";
const parts = pathEnv.split(path.delimiter).filter(Boolean);
if (parts.includes(dirPath)) return;
const next =
position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath];
const next = position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath];
process.env.PATH = next.join(path.delimiter);
}
@@ -106,20 +98,14 @@ function ensureGcloudOnPath(): boolean {
return false;
}
export async function resolvePythonExecutablePath(): Promise<
string | undefined
> {
export async function resolvePythonExecutablePath(): Promise<string | undefined> {
if (cachedPythonPath !== undefined) {
return cachedPythonPath ?? undefined;
}
const candidates = findExecutablesOnPath(["python3", "python"]);
for (const candidate of candidates) {
const res = await runCommandWithTimeout(
[
candidate,
"-c",
"import os, sys; print(os.path.realpath(sys.executable))",
],
[candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))"],
{ timeoutMs: 2_000 },
);
if (res.code !== 0) continue;
@@ -169,9 +155,7 @@ export async function ensureDependency(bin: string, brewArgs: string[]) {
env: brewEnv,
});
if (result.code !== 0) {
throw new Error(
`brew install failed for ${bin}: ${result.stderr || result.stdout}`,
);
throw new Error(`brew install failed for ${bin}: ${result.stderr || result.stdout}`);
}
if (!hasBinary(bin)) {
throw new Error(`${bin} still not available after brew install`);
@@ -204,14 +188,7 @@ export async function ensureTopic(projectId: string, topicName: string) {
30_000,
);
if (describe.code === 0) return;
await runGcloud([
"pubsub",
"topics",
"create",
topicName,
"--project",
projectId,
]);
await runGcloud(["pubsub", "topics", "create", topicName, "--project", projectId]);
}
export async function ensureSubscription(
@@ -221,14 +198,7 @@ export async function ensureSubscription(
pushEndpoint: string,
) {
const describe = await runGcloudCommand(
[
"pubsub",
"subscriptions",
"describe",
subscription,
"--project",
projectId,
],
["pubsub", "subscriptions", "describe", subscription, "--project", projectId],
30_000,
);
if (describe.code === 0) {
@@ -296,21 +266,11 @@ export async function ensureTailscaleEndpoint(params: {
throw new Error("tailscale target missing; set a port or target URL");
}
const pathArg = normalizeServePath(params.path);
const funnelArgs = [
params.mode,
"--bg",
"--set-path",
pathArg,
"--yes",
target,
];
const funnelArgs = [params.mode, "--bg", "--set-path", pathArg, "--yes", target];
const funnelCommand = formatCommand("tailscale", funnelArgs);
const funnelResult = await runCommandWithTimeout(
["tailscale", ...funnelArgs],
{
timeoutMs: 30_000,
},
);
const funnelResult = await runCommandWithTimeout(["tailscale", ...funnelArgs], {
timeoutMs: 30_000,
});
if (funnelResult.code !== 0) {
throw new Error(formatCommandFailure(funnelCommand, funnelResult));
}
@@ -320,9 +280,7 @@ export async function ensureTailscaleEndpoint(params: {
return params.token ? `${baseUrl}?token=${params.token}` : baseUrl;
}
export async function resolveProjectIdFromGogCredentials(): Promise<
string | null
> {
export async function resolveProjectIdFromGogCredentials(): Promise<string | null> {
const candidates = gogCredentialsPaths();
for (const candidate of candidates) {
if (!fs.existsSync(candidate)) continue;
@@ -361,9 +319,7 @@ function gogCredentialsPaths(): string[] {
}
paths.push(resolveUserPath("~/.config/gogcli/credentials.json"));
if (process.platform === "darwin") {
paths.push(
resolveUserPath("~/Library/Application Support/gogcli/credentials.json"),
);
paths.push(resolveUserPath("~/Library/Application Support/gogcli/credentials.json"));
}
return paths;
}
@@ -371,8 +327,7 @@ function gogCredentialsPaths(): string[] {
function extractGogClientId(parsed: Record<string, unknown>): string | null {
const installed = parsed.installed as Record<string, unknown> | undefined;
const web = parsed.web as Record<string, unknown> | undefined;
const candidate =
installed?.client_id || web?.client_id || parsed.client_id || "";
const candidate = installed?.client_id || web?.client_id || parsed.client_id || "";
return typeof candidate === "string" ? candidate : null;
}

View File

@@ -3,14 +3,10 @@ import { isAddressInUseError } from "./gmail-watcher.js";
describe("gmail watcher", () => {
it("detects address already in use errors", () => {
expect(
isAddressInUseError(
"listen tcp 127.0.0.1:8788: bind: address already in use",
),
).toBe(true);
expect(isAddressInUseError("EADDRINUSE: address already in use")).toBe(
expect(isAddressInUseError("listen tcp 127.0.0.1:8788: bind: address already in use")).toBe(
true,
);
expect(isAddressInUseError("EADDRINUSE: address already in use")).toBe(true);
expect(isAddressInUseError("some other error")).toBe(false);
});
});

View File

@@ -48,8 +48,7 @@ async function startGmailWatch(
try {
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
if (result.code !== 0) {
const message =
result.stderr || result.stdout || "gog watch start failed";
const message = result.stderr || result.stdout || "gog watch start failed";
log.error(`watch start failed: ${message}`);
return false;
}
@@ -122,9 +121,7 @@ export type GmailWatcherStartResult = {
* Start the Gmail watcher service.
* Called automatically by the gateway if hooks.gmail is configured.
*/
export async function startGmailWatcher(
cfg: ClawdbotConfig,
): Promise<GmailWatcherStartResult> {
export async function startGmailWatcher(cfg: ClawdbotConfig): Promise<GmailWatcherStartResult> {
// Check if gmail hooks are configured
if (!cfg.hooks?.enabled) {
return { started: false, reason: "hooks not enabled" };

View File

@@ -41,9 +41,7 @@ describe("gmail hook config", () => {
expect(result.value.label).toBe("INBOX");
expect(result.value.includeBody).toBe(true);
expect(result.value.serve.port).toBe(8788);
expect(result.value.hookUrl).toBe(
`http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`,
);
expect(result.value.hookUrl).toBe(`http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`);
}
});
@@ -154,9 +152,7 @@ describe("gmail hook config", () => {
if (result.ok) {
expect(result.value.serve.path).toBe("/custom");
expect(result.value.tailscale.path).toBe("/custom");
expect(result.value.tailscale.target).toBe(
"http://127.0.0.1:8788/custom",
);
expect(result.value.tailscale.target).toBe("http://127.0.0.1:8788/custom");
}
});
});

View File

@@ -63,13 +63,8 @@ export function generateHookToken(bytes = 24): string {
return randomBytes(bytes).toString("hex");
}
export function mergeHookPresets(
existing: string[] | undefined,
preset: string,
): string[] {
const next = new Set(
(existing ?? []).map((item) => item.trim()).filter(Boolean),
);
export function mergeHookPresets(existing: string[] | undefined, preset: string): string[] {
const next = new Set((existing ?? []).map((item) => item.trim()).filter(Boolean));
next.add(preset);
return Array.from(next);
}
@@ -120,8 +115,7 @@ export function resolveGmailHookRuntimeConfig(
return { ok: false, error: "gmail topic required" };
}
const subscription =
overrides.subscription ?? gmail?.subscription ?? DEFAULT_GMAIL_SUBSCRIPTION;
const subscription = overrides.subscription ?? gmail?.subscription ?? DEFAULT_GMAIL_SUBSCRIPTION;
const pushToken = overrides.pushToken ?? gmail?.pushToken ?? "";
if (!pushToken) {
@@ -137,14 +131,11 @@ export function resolveGmailHookRuntimeConfig(
const maxBytesRaw = overrides.maxBytes ?? gmail?.maxBytes;
const maxBytes =
typeof maxBytesRaw === "number" &&
Number.isFinite(maxBytesRaw) &&
maxBytesRaw > 0
typeof maxBytesRaw === "number" && Number.isFinite(maxBytesRaw) && maxBytesRaw > 0
? Math.floor(maxBytesRaw)
: DEFAULT_GMAIL_MAX_BYTES;
const renewEveryMinutesRaw =
overrides.renewEveryMinutes ?? gmail?.renewEveryMinutes;
const renewEveryMinutesRaw = overrides.renewEveryMinutes ?? gmail?.renewEveryMinutes;
const renewEveryMinutes =
typeof renewEveryMinutesRaw === "number" &&
Number.isFinite(renewEveryMinutesRaw) &&
@@ -152,13 +143,10 @@ export function resolveGmailHookRuntimeConfig(
? Math.floor(renewEveryMinutesRaw)
: DEFAULT_GMAIL_RENEW_MINUTES;
const serveBind =
overrides.serveBind ?? gmail?.serve?.bind ?? DEFAULT_GMAIL_SERVE_BIND;
const serveBind = overrides.serveBind ?? gmail?.serve?.bind ?? DEFAULT_GMAIL_SERVE_BIND;
const servePortRaw = overrides.servePort ?? gmail?.serve?.port;
const servePort =
typeof servePortRaw === "number" &&
Number.isFinite(servePortRaw) &&
servePortRaw > 0
typeof servePortRaw === "number" && Number.isFinite(servePortRaw) && servePortRaw > 0
? Math.floor(servePortRaw)
: DEFAULT_GMAIL_SERVE_PORT;
const servePathRaw = overrides.servePath ?? gmail?.serve?.path;
@@ -166,11 +154,9 @@ export function resolveGmailHookRuntimeConfig(
typeof servePathRaw === "string" && servePathRaw.trim().length > 0
? normalizeServePath(servePathRaw)
: DEFAULT_GMAIL_SERVE_PATH;
const tailscaleTargetRaw =
overrides.tailscaleTarget ?? gmail?.tailscale?.target;
const tailscaleTargetRaw = overrides.tailscaleTarget ?? gmail?.tailscale?.target;
const tailscaleMode =
overrides.tailscaleMode ?? gmail?.tailscale?.mode ?? "off";
const tailscaleMode = overrides.tailscaleMode ?? gmail?.tailscale?.mode ?? "off";
const tailscaleTarget =
tailscaleMode !== "off" &&
typeof tailscaleTargetRaw === "string" &&
@@ -265,9 +251,7 @@ export function buildTopicPath(projectId: string, topicName: string): string {
return `projects/${projectId}/topics/${topicName}`;
}
export function parseTopicPath(
topic: string,
): { projectId: string; topicName: string } | null {
export function parseTopicPath(topic: string): { projectId: string; topicName: string } | null {
const match = topic.trim().match(/^projects\/([^/]+)\/topics\/([^/]+)$/i);
if (!match) return null;
return { projectId: match[1] ?? "", topicName: match[2] ?? "" };