mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
Merge pull request #2269 from alipgoldberg/setup-channel-back-nav-pr1
setup: add ← Back option to Discord, WhatsApp, iMessage channel flows
This commit is contained in:
+39
-28
@@ -29,6 +29,7 @@ import path from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import { BACK_TO_CHANNEL_SELECTION } from './lib/back-nav.js';
|
||||
import { runDiscordChannel } from './channels/discord.js';
|
||||
import { runIMessageChannel } from './channels/imessage.js';
|
||||
import { runSignalChannel } from './channels/signal.js';
|
||||
@@ -440,35 +441,45 @@ async function main(): Promise<void> {
|
||||
let channelChoice: ChannelChoice = 'skip';
|
||||
|
||||
if (!skip.has('channel')) {
|
||||
channelChoice = await askChannelChoice();
|
||||
if (channelChoice !== 'skip' && channelChoice !== 'other') {
|
||||
await resolveDisplayName();
|
||||
}
|
||||
if (channelChoice === 'telegram') {
|
||||
await runTelegramChannel(displayName!);
|
||||
} else if (channelChoice === 'discord') {
|
||||
await runDiscordChannel(displayName!);
|
||||
} else if (channelChoice === 'whatsapp') {
|
||||
await runWhatsAppChannel(displayName!);
|
||||
} else if (channelChoice === 'signal') {
|
||||
await runSignalChannel(displayName!);
|
||||
} else if (channelChoice === 'teams') {
|
||||
await runTeamsChannel(displayName!);
|
||||
} else if (channelChoice === 'slack') {
|
||||
await runSlackChannel(displayName!);
|
||||
} else if (channelChoice === 'imessage') {
|
||||
await runIMessageChannel(displayName!);
|
||||
} else if (channelChoice === 'other') {
|
||||
await askOtherChannelName();
|
||||
} else {
|
||||
p.log.info(
|
||||
brandBody(
|
||||
wrapForGutter(
|
||||
'No messaging app for now. You can add one later (like Telegram, Discord, WhatsApp, Teams, Slack, or iMessage).',
|
||||
4,
|
||||
// Loop so a channel sub-flow can return BACK_TO_CHANNEL_SELECTION on
|
||||
// its first prompt and bounce the user back to the chooser without
|
||||
// restarting setup. Channels not yet wired with the back option just
|
||||
// return void and the loop exits after one pass.
|
||||
let backed = true;
|
||||
while (backed) {
|
||||
backed = false;
|
||||
channelChoice = await askChannelChoice();
|
||||
if (channelChoice !== 'skip' && channelChoice !== 'other') {
|
||||
await resolveDisplayName();
|
||||
}
|
||||
let result: void | typeof BACK_TO_CHANNEL_SELECTION;
|
||||
if (channelChoice === 'telegram') {
|
||||
await runTelegramChannel(displayName!);
|
||||
} else if (channelChoice === 'discord') {
|
||||
result = await runDiscordChannel(displayName!);
|
||||
} else if (channelChoice === 'whatsapp') {
|
||||
result = await runWhatsAppChannel(displayName!);
|
||||
} else if (channelChoice === 'signal') {
|
||||
await runSignalChannel(displayName!);
|
||||
} else if (channelChoice === 'teams') {
|
||||
await runTeamsChannel(displayName!);
|
||||
} else if (channelChoice === 'slack') {
|
||||
await runSlackChannel(displayName!);
|
||||
} else if (channelChoice === 'imessage') {
|
||||
result = await runIMessageChannel(displayName!);
|
||||
} else if (channelChoice === 'other') {
|
||||
await askOtherChannelName();
|
||||
} else {
|
||||
p.log.info(
|
||||
brandBody(
|
||||
wrapForGutter(
|
||||
'No messaging app for now. You can add one later (like Telegram, Discord, WhatsApp, Teams, Slack, or iMessage).',
|
||||
4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
if (result === BACK_TO_CHANNEL_SELECTION) backed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
+30
-24
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Channel-flow back-navigation sentinel.
|
||||
*
|
||||
* Each `runXxxChannel(displayName)` in `setup/channels/` may return either
|
||||
* `void` (sub-flow completed normally) or `BACK_TO_CHANNEL_SELECTION` to
|
||||
* signal "the user picked '← Back to channel selection' on my first
|
||||
* prompt; please re-run the channel chooser." `setup/auto.ts` catches
|
||||
* that signal and loops back to `askChannelChoice()`.
|
||||
*
|
||||
* Back is only offered on the *first* interactive prompt of each channel
|
||||
* sub-flow — once the user has answered something, they're committed
|
||||
* (subsequent steps may have side effects like opening browsers, hitting
|
||||
* APIs, or installing adapter packages, none of which are easily undone).
|
||||
*/
|
||||
export const BACK_TO_CHANNEL_SELECTION = Symbol('BACK_TO_CHANNEL_SELECTION');
|
||||
|
||||
export type ChannelFlowResult = void | typeof BACK_TO_CHANNEL_SELECTION;
|
||||
Reference in New Issue
Block a user