setup: add back-to-channels exit at every Teams step gate
Teams setup is 6+ Azure steps over 30+ minutes. Today, every "Done / Stuck / Show again" gate forces continuation; the only escape is Ctrl-C, which kills setup entirely. Add a fourth option at each gate that returns to the channel picker so a stuck operator can pick a different channel without losing the rest of setup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,12 +95,25 @@ export async function runTeamsChannel(_displayName: string): Promise<ChannelFlow
|
|||||||
const prereqsResult = await confirmPrereqs({ collected, completed });
|
const prereqsResult = await confirmPrereqs({ collected, completed });
|
||||||
if (prereqsResult === 'back') return BACK_TO_CHANNEL_SELECTION;
|
if (prereqsResult === 'back') return BACK_TO_CHANNEL_SELECTION;
|
||||||
await stepPublicUrl({ collected, completed });
|
await stepPublicUrl({ collected, completed });
|
||||||
await stepAppRegistration({ collected, completed });
|
if (await stepAppRegistration({ collected, completed }) === 'back') {
|
||||||
await stepClientSecret({ collected, completed });
|
return BACK_TO_CHANNEL_SELECTION;
|
||||||
await stepAzureBot({ collected, completed });
|
}
|
||||||
await stepEnableTeamsChannel({ collected, completed });
|
if (await stepClientSecret({ collected, completed }) === 'back') {
|
||||||
|
return BACK_TO_CHANNEL_SELECTION;
|
||||||
|
}
|
||||||
|
if (await stepAzureBot({ collected, completed }) === 'back') {
|
||||||
|
return BACK_TO_CHANNEL_SELECTION;
|
||||||
|
}
|
||||||
|
if (await stepEnableTeamsChannel({ collected, completed }) === 'back') {
|
||||||
|
return BACK_TO_CHANNEL_SELECTION;
|
||||||
|
}
|
||||||
const manifestResult = await stepGenerateManifest({ collected, completed });
|
const manifestResult = await stepGenerateManifest({ collected, completed });
|
||||||
await stepSideload({ collected, completed, zipPath: manifestResult.zipPath });
|
if (
|
||||||
|
await stepSideload({ collected, completed, zipPath: manifestResult.zipPath })
|
||||||
|
=== 'back'
|
||||||
|
) {
|
||||||
|
return BACK_TO_CHANNEL_SELECTION;
|
||||||
|
}
|
||||||
|
|
||||||
await installAdapter(collected);
|
await installAdapter(collected);
|
||||||
completed.push('Adapter installed and service restarted.');
|
completed.push('Adapter installed and service restarted.');
|
||||||
@@ -229,7 +242,7 @@ async function stepPublicUrl(args: { collected: Collected; completed: string[] }
|
|||||||
async function stepAppRegistration(args: {
|
async function stepAppRegistration(args: {
|
||||||
collected: Collected;
|
collected: Collected;
|
||||||
completed: string[];
|
completed: string[];
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
`1. In ${AZURE_PORTAL_URL}, search "App registrations" → "New registration"`,
|
`1. In ${AZURE_PORTAL_URL}, search "App registrations" → "New registration"`,
|
||||||
@@ -262,15 +275,17 @@ async function stepAppRegistration(args: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await stepGate({
|
const gate = await stepGate({
|
||||||
stepName: 'teams-app-registration',
|
stepName: 'teams-app-registration',
|
||||||
stepDescription: 'registering an app in Azure and collecting App ID + tenant type',
|
stepDescription: 'registering an app in Azure and collecting App ID + tenant type',
|
||||||
reshow: () => stepAppRegistration(args),
|
reshow: () => stepAppRegistration(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
if (gate === 'back') return 'back';
|
||||||
args.completed.push(
|
args.completed.push(
|
||||||
`App registered: ${args.collected.appId} (${args.collected.appType})`,
|
`App registered: ${args.collected.appId} (${args.collected.appType})`,
|
||||||
);
|
);
|
||||||
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askAppType(args: {
|
async function askAppType(args: {
|
||||||
@@ -313,7 +328,7 @@ async function askAppType(args: {
|
|||||||
async function stepClientSecret(args: {
|
async function stepClientSecret(args: {
|
||||||
collected: Collected;
|
collected: Collected;
|
||||||
completed: string[];
|
completed: string[];
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
`1. In your app registration, open "Certificates & secrets"`,
|
`1. In your app registration, open "Certificates & secrets"`,
|
||||||
@@ -356,13 +371,15 @@ async function stepClientSecret(args: {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await stepGate({
|
const gate = await stepGate({
|
||||||
stepName: 'teams-client-secret',
|
stepName: 'teams-client-secret',
|
||||||
stepDescription: 'creating and copying the client secret',
|
stepDescription: 'creating and copying the client secret',
|
||||||
reshow: () => stepClientSecret(args),
|
reshow: () => stepClientSecret(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
if (gate === 'back') return 'back';
|
||||||
args.completed.push('Client secret captured.');
|
args.completed.push('Client secret captured.');
|
||||||
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── step: Azure Bot resource ──────────────────────────────────────────
|
// ─── step: Azure Bot resource ──────────────────────────────────────────
|
||||||
@@ -370,7 +387,7 @@ async function stepClientSecret(args: {
|
|||||||
async function stepAzureBot(args: {
|
async function stepAzureBot(args: {
|
||||||
collected: Collected;
|
collected: Collected;
|
||||||
completed: string[];
|
completed: string[];
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
const endpoint = `${args.collected.publicUrl}/api/webhooks/teams`;
|
const endpoint = `${args.collected.publicUrl}/api/webhooks/teams`;
|
||||||
const tenantFlag =
|
const tenantFlag =
|
||||||
args.collected.appType === 'SingleTenant'
|
args.collected.appType === 'SingleTenant'
|
||||||
@@ -405,14 +422,16 @@ async function stepAzureBot(args: {
|
|||||||
'Step 3 of 6 — Create Azure Bot resource',
|
'Step 3 of 6 — Create Azure Bot resource',
|
||||||
);
|
);
|
||||||
|
|
||||||
await stepGate({
|
const gate = await stepGate({
|
||||||
stepName: 'teams-azure-bot',
|
stepName: 'teams-azure-bot',
|
||||||
stepDescription:
|
stepDescription:
|
||||||
'creating an Azure Bot resource linked to the app registration and setting the messaging endpoint',
|
'creating an Azure Bot resource linked to the app registration and setting the messaging endpoint',
|
||||||
reshow: () => stepAzureBot(args),
|
reshow: () => stepAzureBot(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
if (gate === 'back') return 'back';
|
||||||
args.completed.push('Azure Bot created; messaging endpoint configured.');
|
args.completed.push('Azure Bot created; messaging endpoint configured.');
|
||||||
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── step: enable Teams channel ────────────────────────────────────────
|
// ─── step: enable Teams channel ────────────────────────────────────────
|
||||||
@@ -420,7 +439,7 @@ async function stepAzureBot(args: {
|
|||||||
async function stepEnableTeamsChannel(args: {
|
async function stepEnableTeamsChannel(args: {
|
||||||
collected: Collected;
|
collected: Collected;
|
||||||
completed: string[];
|
completed: string[];
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
'1. Open your Azure Bot resource → Channels',
|
'1. Open your Azure Bot resource → Channels',
|
||||||
@@ -431,13 +450,15 @@ async function stepEnableTeamsChannel(args: {
|
|||||||
].join('\n'),
|
].join('\n'),
|
||||||
'Step 4 of 6 — Enable Teams channel on the bot',
|
'Step 4 of 6 — Enable Teams channel on the bot',
|
||||||
);
|
);
|
||||||
await stepGate({
|
const gate = await stepGate({
|
||||||
stepName: 'teams-enable-channel',
|
stepName: 'teams-enable-channel',
|
||||||
stepDescription: 'enabling the Microsoft Teams channel on the Azure Bot resource',
|
stepDescription: 'enabling the Microsoft Teams channel on the Azure Bot resource',
|
||||||
reshow: () => stepEnableTeamsChannel(args),
|
reshow: () => stepEnableTeamsChannel(args),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
if (gate === 'back') return 'back';
|
||||||
args.completed.push('Teams channel enabled on the bot.');
|
args.completed.push('Teams channel enabled on the bot.');
|
||||||
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── step: manifest zip ────────────────────────────────────────────────
|
// ─── step: manifest zip ────────────────────────────────────────────────
|
||||||
@@ -490,7 +511,7 @@ async function stepSideload(args: {
|
|||||||
collected: Collected;
|
collected: Collected;
|
||||||
completed: string[];
|
completed: string[];
|
||||||
zipPath: string;
|
zipPath: string;
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
'1. Open Microsoft Teams',
|
'1. Open Microsoft Teams',
|
||||||
@@ -505,13 +526,15 @@ async function stepSideload(args: {
|
|||||||
].join('\n'),
|
].join('\n'),
|
||||||
'Step 5 of 6 — Sideload the app into Teams',
|
'Step 5 of 6 — Sideload the app into Teams',
|
||||||
);
|
);
|
||||||
await stepGate({
|
const gate = await stepGate({
|
||||||
stepName: 'teams-sideload',
|
stepName: 'teams-sideload',
|
||||||
stepDescription: 'uploading the generated zip into Teams as a custom app',
|
stepDescription: 'uploading the generated zip into Teams as a custom app',
|
||||||
reshow: () => stepSideload(args),
|
reshow: () => stepSideload({ ...args, zipPath: args.zipPath }),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
if (gate === 'back') return 'back';
|
||||||
args.completed.push('App sideloaded into Teams.');
|
args.completed.push('App sideloaded into Teams.');
|
||||||
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── step: install adapter ─────────────────────────────────────────────
|
// ─── step: install adapter ─────────────────────────────────────────────
|
||||||
@@ -623,9 +646,9 @@ async function finishWithHandoff(
|
|||||||
async function stepGate(args: {
|
async function stepGate(args: {
|
||||||
stepName: string;
|
stepName: string;
|
||||||
stepDescription: string;
|
stepDescription: string;
|
||||||
reshow: () => Promise<void> | Promise<unknown>;
|
reshow: () => Promise<'continue' | 'back'>;
|
||||||
args: { collected: Collected; completed: string[] };
|
args: { collected: Collected; completed: string[] };
|
||||||
}): Promise<void> {
|
}): Promise<'continue' | 'back'> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const choice = ensureAnswer(
|
const choice = ensureAnswer(
|
||||||
await brightSelect({
|
await brightSelect({
|
||||||
@@ -634,10 +657,12 @@ async function stepGate(args: {
|
|||||||
{ value: 'done', label: "Done — let's continue" },
|
{ value: 'done', label: "Done — let's continue" },
|
||||||
{ value: 'help', label: 'Stuck — hand me off to Claude' },
|
{ value: 'help', label: 'Stuck — hand me off to Claude' },
|
||||||
{ value: 'reshow', label: 'Show me the steps again' },
|
{ value: 'reshow', label: 'Show me the steps again' },
|
||||||
|
{ value: 'back', label: '← Back to channel selection' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (choice === 'done') return;
|
if (choice === 'done') return 'continue';
|
||||||
|
if (choice === 'back') return 'back';
|
||||||
if (choice === 'help') {
|
if (choice === 'help') {
|
||||||
await offerHandoff({
|
await offerHandoff({
|
||||||
step: args.stepName,
|
step: args.stepName,
|
||||||
@@ -647,8 +672,7 @@ async function stepGate(args: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (choice === 'reshow') {
|
if (choice === 'reshow') {
|
||||||
await args.reshow();
|
return args.reshow();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user