Skip to content

Commit 072e884

Browse files
improvement(setup): complete knowledge and update flows
1 parent f5c06d4 commit 072e884

15 files changed

Lines changed: 219 additions & 25 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Manage your install with `bun run sim`:
105105

106106
```bash
107107
bun run sim start | stop | restart # bring your install up / down / cycle
108+
bun run sim update # pull/rebuild and apply Compose images
108109
bun run sim status # what's installed and healthy
109110
bun run sim logs # follow logs
110111
bun run sim doctor # diagnose configuration problems

apps/docs/content/docs/en/platform/self-hosting/docker.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ docker compose -f docker-compose.prod.yml logs migrations
149149
# Is the scheduler firing?
150150
docker compose -f docker-compose.prod.yml logs -f cron
151151

152-
# Upgrade: bump SIM_VERSION in .env, then
153-
docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d
152+
# Upgrade: bump SIM_VERSION in .env when pinned, then
153+
bun run sim update
154154
```
155155

156156
<FAQ items={[
@@ -159,4 +159,3 @@ docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compo
159159
{ question: "How do I back up and restore the database?", answer: "Back up with: docker compose -f docker-compose.prod.yml exec db pg_dump -U postgres simstudio > backup.sql. Restore with: docker compose -f docker-compose.prod.yml exec -T db psql -U postgres simstudio < backup.sql. The database data is persisted in a Docker volume named postgres_data."},
160160
{ question: "Can I customize the PostgreSQL credentials?", answer: "Yes. The docker-compose.prod.yml uses environment variable defaults: POSTGRES_USER (default: postgres), POSTGRES_PASSWORD (default: postgres), POSTGRES_DB (default: simstudio), and POSTGRES_PORT (default: 5432). Set these in your .env file to override them." },
161161
]} />
162-

apps/docs/content/docs/en/platform/self-hosting/upgrades.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,14 @@ kubectl logs -n simstudio deploy/sim-app -c migrations --tail=100
142142
<Tab value="Docker Compose">
143143

144144
```bash
145-
docker compose -f docker-compose.prod.yml pull
146-
docker compose -f docker-compose.prod.yml up -d
145+
bun run sim update
147146
docker compose -f docker-compose.prod.yml logs migrations
148147
```
149148

149+
`bun run sim update` pulls the versions configured by `SIM_VERSION` (or `latest` when it is
150+
unset), recreates the changed services, and keeps data volumes. It is equivalent to running
151+
`docker compose pull` followed by `docker compose up -d`.
152+
150153
There is a short window where the app is unavailable while containers restart. Compose has no rolling-update mechanism — plan a maintenance window, or run Kubernetes if you need zero-downtime upgrades.
151154

152155
</Tab>

scripts/setup/capability-config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
defineCapabilitySetup,
1111
EMAIL_SETUP,
1212
getOAuthClientSetupFields,
13+
KNOWLEDGE_EMBEDDINGS_SETUP,
1314
STORAGE_SETUP,
1415
} from './capability-config.ts'
1516
import { getCapabilitySetupOptions } from './capability-setup.ts'
@@ -76,6 +77,12 @@ describe('capability setup configuration', () => {
7677
)
7778
})
7879

80+
it('offers OpenAI first for fresh knowledge embedding setup', () => {
81+
expect(
82+
getCapabilitySetupOptions(KNOWLEDGE_EMBEDDINGS_SETUP).map((option) => option.id)
83+
).toEqual(['openai', 'azure-openai', 'openrouter'])
84+
})
85+
7986
it('maps every OAuth runtime field to a CLI input mode in runtime order', () => {
8087
for (const id of Object.keys(OAUTH_CLIENT_CAPABILITIES) as Array<
8188
keyof typeof OAUTH_CLIENT_CAPABILITIES

scripts/setup/capability-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ export const KNOWLEDGE_EMBEDDINGS_SETUP = defineCapabilitySetup(KNOWLEDGE_EMBEDD
823823
],
824824
},
825825
},
826-
optionOrder: ['azure-openai', 'openai', 'openrouter'],
826+
optionOrder: ['openai', 'azure-openai', 'openrouter'],
827827
})
828828

829829
export const CAPABILITY_SETUPS = [

scripts/setup/capability-setup.ts

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ interface PromptState {
4949
values: Record<string, string>
5050
}
5151

52+
const SKIP_OPTION_ID = '__skip-capability-setup__'
53+
5254
/** Stages a capability transition into a larger setup run without losing prompt context. */
5355
export function stageCapabilitySetupTransition(
5456
currentValues: Map<string, string>,
@@ -176,11 +178,11 @@ export function getCapabilitySetupOptions(
176178
})
177179
}
178180

