setup: add ← Back option to Teams channel flow

Stacked on #2269 (back-nav scaffolding) plus the Telegram and Slack
PRs. They share the same scaffolding file from #2269 — they don't
compile without it, so they have to stack.

Both Teams paths already had a brightSelect at the right place, so we
just extend each with a Back option — no new prompts:

- Existing-credentials path: Yes/No confirm becomes Yes/No/Back
- Fresh-setup path: the very first stepGate ("How did that go?") gets
  a 4th option. Subsequent stepGates keep the original 3 options so
  we never lose mid-flow state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
exe.dev user
2026-05-05 09:47:17 +00:00
parent 6a54b69912
commit c44c7a6669
2 changed files with 44 additions and 13 deletions

View File

@@ -462,7 +462,7 @@ async function main(): Promise<void> {
} else if (channelChoice === 'signal') { } else if (channelChoice === 'signal') {
await runSignalChannel(displayName!); await runSignalChannel(displayName!);
} else if (channelChoice === 'teams') { } else if (channelChoice === 'teams') {
await runTeamsChannel(displayName!); result = await runTeamsChannel(displayName!);
} else if (channelChoice === 'slack') { } else if (channelChoice === 'slack') {
result = await runSlackChannel(displayName!); result = await runSlackChannel(displayName!);
} else if (channelChoice === 'imessage') { } else if (channelChoice === 'imessage') {

View File

@@ -30,6 +30,7 @@ import path from 'path';
import * as p from '@clack/prompts'; import * as p from '@clack/prompts';
import k from 'kleur'; import k from 'kleur';
import { BACK_TO_CHANNEL_SELECTION, type ChannelFlowResult } from '../lib/back-nav.js';
import { brightSelect } from '../lib/bright-select.js'; import { brightSelect } from '../lib/bright-select.js';
import { confirmThenOpen } from '../lib/browser.js'; import { confirmThenOpen } from '../lib/browser.js';
import { import {
@@ -57,18 +58,24 @@ interface Collected {
agentName?: string; agentName?: string;
} }
export async function runTeamsChannel(_displayName: string): Promise<void> { export async function runTeamsChannel(_displayName: string): Promise<ChannelFlowResult> {
const collected: Collected = {}; const collected: Collected = {};
const completed: string[] = []; const completed: string[] = [];
const existingAppId = readEnvKey('TEAMS_APP_ID'); const existingAppId = readEnvKey('TEAMS_APP_ID');
const existingPassword = readEnvKey('TEAMS_APP_PASSWORD'); const existingPassword = readEnvKey('TEAMS_APP_PASSWORD');
if (existingAppId && existingPassword) { 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?`, 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.appId = existingAppId;
collected.appPassword = existingPassword; collected.appPassword = existingPassword;
collected.appType = (readEnvKey('TEAMS_APP_TYPE') as 'SingleTenant' | 'MultiTenant') || 'MultiTenant'; collected.appType = (readEnvKey('TEAMS_APP_TYPE') as 'SingleTenant' | 'MultiTenant') || 'MultiTenant';
@@ -85,7 +92,8 @@ export async function runTeamsChannel(_displayName: string): Promise<void> {
printIntro(); printIntro();
await confirmPrereqs({ collected, completed }); const prereqsResult = await confirmPrereqs({ collected, completed });
if (prereqsResult === 'back') return BACK_TO_CHANNEL_SELECTION;
await stepPublicUrl({ collected, completed }); await stepPublicUrl({ collected, completed });
await stepAppRegistration({ collected, completed }); await stepAppRegistration({ collected, completed });
await stepClientSecret({ 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( note(
[ [
'Before we start, confirm you have:', 'Before we start, confirm you have:',
@@ -131,13 +139,36 @@ async function confirmPrereqs(args: { collected: Collected; completed: string[]
'Prereqs', 'Prereqs',
); );
await stepGate({ // Back-aware variant of stepGate — Back is only offered on the very first
stepName: 'teams-prereqs', // step of the Teams flow so users can bail out before any state is taken.
stepDescription: 'confirming they have the right Microsoft 365 tenant and tunnel', while (true) {
reshow: () => confirmPrereqs(args), const choice = ensureAnswer(
args, 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.'); args.completed.push('Prereqs confirmed.');
return 'continue';
} }
// ─── step: public URL ────────────────────────────────────────────────── // ─── step: public URL ──────────────────────────────────────────────────