Add command modules and tests; commit remaining changes
This commit is contained in:
44
src/commands/send.ts
Normal file
44
src/commands/send.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { info } from "../globals.js";
|
||||
import type { CliDeps, Provider, RuntimeEnv } from "../index.js";
|
||||
|
||||
export async function sendCommand(
|
||||
opts: {
|
||||
to: string;
|
||||
message: string;
|
||||
wait: string;
|
||||
poll: string;
|
||||
provider: Provider;
|
||||
},
|
||||
deps: CliDeps,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
deps.assertProvider(opts.provider);
|
||||
const waitSeconds = Number.parseInt(opts.wait, 10);
|
||||
const pollSeconds = Number.parseInt(opts.poll, 10);
|
||||
|
||||
if (Number.isNaN(waitSeconds) || waitSeconds < 0) {
|
||||
throw new Error("Wait must be >= 0 seconds");
|
||||
}
|
||||
if (Number.isNaN(pollSeconds) || pollSeconds <= 0) {
|
||||
throw new Error("Poll must be > 0 seconds");
|
||||
}
|
||||
|
||||
if (opts.provider === "web") {
|
||||
if (waitSeconds !== 0) {
|
||||
runtime.log(info("Wait/poll are Twilio-only; ignored for provider=web."));
|
||||
}
|
||||
await deps.sendMessageWeb(opts.to, opts.message, { verbose: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deps.sendMessage(opts.to, opts.message, runtime);
|
||||
if (!result) return;
|
||||
if (waitSeconds === 0) return;
|
||||
await deps.waitForFinalStatus(
|
||||
result.client,
|
||||
result.sid,
|
||||
waitSeconds,
|
||||
pollSeconds,
|
||||
runtime,
|
||||
);
|
||||
}
|
||||
30
src/commands/status.ts
Normal file
30
src/commands/status.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CliDeps, RuntimeEnv } from "../index.js";
|
||||
import { formatMessageLine } from "../index.js";
|
||||
|
||||
export async function statusCommand(
|
||||
opts: { limit: string; lookback: string; json?: boolean },
|
||||
deps: CliDeps,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const limit = Number.parseInt(opts.limit, 10);
|
||||
const lookbackMinutes = Number.parseInt(opts.lookback, 10);
|
||||
if (Number.isNaN(limit) || limit <= 0 || limit > 200) {
|
||||
throw new Error("limit must be between 1 and 200");
|
||||
}
|
||||
if (Number.isNaN(lookbackMinutes) || lookbackMinutes <= 0) {
|
||||
throw new Error("lookback must be > 0 minutes");
|
||||
}
|
||||
|
||||
const messages = await deps.listRecentMessages(lookbackMinutes, limit);
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify(messages, null, 2));
|
||||
return;
|
||||
}
|
||||
if (messages.length === 0) {
|
||||
runtime.log("No messages found in the requested window.");
|
||||
return;
|
||||
}
|
||||
for (const m of messages) {
|
||||
runtime.log(formatMessageLine(m));
|
||||
}
|
||||
}
|
||||
27
src/commands/webhook.ts
Normal file
27
src/commands/webhook.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { CliDeps, RuntimeEnv } from "../index.js";
|
||||
|
||||
export async function webhookCommand(
|
||||
opts: {
|
||||
port: string;
|
||||
path: string;
|
||||
reply?: string;
|
||||
verbose?: boolean;
|
||||
yes?: boolean;
|
||||
},
|
||||
deps: CliDeps,
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const port = Number.parseInt(opts.port, 10);
|
||||
if (Number.isNaN(port) || port <= 0 || port >= 65536) {
|
||||
throw new Error("Port must be between 1 and 65535");
|
||||
}
|
||||
await deps.ensurePortAvailable(port);
|
||||
const server = await deps.startWebhook(
|
||||
port,
|
||||
opts.path,
|
||||
opts.reply,
|
||||
Boolean(opts.verbose),
|
||||
runtime,
|
||||
);
|
||||
return server;
|
||||
}
|
||||
29
src/globals.test.ts
Normal file
29
src/globals.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { isVerbose, isYes, logVerbose, setVerbose, setYes } from "./globals.js";
|
||||
|
||||
describe("globals", () => {
|
||||
afterEach(() => {
|
||||
setVerbose(false);
|
||||
setYes(false);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("toggles verbose flag and logs when enabled", () => {
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
setVerbose(false);
|
||||
logVerbose("hidden");
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
|
||||
setVerbose(true);
|
||||
logVerbose("shown");
|
||||
expect(isVerbose()).toBe(true);
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("shown"));
|
||||
});
|
||||
|
||||
it("stores yes flag", () => {
|
||||
setYes(true);
|
||||
expect(isYes()).toBe(true);
|
||||
setYes(false);
|
||||
expect(isYes()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js";
|
||||
import { assertProvider, normalizeE164, toWhatsappJid } from "./index.js";
|
||||
|
||||
describe("normalizeE164", () => {
|
||||
it("strips whatsapp: prefix and whitespace", () => {
|
||||
it("strips whatsapp prefix and whitespace", () => {
|
||||
expect(normalizeE164("whatsapp:+1 555 123 4567")).toBe("+15551234567");
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ describe("withWhatsAppPrefix", () => {
|
||||
|
||||
describe("ensureDir", () => {
|
||||
it("creates nested directory", async () => {
|
||||
const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), "warelay-test-"));
|
||||
const tmp = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), "warelay-test-"),
|
||||
);
|
||||
const target = path.join(tmp, "nested", "dir");
|
||||
await ensureDir(target);
|
||||
expect(fs.existsSync(target)).toBe(true);
|
||||
@@ -60,6 +62,8 @@ describe("assertProvider", () => {
|
||||
describe("normalizeE164 & toWhatsappJid", () => {
|
||||
it("strips formatting and prefixes", () => {
|
||||
expect(normalizeE164("whatsapp:(555) 123-4567")).toBe("+5551234567");
|
||||
expect(toWhatsappJid("whatsapp:+555 123 4567")).toBe("5551234567@s.whatsapp.net");
|
||||
expect(toWhatsappJid("whatsapp:+555 123 4567")).toBe(
|
||||
"5551234567@s.whatsapp.net",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user