Merge branch 'main' into setup-telegram-no-telegram-fallback
This commit is contained in:
@@ -27,6 +27,7 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { confirmThenOpen, formatNoteLink } from '../lib/browser.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
@@ -48,8 +49,10 @@ interface AppInfo {
|
||||
owner: { id: string; username: string } | null;
|
||||
}
|
||||
|
||||
export async function runDiscordChannel(displayName: string): Promise<void> {
|
||||
const hasBot = await askHasBotToken();
|
||||
export async function runDiscordChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
const choice = await askHasBotToken();
|
||||
if (choice === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
const hasBot = choice === 'yes';
|
||||
if (!hasBot) {
|
||||
await walkThroughBotCreation();
|
||||
}
|
||||
@@ -142,17 +145,18 @@ export async function runDiscordChannel(displayName: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function askHasBotToken(): Promise<boolean> {
|
||||
async function askHasBotToken(): Promise<'yes' | 'no' | 'back'> {
|
||||
const answer = ensureAnswer(
|
||||
await brightSelect({
|
||||
message: 'Do you already have a Discord bot?',
|
||||
options: [
|
||||
{ value: 'yes', label: 'Yes, I have a bot token ready' },
|
||||
{ value: 'no', label: "No, walk me through creating one" },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
return answer === 'yes';
|
||||
return answer as 'yes' | 'no' | 'back';
|
||||
}
|
||||
|
||||
async function walkThroughBotCreation(): Promise<void> {
|
||||
|
||||
@@ -33,6 +33,7 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
|
||||
@@ -48,10 +49,11 @@ interface RemoteCreds {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export async function runIMessageChannel(displayName: string): Promise<void> {
|
||||
export async function runIMessageChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
const isMac = os.platform() === 'darwin';
|
||||
|
||||
const mode = await askMode(isMac);
|
||||
if (mode === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
let remoteCreds: RemoteCreds | null = null;
|
||||
|
||||
if (mode === 'local') {
|
||||
@@ -139,34 +141,38 @@ export async function runIMessageChannel(displayName: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function askMode(isMac: boolean): Promise<Mode> {
|
||||
async function askMode(isMac: boolean): Promise<Mode | 'back'> {
|
||||
const baseOptions = isMac
|
||||
? [
|
||||
{
|
||||
value: 'local' as const,
|
||||
label: 'Local (this Mac)',
|
||||
hint: "uses this machine's iMessage account",
|
||||
},
|
||||
{
|
||||
value: 'remote' as const,
|
||||
label: 'Remote (Photon API)',
|
||||
hint: 'the bot lives on another server',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: 'remote' as const,
|
||||
label: 'Remote (Photon API)',
|
||||
hint: 'only option off macOS',
|
||||
},
|
||||
];
|
||||
const choice = ensureAnswer(
|
||||
await brightSelect<Mode>({
|
||||
await brightSelect<Mode | 'back'>({
|
||||
message: 'How should iMessage run?',
|
||||
initialValue: isMac ? 'local' : 'remote',
|
||||
options: isMac
|
||||
? [
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local (this Mac)',
|
||||
hint: "uses this machine's iMessage account",
|
||||
},
|
||||
{
|
||||
value: 'remote',
|
||||
label: 'Remote (Photon API)',
|
||||
hint: 'the bot lives on another server',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: 'remote',
|
||||
label: 'Remote (Photon API)',
|
||||
hint: 'only option off macOS',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
...baseOptions,
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
setupLog.userInput('imessage_mode', String(choice));
|
||||
if (choice !== 'back') setupLog.userInput('imessage_mode', String(choice));
|
||||
return choice;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../../src/install-slug.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import {
|
||||
type Block,
|
||||
type StepResult,
|
||||
@@ -48,7 +50,33 @@ import { accentGreen, fmtDuration, note } from '../lib/theme.js';
|
||||
|
||||
const DEFAULT_AGENT_NAME = 'Nano';
|
||||
|
||||
export async function runSignalChannel(displayName: string): Promise<void> {
|
||||
export async function runSignalChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
note(
|
||||
[
|
||||
"NanoClaw links to Signal as a *secondary* device on your existing",
|
||||
"phone — no new number needed. Your assistant will send and receive",
|
||||
"messages as the number on that phone.",
|
||||
'',
|
||||
"Here's what's about to happen — no input needed for any of it:",
|
||||
'',
|
||||
' 1. Set up signal-cli (auto-installs if missing)',
|
||||
' 2. Install the Signal adapter',
|
||||
' 3. Show a QR code — scan it from Signal → Settings → Linked Devices',
|
||||
' 4. Wire your assistant and send a welcome message',
|
||||
].join('\n'),
|
||||
'Set up Signal',
|
||||
);
|
||||
|
||||
const proceed = ensureAnswer(await brightSelect<'continue' | 'back'>({
|
||||
message: 'Ready to set up Signal?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue' },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
initialValue: 'continue',
|
||||
}));
|
||||
if (proceed === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
|
||||
await ensureSignalCli();
|
||||
|
||||
const install = await runQuietChild(
|
||||
@@ -134,42 +162,74 @@ export async function runSignalChannel(displayName: string): Promise<void> {
|
||||
|
||||
async function ensureSignalCli(): Promise<void> {
|
||||
const cli = process.env.SIGNAL_CLI_PATH || 'signal-cli';
|
||||
const probe = spawnSync(cli, ['--version'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
if (!probe.error && probe.status === 0) return;
|
||||
const probeFor = (): boolean => {
|
||||
const r = spawnSync(cli, ['--version'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
return !r.error && r.status === 0;
|
||||
};
|
||||
if (probeFor()) return;
|
||||
|
||||
note(
|
||||
[
|
||||
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
|
||||
"We'll install it for you now — about 30 seconds, one-time only.",
|
||||
'',
|
||||
process.platform === 'darwin'
|
||||
? "On this Mac we'll use Homebrew (no admin password needed)."
|
||||
: "On Linux we'll grab the native release binary (no Java needed) and install it to ~/.local/bin.",
|
||||
].join('\n'),
|
||||
'Setting up signal-cli',
|
||||
);
|
||||
|
||||
const install = await runQuietChild(
|
||||
'install-signal-cli',
|
||||
'bash',
|
||||
['setup/install-signal-cli.sh'],
|
||||
{
|
||||
running: 'Installing signal-cli…',
|
||||
done: 'signal-cli installed.',
|
||||
},
|
||||
);
|
||||
|
||||
if (install.ok && probeFor()) return;
|
||||
|
||||
const reason = install.terminal?.fields.ERROR;
|
||||
if (process.platform === 'darwin') {
|
||||
note(
|
||||
[
|
||||
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
|
||||
"We couldn't install signal-cli automatically.",
|
||||
reason === 'homebrew_not_installed'
|
||||
? ' Reason: Homebrew is not installed.'
|
||||
: ` Reason: ${reason ?? 'unknown'}.`,
|
||||
'',
|
||||
'The quickest way on macOS is Homebrew:',
|
||||
'You can install it manually:',
|
||||
'',
|
||||
k.cyan(' brew install signal-cli'),
|
||||
'',
|
||||
"Install it in another terminal, then re-run setup.",
|
||||
'Then re-run setup.',
|
||||
].join('\n'),
|
||||
'signal-cli not found',
|
||||
"Couldn't install signal-cli",
|
||||
);
|
||||
} else {
|
||||
note(
|
||||
[
|
||||
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
|
||||
"We couldn't install signal-cli automatically.",
|
||||
` Reason: ${reason ?? 'unknown'}.`,
|
||||
'',
|
||||
'Grab the latest release from GitHub:',
|
||||
'You can install it manually from GitHub:',
|
||||
'',
|
||||
k.cyan(' https://github.com/AsamK/signal-cli/releases'),
|
||||
'',
|
||||
"Install it, make sure `signal-cli --version` works, then re-run setup.",
|
||||
'Then re-run setup.',
|
||||
].join('\n'),
|
||||
'signal-cli not found',
|
||||
"Couldn't install signal-cli",
|
||||
);
|
||||
}
|
||||
await fail(
|
||||
'signal-install',
|
||||
'signal-cli is required but not installed.',
|
||||
'Install it and re-run setup.',
|
||||
'install-signal-cli',
|
||||
'signal-cli is required but the auto-install failed.',
|
||||
'Install it manually and re-run setup.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { confirmThenOpen, formatNoteLink } from '../lib/browser.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { formatNoteLink, openUrl } from '../lib/browser.js';
|
||||
import { isHeadless } from '../platform.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
|
||||
import { readEnvKey } from '../environment.js';
|
||||
@@ -42,8 +45,9 @@ interface WorkspaceInfo {
|
||||
botUserId: string;
|
||||
}
|
||||
|
||||
export async function runSlackChannel(displayName: string): Promise<void> {
|
||||
await walkThroughAppCreation();
|
||||
export async function runSlackChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
const intro = await walkThroughAppCreation();
|
||||
if (intro === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
|
||||
const token = await collectBotToken();
|
||||
const signingSecret = await collectSigningSecret();
|
||||
@@ -121,7 +125,7 @@ export async function runSlackChannel(displayName: string): Promise<void> {
|
||||
showPostInstallChecklist(info);
|
||||
}
|
||||
|
||||
async function walkThroughAppCreation(): Promise<void> {
|
||||
async function walkThroughAppCreation(): Promise<'continue' | 'back'> {
|
||||
note(
|
||||
[
|
||||
"You'll create a Slack app that the assistant talks through.",
|
||||
@@ -140,7 +144,20 @@ async function walkThroughAppCreation(): Promise<void> {
|
||||
].filter((line): line is string => line !== null).join('\n'),
|
||||
'Create a Slack app',
|
||||
);
|
||||
await confirmThenOpen(SLACK_APPS_URL, 'Press Enter to open Slack app settings');
|
||||
|
||||
// Back-aware gate replacing the old `confirmThenOpen` "Press Enter to open
|
||||
// Slack app settings" so users can bail out of Slack before we open the
|
||||
// browser or ask for tokens.
|
||||
const choice = ensureAnswer(await brightSelect<'open' | 'back'>({
|
||||
message: 'Open Slack app settings in your browser?',
|
||||
options: [
|
||||
{ value: 'open', label: 'Open Slack app settings' },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
initialValue: 'open',
|
||||
}));
|
||||
if (choice === 'back') return 'back';
|
||||
if (!isHeadless()) openUrl(SLACK_APPS_URL);
|
||||
|
||||
ensureAnswer(
|
||||
await p.confirm({
|
||||
@@ -148,6 +165,7 @@ async function walkThroughAppCreation(): Promise<void> {
|
||||
initialValue: true,
|
||||
}),
|
||||
);
|
||||
return 'continue';
|
||||
}
|
||||
|
||||
async function collectBotToken(): Promise<string> {
|
||||
|
||||
@@ -30,6 +30,7 @@ import path from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { confirmThenOpen } from '../lib/browser.js';
|
||||
import {
|
||||
@@ -57,18 +58,24 @@ interface Collected {
|
||||
agentName?: string;
|
||||
}
|
||||
|
||||
export async function runTeamsChannel(_displayName: string): Promise<void> {
|
||||
export async function runTeamsChannel(_displayName: string): Promise<ChannelFlowResult> {
|
||||
const collected: Collected = {};
|
||||
const completed: string[] = [];
|
||||
|
||||
const existingAppId = readEnvKey('TEAMS_APP_ID');
|
||||
const existingPassword = readEnvKey('TEAMS_APP_PASSWORD');
|
||||
if (existingAppId && existingPassword) {
|
||||
const reuse = ensureAnswer(await p.confirm({
|
||||
const choice = ensureAnswer(await brightSelect<'yes' | 'no' | 'back'>({
|
||||
message: `Found existing Teams credentials (App ID: ${existingAppId.slice(0, 8)}…). Use them?`,
|
||||
initialValue: true,
|
||||
options: [
|
||||
{ value: 'yes', label: 'Yes, use the existing credentials' },
|
||||
{ value: 'no', label: "No, set up new ones" },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
initialValue: 'yes',
|
||||
}));
|
||||
if (reuse) {
|
||||
if (choice === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
if (choice === 'yes') {
|
||||
collected.appId = existingAppId;
|
||||
collected.appPassword = existingPassword;
|
||||
collected.appType = (readEnvKey('TEAMS_APP_TYPE') as 'SingleTenant' | 'MultiTenant') || 'MultiTenant';
|
||||
@@ -85,7 +92,8 @@ export async function runTeamsChannel(_displayName: string): Promise<void> {
|
||||
|
||||
printIntro();
|
||||
|
||||
await confirmPrereqs({ collected, completed });
|
||||
const prereqsResult = await confirmPrereqs({ collected, completed });
|
||||
if (prereqsResult === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
await stepPublicUrl({ collected, completed });
|
||||
await stepAppRegistration({ collected, completed });
|
||||
await stepClientSecret({ collected, completed });
|
||||
@@ -116,7 +124,7 @@ function printIntro(): void {
|
||||
);
|
||||
}
|
||||
|
||||
async function confirmPrereqs(args: { collected: Collected; completed: string[] }): Promise<void> {
|
||||
async function confirmPrereqs(args: { collected: Collected; completed: string[] }): Promise<'continue' | 'back'> {
|
||||
note(
|
||||
[
|
||||
'Before we start, confirm you have:',
|
||||
@@ -131,13 +139,36 @@ async function confirmPrereqs(args: { collected: Collected; completed: string[]
|
||||
'Prereqs',
|
||||
);
|
||||
|
||||
await stepGate({
|
||||
stepName: 'teams-prereqs',
|
||||
stepDescription: 'confirming they have the right Microsoft 365 tenant and tunnel',
|
||||
reshow: () => confirmPrereqs(args),
|
||||
args,
|
||||
});
|
||||
// Back-aware variant of stepGate — Back is only offered on the very first
|
||||
// step of the Teams flow so users can bail out before any state is taken.
|
||||
while (true) {
|
||||
const choice = ensureAnswer(
|
||||
await brightSelect<'done' | 'help' | 'reshow' | 'back'>({
|
||||
message: 'How did that go?',
|
||||
options: [
|
||||
{ value: 'done', label: "Done — let's continue" },
|
||||
{ value: 'help', label: 'Stuck — hand me off to Claude' },
|
||||
{ value: 'reshow', label: 'Show me the steps again' },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
if (choice === 'back') return 'back';
|
||||
if (choice === 'done') break;
|
||||
if (choice === 'help') {
|
||||
await offerHandoff({
|
||||
step: 'teams-prereqs',
|
||||
stepDescription: 'confirming they have the right Microsoft 365 tenant and tunnel',
|
||||
args,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (choice === 'reshow') {
|
||||
return confirmPrereqs(args);
|
||||
}
|
||||
}
|
||||
args.completed.push('Prereqs confirmed.');
|
||||
return 'continue';
|
||||
}
|
||||
|
||||
// ─── step: public URL ──────────────────────────────────────────────────
|
||||
|
||||
@@ -21,8 +21,10 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { openUrl } from '../lib/browser.js';
|
||||
import { isHeadless } from '../platform.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { confirmThenOpen, formatNoteLink, openUrl } from '../lib/browser.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { askOperatorRole } from '../lib/role-prompt.js';
|
||||
import {
|
||||
type Block,
|
||||
@@ -39,8 +41,10 @@ import { accentGreen, brandBold, fitToWidth, fmtDuration, note } from '../lib/th
|
||||
|
||||
const DEFAULT_AGENT_NAME = 'Nano';
|
||||
|
||||
export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
const token = await collectTelegramToken();
|
||||
export async function runTelegramChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
const tokenOrBack = await collectTelegramToken();
|
||||
if (tokenOrBack === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
const token = tokenOrBack;
|
||||
const botUsername = await validateTelegramToken(token);
|
||||
|
||||
// Deep-link the user into the bot's chat so they're on the right screen
|
||||
@@ -155,17 +159,24 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function collectTelegramToken(): Promise<string> {
|
||||
async function collectTelegramToken(): Promise<string | 'back'> {
|
||||
const existing = readEnvKey('TELEGRAM_BOT_TOKEN');
|
||||
if (existing && /^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(existing)) {
|
||||
const reuse = ensureAnswer(await p.confirm({
|
||||
const choice = ensureAnswer(await brightSelect<'yes' | 'no' | 'back'>({
|
||||
message: `Found an existing Telegram bot token (${existing.slice(0, 8)}…). Use it?`,
|
||||
initialValue: true,
|
||||
options: [
|
||||
{ value: 'yes', label: 'Yes, use the existing token' },
|
||||
{ value: 'no', label: 'No, paste a new one' },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
initialValue: 'yes',
|
||||
}));
|
||||
if (reuse) {
|
||||
if (choice === 'back') return 'back';
|
||||
if (choice === 'yes') {
|
||||
setupLog.userInput('telegram_token', 'reused-existing');
|
||||
return existing;
|
||||
}
|
||||
// 'no' falls through to the paste flow below
|
||||
}
|
||||
|
||||
note(
|
||||
@@ -183,6 +194,19 @@ async function collectTelegramToken(): Promise<string> {
|
||||
'Set up your Telegram bot',
|
||||
);
|
||||
|
||||
// Back-aware gate before the password prompt — `p.password` doesn't
|
||||
// accept extra options, so we offer Back as a separate brightSelect
|
||||
// immediately after the BotFather instructions and before the paste.
|
||||
const proceed = ensureAnswer(await brightSelect<'continue' | 'back'>({
|
||||
message: 'Ready to paste your bot token?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Yes, paste it on the next prompt' },
|
||||
{ value: 'back', label: '← Back to channel selection' },
|
||||
],
|
||||
initialValue: 'continue',
|
||||
}));
|
||||
if (proceed === 'back') return 'back';
|
||||
|
||||
const answer = ensureAnswer(
|
||||
await p.password({
|
||||
message: 'Paste your bot token',
|
||||
|
||||
@@ -33,6 +33,7 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../../src/install-slug.js';
|
||||
import {
|
||||
@@ -53,8 +54,9 @@ const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json');
|
||||
|
||||
type AuthMethod = 'qr' | 'pairing-code';
|
||||
|
||||
export async function runWhatsAppChannel(displayName: string): Promise<void> {
|
||||
export async function runWhatsAppChannel(displayName: string): Promise<ChannelFlowResult> {
|
||||
const method = await askAuthMethod();
|
||||
if (method === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||
const phone = method === 'pairing-code' ? await askPhoneNumber() : undefined;
|
||||
|
||||
const install = await runQuietChild(
|
||||
@@ -148,7 +150,7 @@ export async function runWhatsAppChannel(displayName: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function askAuthMethod(): Promise<AuthMethod> {
|
||||
async function askAuthMethod(): Promise<AuthMethod | 'back'> {
|
||||
const choice = ensureAnswer(
|
||||
await brightSelect({
|
||||
message: 'How would you like to authenticate with WhatsApp?',
|
||||
@@ -163,10 +165,14 @@ async function askAuthMethod(): Promise<AuthMethod> {
|
||||
label: 'Enter a pairing code on your phone',
|
||||
hint: 'no camera needed',
|
||||
},
|
||||
{
|
||||
value: 'back',
|
||||
label: '← Back to channel selection',
|
||||
},
|
||||
],
|
||||
}),
|
||||
) as AuthMethod;
|
||||
setupLog.userInput('whatsapp_auth_method', choice);
|
||||
) as AuthMethod | 'back';
|
||||
if (choice !== 'back') setupLog.userInput('whatsapp_auth_method', choice);
|
||||
return choice;
|
||||
}
|
||||
|
||||
@@ -312,7 +318,7 @@ async function renderQr(qr: string): Promise<string[]> {
|
||||
const QRCode = await import('qrcode');
|
||||
const qrText = await QRCode.toString(qr, { type: 'terminal', small: true });
|
||||
const caption = k.dim(
|
||||
' Open WhatsApp → Settings → Linked Devices → Link a Device → scan.',
|
||||
' Open WhatsApp → You / Settings → Linked Devices → Link a Device → scan.',
|
||||
);
|
||||
return [...qrText.trimEnd().split('\n'), '', caption];
|
||||
} catch {
|
||||
@@ -328,7 +334,7 @@ function formatPairingCard(code: string): string {
|
||||
'',
|
||||
` ${brandBold(spaced)}`,
|
||||
'',
|
||||
k.dim(' Open WhatsApp → Settings → Linked Devices → Link a Device'),
|
||||
k.dim(' Open WhatsApp → You / Settings → Linked Devices → Link a Device'),
|
||||
k.dim(' → "Link with phone number instead" → enter this code.'),
|
||||
k.dim(' It expires in ~60 seconds.'),
|
||||
].join('\n');
|
||||
|
||||
Reference in New Issue
Block a user