Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ npx sim-setup add sandbox
npx sim-setup add jobs
Comment thread
TheodoreSpeaks marked this conversation as resolved.
npx sim-setup add cache
npx sim-setup add knowledge
npx sim-setup add chat
npx sim-setup add llm
npx sim-setup add integration slack
```
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/deployment-config/src/env-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,9 +1339,12 @@ export const DEPLOYMENT_CONFIGURATION_KEYS: readonly string[] = [
...new Set([
...CORE_CONFIGURATION_KEYS,
...ENV_CAPABILITIES.flatMap(capabilityKeys),
'COPILOT_API_KEY',
'EMAIL_VERIFICATION_ENABLED',
'NEXT_PUBLIC_CHAT_DISABLED',
'NEXT_PUBLIC_E2B_ENABLED',
'NEXT_PUBLIC_SANDBOXES_ENABLED',
'SIM_AGENT_API_URL',
...Object.values(LLM_KEY_POOLS).flatMap((pool) => [
...pool.keys,
...('fallbackKey' in pool ? [pool.fallbackKey] : []),
Expand Down
6 changes: 6 additions & 0 deletions packages/sim-setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ npx sim-setup
Outside a Sim source checkout, the command creates a Docker Compose installation using published
images. Inside a Sim source checkout, use `bun run sim-setup` to expose the complete development
and deployment wizard.

To connect or replace the Chat API key without rerunning the full wizard:

```bash
npx sim-setup add chat
```
2 changes: 1 addition & 1 deletion packages/sim-setup/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sim-setup",
"version": "1.0.1",
"version": "1.0.2",
"description": "Set up and manage a self-hosted Sim installation",
"type": "module",
"bin": {
Expand Down
3 changes: 2 additions & 1 deletion packages/sim-setup/src/capability-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,13 +847,14 @@ if (missingCapabilitySetups.length > 0) {
}

export type CapabilitySetupId = (typeof CAPABILITY_SETUPS)[number]['definition']['id']
export type SetupFeatureId = CapabilitySetupId | 'llm' | 'integration'
export type SetupFeatureId = CapabilitySetupId | 'chat' | 'llm' | 'integration'

export const SETUP_FEATURES: readonly { id: SetupFeatureId; label: string }[] = [
...CAPABILITY_SETUPS.map((setup) => ({
id: setup.definition.id,
label: setup.label,
})),
{ id: 'chat', label: 'Chat' },
{ id: 'llm', label: 'LLM API keys' },
{ id: 'integration', label: 'OAuth integration' },
]
Expand Down
2 changes: 1 addition & 1 deletion packages/sim-setup/src/capability-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@sim/deployment-config/env-capabilities'
import { SETUP_FEATURES, type SetupFeatureId } from './capability-config'

export type SetupStatusFeatureId = Exclude<SetupFeatureId, 'integration'>
export type SetupStatusFeatureId = Exclude<SetupFeatureId, 'chat' | 'integration'>
export type CapabilityStatusState = 'default' | 'configured' | 'missing' | 'partial' | 'invalid'

export interface CapabilityStatusIssue {
Expand Down
23 changes: 23 additions & 0 deletions packages/sim-setup/src/configuration-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ describe('discoverConfigurationSources', () => {
expect(sources[0].values?.get('RESEND_API_KEY')).toBe('current')
})

it('retains Chat configuration from a prepared Compose .env file', () => {
const root = temporaryDirectory()
writeFileSync(
path.join(root, '.env'),
[
'COPILOT_API_KEY=existing-chat-key',
'NEXT_PUBLIC_CHAT_DISABLED=false',
'SIM_AGENT_API_URL=https://copilot.example.com',
].join('\n')
)
writeFileSync(
path.join(root, 'docker-compose.prod.yml'),
'services:\n simstudio:\n image: ghcr.io/simstudioai/simstudio:latest\n env_file: .env\n'
)

const sources = discoverConfigurationSources({ root, runner: () => commandResult(1) })

expect(sources).toHaveLength(1)
expect(sources[0].values?.get('COPILOT_API_KEY')).toBe('existing-chat-key')
expect(sources[0].values?.get('NEXT_PUBLIC_CHAT_DISABLED')).toBe('false')
expect(sources[0].values?.get('SIM_AGENT_API_URL')).toBe('https://copilot.example.com')
})

it('uses the effective environment of a stopped Compose app container', () => {
const parent = temporaryDirectory()
const root = path.join(parent, 'checkout')
Expand Down
85 changes: 85 additions & 0 deletions packages/sim-setup/src/feature-setup-chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
mockDiscoverConfigurationSources,
mockReconcileEnvValues,
mockPromptCopilotKey,
mockMothershipOverride,
mockChatFlagValues,
mockOutro,
} = vi.hoisted(() => ({
mockDiscoverConfigurationSources: vi.fn(),
mockReconcileEnvValues: vi.fn(),
mockPromptCopilotKey: vi.fn(),
mockMothershipOverride: vi.fn(),
mockChatFlagValues: vi.fn(),
mockOutro: vi.fn(),
}))

vi.mock('./configuration-sources', () => ({
discoverConfigurationSources: mockDiscoverConfigurationSources,
}))

vi.mock('./env-files', () => ({
reconcileEnvValues: mockReconcileEnvValues,
}))

vi.mock('./prompter', () => ({
outro: mockOutro,
}))

vi.mock('./steps', () => ({
chatFlagValues: mockChatFlagValues,
mothershipOverride: mockMothershipOverride,
promptCopilotKey: mockPromptCopilotKey,
}))

vi.mock('./theme', () => ({
theme: { accent: (value: string) => value },
}))

import { runFeatureSetup } from './feature-setup'

describe('Chat feature setup', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDiscoverConfigurationSources.mockReturnValue([
{
kind: 'compose',
label: 'Docker Compose',
location: '.env',
values: new Map([['COPILOT_API_KEY', 'existing-key']]),
managedByCurrentCheckout: true,
},
])
mockMothershipOverride.mockReturnValue({
SIM_AGENT_API_URL: 'https://copilot.example.com',
})
mockChatFlagValues.mockReturnValue({ NEXT_PUBLIC_CHAT_DISABLED: 'false' })
})

it('writes only the Chat configuration to the detected install', async () => {
mockPromptCopilotKey.mockResolvedValue('new-key')

await runFeatureSetup('chat', [])

expect(mockPromptCopilotKey).toHaveBeenCalledWith('existing-key')
expect(mockReconcileEnvValues).toHaveBeenCalledWith('root', [], {
COPILOT_API_KEY: 'new-key',
NEXT_PUBLIC_CHAT_DISABLED: 'false',
SIM_AGENT_API_URL: 'https://copilot.example.com',
})
expect(mockOutro).toHaveBeenCalledWith(
'Chat written to .env. Recreate the app container for it to take effect.'
)
})

it('fails without changing configuration when no key is received', async () => {
mockPromptCopilotKey.mockResolvedValue(null)

await expect(runFeatureSetup('chat', [])).rejects.toThrow(
'Chat setup did not receive an API key. No configuration was changed.'
)
expect(mockReconcileEnvValues).not.toHaveBeenCalled()
})
})
17 changes: 17 additions & 0 deletions packages/sim-setup/src/feature-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { promptCapabilitySetup } from './capability-setup'
import { type ConfigurationSource, discoverConfigurationSources } from './configuration-sources'
import { type EnvTarget, reconcileEnvValues } from './env-files'
import * as p from './prompter'
import { chatFlagValues, mothershipOverride, promptCopilotKey } from './steps'
import { theme } from './theme'

function isSetupFeatureId(value: string): value is SetupFeatureId {
Expand Down Expand Up @@ -113,6 +114,19 @@ async function setupLlm(vars: Map<string, string>): Promise<LlmSetupResult> {
return reconcileLlmSetup(provider, values)
}

async function setupChat(vars: Map<string, string>): Promise<Record<string, string>> {
const overrides = mothershipOverride()
const copilotKey = await promptCopilotKey(vars.get('COPILOT_API_KEY'))
if (!copilotKey) {
throw new Error('Chat setup did not receive an API key. No configuration was changed.')
}
return {
...overrides,
COPILOT_API_KEY: copilotKey,
...chatFlagValues(copilotKey),
}
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.

export function setupFeatureUsage(): string {
return SETUP_FEATURES.map((feature) =>
feature.id === 'integration' ? 'integration <slug>' : feature.id
Expand Down Expand Up @@ -182,6 +196,9 @@ export async function runFeatureSetup(feature: string, args: readonly string[]):
})
values = result.values
remove = result.remove
} else if (feature === 'chat') {
values = await setupChat(vars)
remove = []
} else if (feature === 'integration') {
values = await setupIntegration(args[0], vars)
remove = []
Expand Down
2 changes: 1 addition & 1 deletion packages/sim-setup/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { theme } from './theme'
import { SETUP_VERSION } from './version'

const SETUP_FEATURES =
'email | storage | sandbox | jobs | cache | knowledge | knowledge-embeddings | llm | integration <slug>'
'email | storage | sandbox | jobs | cache | knowledge | knowledge-embeddings | chat | llm | integration <slug>'

const USAGE = `Usage:
sim-setup [--quick] [--dir <path>] [--mode compose|dev|k8s]
Expand Down
2 changes: 1 addition & 1 deletion packages/sim-setup/src/setup-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface SetupStatusReport {
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]
feature.id === 'chat' || feature.id === 'integration' ? [] : [feature.id]
)

function readString(values: EnvCapabilityValues, key: string): string | undefined {
Expand Down
Loading