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
206 lines
7.2 KiB
TypeScript
206 lines
7.2 KiB
TypeScript
import {
|
|
LLM_KEY_POOLS,
|
|
OAUTH_CLIENT_CAPABILITIES,
|
|
resolveOAuthClientCapabilityId,
|
|
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
|
|
import {
|
|
getCapabilitySetup,
|
|
getOAuthClientSetupFields,
|
|
SETUP_FEATURES,
|
|
type SetupFeatureId,
|
|
} from './capability-config.ts'
|
|
import { promptCapabilitySetup } from './capability-setup.ts'
|
|
import { type ConfigurationSource, discoverConfigurationSources } from './configuration-sources.ts'
|
|
import { type EnvTarget, reconcileEnvValues } from './env-files.ts'
|
|
import * as p from './prompter.ts'
|
|
import { theme } from './theme.ts'
|
|
|
|
function isSetupFeatureId(value: string): value is SetupFeatureId {
|
|
return SETUP_FEATURES.some((feature) => feature.id === value)
|
|
}
|
|
|
|
async function setupIntegration(
|
|
requestedId: string | undefined,
|
|
vars: Map<string, string>
|
|
): Promise<Record<string, string>> {
|
|
if (!requestedId) {
|
|
throw new Error('Missing integration id. Example: bun run setup integration slack')
|
|
}
|
|
const providerId = resolveOAuthClientCapabilityId(requestedId)
|
|
if (!providerId) {
|
|
throw new Error(
|
|
`Unknown OAuth integration "${requestedId}". Expected one of: ${Object.keys(OAUTH_CLIENT_CAPABILITIES).join(', ')}`
|
|
)
|
|
}
|
|
const fields = getOAuthClientSetupFields(providerId)
|
|
|
|
const values: Record<string, string> = {}
|
|
for (const field of fields) {
|
|
const existing = vars.get(field.key)
|
|
if (field.input === 'secret') {
|
|
const value = await p.password({
|
|
message: existing ? `${field.key} (Currently used); leave empty to keep it` : field.key,
|
|
validate: (candidate) => (candidate || existing ? undefined : 'required'),
|
|
})
|
|
const resolved = value || existing
|
|
if (!resolved) throw new Error(`${field.key} was not provided`)
|
|
values[field.key] = resolved
|
|
} else {
|
|
values[field.key] = await p.text({
|
|
message: `${field.key}${existing ? ' (Currently used)' : ''}`,
|
|
initialValue: existing,
|
|
validate: (candidate) => (candidate ? undefined : 'required'),
|
|
})
|
|
}
|
|
}
|
|
p.log.info(`Configured the ${providerId} OAuth client.`)
|
|
return values
|
|
}
|
|
|
|
type LlmKeyPoolId = keyof typeof LLM_KEY_POOLS
|
|
|
|
export interface LlmSetupResult {
|
|
remove: readonly string[]
|
|
values: Record<string, string>
|
|
}
|
|
|
|
/** Reconciles every rotation and legacy fallback key owned by the selected pool. */
|
|
export function reconcileLlmSetup(
|
|
providerId: LlmKeyPoolId,
|
|
values: Record<string, string>
|
|
): LlmSetupResult {
|
|
const pool = LLM_KEY_POOLS[providerId]
|
|
const fields = [...pool.keys, ...('fallbackKey' in pool ? [pool.fallbackKey] : [])]
|
|
return {
|
|
values,
|
|
remove: fields.filter((key) => !Object.hasOwn(values, key)),
|
|
}
|
|
}
|
|
|
|
async function setupLlm(vars: Map<string, string>): Promise<LlmSetupResult> {
|
|
const currentProvider = Object.entries(LLM_KEY_POOLS).find(([, pool]) =>
|
|
[...pool.keys, ...('fallbackKey' in pool ? [pool.fallbackKey] : [])].some((key) =>
|
|
vars.has(key)
|
|
)
|
|
)?.[0] as LlmKeyPoolId | undefined
|
|
const provider = await p.select<LlmKeyPoolId>({
|
|
message: 'LLM key pool?',
|
|
options: Object.keys(LLM_KEY_POOLS).map((id) => ({
|
|
value: id as LlmKeyPoolId,
|
|
label: id,
|
|
hint: id === currentProvider ? 'Currently used' : undefined,
|
|
})),
|
|
initialValue: currentProvider,
|
|
})
|
|
const pool = LLM_KEY_POOLS[provider]
|
|
const keys = pool.keys
|
|
const values: Record<string, string> = {}
|
|
for (const [index, key] of keys.entries()) {
|
|
const legacyKey = index === 0 && 'fallbackKey' in pool ? pool.fallbackKey : undefined
|
|
const existingKey = vars.has(key) ? key : legacyKey
|
|
const existing = existingKey ? vars.get(existingKey) : undefined
|
|
const value = await p.password({
|
|
message: existing
|
|
? `${key} (${existingKey} is currently used); leave empty to keep it`
|
|
: `${key}${index === 0 ? '' : ' (empty to finish)'}`,
|
|
validate:
|
|
index === 0 ? (candidate) => (candidate || existing ? undefined : 'required') : undefined,
|
|
})
|
|
const resolved = value || existing
|
|
if (!resolved) break
|
|
values[key] = resolved
|
|
}
|
|
return reconcileLlmSetup(provider, values)
|
|
}
|
|
|
|
export function setupFeatureUsage(): string {
|
|
return SETUP_FEATURES.map((feature) =>
|
|
feature.id === 'integration' ? 'integration <slug>' : feature.id
|
|
).join(' | ')
|
|
}
|
|
|
|
export interface FeatureSetupDestination {
|
|
source: ConfigurationSource
|
|
target: Extract<EnvTarget, 'sim' | 'root'>
|
|
vars: Map<string, string>
|
|
containerized: boolean
|
|
}
|
|
|
|
/** Resolves the one effective configuration this checkout can safely update. */
|
|
export function resolveFeatureSetupDestination(
|
|
sources: readonly ConfigurationSource[]
|
|
): FeatureSetupDestination {
|
|
if (sources.length === 0) {
|
|
throw new Error('No Sim configuration was detected. Run bun run setup first.')
|
|
}
|
|
|
|
const managed = sources.filter((source) => source.managedByCurrentCheckout)
|
|
if (managed.length === 0) {
|
|
throw new Error(
|
|
'No effective configuration is safely writable by this checkout. Process overrides, higher-precedence development env files, external Compose projects, and Helm releases must be updated at their source. Run bun run setup status for the detected sources.'
|
|
)
|
|
}
|
|
if (managed.length > 1) {
|
|
throw new Error(
|
|
`More than one effective configuration is writable by this checkout (${managed.map((source) => source.label).join(', ')}). Run bun run setup status and remove the ambiguity before configuring a feature.`
|
|
)
|
|
}
|
|
|
|
const source = managed[0]
|
|
if (!source.values) {
|
|
throw new Error(
|
|
`${source.label} is managed by this checkout, but its effective environment could not be resolved. Run bun run setup status and fix the reported source error first.`
|
|
)
|
|
}
|
|
if (source.kind === 'helm') {
|
|
throw new Error(
|
|
'Helm configuration cannot be updated by bun run setup. Update the release Secret or values and upgrade the release.'
|
|
)
|
|
}
|
|
|
|
return {
|
|
source,
|
|
target: source.kind === 'compose' ? 'root' : 'sim',
|
|
vars: source.values,
|
|
containerized: source.kind === 'compose',
|
|
}
|
|
}
|
|
|
|
export async function runFeatureSetup(feature: string, args: readonly string[]): Promise<void> {
|
|
if (!isSetupFeatureId(feature)) {
|
|
throw new Error(`Unknown setup feature "${feature}". Expected: ${setupFeatureUsage()}`)
|
|
}
|
|
const destination = resolveFeatureSetupDestination(discoverConfigurationSources())
|
|
const { target, vars } = destination
|
|
let values: Record<string, string>
|
|
let remove: readonly string[]
|
|
const capabilitySetup = getCapabilitySetup(feature)
|
|
|
|
if (capabilitySetup) {
|
|
const result = await promptCapabilitySetup(capabilitySetup, vars, {
|
|
containerized: destination.containerized,
|
|
})
|
|
values = result.values
|
|
remove = result.remove
|
|
} else if (feature === 'integration') {
|
|
values = await setupIntegration(args[0], vars)
|
|
remove = []
|
|
} else if (feature === 'llm') {
|
|
const result = await setupLlm(vars)
|
|
values = result.values
|
|
remove = result.remove
|
|
} else {
|
|
throw new Error(`Setup feature ${feature} has no handler`)
|
|
}
|
|
|
|
reconcileEnvValues(target, remove, values)
|
|
const label = SETUP_FEATURES.find((item) => item.id === feature)?.label
|
|
p.outro(
|
|
theme.accent(
|
|
destination.containerized
|
|
? `${label} written to .env. Recreate the app container for it to take effect.`
|
|
: `${label} configured.`
|
|
)
|
|
)
|
|
}
|