179-
/** Resolves the setup option representing the effective current configuration. */
181+
/** Resolves the setup option representing the effective current configuration, if one exists. */
180182
export function resolveCurrentCapabilitySetupOptionId(
181183
setup: CapabilitySetupDefinition,
182184
values: EnvCapabilityValues
183-
): string {
185+
): string | undefined {
184186
const options = getCapabilitySetupOptions(setup)
185187
const explicitAction = options.find(
186188
(option) =>
@@ -214,9 +216,10 @@ export function resolveCurrentCapabilitySetupOptionId(
214216

215217
const firstAction = options.find((option) => option.kind === 'action')
216218
if (firstAction) return firstAction.id
217-
throw new Error(
218-
`Capability ${setup.definition.id} has no setup option for its current configuration`
219-
)
219+
if (options.length === 0) {
220+
throw new Error(`Capability ${setup.definition.id} has no setup options`)
221+
}
222+
return undefined
220223
}
221224

222225
/** Applies selector and activation inference to the CLI-entered values. */
@@ -473,6 +476,21 @@ async function renderPrompts(prompts: readonly SetupPrompt[], state: PromptState
473476
}
474477
}
475478

479+
async function promptSelectedCapabilitySetup(
480+
setup: CapabilitySetupDefinition,
481+
selected: ResolvedSetupOption,
482+
currentValues: ReadonlyMap<string, string>
483+
): Promise<EnvCapabilitySetupTransition> {
484+
const state: PromptState = {
485+
setup,
486+
optionId: selected.id,
487+
currentValues,
488+
values: {},
489+
}
490+
await renderPrompts(selected.prompts, state)
491+
return buildCapabilitySetupTransition(setup, selected.id, state.values, currentValues)
492+
}
493+
476494
/** Renders a CLI-owned capability setup and returns its validated environment transition. */
477495
export async function promptCapabilitySetup(
478496
setup: CapabilitySetupDefinition,
@@ -497,12 +515,44 @@ export async function promptCapabilitySetup(
497515
)
498516
}
499517

500-
const state: PromptState = {
501-
setup,
502-
optionId: selected.id,
503-
currentValues,
504-
values: {},
518+
return promptSelectedCapabilitySetup(setup, selected, currentValues)
519+
}
520+
521+
/** Offers capability providers while allowing the user to leave configuration unchanged. */
522+
export async function promptOptionalCapabilitySetup(
523+
setup: CapabilitySetupDefinition,
524+
currentValues: ReadonlyMap<string, string>,
525+
context: CapabilitySetupContext,
526+
skipHint: string
527+
): Promise<EnvCapabilitySetupTransition | null> {
528+
const options = getCapabilitySetupOptions(setup)
529+
if (options.some((option) => option.id === SKIP_OPTION_ID)) {
530+
throw new Error(`Capability ${setup.definition.id} uses reserved option ${SKIP_OPTION_ID}`)
505531
}
506-
await renderPrompts(selected.prompts, state)
507-
return buildCapabilitySetupTransition(setup, selected.id, state.values, currentValues)
532+
const currentOptionId = resolveCurrentCapabilitySetupOptionId(setup, currentValues)
533+
const selectedOptionId = await p.select({
534+
message: setup.message,
535+
options: [
536+
...options.map((option) => ({
537+
value: option.id,
538+
label: option.label,
539+
hint: markCurrentlyUsed(resolveHint(option.hint, context), option.id === currentOptionId),
540+
})),
541+
{
542+
value: SKIP_OPTION_ID,
543+
label: 'Not now',
544+
hint: currentOptionId ? 'leave the current configuration unchanged' : skipHint,
545+
},
546+
],
547+
initialValue: currentOptionId ?? options[0]?.id,
548+
})
549+
if (selectedOptionId === SKIP_OPTION_ID) return null
550+
551+
const selected = options.find((option) => option.id === selectedOptionId)
552+
if (!selected) {
553+
throw new Error(
554+
`Capability ${setup.definition.id} returned unknown setup option ${selectedOptionId}`
555+
)
556+
}
557+
return promptSelectedCapabilitySetup(setup, selected, currentValues)
508558
}

scripts/setup/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const USAGE = `Usage:
1818
bun run sim setup <feature> configure one feature
1919
bun run sim doctor [--fix] [--json] check your setup
2020
bun run sim start | stop | restart bring your install up / down / cycle
21+
bun run sim update pull/rebuild and apply Compose images
2122
bun run sim status what's installed and healthy
2223
bun run sim logs follow logs
2324
bun run sim down remove containers (data kept)

scripts/setup/lifecycle.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { getComposeUpdateMode, isLifecycleCommand } from './lifecycle.ts'
3+
4+
describe('setup lifecycle', () => {
5+
it('recognizes update as a lifecycle command', () => {
6+
expect(isLifecycleCommand('update')).toBe(true)
7+
})
8+
9+
it('pulls published installs and rebuilds source installs', () => {
10+
expect(getComposeUpdateMode('/repo/docker-compose.prod.yml')).toBe('pull')
11+
expect(getComposeUpdateMode('/repo/docker-compose.local.yml')).toBe('build')
12+
expect(() => getComposeUpdateMode('/repo/compose.yml')).toThrow(/Unsupported Sim Compose file/)
13+
})
14+
})

scripts/setup/lifecycle.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const LIFECYCLE_COMMANDS = [
2020
'start',
2121
'stop',
2222
'restart',
23+
'update',
2324
'status',
2425
'logs',
2526
'down',
@@ -325,6 +326,50 @@ function restart(install: Install): void {
325326
p.note(k8sReachHints(install.context), 'Kubernetes is managed with kubectl')
326327
}
327328

329+
export type ComposeUpdateMode = 'pull' | 'build'
330+
331+
/** Resolves how a setup-managed Compose install obtains its next image. */
332+
export function getComposeUpdateMode(file: string): ComposeUpdateMode {
333+
const name = path.basename(file)
334+
if (name === 'docker-compose.prod.yml') return 'pull'
335+
if (name === 'docker-compose.local.yml') return 'build'
336+
throw new Error(`Unsupported Sim Compose file: ${file}`)
337+
}
338+
339+
function update(install: Install): void {
340+
if (install.kind === 'dev') {
341+
throw new SetupError('sim update is only available for Docker Compose installs.', [
342+
'update the source checkout with git, run bun install, then restart bun run dev:full',
343+
])
344+
}
345+
if (install.kind === 'k8s') {
346+
throw new SetupError('sim update does not upgrade Kubernetes releases.', [
347+
'upgrade the release with helm after reviewing the chart and release notes',
348+
])
349+
}
350+
351+
const mode = getComposeUpdateMode(install.file)
352+
const spin = p.spinner()
353+
if (mode === 'pull') {
354+
spin.start('Pulling configured Sim images…')
355+
dockerRun(composeArgs(install, 'pull'), 'docker compose pull failed', install.dir)
356+
} else {
357+
spin.start('Rebuilding Sim images with current base images…')
358+
dockerRun(composeArgs(install, 'build', '--pull'), 'docker compose build failed', install.dir)
359+
}
360+
spin.message('Applying updated images and running migrations…')
361+
dockerRun(composeArgs(install, 'up', '-d'), 'docker compose up failed', install.dir)
362+
spin.stop('Sim updated (data volumes kept)')
363+
p.note(
364+
[
365+
`version: ${theme.command(`SIM_VERSION in ${path.join(install.dir, '.env')}`)} (latest when unset)`,
366+
`check: ${theme.command('bun run sim status')}`,
367+
`logs: ${theme.command('bun run sim logs')}`,
368+
].join('\n'),
369+
'Update complete'
370+
)
371+
}
372+
328373
function showLogs(install: Install): void {
329374
if (install.kind === 'compose') {
330375
dockerInherit(composeArgs(install, 'logs', '-f', '--tail', '100'), install.dir)
@@ -488,6 +533,8 @@ export async function runLifecycle(command: LifecycleCommand): Promise<void> {
488533
return stop(install)
489534
case 'restart':
490535
return restart(install)
536+
case 'update':
537+
return update(install)
491538
case 'logs':
492539
return showLogs(install)
493540
case 'down':

scripts/setup/modes/compose.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
collectSecrets,
1414
mothershipOverride,
1515
promptCopilotKey,
16+
promptKnowledgeEmbeddings,
1617
promptLlmKeys,
1718
promptSecurity,
1819
promptSignInProviders,
@@ -117,9 +118,13 @@ export async function runComposeMode(detection: Detection, quick: boolean): Prom
117118
if (copilotKey) values.COPILOT_API_KEY = copilotKey
118119
Object.assign(values, chatFlagValues(copilotKey))
119120
Object.assign(values, await promptLlmKeys(detection, !quick))
121+
const stagedVars = new Map(root.vars)
122+
for (const [key, value] of Object.entries(values)) stagedVars.set(key, value)
123+
const embeddings = await promptKnowledgeEmbeddings(stagedVars, { containerized: true })
124+
if (embeddings) {
125+
stageCapabilitySetupTransition(stagedVars, values, remove, embeddings)
126+
}
120127
if (!quick) {
121-
const stagedVars = new Map(root.vars)
122-
for (const [key, value] of Object.entries(values)) stagedVars.set(key, value)
123128
const storage = await promptCapabilitySetup(STORAGE_SETUP, stagedVars, {
124129
containerized: true,
125130
})

0 commit comments

Comments
 (0)