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

359 lines
13 KiB
TypeScript

import {
type EnvCapabilityValues,
hasEnvCapabilityValue,
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
import {
type IntegrationAvailability,
resolveIntegrationAvailability,
} from '../../apps/sim/lib/integrations/availability.ts'
import { SETUP_FEATURES } from './capability-config.ts'
import {
buildEnvCapabilityStatus,
type EnvCapabilityFeatureStatuses,
type SetupStatusFeatureId,
} from './capability-status.ts'
import { REQUIRED_APP_KEYS } from './checks.ts'
import { type ConfigurationSource, discoverConfigurationSources } from './configuration-sources.ts'
import { isPlaceholder, isUsableSecret } from './env-files.ts'
import { glyph, theme } from './theme.ts'
type FeatureStatus = EnvCapabilityFeatureStatuses[keyof EnvCapabilityFeatureStatuses]
interface CoreConfigurationIssue {
key: (typeof REQUIRED_APP_KEYS)[number]
reason: 'missing' | 'placeholder' | 'invalid secret' | 'invalid URL'
}
export type SetupStatusSource = Omit<ConfigurationSource, 'values'>
export interface SetupStatusReport {
source: SetupStatusSource
environmentAvailable: boolean
coreIssues: readonly CoreConfigurationIssue[]
capabilityStatus: ReturnType<typeof buildEnvCapabilityStatus> | null
integrationAvailability: readonly IntegrationAvailability[] | null
failed: boolean
}
const SECRET_KEYS = new Set(['BETTER_AUTH_SECRET', 'ENCRYPTION_KEY', 'INTERNAL_API_SECRET'])
const URL_KEYS = new Set(['DATABASE_URL', 'BETTER_AUTH_URL', 'NEXT_PUBLIC_APP_URL'])
const FEATURE_ORDER: readonly SetupStatusFeatureId[] = SETUP_FEATURES.flatMap((feature) =>
feature.id === 'integration' ? [] : [feature.id]
)
function readString(values: EnvCapabilityValues, key: string): string | undefined {
const value =
values instanceof Map ? values.get(key) : (values as Readonly<Record<string, unknown>>)[key]
return value === undefined || value === null ? undefined : String(value)
}
function inspectCoreConfiguration(values: EnvCapabilityValues): CoreConfigurationIssue[] {
const issues: CoreConfigurationIssue[] = []
for (const key of REQUIRED_APP_KEYS) {
const value = readString(values, key)
if (!hasEnvCapabilityValue(values, key)) {
issues.push({ key, reason: 'missing' })
} else if (value && isPlaceholder(value)) {
issues.push({ key, reason: 'placeholder' })
} else if (value && SECRET_KEYS.has(key) && !isUsableSecret(key, value)) {
issues.push({ key, reason: 'invalid secret' })
} else if (value && URL_KEYS.has(key)) {
try {
new URL(value)
} catch {
issues.push({ key, reason: 'invalid URL' })
}
}
}
return issues
}
function titleCase(value: string): string {
if (value === 'openai') return 'OpenAI'
return value
.split(/[-_]/)
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
.join(' ')
}
function featureDetail(feature: FeatureStatus): string {
switch (feature.id) {
case 'email':
return feature.providerIds.length > 0
? feature.providerIds
.map(
(id) =>
feature.providers.find((provider) => provider.id === id)?.label ?? titleCase(id)
)
.join(' → ')
: 'Not configured'
case 'storage':
if (feature.providerId === 'local') return 'Local disk (default)'
return (
feature.providers.find((provider) => provider.id === feature.providerId)?.label ??
(feature.providerId ? titleCase(feature.providerId) : 'Not configured')
)
case 'sandbox':
if (feature.providerId === 'disabled') return 'Disabled (local JavaScript only)'
return feature.providerId ? titleCase(feature.providerId) : 'Not configured'
case 'jobs':
return feature.providerId === 'database' ? 'Database queue (default)' : 'Trigger.dev'
case 'cache':
return feature.providerId === 'database' ? 'Postgres (default)' : 'Redis'
case 'knowledge':
if (feature.providerId === 'local') return 'Local parser (default)'
if (feature.providerId === 'azure-mistral') return 'Azure Mistral OCR'
return feature.providerId === 'mistral' ? 'Mistral OCR' : 'Not configured'
case 'llm': {
const configured = Object.values(feature.pools)
.filter((pool) => pool.state === 'configured')
.map((pool) => `${titleCase(pool.id)} (${pool.effectiveKeyCount})`)
return configured.length > 0 ? configured.join(', ') : 'No global key pools'
}
}
}
function featureGlyph(feature: FeatureStatus): string {
if (feature.issue && (feature.state === 'configured' || feature.state === 'default')) {
return glyph.warn
}
if (feature.issue || feature.state === 'partial' || feature.state === 'invalid') return glyph.fail
if (feature.state === 'missing') return glyph.skip
return glyph.pass
}
function withoutSetupCommand(message: string): string {
return message.replace(/\s+Run bun run setup[^.]*\.$/, '')
}
function setupHint(source: SetupStatusSource, command: string): string | null {
if (source.managedByCurrentCheckout) return command
if (source.kind === 'helm') return 'update the app Secret/values and upgrade this Helm release'
if (source.kind === 'compose') return 'update this Compose project and recreate its app container'
return null
}
function uniqueNames(integrations: readonly IntegrationAvailability[]): string[] {
return [...new Set(integrations.map((integration) => integration.name))].sort((a, b) =>
a.localeCompare(b)
)
}
interface IntegrationGroup {
setupCommand?: string
missingFields: readonly string[]
names: string[]
}
function groupIntegrations(
integrations: readonly IntegrationAvailability[]
): readonly IntegrationGroup[] {
const groups = new Map<string, IntegrationGroup>()
for (const integration of integrations) {
const missingFields = [...integration.missingFields].sort()
const key = `${integration.setupCommand ?? 'clientless'}:${missingFields.join(',')}`
const existing = groups.get(key)
if (existing) {
if (!existing.names.includes(integration.name)) existing.names.push(integration.name)
continue
}
groups.set(key, {
setupCommand: integration.setupCommand,
missingFields,
names: [integration.name],
})
}
return [...groups.values()].map((group) => ({
...group,
names: group.names.sort((left, right) => left.localeCompare(right)),
}))
}
function renderIntegrationGroup(
source: SetupStatusSource,
marker: string,
group: IntegrationGroup
): string[] {
const missing =
group.missingFields.length > 0 ? ` — missing ${group.missingFields.join(', ')}` : ''
const lines = [` ${marker} ${group.names.join(', ')}${missing}`]
if (group.setupCommand) {
const hint = setupHint(source, group.setupCommand)
if (hint) lines.push(` ${theme.muted(`configure: ${hint}`)}`)
}
return lines
}
/** Builds a non-secret report for one effective deployment configuration. */
export function buildSetupStatusReport(source: ConfigurationSource): SetupStatusReport {
const safeSource: SetupStatusSource = {
kind: source.kind,
label: source.label,
location: source.location,
managedByCurrentCheckout: source.managedByCurrentCheckout,
...(source.warning ? { warning: source.warning } : {}),
...(source.configurationIssues ? { configurationIssues: source.configurationIssues } : {}),
}
if (!source.values) {
return {
source: safeSource,
environmentAvailable: false,
coreIssues: [],
capabilityStatus: null,
integrationAvailability: null,
failed: true,
}
}
const coreIssues = inspectCoreConfiguration(source.values)
const capabilityStatus = buildEnvCapabilityStatus(source.values)
const integrationAvailability = resolveIntegrationAvailability(source.values)
const brokenCapability = Object.values(capabilityStatus.features).some(
(feature) =>
feature.state === 'partial' ||
feature.state === 'invalid' ||
(feature.state === 'missing' && Boolean(feature.issue))
)
const brokenIntegration = integrationAvailability.some(
(integration) => integration.state === 'misconfigured'
)
const brokenOAuthClient =
capabilityStatus.oauthClients.partialCount > 0 || capabilityStatus.oauthClients.invalidCount > 0
return {
source: safeSource,
environmentAvailable: true,
coreIssues,
capabilityStatus,
integrationAvailability,
failed:
coreIssues.length > 0 ||
Boolean(source.configurationIssues?.length) ||
brokenCapability ||
brokenIntegration ||
brokenOAuthClient,
}
}
/** Renders a report without including any configured environment values. */
export function renderSetupStatusReport(report: SetupStatusReport): string {
const { source } = report
const lines = [
theme.heading(source.label),
` ${report.environmentAvailable ? glyph.pass : glyph.fail} ${source.location}`,
]
if (source.warning) lines.push(` ${glyph.warn} ${source.warning}`)
lines.push('')
if (!report.environmentAvailable || !report.capabilityStatus || !report.integrationAvailability) {
lines.push(theme.heading('Configuration'))
lines.push(` ${glyph.fail} Effective environment is unavailable.`)
return lines.join('\n')
}
lines.push(theme.heading('Core configuration'))
if (report.coreIssues.length === 0 && !source.configurationIssues?.length) {
lines.push(` ${glyph.pass} All ${REQUIRED_APP_KEYS.length} required app values are present`)
} else {
for (const coreIssue of report.coreIssues) {
lines.push(` ${glyph.fail} ${coreIssue.key}: ${coreIssue.reason}`)
}
for (const configurationIssue of source.configurationIssues ?? []) {
lines.push(` ${glyph.fail} ${configurationIssue}`)
}
}
lines.push('')
lines.push(theme.heading('Capabilities'))
for (const id of FEATURE_ORDER) {
const feature = report.capabilityStatus.features[id]
lines.push(` ${featureGlyph(feature)} ${feature.label}: ${featureDetail(feature)}`)
if (feature.issue) {
lines.push(` ${theme.muted(withoutSetupCommand(feature.issue.message))}`)
}
if (feature.state === 'missing' || feature.issue) {
const hint = setupHint(source, feature.setupCommand)
if (hint) lines.push(` ${theme.muted(`configure: ${hint}`)}`)
}
}
lines.push('')
const deploymentIntegrations = report.integrationAvailability.filter(
(integration) => integration.setupCommand || integration.serviceAccountAvailable
)
const ready = deploymentIntegrations.filter((integration) => integration.state === 'ready')
const limited = deploymentIntegrations.filter((integration) => integration.state === 'limited')
const unavailable = deploymentIntegrations.filter(
(integration) => integration.state === 'unavailable'
)
const misconfigured = deploymentIntegrations.filter(
(integration) => integration.state === 'misconfigured'
)
lines.push(theme.heading('OAuth integrations'))
const readyNames = uniqueNames(ready)
lines.push(
readyNames.length > 0
? ` ${glyph.pass} OAuth ready (${readyNames.length}): ${readyNames.join(', ')}`
: ` ${glyph.skip} OAuth ready: none`
)
const limitedNames = uniqueNames(limited)
if (limitedNames.length > 0) {
lines.push(
` ${glyph.warn} OAuth unavailable; workspace credential option exists (${limitedNames.length}): ${limitedNames.join(', ')}`
)
for (const group of groupIntegrations(limited)) {
lines.push(...renderIntegrationGroup(source, glyph.warn, group))
}
}
if (unavailable.length > 0) {
lines.push(` ${glyph.skip} Unavailable (${uniqueNames(unavailable).length})`)
for (const group of groupIntegrations(unavailable)) {
lines.push(...renderIntegrationGroup(source, glyph.skip, group))
}
}
if (misconfigured.length > 0) {
lines.push(` ${glyph.fail} Misconfigured (${uniqueNames(misconfigured).length})`)
for (const group of groupIntegrations(misconfigured)) {
lines.push(...renderIntegrationGroup(source, glyph.fail, group))
}
}
const representedOAuthClients = new Set(
deploymentIntegrations.flatMap((integration) => {
if (!integration.setupCommand) return []
return [integration.setupCommand.replace('bun run setup integration ', '')]
})
)
const additionalOAuthClients = Object.values(report.capabilityStatus.oauthClients.clients).filter(
(client) => !representedOAuthClients.has(client.id) && client.state !== 'absent'
)
for (const client of additionalOAuthClients) {
const marker = client.state === 'ready' ? glyph.pass : glyph.fail
const missing =
client.missingFields.length > 0 ? ` — missing ${client.missingFields.join(', ')}` : ''
lines.push(` ${marker} ${titleCase(client.id)} OAuth client: ${client.state}${missing}`)
if (client.state !== 'ready') {
const hint = setupHint(source, client.setupCommand)
if (hint) lines.push(` ${theme.muted(`configure: ${hint}`)}`)
}
}
lines.push(
` ${theme.muted('API-key integrations are configured per workspace and are not listed.')}`
)
return lines.join('\n')
}
export async function runSetupStatus(): Promise<number> {
const sources = await discoverConfigurationSources()
console.log(`\n${theme.heading('◆ Sim setup status')}\n`)
if (sources.length === 0) {
console.log(` ${glyph.fail} No local-dev, Docker Compose, or Helm configuration detected.`)
console.log(` ${theme.muted('run: bun run setup')}`)
return 1
}
const reports = sources.map(buildSetupStatusReport)
console.log(reports.map(renderSetupStatusReport).join('\n\n'))
return reports.some((report) => report.failed) ? 1 : 0
}