setup: add ← Back option to Discord, WhatsApp, iMessage channel flows

Picking the wrong messaging channel during setup left users with no way
to bail out — they had to either complete the chosen flow or kill setup
and start over. This adds a Back option to the first prompt of three
channel sub-flows that share the same simple shape (one leading
brightSelect that's easy to extend).

Mechanics:
- New `setup/lib/back-nav.ts` exports a BACK_TO_CHANNEL_SELECTION
  sentinel and ChannelFlowResult type.
- `setup/auto.ts` wraps the channel dispatch in a while-loop; channels
  return BACK_TO_CHANNEL_SELECTION to bounce back to the chooser
  without restarting setup. Channels not yet wired return void and the
  loop exits after one pass, so the change is backwards compatible.
- Discord, WhatsApp, iMessage each add a `← Back to channel selection`
  option to their first prompt.

Telegram, Slack, Teams, and Signal will follow as separate PRs — they
each need a slightly different shape (extra prompt insertions, gating
inside multi-step flows, etc.) and are easier to review independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
exe.dev user
2026-05-05 09:20:17 +00:00
parent 948a0dcada
commit c795ecff6e
5 changed files with 104 additions and 60 deletions

View File

@@ -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> {

View File

@@ -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;
}

View File

@@ -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;
}