Files
WeHub Mirror 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
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

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.`
)
)
}