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
943 lines
27 KiB
TypeScript
943 lines
27 KiB
TypeScript
/**
|
|
* CLI-only labels, hints, prompts, and actions for runtime deployment capabilities.
|
|
* Provider IDs and environment fields are checked against the application catalog at load time.
|
|
*
|
|
* @packageDocumentation
|
|
*/
|
|
import {
|
|
ASYNC_JOBS_CAPABILITY,
|
|
CACHE_CAPABILITY,
|
|
type CapabilityDefinition,
|
|
EMAIL_CAPABILITY,
|
|
ENV_CAPABILITIES,
|
|
type EnvCapabilityValues,
|
|
getCapabilityFields,
|
|
hasEnvCapabilityValue,
|
|
OAUTH_CLIENT_CAPABILITIES,
|
|
type OAuthClientCapabilityField,
|
|
type OAuthClientCapabilityId,
|
|
OCR_CAPABILITY,
|
|
SANDBOX_CAPABILITY,
|
|
STORAGE_CAPABILITY,
|
|
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
|
|
|
|
export type SetupHint =
|
|
| string
|
|
| {
|
|
development: string
|
|
containerized: string
|
|
}
|
|
|
|
export type SetupCondition =
|
|
| { kind: 'present'; key: string }
|
|
| { kind: 'truthy'; key: string }
|
|
| { kind: 'equals'; key: string; value: string }
|
|
| { kind: 'all'; conditions: readonly SetupCondition[] }
|
|
| { kind: 'any'; conditions: readonly SetupCondition[] }
|
|
| { kind: 'not'; condition: SetupCondition }
|
|
|
|
export type SetupPromptCondition = SetupCondition | { kind: 'provider-missing-field'; key: string }
|
|
|
|
export interface SetupFieldPrompt {
|
|
type: 'field'
|
|
key: string
|
|
input: 'text' | 'secret'
|
|
message?: string
|
|
hint?: string
|
|
required?: boolean
|
|
defaultValue?: string
|
|
validate?: boolean
|
|
when?: SetupPromptCondition
|
|
}
|
|
|
|
export interface SetupConfirmPrompt {
|
|
type: 'confirm'
|
|
key: string
|
|
message: string
|
|
defaultValue?: boolean
|
|
when?: SetupPromptCondition
|
|
}
|
|
|
|
export interface SetupChoiceOption {
|
|
id: string
|
|
label: string
|
|
hint?: string
|
|
currentWhen?: SetupCondition
|
|
env?: Readonly<Record<string, string>>
|
|
prompts?: readonly SetupPrompt[]
|
|
}
|
|
|
|
export interface SetupChoicePrompt {
|
|
type: 'choice'
|
|
id: string
|
|
message: string
|
|
options: readonly SetupChoiceOption[]
|
|
when?: SetupPromptCondition
|
|
}
|
|
|
|
export type SetupPrompt = SetupFieldPrompt | SetupConfirmPrompt | SetupChoicePrompt
|
|
|
|
type ProviderId<TDefinition extends CapabilityDefinition> = TDefinition['providers'][number]['id']
|
|
|
|
export interface ProviderSetupDefinition {
|
|
hint?: SetupHint
|
|
env?: Readonly<Record<string, string>>
|
|
prompts: readonly SetupPrompt[]
|
|
currentWhen?: SetupCondition
|
|
}
|
|
|
|
type ProviderSetupMap<TDefinition extends CapabilityDefinition> = {
|
|
[TId in ProviderId<TDefinition>]: ProviderSetupDefinition
|
|
}
|
|
|
|
export interface SetupActionDefinition {
|
|
label: string
|
|
hint?: SetupHint
|
|
env?: Readonly<Record<string, string>>
|
|
currentWhen?: SetupCondition
|
|
}
|
|
|
|
type DefaultOptionId<TDefinition extends CapabilityDefinition> = TDefinition extends {
|
|
defaultProvider: { kind: 'built-in'; id: infer TId extends string }
|
|
}
|
|
? TId
|
|
: never
|
|
|
|
export interface CapabilitySetupDefinition<
|
|
TDefinition extends CapabilityDefinition = CapabilityDefinition,
|
|
TActions extends Readonly<Record<string, SetupActionDefinition>> = Readonly<
|
|
Record<string, SetupActionDefinition>
|
|
>,
|
|
> {
|
|
definition: TDefinition
|
|
label: string
|
|
message: string
|
|
providers: ProviderSetupMap<TDefinition>
|
|
actions: TActions
|
|
defaultOption?: { hint?: SetupHint }
|
|
optionOrder: readonly (ProviderId<TDefinition> | DefaultOptionId<TDefinition> | keyof TActions)[]
|
|
}
|
|
|
|
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 conditionKeys(condition: SetupPromptCondition | undefined): string[] {
|
|
if (!condition) return []
|
|
if (
|
|
condition.kind === 'present' ||
|
|
condition.kind === 'truthy' ||
|
|
condition.kind === 'equals' ||
|
|
condition.kind === 'provider-missing-field'
|
|
) {
|
|
return [condition.key]
|
|
}
|
|
if (condition.kind === 'all' || condition.kind === 'any') {
|
|
return condition.conditions.flatMap(conditionKeys)
|
|
}
|
|
return conditionKeys(condition.condition)
|
|
}
|
|
|
|
function promptConditionKeys(prompts: readonly SetupPrompt[] = []): string[] {
|
|
return prompts.flatMap((prompt) => [
|
|
...conditionKeys(prompt.when),
|
|
...(prompt.type === 'choice'
|
|
? prompt.options.flatMap((option) => [
|
|
...conditionKeys(option.currentWhen),
|
|
...promptConditionKeys(option.prompts),
|
|
])
|
|
: []),
|
|
])
|
|
}
|
|
|
|
function requirementKeys(
|
|
requirement: CapabilityDefinition['providers'][number]['requires']
|
|
): string[] {
|
|
return requirement.type === 'field'
|
|
? [requirement.key]
|
|
: requirement.requirements.flatMap(requirementKeys)
|
|
}
|
|
|
|
function providerOwnedInputKeys(
|
|
provider: CapabilityDefinition['providers'][number]
|
|
): readonly string[] {
|
|
return [
|
|
...requirementKeys(provider.requires),
|
|
...(provider.optionalFields ?? []).map((field) => field.key),
|
|
...(provider.pairedFields ?? []).flat(),
|
|
]
|
|
}
|
|
|
|
function providerOwnedSetupKeys(
|
|
provider: CapabilityDefinition['providers'][number]
|
|
): readonly string[] {
|
|
return [
|
|
...providerOwnedInputKeys(provider),
|
|
...(provider.activation.mode === 'enabled'
|
|
? [provider.activation.key]
|
|
: provider.activation.keys),
|
|
]
|
|
}
|
|
|
|
export function defineCapabilitySetup<
|
|
const TDefinition extends CapabilityDefinition,
|
|
const TActions extends Readonly<Record<string, SetupActionDefinition>>,
|
|
>(
|
|
definition: TDefinition,
|
|
setup: Omit<CapabilitySetupDefinition<TDefinition, TActions>, 'definition'>
|
|
): CapabilitySetupDefinition<TDefinition, TActions> {
|
|
const providerIds = definition.providers.map((provider) => provider.id)
|
|
const configuredProviderIds = Object.keys(setup.providers)
|
|
const missingProviders = providerIds.filter((id) => !configuredProviderIds.includes(id))
|
|
const unknownProviders = configuredProviderIds.filter((id) => !providerIds.includes(id))
|
|
if (missingProviders.length > 0 || unknownProviders.length > 0) {
|
|
throw new Error(
|
|
`Setup ${definition.id} provider mapping drifted (missing: ${missingProviders.join(', ') || 'none'}; unknown: ${unknownProviders.join(', ') || 'none'})`
|
|
)
|
|
}
|
|
|
|
for (const provider of definition.providers) {
|
|
const providerSetup = (setup.providers as Record<string, ProviderSetupDefinition>)[provider.id]
|
|
if (!providerSetup) throw new Error(`Setup ${definition.id} has no provider ${provider.id}`)
|
|
const ownedFields = providerOwnedInputKeys(provider)
|
|
const setupFields = providerOwnedSetupKeys(provider)
|
|
const configuredFields = [
|
|
...promptKeys(providerSetup.prompts),
|
|
...Object.keys(providerSetup.env ?? {}),
|
|
]
|
|
const referencedFields = [
|
|
...configuredFields,
|
|
...conditionKeys(providerSetup.currentWhen),
|
|
...promptConditionKeys(providerSetup.prompts),
|
|
]
|
|
const unknownFields = referencedFields.filter((key) => !setupFields.includes(key))
|
|
const missingFields = ownedFields.filter((key) => !configuredFields.includes(key))
|
|
if (unknownFields.length > 0 || missingFields.length > 0) {
|
|
throw new Error(
|
|
`Setup ${definition.id}/${provider.id} field mapping drifted (missing: ${missingFields.join(', ') || 'none'}; unknown: ${unknownFields.join(', ') || 'none'})`
|
|
)
|
|
}
|
|
}
|
|
|
|
const capabilityFields = getCapabilityFields(definition)
|
|
for (const [actionId, action] of Object.entries(setup.actions)) {
|
|
const unknownFields = [
|
|
...Object.keys(action.env ?? {}),
|
|
...conditionKeys(action.currentWhen),
|
|
].filter((key) => !capabilityFields.includes(key))
|
|
if (unknownFields.length > 0) {
|
|
throw new Error(
|
|
`Setup ${definition.id}/${actionId} action writes unknown fields: ${unknownFields.join(', ')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
const optionIds = [
|
|
...providerIds,
|
|
...Object.keys(setup.actions),
|
|
...(definition.strategy === 'selected' && definition.defaultProvider.kind === 'built-in'
|
|
? [definition.defaultProvider.id]
|
|
: []),
|
|
]
|
|
if (
|
|
setup.optionOrder.length !== optionIds.length ||
|
|
setup.optionOrder.some((id) => !optionIds.includes(String(id))) ||
|
|
optionIds.some((id) => !setup.optionOrder.includes(id))
|
|
) {
|
|
throw new Error(`Setup ${definition.id} option order must list every option once`)
|
|
}
|
|
|
|
return { definition, ...setup }
|
|
}
|
|
|
|
export const EMAIL_SETUP = defineCapabilitySetup(EMAIL_CAPABILITY, {
|
|
label: 'Email delivery',
|
|
message: 'Email sending?',
|
|
actions: {
|
|
none: {
|
|
label: 'None',
|
|
hint: 'emails are logged to the console — fine for local',
|
|
},
|
|
},
|
|
providers: {
|
|
resend: {
|
|
hint: 'paste an API key',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'RESEND_API_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
ses: {
|
|
hint: 'uses the AWS SDK credential chain',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AWS_SES_REGION',
|
|
input: 'text',
|
|
required: true,
|
|
defaultValue: 'us-east-1',
|
|
},
|
|
],
|
|
},
|
|
smtp: {
|
|
hint: 'any SMTP relay',
|
|
prompts: [
|
|
{ type: 'field', key: 'SMTP_HOST', input: 'text', required: true },
|
|
{
|
|
type: 'field',
|
|
key: 'SMTP_PORT',
|
|
input: 'text',
|
|
required: true,
|
|
defaultValue: '587',
|
|
validate: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'SMTP_USER',
|
|
input: 'text',
|
|
hint: 'leave empty for an unauthenticated relay',
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'SMTP_PASS',
|
|
input: 'secret',
|
|
required: true,
|
|
when: { kind: 'present', key: 'SMTP_USER' },
|
|
},
|
|
],
|
|
},
|
|
azure: {
|
|
hint: 'connection string',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AZURE_ACS_CONNECTION_STRING',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
gmail: {
|
|
hint: 'Workspace service account delegation',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'GMAIL_CREDENTIALS_JSON',
|
|
input: 'secret',
|
|
required: true,
|
|
validate: true,
|
|
},
|
|
{ type: 'field', key: 'GMAIL_SENDER', input: 'text', required: true },
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['none', 'resend', 'ses', 'smtp', 'azure', 'gmail'],
|
|
})
|
|
|
|
export const STORAGE_SETUP = defineCapabilitySetup(STORAGE_CAPABILITY, {
|
|
label: 'File storage',
|
|
message: 'File storage?',
|
|
actions: {},
|
|
defaultOption: {
|
|
hint: {
|
|
development:
|
|
'fine for local dev (external-fetch flows like Instagram publish need cloud storage)',
|
|
containerized: 'files live in the container — LOST on restart; evaluation only',
|
|
},
|
|
},
|
|
providers: {
|
|
azure: {
|
|
hint: 'connection string or account name + key',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AZURE_STORAGE_CONTAINER_NAME',
|
|
input: 'text',
|
|
required: true,
|
|
defaultValue: 'sim-files',
|
|
},
|
|
{
|
|
type: 'choice',
|
|
id: 'azure-credentials',
|
|
message: 'Azure credentials?',
|
|
options: [
|
|
{
|
|
id: 'connection-string',
|
|
label: 'Connection string',
|
|
currentWhen: { kind: 'present', key: 'AZURE_CONNECTION_STRING' },
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AZURE_CONNECTION_STRING',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'account-key',
|
|
label: 'Account name and key',
|
|
currentWhen: {
|
|
kind: 'any',
|
|
conditions: [
|
|
{ kind: 'present', key: 'AZURE_ACCOUNT_NAME' },
|
|
{ kind: 'present', key: 'AZURE_ACCOUNT_KEY' },
|
|
],
|
|
},
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AZURE_ACCOUNT_NAME',
|
|
input: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'AZURE_ACCOUNT_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
s3: {
|
|
hint: 'AWS S3 or an S3-compatible endpoint',
|
|
env: { S3_FORCE_PATH_STYLE: 'false' },
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'S3_ENDPOINT',
|
|
input: 'text',
|
|
hint: 'optional for R2, MinIO, B2, or another S3-compatible service',
|
|
validate: true,
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
key: 'S3_FORCE_PATH_STYLE',
|
|
message: 'Force path-style addressing? (required for MinIO/Ceph, not for R2)',
|
|
when: { kind: 'present', key: 'S3_ENDPOINT' },
|
|
},
|
|
{ type: 'field', key: 'AWS_REGION', input: 'text', required: true },
|
|
{ type: 'field', key: 'S3_BUCKET_NAME', input: 'text', required: true },
|
|
{
|
|
type: 'choice',
|
|
id: 'aws-credentials',
|
|
message: 'AWS credentials?',
|
|
options: [
|
|
{
|
|
id: 'chain',
|
|
label: 'Default credential chain',
|
|
hint: 'IAM role, IRSA, profile, or other SDK source',
|
|
currentWhen: {
|
|
kind: 'all',
|
|
conditions: [
|
|
{ kind: 'present', key: 'S3_BUCKET_NAME' },
|
|
{
|
|
kind: 'not',
|
|
condition: {
|
|
kind: 'any',
|
|
conditions: [
|
|
{ kind: 'present', key: 'AWS_ACCESS_KEY_ID' },
|
|
{ kind: 'present', key: 'AWS_SECRET_ACCESS_KEY' },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: 'static',
|
|
label: 'Access key and secret',
|
|
hint: 'stored in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY',
|
|
currentWhen: {
|
|
kind: 'any',
|
|
conditions: [
|
|
{ kind: 'present', key: 'AWS_ACCESS_KEY_ID' },
|
|
{ kind: 'present', key: 'AWS_SECRET_ACCESS_KEY' },
|
|
],
|
|
},
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'AWS_ACCESS_KEY_ID',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'AWS_SECRET_ACCESS_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
gcs: {
|
|
hint: 'bucket; credentials via ADC by default',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'GCS_BUCKET_NAME',
|
|
input: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'GCS_PROJECT_ID',
|
|
input: 'text',
|
|
hint: 'optional; inferred from credentials or ADC when empty',
|
|
},
|
|
{
|
|
type: 'choice',
|
|
id: 'gcs-credentials',
|
|
message: 'Google Cloud credentials?',
|
|
options: [
|
|
{
|
|
id: 'adc',
|
|
label: 'Application Default Credentials',
|
|
hint: 'Workload Identity or GOOGLE_APPLICATION_CREDENTIALS',
|
|
currentWhen: {
|
|
kind: 'all',
|
|
conditions: [
|
|
{ kind: 'present', key: 'GCS_BUCKET_NAME' },
|
|
{
|
|
kind: 'not',
|
|
condition: { kind: 'present', key: 'GCS_CREDENTIALS_JSON' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: 'json',
|
|
label: 'Inline service account JSON',
|
|
hint: 'stored in GCS_CREDENTIALS_JSON',
|
|
currentWhen: { kind: 'present', key: 'GCS_CREDENTIALS_JSON' },
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'GCS_CREDENTIALS_JSON',
|
|
input: 'secret',
|
|
required: true,
|
|
validate: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['local', 's3', 'azure', 'gcs'],
|
|
})
|
|
|
|
export const SANDBOX_SETUP = defineCapabilitySetup(SANDBOX_CAPABILITY, {
|
|
label: 'Function sandboxes',
|
|
message: 'Function sandbox provider?',
|
|
actions: {
|
|
disabled: {
|
|
label: 'Disabled',
|
|
hint: 'local JavaScript execution only',
|
|
env: {
|
|
NEXT_PUBLIC_E2B_ENABLED: 'false',
|
|
NEXT_PUBLIC_SANDBOXES_ENABLED: 'false',
|
|
},
|
|
currentWhen: {
|
|
kind: 'all',
|
|
conditions: [
|
|
{ kind: 'not', condition: { kind: 'truthy', key: 'E2B_ENABLED' } },
|
|
{
|
|
kind: 'not',
|
|
condition: { kind: 'present', key: 'DAYTONA_API_KEY' },
|
|
},
|
|
{
|
|
kind: 'not',
|
|
condition: { kind: 'present', key: 'DAYTONA_FUNCTION_SNAPSHOT_ID' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
providers: {
|
|
e2b: {
|
|
hint: 'dedicated Function code and CLI sandboxes',
|
|
env: {
|
|
NEXT_PUBLIC_E2B_ENABLED: 'true',
|
|
NEXT_PUBLIC_SANDBOXES_ENABLED: 'true',
|
|
},
|
|
prompts: [
|
|
{ type: 'field', key: 'E2B_API_KEY', input: 'secret', required: true },
|
|
{
|
|
type: 'field',
|
|
key: 'E2B_FUNCTION_TEMPLATE_ID',
|
|
input: 'text',
|
|
required: true,
|
|
validate: true,
|
|
hint: 'immutable <template>:<build-id> ref printed by the Function E2B builder',
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'E2B_FUNCTION_TEMPLATE_GENERATION',
|
|
input: 'text',
|
|
required: true,
|
|
validate: true,
|
|
hint: 'release generation printed by the Function E2B builder',
|
|
},
|
|
],
|
|
currentWhen: { kind: 'truthy', key: 'E2B_ENABLED' },
|
|
},
|
|
daytona: {
|
|
hint: 'dedicated Function code and CLI sandboxes',
|
|
env: {
|
|
NEXT_PUBLIC_E2B_ENABLED: 'false',
|
|
NEXT_PUBLIC_SANDBOXES_ENABLED: 'true',
|
|
},
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'DAYTONA_API_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'DAYTONA_FUNCTION_SNAPSHOT_ID',
|
|
input: 'text',
|
|
required: true,
|
|
validate: true,
|
|
hint: 'immutable snapshot ID printed by the Function Daytona builder',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['disabled', 'e2b', 'daytona'],
|
|
})
|
|
|
|
export const JOBS_SETUP = defineCapabilitySetup(ASYNC_JOBS_CAPABILITY, {
|
|
label: 'Async jobs',
|
|
message: 'Async job provider?',
|
|
actions: {},
|
|
defaultOption: { hint: 'built-in default' },
|
|
providers: {
|
|
'trigger-dev': {
|
|
hint: 'external background jobs',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'TRIGGER_PROJECT_ID',
|
|
input: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'TRIGGER_SECRET_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['database', 'trigger-dev'],
|
|
})
|
|
|
|
export const CACHE_SETUP = defineCapabilitySetup(CACHE_CAPABILITY, {
|
|
label: 'Redis cache',
|
|
message: 'Cache and realtime coordination?',
|
|
actions: {},
|
|
defaultOption: { hint: 'built-in default' },
|
|
providers: {
|
|
redis: {
|
|
hint: 'recommended for multiple replicas',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'REDIS_URL',
|
|
input: 'text',
|
|
required: true,
|
|
defaultValue: 'redis://localhost:6379',
|
|
validate: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'REDIS_TLS_SERVERNAME',
|
|
input: 'text',
|
|
required: true,
|
|
when: { kind: 'provider-missing-field', key: 'REDIS_TLS_SERVERNAME' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['database', 'redis'],
|
|
})
|
|
|
|
export const KNOWLEDGE_SETUP = defineCapabilitySetup(OCR_CAPABILITY, {
|
|
label: 'Knowledge and OCR',
|
|
message: 'PDF OCR provider?',
|
|
actions: {},
|
|
defaultOption: { hint: 'built-in default' },
|
|
providers: {
|
|
'azure-mistral': {
|
|
hint: 'Azure model deployment',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'OCR_AZURE_ENDPOINT',
|
|
input: 'text',
|
|
required: true,
|
|
validate: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'OCR_AZURE_MODEL_NAME',
|
|
input: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
type: 'field',
|
|
key: 'OCR_AZURE_API_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
mistral: {
|
|
hint: 'Mistral API key',
|
|
prompts: [
|
|
{
|
|
type: 'field',
|
|
key: 'MISTRAL_API_KEY',
|
|
input: 'secret',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
optionOrder: ['local', 'mistral', 'azure-mistral'],
|
|
})
|
|
|
|
export const CAPABILITY_SETUPS = [
|
|
EMAIL_SETUP,
|
|
STORAGE_SETUP,
|
|
SANDBOX_SETUP,
|
|
JOBS_SETUP,
|
|
CACHE_SETUP,
|
|
KNOWLEDGE_SETUP,
|
|
] as const
|
|
|
|
const configuredCapabilityIds = new Set(CAPABILITY_SETUPS.map((setup) => setup.definition.id))
|
|
const missingCapabilitySetups = ENV_CAPABILITIES.filter(
|
|
(capability) => !configuredCapabilityIds.has(capability.id)
|
|
)
|
|
if (missingCapabilitySetups.length > 0) {
|
|
throw new Error(
|
|
`Missing CLI setup for capabilities: ${missingCapabilitySetups.map((capability) => capability.id).join(', ')}`
|
|
)
|
|
}
|
|
|
|
export type CapabilitySetupId = (typeof CAPABILITY_SETUPS)[number]['definition']['id']
|
|
export type SetupFeatureId = CapabilitySetupId | 'llm' | 'integration'
|
|
|
|
export const SETUP_FEATURES: readonly { id: SetupFeatureId; label: string }[] = [
|
|
...CAPABILITY_SETUPS.map((setup) => ({
|
|
id: setup.definition.id,
|
|
label: setup.label,
|
|
})),
|
|
{ id: 'llm', label: 'LLM API keys' },
|
|
{ id: 'integration', label: 'OAuth integration' },
|
|
]
|
|
|
|
export function getCapabilitySetup(id: string): CapabilitySetupDefinition | null {
|
|
return CAPABILITY_SETUPS.find((setup) => setup.definition.id === id) ?? null
|
|
}
|
|
|
|
export function getSetupCommand(id: string): string {
|
|
return `bun run setup ${id}`
|
|
}
|
|
|
|
type OAuthClientSetupFields = {
|
|
[TId in OAuthClientCapabilityId]: Record<
|
|
OAuthClientCapabilityField<TId>,
|
|
{ input: 'text' | 'secret' }
|
|
>
|
|
}
|
|
|
|
export const OAUTH_CLIENT_SETUP_FIELDS = {
|
|
google: {
|
|
GOOGLE_CLIENT_ID: { input: 'text' },
|
|
GOOGLE_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
x: { X_CLIENT_ID: { input: 'text' }, X_CLIENT_SECRET: { input: 'secret' } },
|
|
tiktok: {
|
|
TIKTOK_CLIENT_ID: { input: 'text' },
|
|
TIKTOK_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
confluence: {
|
|
CONFLUENCE_CLIENT_ID: { input: 'text' },
|
|
CONFLUENCE_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
jira: {
|
|
JIRA_CLIENT_ID: { input: 'text' },
|
|
JIRA_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
calcom: { CALCOM_CLIENT_ID: { input: 'text' } },
|
|
airtable: {
|
|
AIRTABLE_CLIENT_ID: { input: 'text' },
|
|
AIRTABLE_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
notion: {
|
|
NOTION_CLIENT_ID: { input: 'text' },
|
|
NOTION_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
microsoft: {
|
|
MICROSOFT_CLIENT_ID: { input: 'text' },
|
|
MICROSOFT_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
clickup: {
|
|
CLICKUP_CLIENT_ID: { input: 'text' },
|
|
CLICKUP_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
linear: {
|
|
LINEAR_CLIENT_ID: { input: 'text' },
|
|
LINEAR_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
attio: {
|
|
ATTIO_CLIENT_ID: { input: 'text' },
|
|
ATTIO_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
box: {
|
|
BOX_CLIENT_ID: { input: 'text' },
|
|
BOX_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
docusign: {
|
|
DOCUSIGN_CLIENT_ID: { input: 'text' },
|
|
DOCUSIGN_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
dropbox: {
|
|
DROPBOX_CLIENT_ID: { input: 'text' },
|
|
DROPBOX_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
slack: {
|
|
SLACK_CLIENT_ID: { input: 'text' },
|
|
SLACK_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
reddit: {
|
|
REDDIT_CLIENT_ID: { input: 'text' },
|
|
REDDIT_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
wealthbox: {
|
|
WEALTHBOX_CLIENT_ID: { input: 'text' },
|
|
WEALTHBOX_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
webflow: {
|
|
WEBFLOW_CLIENT_ID: { input: 'text' },
|
|
WEBFLOW_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
asana: {
|
|
ASANA_CLIENT_ID: { input: 'text' },
|
|
ASANA_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
pipedrive: {
|
|
PIPEDRIVE_CLIENT_ID: { input: 'text' },
|
|
PIPEDRIVE_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
hubspot: {
|
|
HUBSPOT_CLIENT_ID: { input: 'text' },
|
|
HUBSPOT_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
linkedin: {
|
|
LINKEDIN_CLIENT_ID: { input: 'text' },
|
|
LINKEDIN_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
instagram: {
|
|
INSTAGRAM_CLIENT_ID: { input: 'text' },
|
|
INSTAGRAM_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
salesforce: {
|
|
SALESFORCE_CLIENT_ID: { input: 'text' },
|
|
SALESFORCE_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
shopify: {
|
|
SHOPIFY_CLIENT_ID: { input: 'text' },
|
|
SHOPIFY_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
zoom: {
|
|
ZOOM_CLIENT_ID: { input: 'text' },
|
|
ZOOM_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
wordpress: {
|
|
WORDPRESS_CLIENT_ID: { input: 'text' },
|
|
WORDPRESS_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
spotify: {
|
|
SPOTIFY_CLIENT_ID: { input: 'text' },
|
|
SPOTIFY_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
monday: {
|
|
MONDAY_CLIENT_ID: { input: 'text' },
|
|
MONDAY_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
trello: { TRELLO_API_KEY: { input: 'secret' } },
|
|
'zoho-desk': {
|
|
ZOHO_CLIENT_ID: { input: 'text' },
|
|
ZOHO_CLIENT_SECRET: { input: 'secret' },
|
|
},
|
|
} satisfies OAuthClientSetupFields
|
|
|
|
export function getOAuthClientSetupFields(
|
|
id: OAuthClientCapabilityId
|
|
): readonly { key: string; input: 'text' | 'secret' }[] {
|
|
const runtimeFields = OAUTH_CLIENT_CAPABILITIES[id] as readonly string[]
|
|
const setupFields = OAUTH_CLIENT_SETUP_FIELDS[id] as Record<string, { input: 'text' | 'secret' }>
|
|
const unknownFields = Object.keys(setupFields).filter((key) => !runtimeFields.includes(key))
|
|
const missingFields = runtimeFields.filter((key) => !Object.hasOwn(setupFields, key))
|
|
if (unknownFields.length > 0 || missingFields.length > 0) {
|
|
throw new Error(
|
|
`OAuth setup ${id} field mapping drifted (missing: ${missingFields.join(', ') || 'none'}; unknown: ${unknownFields.join(', ') || 'none'})`
|
|
)
|
|
}
|
|
return runtimeFields.map((key) => ({ key, input: setupFields[key].input }))
|
|
}
|
|
|
|
export function matchesSetupCondition(
|
|
condition: SetupCondition,
|
|
values: EnvCapabilityValues
|
|
): boolean {
|
|
if (condition.kind === 'present') return hasEnvCapabilityValue(values, condition.key)
|
|
if (condition.kind === 'truthy') {
|
|
const value =
|
|
values instanceof Map
|
|
? values.get(condition.key)
|
|
: (values as Readonly<Record<string, unknown>>)[condition.key]
|
|
return value === true || value === 1 || String(value).toLowerCase() === 'true'
|
|
}
|
|
if (condition.kind === 'equals') {
|
|
const value =
|
|
values instanceof Map
|
|
? values.get(condition.key)
|
|
: (values as Readonly<Record<string, unknown>>)[condition.key]
|
|
return hasEnvCapabilityValue(values, condition.key) && String(value).trim() === condition.value
|
|
}
|
|
if (condition.kind === 'all') {
|
|
return condition.conditions.every((child) => matchesSetupCondition(child, values))
|
|
}
|
|
if (condition.kind === 'any') {
|
|
return condition.conditions.some((child) => matchesSetupCondition(child, values))
|
|
}
|
|
return !matchesSetupCondition(condition.condition, values)
|
|
}
|