feat: add dry-run options and retry helper

main
Peter Steinberger 2025-11-25 03:57:50 +01:00
parent af577f07da
commit fdfb1df0de
4 changed files with 32 additions and 1 deletions

View File

@ -179,6 +179,7 @@ Examples:
.option("--path <path>", "Webhook path", "/webhook/whatsapp")
.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",
`
@ -220,6 +221,7 @@ With Tailscale:
.option("--path <path>", "Webhook path", "/webhook/whatsapp")
.option("--verbose", "Verbose logging during setup/webhook", false)
.option("-y, --yes", "Auto-confirm prompts when possible", false)
.option("--dry-run", "Print planned actions without touching network", false)
// istanbul ignore next
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));

View File

@ -3,7 +3,7 @@ import type { RuntimeEnv } from "../runtime.js";
import { waitForever as defaultWaitForever } from "../cli/wait.js";
export async function upCommand(
opts: { port: string; path: string; verbose?: boolean; yes?: boolean },
opts: { port: string; path: string; verbose?: boolean; yes?: boolean; dryRun?: boolean },
deps: CliDeps,
runtime: RuntimeEnv,
waiter: typeof defaultWaitForever = defaultWaitForever,
@ -15,6 +15,13 @@ export async function upCommand(
await deps.ensurePortAvailable(port);
const env = deps.readEnv(runtime);
if (opts.dryRun) {
runtime.log(`[dry-run] would enable funnel on port ${port}`);
runtime.log(`[dry-run] would start webhook at path ${opts.path}`);
runtime.log(`[dry-run] would update Twilio sender webhook`);
const publicUrl = `https://dry-run${opts.path}`;
return { server: undefined, publicUrl, senderSid: undefined, waiter };
}
await deps.ensureBinary("tailscale", undefined, runtime);
await deps.ensureFunnel(port, undefined, runtime);
const host = await deps.getTailnetHostname();

View File

@ -17,6 +17,10 @@ export async function webhookCommand(
throw new Error("Port must be between 1 and 65535");
}
await deps.ensurePortAvailable(port);
if (opts.reply === "dry-run") {
runtime.log(`[dry-run] would start webhook on port ${port} path ${opts.path}`);
return undefined;
}
const server = await deps.startWebhook(
port,
opts.path,

18
src/infra/retry.ts Normal file
View File

@ -0,0 +1,18 @@
export async function retryAsync<T>(
fn: () => Promise<T>,
attempts = 3,
initialDelayMs = 300,
): Promise<T> {
let lastErr: unknown;
for (let i = 0; i < attempts; i += 1) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (i === attempts - 1) break;
const delay = initialDelayMs * 2 ** i;
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastErr;
}