chore: drop twilio and go web-only

This commit is contained in:
Peter Steinberger
2025-12-05 19:03:59 +00:00
parent 869cc3d497
commit 7c7314f673
50 changed files with 335 additions and 5019 deletions

View File

@@ -1,110 +1,13 @@
import { autoReplyIfConfigured } from "../auto-reply/reply.js";
import { readEnv } from "../env.js";
import { info } from "../globals.js";
import { ensureBinary } from "../infra/binaries.js";
import { ensurePortAvailable, handlePortError } from "../infra/ports.js";
import { ensureFunnel, getTailnetHostname } from "../infra/tailscale.js";
import { ensureMediaHosted } from "../media/host.js";
import {
logWebSelfId,
monitorWebProvider,
sendMessageWeb,
} from "../providers/web/index.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { createClient } from "../twilio/client.js";
import { listRecentMessages } from "../twilio/messages.js";
import { monitorTwilio as monitorTwilioImpl } from "../twilio/monitor.js";
import { sendMessage, waitForFinalStatus } from "../twilio/send.js";
import { findWhatsappSenderSid } from "../twilio/senders.js";
import { assertProvider, sleep } from "../utils.js";
import { startWebhook } from "../webhook/server.js";
import { updateWebhook } from "../webhook/update.js";
import { waitForever } from "./wait.js";
import { logWebSelfId, sendMessageWeb } from "../providers/web/index.js";
export type CliDeps = {
sendMessage: typeof sendMessage;
sendMessageWeb: typeof sendMessageWeb;
waitForFinalStatus: typeof waitForFinalStatus;
assertProvider: typeof assertProvider;
createClient?: typeof createClient;
monitorTwilio: typeof monitorTwilio;
listRecentMessages: typeof listRecentMessages;
ensurePortAvailable: typeof ensurePortAvailable;
startWebhook: typeof startWebhook;
waitForever: typeof waitForever;
ensureBinary: typeof ensureBinary;
ensureFunnel: typeof ensureFunnel;
getTailnetHostname: typeof getTailnetHostname;
readEnv: typeof readEnv;
findWhatsappSenderSid: typeof findWhatsappSenderSid;
updateWebhook: typeof updateWebhook;
handlePortError: typeof handlePortError;
monitorWebProvider: typeof monitorWebProvider;
resolveTwilioMediaUrl: (
source: string,
opts: { serveMedia: boolean; runtime: RuntimeEnv },
) => Promise<string>;
};
export async function monitorTwilio(
intervalSeconds: number,
lookbackMinutes: number,
clientOverride?: ReturnType<typeof createClient>,
maxIterations = Infinity,
) {
// Adapter that wires default deps/runtime for the Twilio monitor loop.
return monitorTwilioImpl(intervalSeconds, lookbackMinutes, {
client: clientOverride,
maxIterations,
deps: {
autoReplyIfConfigured,
listRecentMessages,
readEnv,
createClient,
sleep,
},
runtime: defaultRuntime,
});
}
export function createDefaultDeps(): CliDeps {
// Default dependency bundle used by CLI commands and tests.
return {
sendMessage,
sendMessageWeb,
waitForFinalStatus,
assertProvider,
createClient,
monitorTwilio,
listRecentMessages,
ensurePortAvailable,
startWebhook,
waitForever,
ensureBinary,
ensureFunnel,
getTailnetHostname,
readEnv,
findWhatsappSenderSid,
updateWebhook,
handlePortError,
monitorWebProvider,
resolveTwilioMediaUrl: async (source, { serveMedia, runtime }) => {
if (/^https?:\/\//i.test(source)) return source;
const hosted = await ensureMediaHosted(source, {
startServer: serveMedia,
runtime,
});
return hosted.url;
},
};
}
export function logTwilioFrom(runtime: RuntimeEnv = defaultRuntime) {
// Log the configured Twilio sender for clarity in CLI output.
const env = readEnv(runtime);
runtime.log(
info(`Provider: twilio (polling inbound) | from ${env.whatsappFrom}`),
);
}
export { logWebSelfId };

View File

@@ -2,16 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const sendCommand = vi.fn();
const statusCommand = vi.fn();
const webhookCommand = vi.fn().mockResolvedValue(undefined);
const ensureTwilioEnv = vi.fn();
const loginWeb = vi.fn();
const monitorWebProvider = vi.fn();
const pickProvider = vi.fn();
const monitorTwilio = vi.fn();
const logTwilioFrom = vi.fn();
const logWebSelfId = vi.fn();
const waitForever = vi.fn();
const spawnRelayTmux = vi.fn().mockResolvedValue("warelay-relay");
const spawnRelayTmux = vi.fn().mockResolvedValue("clawdis-relay");
const runtime = {
log: vi.fn(),
@@ -23,19 +18,14 @@ const runtime = {
vi.mock("../commands/send.js", () => ({ sendCommand }));
vi.mock("../commands/status.js", () => ({ statusCommand }));
vi.mock("../commands/webhook.js", () => ({ webhookCommand }));
vi.mock("../env.js", () => ({ ensureTwilioEnv }));
vi.mock("../runtime.js", () => ({ defaultRuntime: runtime }));
vi.mock("../provider-web.js", () => ({
loginWeb,
monitorWebProvider,
pickProvider,
}));
vi.mock("./deps.js", () => ({
createDefaultDeps: () => ({ waitForever }),
logTwilioFrom,
logWebSelfId,
monitorTwilio,
}));
vi.mock("./relay_tmux.js", () => ({ spawnRelayTmux }));
@@ -54,55 +44,15 @@ describe("cli program", () => {
expect(sendCommand).toHaveBeenCalled();
});
it("rejects invalid relay provider", async () => {
const program = buildProgram();
await expect(
program.parseAsync(["relay", "--provider", "bogus"], { from: "user" }),
).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith(
"--provider must be auto, web, or twilio",
);
});
it("falls back to twilio when web relay fails", async () => {
pickProvider.mockResolvedValue("web");
monitorWebProvider.mockRejectedValue(new Error("no web"));
const program = buildProgram();
await expect(
program.parseAsync(
["relay", "--provider", "auto", "--interval", "2", "--lookback", "1"],
{ from: "user" },
),
).rejects.toThrow("exit");
expect(logWebSelfId).toHaveBeenCalled();
expect(ensureTwilioEnv).not.toHaveBeenCalled();
expect(monitorTwilio).not.toHaveBeenCalled();
});
it("runs relay tmux attach command", async () => {
const originalIsTTY = process.stdout.isTTY;
(process.stdout as typeof process.stdout & { isTTY?: boolean }).isTTY =
true;
const program = buildProgram();
await program.parseAsync(["relay:tmux:attach"], { from: "user" });
expect(spawnRelayTmux).toHaveBeenCalledWith(
"pnpm clawdis relay --verbose",
true,
false,
);
(process.stdout as typeof process.stdout & { isTTY?: boolean }).isTTY =
originalIsTTY;
});
it("runs relay heartbeat command", async () => {
pickProvider.mockResolvedValue("web");
it("starts relay with heartbeat tuning", async () => {
monitorWebProvider.mockResolvedValue(undefined);
const originalExit = runtime.exit;
runtime.exit = vi.fn();
const program = buildProgram();
await program.parseAsync(["relay:heartbeat"], { from: "user" });
await program.parseAsync(
["relay", "--web-heartbeat", "90", "--heartbeat-now"],
{
from: "user",
},
);
expect(logWebSelfId).toHaveBeenCalled();
expect(monitorWebProvider).toHaveBeenCalledWith(
false,
@@ -111,8 +61,17 @@ describe("cli program", () => {
undefined,
runtime,
undefined,
{ replyHeartbeatNow: true },
{ heartbeatSeconds: 90, replyHeartbeatNow: true },
);
});
it("runs relay heartbeat command", async () => {
monitorWebProvider.mockResolvedValue(undefined);
const originalExit = runtime.exit;
runtime.exit = vi.fn();
const program = buildProgram();
await program.parseAsync(["relay:heartbeat"], { from: "user" });
expect(logWebSelfId).toHaveBeenCalled();
expect(runtime.exit).not.toHaveBeenCalled();
runtime.exit = originalExit;
});
@@ -126,4 +85,10 @@ describe("cli program", () => {
shouldAttach,
);
});
it("runs status command", async () => {
const program = buildProgram();
await program.parseAsync(["status"], { from: "user" });
expect(statusCommand).toHaveBeenCalled();
});
});

View File

@@ -3,45 +3,35 @@ import { Command } from "commander";
import { agentCommand } from "../commands/agent.js";
import { sendCommand } from "../commands/send.js";
import { statusCommand } from "../commands/status.js";
import { webhookCommand } from "../commands/webhook.js";
import { loadConfig } from "../config/config.js";
import { ensureTwilioEnv } from "../env.js";
import { danger, info, setVerbose, setYes } from "../globals.js";
import { danger, info, setVerbose } from "../globals.js";
import { getResolvedLoggerSettings } from "../logging.js";
import {
loginWeb,
logoutWeb,
monitorWebProvider,
pickProvider,
resolveHeartbeatRecipients,
runWebHeartbeatOnce,
type WebMonitorTuning,
} from "../provider-web.js";
import { defaultRuntime } from "../runtime.js";
import { runTwilioHeartbeatOnce } from "../twilio/heartbeat.js";
import type { Provider } from "../utils.js";
import { VERSION } from "../version.js";
import {
resolveHeartbeatSeconds,
resolveReconnectPolicy,
} from "../web/reconnect.js";
import {
createDefaultDeps,
logTwilioFrom,
logWebSelfId,
monitorTwilio,
} from "./deps.js";
import { createDefaultDeps, logWebSelfId } from "./deps.js";
import { spawnRelayTmux } from "./relay_tmux.js";
export function buildProgram() {
const program = new Command();
const PROGRAM_VERSION = VERSION;
const TAGLINE =
"Send, receive, and auto-reply on WhatsApp—Twilio-backed or QR-linked.";
"Send, receive, and auto-reply on WhatsApp—Baileys (web) only.";
program
.name("clawdis")
.description("WhatsApp relay CLI (Twilio or WhatsApp Web session)")
.description("WhatsApp relay CLI (WhatsApp Web session only)")
.version(PROGRAM_VERSION);
const formatIntroLine = (version: string, rich = true) => {
@@ -80,24 +70,24 @@ export function buildProgram() {
"Link personal WhatsApp Web and show QR + connection logs.",
],
[
'clawdis send --to +15551234567 --message "Hi" --provider web --json',
'clawdis send --to +15551234567 --message "Hi" --json',
"Send via your web session and print JSON result.",
],
[
"clawdis relay --provider auto --interval 5 --lookback 15 --verbose",
"Auto-reply loop: prefer Web when logged in, otherwise Twilio polling.",
"clawdis relay --verbose",
"Auto-reply loop using your linked web session.",
],
[
"clawdis webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose",
"Start webhook + Tailscale Funnel and update Twilio callbacks.",
"clawdis heartbeat --verbose",
"Send a heartbeat ping to your active session or first allowFrom contact.",
],
[
"clawdis status --limit 10 --lookback 60 --json",
"Show last 10 messages from the past hour as JSON.",
"clawdis status",
"Show web session health and recent session recipients.",
],
[
'clawdis agent --to +15551234567 --message "Run summary" --thinking high',
"Talk directly to the agent using the same session handling, no WhatsApp send.",
'clawdis agent --to +15551234567 --message "Run summary" --deliver',
"Talk directly to the agent using the same session handling; optionally send the reply.",
],
] as const;
@@ -138,7 +128,7 @@ export function buildProgram() {
program
.command("send")
.description("Send a WhatsApp message")
.description("Send a WhatsApp message (web provider)")
.requiredOption(
"-t, --to <number>",
"Recipient number in E.164 (e.g. +15551234567)",
@@ -146,20 +136,8 @@ export function buildProgram() {
.requiredOption("-m, --message <text>", "Message body")
.option(
"--media <path-or-url>",
"Attach image (<=5MB). Web: path or URL. Twilio: https URL or local path hosted via webhook/funnel.",
"Attach media (image/audio/video/document). Accepts local paths or URLs.",
)
.option(
"--serve-media",
"For Twilio: start a temporary media server if webhook is not running",
false,
)
.option(
"-w, --wait <seconds>",
"Wait for delivery status (0 to skip)",
"20",
)
.option("-p, --poll <seconds>", "Polling interval while waiting", "2")
.option("--provider <provider>", "Provider: twilio | web", "twilio")
.option("--dry-run", "Print payload and skip sending", false)
.option("--json", "Output result as JSON", false)
.option("--verbose", "Verbose logging", false)
@@ -167,10 +145,10 @@ export function buildProgram() {
"after",
`
Examples:
clawdis send --to +15551234567 --message "Hi" # wait 20s for delivery (default)
clawdis send --to +15551234567 --message "Hi" --wait 0 # fire-and-forget
clawdis send --to +15551234567 --message "Hi"
clawdis send --to +15551234567 --message "Hi" --media photo.jpg
clawdis send --to +15551234567 --message "Hi" --dry-run # print payload only
clawdis send --to +15551234567 --message "Hi" --wait 60 --poll 3`,
clawdis send --to +15551234567 --message "Hi" --json # machine-readable result`,
)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
@@ -204,11 +182,6 @@ Examples:
"Send the agent's reply back to WhatsApp (requires --to)",
false,
)
.option(
"--provider <provider>",
"Provider to deliver via when using --deliver (auto | web | twilio)",
"auto",
)
.option("--json", "Output result as JSON", false)
.option(
"--timeout <seconds>",
@@ -221,7 +194,7 @@ Examples:
clawdis agent --to +15551234567 --message "status update"
clawdis agent --session-id 1234 --message "Summarize inbox" --thinking medium
clawdis agent --to +15551234567 --message "Trace logs" --verbose on --json
clawdis agent --to +15551234567 --message "Summon reply" --deliver --provider web
clawdis agent --to +15551234567 --message "Summon reply" --deliver
`,
)
.action(async (opts) => {
@@ -240,10 +213,7 @@ Examples:
program
.command("heartbeat")
.description(
"Trigger a heartbeat or manual send once (web or twilio, no tmux)",
)
.option("--provider <provider>", "auto | web | twilio", "auto")
.description("Trigger a heartbeat or manual send once (web only, no tmux)")
.option("--to <number>", "Override target E.164; defaults to allowFrom[0]")
.option(
"--session-id <id>",
@@ -256,7 +226,7 @@ Examples:
)
.option(
"--message <text>",
"Send a custom message instead of the heartbeat probe (web or twilio provider)",
"Send a custom message instead of the heartbeat probe",
)
.option("--body <text>", "Alias for --message")
.option("--dry-run", "Print the resolved payload without sending", false)
@@ -269,7 +239,7 @@ Examples:
clawdis heartbeat --verbose # prints detailed heartbeat logs
clawdis heartbeat --to +1555123 # override destination
clawdis heartbeat --session-id <uuid> --to +1555123 # resume a specific session
clawdis heartbeat --message "Ping" --provider twilio
clawdis heartbeat --message "Ping"
clawdis heartbeat --all # send to every active session recipient or allowFrom entry`,
)
.action(async (opts) => {
@@ -302,11 +272,6 @@ Examples:
);
defaultRuntime.exit(1);
}
const providerPref = String(opts.provider ?? "auto");
if (!["auto", "web", "twilio"].includes(providerPref)) {
defaultRuntime.error("--provider must be auto, web, or twilio");
defaultRuntime.exit(1);
}
const overrideBody =
(opts.message as string | undefined) ||
@@ -314,32 +279,16 @@ Examples:
undefined;
const dryRun = Boolean(opts.dryRun);
const provider =
providerPref === "twilio"
? "twilio"
: await pickProvider(providerPref as "auto" | "web");
if (provider === "twilio") ensureTwilioEnv();
try {
for (const to of recipients) {
if (provider === "web") {
await runWebHeartbeatOnce({
to,
verbose: Boolean(opts.verbose),
runtime: defaultRuntime,
sessionId: opts.sessionId,
overrideBody,
dryRun,
});
} else {
await runTwilioHeartbeatOnce({
to,
verbose: Boolean(opts.verbose),
runtime: defaultRuntime,
overrideBody,
dryRun,
});
}
await runWebHeartbeatOnce({
to,
verbose: Boolean(opts.verbose),
runtime: defaultRuntime,
sessionId: opts.sessionId,
overrideBody,
dryRun,
});
}
} catch {
defaultRuntime.exit(1);
@@ -348,14 +297,7 @@ Examples:
program
.command("relay")
.description("Auto-reply to inbound messages (auto-selects web or twilio)")
.option("--provider <provider>", "auto | web | twilio", "auto")
.option("-i, --interval <seconds>", "Polling interval for twilio mode", "5")
.option(
"-l, --lookback <minutes>",
"Initial lookback window for twilio mode",
"5",
)
.description("Auto-reply to inbound messages (web only)")
.option(
"--web-heartbeat <seconds>",
"Heartbeat interval for web relay health logs (seconds)",
@@ -371,7 +313,7 @@ Examples:
.option("--web-retry-max <ms>", "Max reconnect backoff for web relay (ms)")
.option(
"--heartbeat-now",
"Run a heartbeat immediately when relay starts (web provider)",
"Run a heartbeat immediately when relay starts",
false,
)
.option("--verbose", "Verbose logging", false)
@@ -379,10 +321,8 @@ Examples:
"after",
`
Examples:
clawdis relay # auto: web if logged-in, else twilio poll
clawdis relay --provider web # force personal web session
clawdis relay --provider twilio # force twilio poll
clawdis relay --provider twilio --interval 2 --lookback 30
clawdis relay # uses your linked web session
clawdis relay --web-heartbeat 60 # override heartbeat interval
# Troubleshooting: docs/refactor/web-relay-troubleshooting.md
`,
)
@@ -390,13 +330,6 @@ Examples:
setVerbose(Boolean(opts.verbose));
const { file: logFile, level: logLevel } = getResolvedLoggerSettings();
defaultRuntime.log(info(`logs: ${logFile} (level ${logLevel})`));
const providerPref = String(opts.provider ?? "auto");
if (!["auto", "web", "twilio"].includes(providerPref)) {
defaultRuntime.error("--provider must be auto, web, or twilio");
defaultRuntime.exit(1);
}
const intervalSeconds = Number.parseInt(opts.interval, 10);
const lookbackMinutes = Number.parseInt(opts.lookback, 10);
const webHeartbeat =
opts.webHeartbeat !== undefined
? Number.parseInt(String(opts.webHeartbeat), 10)
@@ -414,14 +347,6 @@ Examples:
? Number.parseInt(String(opts.webRetryMax), 10)
: undefined;
const heartbeatNow = Boolean(opts.heartbeatNow);
if (Number.isNaN(intervalSeconds) || intervalSeconds <= 0) {
defaultRuntime.error("Interval must be a positive integer");
defaultRuntime.exit(1);
}
if (Number.isNaN(lookbackMinutes) || lookbackMinutes < 0) {
defaultRuntime.error("Lookback must be >= 0 minutes");
defaultRuntime.exit(1);
}
if (
webHeartbeat !== undefined &&
(Number.isNaN(webHeartbeat) || webHeartbeat <= 0)
@@ -469,49 +394,37 @@ Examples:
if (Object.keys(reconnect).length > 0) {
webTuning.reconnect = reconnect;
}
const provider = await pickProvider(providerPref as Provider | "auto");
if (provider === "web") {
logWebSelfId(defaultRuntime, true);
const cfg = loadConfig();
const effectiveHeartbeat = resolveHeartbeatSeconds(
cfg,
webTuning.heartbeatSeconds,
logWebSelfId(defaultRuntime, true);
const cfg = loadConfig();
const effectiveHeartbeat = resolveHeartbeatSeconds(
cfg,
webTuning.heartbeatSeconds,
);
const effectivePolicy = resolveReconnectPolicy(cfg, webTuning.reconnect);
defaultRuntime.log(
info(
`Web relay health: heartbeat ${effectiveHeartbeat}s, retries ${effectivePolicy.maxAttempts || "∞"}, backoff ${effectivePolicy.initialMs}${effectivePolicy.maxMs}ms x${effectivePolicy.factor} (jitter ${Math.round(effectivePolicy.jitter * 100)}%)`,
),
);
try {
await monitorWebProvider(
Boolean(opts.verbose),
undefined,
true,
undefined,
defaultRuntime,
undefined,
webTuning,
);
const effectivePolicy = resolveReconnectPolicy(
cfg,
webTuning.reconnect,
);
defaultRuntime.log(
info(
`Web relay health: heartbeat ${effectiveHeartbeat}s, retries ${effectivePolicy.maxAttempts || "∞"}, backoff ${effectivePolicy.initialMs}${effectivePolicy.maxMs}ms x${effectivePolicy.factor} (jitter ${Math.round(effectivePolicy.jitter * 100)}%)`,
return;
} catch (err) {
defaultRuntime.error(
danger(
`Web relay failed: ${String(err)}. Re-link with 'clawdis login --verbose'.`,
),
);
try {
await monitorWebProvider(
Boolean(opts.verbose),
undefined,
true,
undefined,
defaultRuntime,
undefined,
webTuning,
);
return;
} catch (err) {
defaultRuntime.error(
danger(
`Web relay failed: ${String(err)}. Not falling back; re-link with 'clawdis login --provider web'.`,
),
);
defaultRuntime.exit(1);
}
defaultRuntime.exit(1);
}
ensureTwilioEnv();
logTwilioFrom();
await monitorTwilio(intervalSeconds, lookbackMinutes);
});
program
@@ -519,28 +432,11 @@ Examples:
.description(
"Run relay with an immediate heartbeat (no tmux); requires web provider",
)
.option("--provider <provider>", "auto | web", "auto")
.option("--verbose", "Verbose logging", false)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const { file: logFile, level: logLevel } = getResolvedLoggerSettings();
defaultRuntime.log(info(`logs: ${logFile} (level ${logLevel})`));
const providerPref = String(opts.provider ?? "auto");
if (!["auto", "web"].includes(providerPref)) {
defaultRuntime.error("--provider must be auto or web");
defaultRuntime.exit(1);
return;
}
const provider = await pickProvider(providerPref as "auto" | "web");
if (provider !== "web") {
defaultRuntime.error(
danger(
"Heartbeat relay is only supported for the web provider. Link with `clawdis login --verbose`.",
),
);
defaultRuntime.exit(1);
return;
}
logWebSelfId(defaultRuntime, true);
const cfg = loadConfig();
@@ -574,75 +470,20 @@ Examples:
program
.command("status")
.description("Show recent WhatsApp messages (sent and received)")
.option("-l, --limit <count>", "Number of messages to show", "20")
.option("-b, --lookback <minutes>", "How far back to fetch messages", "240")
.description("Show web session health and recent session recipients")
.option("--json", "Output JSON instead of text", false)
.option("--verbose", "Verbose logging", false)
.addHelpText(
"after",
`
Examples:
clawdis status # last 20 msgs in past 4h
clawdis status --limit 5 --lookback 30 # last 5 msgs in past 30m
clawdis status --json --limit 50 # machine-readable output`,
clawdis status # show linked account + session store summary
clawdis status --json # machine-readable output`,
)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const deps = createDefaultDeps();
try {
await statusCommand(opts, deps, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
program
.command("webhook")
.description(
"Run inbound webhook. ingress=tailscale updates Twilio; ingress=none stays local-only.",
)
.option("-p, --port <port>", "Port to listen on", "42873")
.option("-r, --reply <text>", "Optional auto-reply text")
.option("--path <path>", "Webhook path", "/webhook/whatsapp")
.option(
"--ingress <mode>",
"Ingress: tailscale (funnel + Twilio update) | none (local only)",
"tailscale",
)
.option("--verbose", "Log inbound and auto-replies", false)
.option("-y, --yes", "Auto-confirm prompts when possible", false)
.option("--dry-run", "Print planned actions without starting server", false)
.addHelpText(
"after",
`
Examples:
clawdis webhook # ingress=tailscale (funnel + Twilio update)
clawdis webhook --ingress none # local-only server (no funnel / no Twilio update)
clawdis webhook --port 45000 # pick a high, less-colliding port
clawdis webhook --reply "Got it!" # static auto-reply; otherwise use config file`,
)
// istanbul ignore next
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
setYes(Boolean(opts.yes));
const deps = createDefaultDeps();
try {
const server = await webhookCommand(opts, deps, defaultRuntime);
if (!server) {
defaultRuntime.log(
info("Webhook dry-run complete; no server started."),
);
return;
}
process.on("SIGINT", () => {
server.close(() => {
console.log("\n👋 Webhook stopped");
defaultRuntime.exit(0);
});
});
await deps.waitForever();
await statusCommand(opts, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);

View File

@@ -1,45 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mocks must be defined via vi.hoisted to avoid TDZ with ESM hoisting.
const { monitorWebProvider, pickProvider, logWebSelfId, monitorTwilio } =
vi.hoisted(() => {
return {
monitorWebProvider: vi.fn().mockResolvedValue(undefined),
pickProvider: vi.fn().mockResolvedValue("web"),
logWebSelfId: vi.fn(),
monitorTwilio: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock("../provider-web.js", () => ({
monitorWebProvider,
pickProvider,
logWebSelfId,
}));
vi.mock("../twilio/monitor.js", () => ({
monitorTwilio,
}));
import { buildProgram } from "./program.js";
describe("CLI relay command (e2e-ish)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("runs relay in web mode without crashing", async () => {
const program = buildProgram();
program.exitOverride(); // throw instead of exiting process on error
await expect(
program.parseAsync(["relay", "--provider", "web"], { from: "user" }),
).resolves.toBeInstanceOf(Object);
expect(pickProvider).toHaveBeenCalledWith("web");
expect(logWebSelfId).toHaveBeenCalledTimes(1);
expect(monitorWebProvider).toHaveBeenCalledTimes(1);
expect(monitorWebProvider.mock.calls[0][0]).toBe(false);
expect(monitorTwilio).not.toHaveBeenCalled();
});
});