feat: add gmail hooks wizard
This commit is contained in:
182
src/cli/hooks-cli.ts
Normal file
182
src/cli/hooks-cli.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { Command } from "commander";
|
||||
|
||||
import { danger } from "../globals.js";
|
||||
import {
|
||||
DEFAULT_GMAIL_LABEL,
|
||||
DEFAULT_GMAIL_MAX_BYTES,
|
||||
DEFAULT_GMAIL_RENEW_MINUTES,
|
||||
DEFAULT_GMAIL_SERVE_BIND,
|
||||
DEFAULT_GMAIL_SERVE_PATH,
|
||||
DEFAULT_GMAIL_SERVE_PORT,
|
||||
DEFAULT_GMAIL_SUBSCRIPTION,
|
||||
DEFAULT_GMAIL_TOPIC,
|
||||
} from "../hooks/gmail.js";
|
||||
import {
|
||||
type GmailRunOptions,
|
||||
type GmailSetupOptions,
|
||||
runGmailService,
|
||||
runGmailSetup,
|
||||
} from "../hooks/gmail-ops.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
|
||||
export function registerHooksCli(program: Command) {
|
||||
const hooks = program
|
||||
.command("hooks")
|
||||
.description("Webhook helpers and hook-based integrations");
|
||||
|
||||
const gmail = hooks
|
||||
.command("gmail")
|
||||
.description("Gmail Pub/Sub hooks (via gogcli)");
|
||||
|
||||
gmail
|
||||
.command("setup")
|
||||
.description("Configure Gmail watch + Pub/Sub + Clawdis hooks")
|
||||
.requiredOption("--account <email>", "Gmail account to watch")
|
||||
.option("--project <id>", "GCP project id (OAuth client owner)")
|
||||
.option("--topic <name>", "Pub/Sub topic name", DEFAULT_GMAIL_TOPIC)
|
||||
.option(
|
||||
"--subscription <name>",
|
||||
"Pub/Sub subscription name",
|
||||
DEFAULT_GMAIL_SUBSCRIPTION,
|
||||
)
|
||||
.option("--label <label>", "Gmail label to watch", DEFAULT_GMAIL_LABEL)
|
||||
.option("--hook-url <url>", "Clawdis hook URL")
|
||||
.option("--hook-token <token>", "Clawdis hook token")
|
||||
.option("--push-token <token>", "Push token for gog watch serve")
|
||||
.option(
|
||||
"--bind <host>",
|
||||
"gog watch serve bind host",
|
||||
DEFAULT_GMAIL_SERVE_BIND,
|
||||
)
|
||||
.option(
|
||||
"--port <port>",
|
||||
"gog watch serve port",
|
||||
String(DEFAULT_GMAIL_SERVE_PORT),
|
||||
)
|
||||
.option("--path <path>", "gog watch serve path", DEFAULT_GMAIL_SERVE_PATH)
|
||||
.option("--include-body", "Include email body snippets", true)
|
||||
.option(
|
||||
"--max-bytes <n>",
|
||||
"Max bytes for body snippets",
|
||||
String(DEFAULT_GMAIL_MAX_BYTES),
|
||||
)
|
||||
.option(
|
||||
"--renew-minutes <n>",
|
||||
"Renew watch every N minutes",
|
||||
String(DEFAULT_GMAIL_RENEW_MINUTES),
|
||||
)
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
"Expose push endpoint via tailscale (funnel|serve|off)",
|
||||
"funnel",
|
||||
)
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.option("--push-endpoint <url>", "Explicit Pub/Sub push endpoint")
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const parsed = parseGmailSetupOptions(opts);
|
||||
await runGmailSetup(parsed);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
gmail
|
||||
.command("run")
|
||||
.description("Run gog watch serve + auto-renew loop")
|
||||
.option("--account <email>", "Gmail account to watch")
|
||||
.option("--topic <topic>", "Pub/Sub topic path (projects/.../topics/..)")
|
||||
.option("--subscription <name>", "Pub/Sub subscription name")
|
||||
.option("--label <label>", "Gmail label to watch")
|
||||
.option("--hook-url <url>", "Clawdis hook URL")
|
||||
.option("--hook-token <token>", "Clawdis hook token")
|
||||
.option("--push-token <token>", "Push token for gog watch serve")
|
||||
.option("--bind <host>", "gog watch serve bind host")
|
||||
.option("--port <port>", "gog watch serve port")
|
||||
.option("--path <path>", "gog watch serve path")
|
||||
.option("--include-body", "Include email body snippets")
|
||||
.option("--max-bytes <n>", "Max bytes for body snippets")
|
||||
.option("--renew-minutes <n>", "Renew watch every N minutes")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
"Expose push endpoint via tailscale (funnel|serve|off)",
|
||||
)
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const parsed = parseGmailRunOptions(opts);
|
||||
await runGmailService(parsed);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseGmailSetupOptions(
|
||||
raw: Record<string, unknown>,
|
||||
): GmailSetupOptions {
|
||||
const accountRaw = raw.account;
|
||||
const account = typeof accountRaw === "string" ? accountRaw.trim() : "";
|
||||
if (!account) throw new Error("--account is required");
|
||||
return {
|
||||
account,
|
||||
project: stringOption(raw.project),
|
||||
topic: stringOption(raw.topic),
|
||||
subscription: stringOption(raw.subscription),
|
||||
label: stringOption(raw.label),
|
||||
hookUrl: stringOption(raw.hookUrl),
|
||||
hookToken: stringOption(raw.hookToken),
|
||||
pushToken: stringOption(raw.pushToken),
|
||||
bind: stringOption(raw.bind),
|
||||
port: numberOption(raw.port),
|
||||
path: stringOption(raw.path),
|
||||
includeBody: booleanOption(raw.includeBody),
|
||||
maxBytes: numberOption(raw.maxBytes),
|
||||
renewEveryMinutes: numberOption(raw.renewMinutes),
|
||||
tailscale: stringOption(raw.tailscale) as GmailSetupOptions["tailscale"],
|
||||
tailscalePath: stringOption(raw.tailscalePath),
|
||||
pushEndpoint: stringOption(raw.pushEndpoint),
|
||||
json: Boolean(raw.json),
|
||||
};
|
||||
}
|
||||
|
||||
function parseGmailRunOptions(raw: Record<string, unknown>): GmailRunOptions {
|
||||
return {
|
||||
account: stringOption(raw.account),
|
||||
topic: stringOption(raw.topic),
|
||||
subscription: stringOption(raw.subscription),
|
||||
label: stringOption(raw.label),
|
||||
hookUrl: stringOption(raw.hookUrl),
|
||||
hookToken: stringOption(raw.hookToken),
|
||||
pushToken: stringOption(raw.pushToken),
|
||||
bind: stringOption(raw.bind),
|
||||
port: numberOption(raw.port),
|
||||
path: stringOption(raw.path),
|
||||
includeBody: booleanOption(raw.includeBody),
|
||||
maxBytes: numberOption(raw.maxBytes),
|
||||
renewEveryMinutes: numberOption(raw.renewMinutes),
|
||||
tailscale: stringOption(raw.tailscale) as GmailRunOptions["tailscale"],
|
||||
tailscalePath: stringOption(raw.tailscalePath),
|
||||
};
|
||||
}
|
||||
|
||||
function stringOption(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function numberOption(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
const n = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) return undefined;
|
||||
return Math.floor(n);
|
||||
}
|
||||
|
||||
function booleanOption(value: unknown): boolean | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
return Boolean(value);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { registerCronCli } from "./cron-cli.js";
|
||||
import { createDefaultDeps } from "./deps.js";
|
||||
import { registerDnsCli } from "./dns-cli.js";
|
||||
import { registerGatewayCli } from "./gateway-cli.js";
|
||||
import { registerHooksCli } from "./hooks-cli.js";
|
||||
import { registerNodesCli } from "./nodes-cli.js";
|
||||
import { forceFreePort } from "./ports.js";
|
||||
|
||||
@@ -240,6 +241,7 @@ Examples:
|
||||
registerNodesCli(program);
|
||||
registerCronCli(program);
|
||||
registerDnsCli(program);
|
||||
registerHooksCli(program);
|
||||
|
||||
program
|
||||
.command("status")
|
||||
|
||||
Reference in New Issue
Block a user