chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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] ?? "" };
|
||||
|
||||
Reference in New Issue
Block a user