feat(cli): move provider login/logout

This commit is contained in:
Peter Steinberger
2026-01-08 07:40:08 +01:00
parent 903f5af59c
commit e83c6ac088
19 changed files with 153 additions and 68 deletions

View File

@@ -23,11 +23,10 @@ import {
} from "../config/config.js";
import { danger, setVerbose } from "../globals.js";
import { autoMigrateLegacyState } from "../infra/state-migrations.js";
import { loginWeb, logoutWeb } from "../provider-web.js";
import { runProviderLogin, runProviderLogout } from "./provider-auth.js";
import { defaultRuntime } from "../runtime.js";
import { isRich, theme } from "../terminal/theme.js";
import { VERSION } from "../version.js";
import { resolveWhatsAppAccount } from "../web/accounts.js";
import { emitCliBanner, formatCliBannerLine } from "./banner.js";
import { registerBrowserCli } from "./browser-cli.js";
import { hasExplicitOptions } from "./command-options.js";
@@ -138,7 +137,7 @@ export function buildProgram() {
});
const examples = [
[
"clawdbot login --verbose",
"clawdbot providers login --verbose",
"Link personal WhatsApp Web and show QR + connection logs.",
],
[
@@ -342,23 +341,22 @@ export function buildProgram() {
}
});
// Deprecated hidden aliases: use `clawdbot providers login/logout`. Remove in a future major.
program
program
.command("login")
.command("login", { hidden: true })
.description("Link your personal WhatsApp via QR (web provider)")
.option("--verbose", "Verbose connection logs", false)
.option("--provider <provider>", "Provider alias (default: whatsapp)")
.option("--account <id>", "WhatsApp account id (accountId)")
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
try {
const provider = opts.provider ?? "whatsapp";
await loginWeb(
Boolean(opts.verbose),
provider,
undefined,
await runProviderLogin(
{
provider: opts.provider as string | undefined,
account: opts.account as string | undefined,
verbose: Boolean(opts.verbose),
},
defaultRuntime,
opts.account as string | undefined,
);
} catch (err) {
defaultRuntime.error(danger(`Web login failed: ${String(err)}`));
@@ -367,23 +365,19 @@ export function buildProgram() {
});
program
.command("logout")
.description("Clear cached WhatsApp Web credentials")
.command("logout", { hidden: true })
.description("Log out of WhatsApp Web (keeps config)")
.option("--provider <provider>", "Provider alias (default: whatsapp)")
.option("--account <id>", "WhatsApp account id (accountId)")
.action(async (opts) => {
try {
void opts.provider; // placeholder for future multi-provider; currently web only.
const cfg = loadConfig();
const account = resolveWhatsAppAccount({
cfg,
accountId: opts.account as string | undefined,
});
await logoutWeb({
runtime: defaultRuntime,
authDir: account.authDir,
isLegacyAuthDir: account.isLegacyAuthDir,
});
await runProviderLogout(
{
provider: opts.provider as string | undefined,
account: opts.account as string | undefined,
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(danger(`Logout failed: ${String(err)}`));
defaultRuntime.exit(1);

51
src/cli/provider-auth.ts Normal file
View File

@@ -0,0 +1,51 @@
import { loadConfig } from "../config/config.js";
import { setVerbose } from "../globals.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { loginWeb, logoutWeb } from "../provider-web.js";
import { resolveWhatsAppAccount } from "../web/accounts.js";
type ProviderAuthOptions = {
provider?: string;
account?: string;
verbose?: boolean;
};
function normalizeProvider(raw?: string): "whatsapp" | "web" {
const value = String(raw ?? "whatsapp").trim().toLowerCase();
if (value === "whatsapp" || value === "web") return value;
throw new Error(`Unsupported provider: ${value}`);
}
export async function runProviderLogin(
opts: ProviderAuthOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
const provider = normalizeProvider(opts.provider);
// Auth-only flow: do not mutate provider config here.
setVerbose(Boolean(opts.verbose));
await loginWeb(
Boolean(opts.verbose),
provider,
undefined,
runtime,
opts.account,
);
}
export async function runProviderLogout(
opts: ProviderAuthOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
const provider = normalizeProvider(opts.provider);
// Auth-only flow: resolve account + clear session state only.
const cfg = loadConfig();
const account = resolveWhatsAppAccount({
cfg,
accountId: opts.account,
});
await logoutWeb({
runtime,
authDir: account.authDir,
isLegacyAuthDir: account.isLegacyAuthDir,
});
}

View File

@@ -7,7 +7,9 @@ import {
providersStatusCommand,
} from "../commands/providers.js";
import { listChatProviders } from "../providers/registry.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import { runProviderLogin, runProviderLogout } from "./provider-auth.js";
import { hasExplicitOptions } from "./command-options.js";
const optionNamesAdd = [
@@ -116,4 +118,46 @@ export function registerProvidersCli(program: Command) {
defaultRuntime.exit(1);
}
});
providers
.command("login")
.description("Link a provider account (WhatsApp Web only)")
.option("--provider <provider>", "Provider alias (default: whatsapp)")
.option("--account <id>", "WhatsApp account id (accountId)")
.option("--verbose", "Verbose connection logs", false)
.action(async (opts) => {
try {
await runProviderLogin(
{
provider: opts.provider as string | undefined,
account: opts.account as string | undefined,
verbose: Boolean(opts.verbose),
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(danger(`Provider login failed: ${String(err)}`));
defaultRuntime.exit(1);
}
});
providers
.command("logout")
.description("Log out of a provider session (WhatsApp Web only)")
.option("--provider <provider>", "Provider alias (default: whatsapp)")
.option("--account <id>", "WhatsApp account id (accountId)")
.action(async (opts) => {
try {
await runProviderLogout(
{
provider: opts.provider as string | undefined,
account: opts.account as string | undefined,
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(danger(`Provider logout failed: ${String(err)}`));
defaultRuntime.exit(1);
}
});
}

View File

@@ -143,7 +143,7 @@ export async function healthCommand(
runtime.log(
summary.web.linked
? `Web: linked (auth age ${summary.web.authAgeMs ? `${Math.round(summary.web.authAgeMs / 60000)}m` : "unknown"})`
: "Web: not linked (run clawdbot login)",
: "Web: not linked (run clawdbot providers login)",
);
if (summary.web.linked) {
const cfg = loadConfig();

View File

@@ -856,7 +856,7 @@ export async function setupProviders(
}
} else if (!whatsappLinked) {
await prompter.note(
"Run `clawdbot login` later to link WhatsApp.",
"Run `clawdbot providers login` later to link WhatsApp.",
"WhatsApp",
);
}

View File

@@ -1611,7 +1611,7 @@ export async function monitorWebProvider(
if (loggedOut) {
runtime.error(
"WhatsApp session logged out. Run `clawdbot login --provider web` to relink.",
"WhatsApp session logged out. Run `clawdbot providers login --provider web` to relink.",
);
await closeListener();
break;

View File

@@ -70,7 +70,7 @@ export async function loginWeb(
});
console.error(
danger(
"WhatsApp reported the session is logged out. Cleared cached web session; please rerun clawdbot login and scan the QR again.",
"WhatsApp reported the session is logged out. Cleared cached web session; please rerun clawdbot providers login and scan the QR again.",
),
);
throw new Error("Session logged out; cache cleared. Re-run login.");

View File

@@ -169,7 +169,9 @@ export async function createWaSocket(
const status = getStatusCode(lastDisconnect?.error);
if (status === DisconnectReason.loggedOut) {
console.error(
danger("WhatsApp session logged out. Run: clawdbot login"),
danger(
"WhatsApp session logged out. Run: clawdbot providers login",
),
);
}
}
@@ -454,7 +456,7 @@ export async function pickProvider(
const hasWeb = await webAuthExists(authDir);
if (!hasWeb) {
throw new Error(
"No WhatsApp Web session found. Run `clawdbot login --verbose` to link.",
"No WhatsApp Web session found. Run `clawdbot providers login --verbose` to link.",
);
}
return choice;