6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
509 lines
18 KiB
TypeScript
509 lines
18 KiB
TypeScript
/**
|
|
* Generic prompt rendering and environment transitions for the CLI setup catalog.
|
|
*
|
|
* @packageDocumentation
|
|
*/
|
|
import {
|
|
EnvCapabilityConfigurationError,
|
|
type EnvCapabilityValue,
|
|
type EnvCapabilityValues,
|
|
getProviderFields,
|
|
hasEnvCapabilityValue,
|
|
inspectCapability,
|
|
isTruthyEnvCapabilityValue,
|
|
validateCapabilityFieldInput,
|
|
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
|
|
import {
|
|
type CapabilitySetupDefinition,
|
|
matchesSetupCondition,
|
|
type SetupCondition,
|
|
type SetupHint,
|
|
type SetupPrompt,
|
|
} from './capability-config.ts'
|
|
import * as p from './prompter.ts'
|
|
|
|
export interface CapabilitySetupContext {
|
|
containerized: boolean
|
|
}
|
|
|
|
export interface EnvCapabilitySetupTransition {
|
|
values: Record<string, string>
|
|
remove: readonly string[]
|
|
}
|
|
|
|
interface ResolvedSetupOption {
|
|
id: string
|
|
label: string
|
|
hint?: SetupHint
|
|
kind: 'provider' | 'default' | 'action'
|
|
providerId?: string
|
|
env: Readonly<Record<string, string>>
|
|
prompts: readonly SetupPrompt[]
|
|
currentWhen?: SetupCondition
|
|
}
|
|
|
|
interface PromptState {
|
|
setup: CapabilitySetupDefinition
|
|
optionId: string
|
|
currentValues: ReadonlyMap<string, string>
|
|
values: Record<string, string>
|
|
}
|
|
|
|
/** Stages a capability transition into a larger setup run without losing prompt context. */
|
|
export function stageCapabilitySetupTransition(
|
|
currentValues: Map<string, string>,
|
|
values: Record<string, string>,
|
|
remove: Set<string>,
|
|
transition: EnvCapabilitySetupTransition
|
|
): void {
|
|
for (const key of transition.remove) {
|
|
currentValues.delete(key)
|
|
Reflect.deleteProperty(values, key)
|
|
remove.add(key)
|
|
}
|
|
for (const [key, value] of Object.entries(transition.values)) {
|
|
currentValues.set(key, value)
|
|
values[key] = value
|
|
remove.delete(key)
|
|
}
|
|
}
|
|
|
|
function resolveHint(
|
|
hint: SetupHint | undefined,
|
|
context: CapabilitySetupContext
|
|
): string | undefined {
|
|
if (!hint || typeof hint === 'string') return hint
|
|
return context.containerized ? hint.containerized : hint.development
|
|
}
|
|
|
|
/** Adds the standard status marker without discarding an option's explanatory hint. */
|
|
export function markCurrentlyUsed(hint: string | undefined, current: boolean): string | undefined {
|
|
if (!current) return hint
|
|
return hint ? `${hint} · Currently used` : 'Currently used'
|
|
}
|
|
|
|
function promptKeys(prompts: readonly SetupPrompt[] = []): string[] {
|
|
return prompts.flatMap((prompt) => {
|
|
if (prompt.type === 'field' || prompt.type === 'confirm') return [prompt.key]
|
|
return prompt.options.flatMap((option) => [
|
|
...Object.keys(option.env ?? {}),
|
|
...promptKeys(option.prompts),
|
|
])
|
|
})
|
|
}
|
|
|
|
function providerSetupFields(
|
|
setup: CapabilitySetupDefinition,
|
|
providerId: string
|
|
): readonly string[] {
|
|
const provider = setup.definition.providers.find((candidate) => candidate.id === providerId)
|
|
if (!provider) throw new Error(`Capability ${setup.definition.id} has no provider ${providerId}`)
|
|
const providerSetup = setup.providers[providerId]
|
|
if (!providerSetup) throw new Error(`Setup ${setup.definition.id} has no provider ${providerId}`)
|
|
return [
|
|
...(provider.activation.mode === 'enabled' ? [provider.activation.key] : []),
|
|
...Object.keys(providerSetup.env ?? {}),
|
|
...promptKeys(providerSetup.prompts),
|
|
]
|
|
}
|
|
|
|
/** Returns fields the setup flow writes or prompts for, including inferred selectors and flags. */
|
|
export function getCapabilitySetupFields(setup: CapabilitySetupDefinition): readonly string[] {
|
|
return [
|
|
...new Set([
|
|
...(setup.definition.strategy === 'selected' && setup.definition.selectorKey
|
|
? [setup.definition.selectorKey]
|
|
: []),
|
|
...setup.definition.providers.flatMap((provider) =>
|
|
provider.activation.mode === 'enabled' ? [provider.activation.key] : []
|
|
),
|
|
...Object.entries(setup.providers).flatMap(([providerId]) =>
|
|
providerSetupFields(setup, providerId)
|
|
),
|
|
...Object.values(setup.actions).flatMap((action) => Object.keys(action.env ?? {})),
|
|
]),
|
|
]
|
|
}
|
|
|
|
/** Expands the runtime providers and CLI-only actions into renderable options. */
|
|
export function getCapabilitySetupOptions(
|
|
setup: CapabilitySetupDefinition
|
|
): readonly ResolvedSetupOption[] {
|
|
const definition = setup.definition
|
|
const options: ResolvedSetupOption[] = definition.providers.map((provider) => {
|
|
const providerSetup = setup.providers[provider.id]
|
|
if (!providerSetup) throw new Error(`Setup ${definition.id} has no provider ${provider.id}`)
|
|
return {
|
|
id: provider.id,
|
|
label: provider.label,
|
|
hint: providerSetup.hint,
|
|
kind: 'provider',
|
|
providerId: provider.id,
|
|
env: providerSetup.env ?? {},
|
|
prompts: providerSetup.prompts,
|
|
currentWhen: providerSetup.currentWhen,
|
|
}
|
|
})
|
|
|
|
if (definition.strategy === 'selected' && definition.defaultProvider.kind === 'built-in') {
|
|
options.push({
|
|
id: definition.defaultProvider.id,
|
|
label: definition.defaultProvider.label,
|
|
hint: setup.defaultOption?.hint,
|
|
kind: 'default',
|
|
env: {},
|
|
prompts: [],
|
|
})
|
|
}
|
|
|
|
for (const [id, action] of Object.entries(setup.actions)) {
|
|
options.push({
|
|
id,
|
|
label: action.label,
|
|
hint: action.hint,
|
|
kind: 'action',
|
|
env: action.env ?? {},
|
|
prompts: [],
|
|
currentWhen: action.currentWhen,
|
|
})
|
|
}
|
|
|
|
const byId = new Map(options.map((option) => [option.id, option]))
|
|
return setup.optionOrder.map((id) => {
|
|
const option = byId.get(String(id))
|
|
if (!option) throw new Error(`Setup ${definition.id} option order references ${String(id)}`)
|
|
return option
|
|
})
|
|
}
|
|
|
|
/** Resolves the setup option representing the effective current configuration. */
|
|
export function resolveCurrentCapabilitySetupOptionId(
|
|
setup: CapabilitySetupDefinition,
|
|
values: EnvCapabilityValues
|
|
): string {
|
|
const options = getCapabilitySetupOptions(setup)
|
|
const explicitAction = options.find(
|
|
(option) =>
|
|
option.kind === 'action' &&
|
|
option.currentWhen &&
|
|
matchesSetupCondition(option.currentWhen, values)
|
|
)
|
|
if (explicitAction) return explicitAction.id
|
|
|
|
const inspection = inspectCapability(setup.definition, values)
|
|
const selectedProviderId =
|
|
inspection.strategy === 'selected'
|
|
? (inspection.providerId ?? inspection.providers.find((provider) => provider.active)?.id)
|
|
: (inspection.providerIds[0] ?? inspection.providers.find((provider) => provider.active)?.id)
|
|
if (selectedProviderId) {
|
|
const provider = options.find(
|
|
(option) =>
|
|
option.kind === 'provider' &&
|
|
option.providerId === selectedProviderId &&
|
|
(!option.currentWhen || matchesSetupCondition(option.currentWhen, values))
|
|
)
|
|
if (provider) return provider.id
|
|
}
|
|
|
|
if (
|
|
setup.definition.strategy === 'selected' &&
|
|
setup.definition.defaultProvider.kind === 'built-in'
|
|
) {
|
|
return setup.definition.defaultProvider.id
|
|
}
|
|
|
|
const firstAction = options.find((option) => option.kind === 'action')
|
|
if (firstAction) return firstAction.id
|
|
throw new Error(
|
|
`Capability ${setup.definition.id} has no setup option for its current configuration`
|
|
)
|
|
}
|
|
|
|
/** Applies selector and activation inference to the CLI-entered values. */
|
|
export function getCapabilitySetupDraftValues(
|
|
setup: CapabilitySetupDefinition,
|
|
optionId: string,
|
|
promptedValues: Readonly<Record<string, string>>
|
|
): Record<string, string> {
|
|
const definition = setup.definition
|
|
const option = getCapabilitySetupOptions(setup).find((candidate) => candidate.id === optionId)
|
|
if (!option) throw new Error(`Capability ${definition.id} has no setup option ${optionId}`)
|
|
|
|
const values: Record<string, string> = { ...option.env }
|
|
if (definition.strategy === 'selected' && definition.selectorKey) {
|
|
if (option.providerId) values[definition.selectorKey] = option.providerId
|
|
else if (option.kind === 'default' && option.id === definition.defaultProvider.id) {
|
|
values[definition.selectorKey] = option.id
|
|
}
|
|
}
|
|
for (const provider of definition.providers) {
|
|
if (provider.activation.mode !== 'enabled') continue
|
|
values[provider.activation.key] = provider.id === option.providerId ? 'true' : 'false'
|
|
}
|
|
Object.assign(values, promptedValues)
|
|
return values
|
|
}
|
|
|
|
function copyValues(values: EnvCapabilityValues): Record<string, EnvCapabilityValue> {
|
|
return values instanceof Map
|
|
? Object.fromEntries(values)
|
|
: { ...(values as Readonly<Record<string, EnvCapabilityValue>>) }
|
|
}
|
|
|
|
function fieldsToReplace(
|
|
setup: CapabilitySetupDefinition,
|
|
option: ResolvedSetupOption
|
|
): readonly string[] {
|
|
if (setup.definition.strategy === 'fallback' && option.providerId) {
|
|
return providerSetupFields(setup, option.providerId)
|
|
}
|
|
const selectedProviderFields = new Set(
|
|
setup.definition.providers
|
|
.filter((provider) => provider.id === option.providerId)
|
|
.flatMap(getProviderFields)
|
|
)
|
|
const inactiveProviderFields = setup.definition.providers
|
|
.filter((provider) => provider.id !== option.providerId)
|
|
.flatMap(getProviderFields)
|
|
.filter((field) => !selectedProviderFields.has(field))
|
|
return [...new Set([...getCapabilitySetupFields(setup), ...inactiveProviderFields])]
|
|
}
|
|
|
|
function proposedCapabilitySetupValues(
|
|
setup: CapabilitySetupDefinition,
|
|
option: ResolvedSetupOption,
|
|
values: Readonly<Record<string, string>>,
|
|
currentValues: EnvCapabilityValues
|
|
): Record<string, EnvCapabilityValue> {
|
|
const proposed = copyValues(currentValues)
|
|
for (const key of fieldsToReplace(setup, option)) Reflect.deleteProperty(proposed, key)
|
|
Object.assign(proposed, values)
|
|
return proposed
|
|
}
|
|
|
|
/** Returns provider requirements discovered after earlier prompts have been answered. */
|
|
export function getCapabilitySetupProviderMissingFields(
|
|
setup: CapabilitySetupDefinition,
|
|
optionId: string,
|
|
promptedValues: Readonly<Record<string, string>>,
|
|
currentValues: EnvCapabilityValues
|
|
): readonly string[] {
|
|
const option = getCapabilitySetupOptions(setup).find((candidate) => candidate.id === optionId)
|
|
if (!option?.providerId) return []
|
|
const values = getCapabilitySetupDraftValues(setup, optionId, promptedValues)
|
|
const inspection = inspectCapability(
|
|
setup.definition,
|
|
proposedCapabilitySetupValues(setup, option, values, currentValues)
|
|
)
|
|
return (
|
|
inspection.providers.find((provider) => provider.id === option.providerId)?.missingFields ?? []
|
|
)
|
|
}
|
|
|
|
/** Builds and validates the complete environment transition for one setup option. */
|
|
export function buildCapabilitySetupTransition(
|
|
setup: CapabilitySetupDefinition,
|
|
optionId: string,
|
|
promptedValues: Readonly<Record<string, string>>,
|
|
currentValues: EnvCapabilityValues
|
|
): EnvCapabilitySetupTransition {
|
|
const definition = setup.definition
|
|
const option = getCapabilitySetupOptions(setup).find((candidate) => candidate.id === optionId)
|
|
if (!option) throw new Error(`Capability ${definition.id} has no setup option ${optionId}`)
|
|
|
|
const values = getCapabilitySetupDraftValues(setup, optionId, promptedValues)
|
|
const ownedFields = getCapabilitySetupFields(setup)
|
|
const unexpected = Object.keys(values).filter((key) => !ownedFields.includes(key))
|
|
if (unexpected.length > 0) {
|
|
throw new Error(
|
|
`Capability ${definition.id} setup produced unowned fields: ${unexpected.join(', ')}`
|
|
)
|
|
}
|
|
|
|
const replacedFields = fieldsToReplace(setup, option)
|
|
const remove = replacedFields.filter((key) => !Object.hasOwn(values, key))
|
|
const proposed = proposedCapabilitySetupValues(setup, option, values, currentValues)
|
|
const inspection = inspectCapability(definition, proposed)
|
|
if (inspection.error) throw inspection.error
|
|
|
|
if (option.providerId) {
|
|
const provider = inspection.providers.find((candidate) => candidate.id === option.providerId)
|
|
if (!provider || provider.state !== 'ready') {
|
|
throw new EnvCapabilityConfigurationError(
|
|
definition.id,
|
|
`${definition.label} setup option ${option.id} did not configure ${option.providerId}`
|
|
)
|
|
}
|
|
if (inspection.strategy === 'selected' && inspection.providerId !== option.providerId) {
|
|
throw new EnvCapabilityConfigurationError(
|
|
definition.id,
|
|
`${definition.label} setup selected ${inspection.providerId ?? 'nothing'} instead of ${option.providerId}`
|
|
)
|
|
}
|
|
} else {
|
|
const activeProvider = inspection.providers.find((provider) => provider.active)
|
|
if (activeProvider) {
|
|
throw new EnvCapabilityConfigurationError(
|
|
definition.id,
|
|
`${definition.label} setup option ${option.id} left ${activeProvider.id} active`
|
|
)
|
|
}
|
|
}
|
|
|
|
return { values, remove }
|
|
}
|
|
|
|
function proposedPromptValues(state: PromptState): EnvCapabilityValues {
|
|
const option = getCapabilitySetupOptions(state.setup).find(
|
|
(candidate) => candidate.id === state.optionId
|
|
)
|
|
if (!option) {
|
|
throw new Error(`Capability ${state.setup.definition.id} has no setup option ${state.optionId}`)
|
|
}
|
|
return proposedCapabilitySetupValues(
|
|
state.setup,
|
|
option,
|
|
getCapabilitySetupDraftValues(state.setup, state.optionId, state.values),
|
|
state.currentValues
|
|
)
|
|
}
|
|
|
|
function shouldRenderPrompt(prompt: SetupPrompt, state: PromptState): boolean {
|
|
if (!prompt.when) return true
|
|
if (prompt.when.kind === 'provider-missing-field') {
|
|
return getCapabilitySetupProviderMissingFields(
|
|
state.setup,
|
|
state.optionId,
|
|
state.values,
|
|
state.currentValues
|
|
).includes(prompt.when.key)
|
|
}
|
|
return matchesSetupCondition(prompt.when, proposedPromptValues(state))
|
|
}
|
|
|
|
/** Formats current-value status without ever rendering the configured value. */
|
|
export function formatCapabilitySetupFieldMessage(
|
|
prompt: Extract<SetupPrompt, { type: 'field' }>,
|
|
current: boolean,
|
|
keepExisting = false
|
|
): string {
|
|
const label = prompt.message ?? prompt.key
|
|
const status = current ? ' (Currently used)' : ''
|
|
const hint = prompt.hint ? ` — ${prompt.hint}` : ''
|
|
const preservation = current && keepExisting ? '; leave empty to keep it' : ''
|
|
return `${label}${status}${hint}${preservation}`
|
|
}
|
|
|
|
async function promptField(
|
|
prompt: Extract<SetupPrompt, { type: 'field' }>,
|
|
state: PromptState
|
|
): Promise<void> {
|
|
const existing = state.currentValues.get(prompt.key)
|
|
const current = hasEnvCapabilityValue(state.currentValues, prompt.key)
|
|
const draft = getCapabilitySetupDraftValues(state.setup, state.optionId, state.values)
|
|
const initialValue = existing ?? draft[prompt.key] ?? prompt.defaultValue
|
|
const validate = (value: string): string | undefined => {
|
|
if (!value) return prompt.required && !existing ? 'required' : undefined
|
|
return prompt.validate
|
|
? validateCapabilityFieldInput(state.setup.definition, prompt.key, value)
|
|
: undefined
|
|
}
|
|
|
|
let value: string
|
|
if (prompt.input === 'secret') {
|
|
value = await p.password({
|
|
message: formatCapabilitySetupFieldMessage(prompt, current, true),
|
|
validate,
|
|
})
|
|
if (!value && existing) value = existing
|
|
} else {
|
|
value = await p.text({
|
|
message: formatCapabilitySetupFieldMessage(prompt, current),
|
|
initialValue,
|
|
defaultValue: initialValue ? undefined : prompt.defaultValue,
|
|
validate,
|
|
})
|
|
}
|
|
|
|
if (value) state.values[prompt.key] = value
|
|
else Reflect.deleteProperty(state.values, prompt.key)
|
|
}
|
|
|
|
async function renderPrompts(prompts: readonly SetupPrompt[], state: PromptState): Promise<void> {
|
|
for (const prompt of prompts) {
|
|
if (!shouldRenderPrompt(prompt, state)) continue
|
|
if (prompt.type === 'field') {
|
|
await promptField(prompt, state)
|
|
continue
|
|
}
|
|
if (prompt.type === 'confirm') {
|
|
const proposed = proposedPromptValues(state)
|
|
const current = hasEnvCapabilityValue(state.currentValues, prompt.key)
|
|
const enabled = await p.confirm({
|
|
message: `${prompt.message}${current ? ' (Currently used)' : ''}`,
|
|
initialValue: hasEnvCapabilityValue(proposed, prompt.key)
|
|
? isTruthyEnvCapabilityValue(proposed, prompt.key)
|
|
: (prompt.defaultValue ?? false),
|
|
})
|
|
state.values[prompt.key] = enabled ? 'true' : 'false'
|
|
continue
|
|
}
|
|
|
|
const currentOption = prompt.options.find(
|
|
(option) =>
|
|
option.currentWhen && matchesSetupCondition(option.currentWhen, state.currentValues)
|
|
)
|
|
const initialOption = currentOption ?? prompt.options[0]
|
|
if (!initialOption) throw new Error(`Setup choice ${prompt.id} has no options`)
|
|
const selectedId = await p.select({
|
|
message: prompt.message,
|
|
options: prompt.options.map((option) => ({
|
|
value: option.id,
|
|
label: option.label,
|
|
hint: markCurrentlyUsed(option.hint, option.id === currentOption?.id),
|
|
})),
|
|
initialValue: initialOption.id,
|
|
})
|
|
const selected = prompt.options.find((option) => option.id === selectedId)
|
|
if (!selected) {
|
|
throw new Error(`Setup choice ${prompt.id} returned unknown option ${selectedId}`)
|
|
}
|
|
Object.assign(state.values, selected.env ?? {})
|
|
await renderPrompts(selected.prompts ?? [], state)
|
|
}
|
|
}
|
|
|
|
/** Renders a CLI-owned capability setup and returns its validated environment transition. */
|
|
export async function promptCapabilitySetup(
|
|
setup: CapabilitySetupDefinition,
|
|
currentValues: ReadonlyMap<string, string>,
|
|
context: CapabilitySetupContext
|
|
): Promise<EnvCapabilitySetupTransition> {
|
|
const options = getCapabilitySetupOptions(setup)
|
|
const currentOptionId = resolveCurrentCapabilitySetupOptionId(setup, currentValues)
|
|
const selectedOptionId = await p.select({
|
|
message: setup.message,
|
|
options: options.map((option) => ({
|
|
value: option.id,
|
|
label: option.label,
|
|
hint: markCurrentlyUsed(resolveHint(option.hint, context), option.id === currentOptionId),
|
|
})),
|
|
initialValue: currentOptionId,
|
|
})
|
|
const selected = options.find((option) => option.id === selectedOptionId)
|
|
if (!selected) {
|
|
throw new Error(
|
|
`Capability ${setup.definition.id} returned unknown setup option ${selectedOptionId}`
|
|
)
|
|
}
|
|
|
|
const state: PromptState = {
|
|
setup,
|
|
optionId: selected.id,
|
|
currentValues,
|
|
values: {},
|
|
}
|
|
await renderPrompts(selected.prompts, state)
|
|
return buildCapabilitySetupTransition(setup, selected.id, state.values, currentValues)
|
|
}
|