diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts index 6a9721b9..36ff659e 100644 --- a/packages/cli/src/setup-agents.ts +++ b/packages/cli/src/setup-agents.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { cancel, isCancel, multiselect, select } from '@clack/prompts'; +import { cancel, confirm, isCancel, multiselect, select } from '@clack/prompts'; import { loadKtxProject, markKtxSetupStateStepComplete, @@ -277,12 +277,23 @@ function createPromptAdapter(): KtxSetupAgentsPromptAdapter { return String(value); }, async multiselect(options) { - const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); - if (isCancel(value)) { - cancel('Setup cancelled.'); - return ['back']; + while (true) { + const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); + if (isCancel(value)) { + cancel('Setup cancelled.'); + return ['back']; + } + const selected = [...value] as string[]; + if (selected.length === 0 && !options.required) { + const skipConfirmed = await confirm({ message: 'Nothing selected. Skip this step?', initialValue: false }); + if (isCancel(skipConfirmed)) { + cancel('Setup cancelled.'); + return ['back']; + } + if (!skipConfirmed) continue; + } + return selected; } - return [...value] as string[]; }, cancel(message) { cancel(message); diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts index 18ff7e74..caac2841 100644 --- a/packages/cli/src/setup-databases.ts +++ b/packages/cli/src/setup-databases.ts @@ -1,5 +1,5 @@ import { writeFile } from 'node:fs/promises'; -import { cancel, isCancel, multiselect, password, select, text } from '@clack/prompts'; +import { cancel, confirm, isCancel, multiselect, password, select, text } from '@clack/prompts'; import type { HistoricSqlDialect } from '@ktx/context/ingest'; import { type KtxProjectConnectionConfig, @@ -203,12 +203,23 @@ function missingConnectionDetailsPrompt( function createPromptAdapter(): KtxSetupDatabasesPromptAdapter { return { async multiselect(options) { - const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); - if (isCancel(value)) { - cancel('Setup cancelled.'); - return ['back']; + while (true) { + const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); + if (isCancel(value)) { + cancel('Setup cancelled.'); + return ['back']; + } + const selected = [...value] as string[]; + if (selected.length === 0 && !options.required) { + const skipConfirmed = await confirm({ message: 'Nothing selected. Skip this step?', initialValue: false }); + if (isCancel(skipConfirmed)) { + cancel('Setup cancelled.'); + return ['back']; + } + if (!skipConfirmed) continue; + } + return selected; } - return [...value] as string[]; }, async select(options) { const value = await withSetupInterruptConfirmation(() => select(withMenuOptionsSpacing(options))); diff --git a/packages/cli/src/setup-sources.ts b/packages/cli/src/setup-sources.ts index dc010b0a..695fc1c1 100644 --- a/packages/cli/src/setup-sources.ts +++ b/packages/cli/src/setup-sources.ts @@ -2,7 +2,7 @@ import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, relative, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { cancel, isCancel, log, multiselect, password, select, text } from '@clack/prompts'; +import { cancel, confirm, isCancel, log, multiselect, password, select, text } from '@clack/prompts'; import { localConnectionTypeForConfig, resolveNotionAuthToken } from '@ktx/context/connections'; import { resolveKtxConfigReference } from '@ktx/context/core'; import { @@ -136,12 +136,23 @@ const PRIMARY_SOURCE_DRIVERS = new Set([ function createPromptAdapter(): KtxSetupSourcesPromptAdapter { return { async multiselect(options) { - const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); - if (isCancel(value)) { - cancel('Setup cancelled.'); - return ['back']; + while (true) { + const value = await withSetupInterruptConfirmation(() => multiselect(withMenuOptionsSpacing(options))); + if (isCancel(value)) { + cancel('Setup cancelled.'); + return ['back']; + } + const selected = [...value] as string[]; + if (selected.length === 0 && !options.required) { + const skipConfirmed = await confirm({ message: 'Nothing selected. Skip this step?', initialValue: false }); + if (isCancel(skipConfirmed)) { + cancel('Setup cancelled.'); + return ['back']; + } + if (!skipConfirmed) continue; + } + return selected; } - return [...value] as string[]; }, async select(options) { const value = await withSetupInterruptConfirmation(() => select(withMenuOptionsSpacing(options)));