Skip to content

Commit 5478a69

Browse files
improvement(setup): complete knowledge and update flows (#6521)
1 parent daac4f3 commit 5478a69

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)