feat: add dry-run options and retry helper

This commit is contained in:
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("--path <path>", "Webhook path", "/webhook/whatsapp")
.option("--verbose", "Log inbound and auto-replies", false) .option("--verbose", "Log inbound and auto-replies", false)
.option("-y, --yes", "Auto-confirm prompts when possible", false) .option("-y, --yes", "Auto-confirm prompts when possible", false)
.option("--dry-run", "Print planned actions without starting server", false)
.addHelpText( .addHelpText(
"after", "after",
` `
@@ -220,6 +221,7 @@ With Tailscale:
.option("--path <path>", "Webhook path", "/webhook/whatsapp") .option("--path <path>", "Webhook path", "/webhook/whatsapp")
.option("--verbose", "Verbose logging during setup/webhook", false) .option("--verbose", "Verbose logging during setup/webhook", false)
.option("-y, --yes", "Auto-confirm prompts when possible", false) .option("-y, --yes", "Auto-confirm prompts when possible", false)
.option("--dry-run", "Print planned actions without touching network", false)
// istanbul ignore next // istanbul ignore next
.action(async (opts) => { .action(async (opts) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));

View File

@@ -3,7 +3,7 @@ import type { RuntimeEnv } from "../runtime.js";
import { waitForever as defaultWaitForever } from "../cli/wait.js"; import { waitForever as defaultWaitForever } from "../cli/wait.js";
export async function upCommand( 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, deps: CliDeps,
runtime: RuntimeEnv, runtime: RuntimeEnv,
waiter: typeof defaultWaitForever = defaultWaitForever, waiter: typeof defaultWaitForever = defaultWaitForever,
@@ -15,6 +15,13 @@ export async function upCommand(
await deps.ensurePortAvailable(port); await deps.ensurePortAvailable(port);
const env = deps.readEnv(runtime); 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.ensureBinary("tailscale", undefined, runtime);
await deps.ensureFunnel(port, undefined, runtime); await deps.ensureFunnel(port, undefined, runtime);
const host = await deps.getTailnetHostname(); 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"); throw new Error("Port must be between 1 and 65535");
} }
await deps.ensurePortAvailable(port); 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( const server = await deps.startWebhook(
port, port,
opts.path, 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;
}