Storage, ops, and migration
Where doc.haus keeps data, how the services fit together, and how older installations move forward — the operational backbone behind every matter.
scripts/migrate-settings.ts49 lines · migrateSettings L7–39
Outline 1 symbols
- migrateSettings function export
1import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, writeFileSync } from "node:fs"
2import path from "node:path"
3import { mergeSettings, readSettingsFile, type EngineSettings } from "../services/engine/src/effective-settings"
4import { validateSettingsPatch, writeEngineOverrides } from "../services/ingest/src/engine-config"
5
6// Explicit one-off only. Dry-run prints field names, never source values.
7export function migrateSettings(input: { source: string; runtime: string; apply?: boolean }) {
8 if (!path.isAbsolute(input.source) || !path.isAbsolute(input.runtime)) throw new Error("Source and runtime must be explicit absolute paths.")
9 if (!lstatSync(input.source).isFile() || lstatSync(input.source).isSymbolicLink()) throw new Error("Source must be a regular file.")
10 const directory = path.join(input.runtime, "config", "opencode"), target = path.join(directory, "opencode.json")
11 if (path.resolve(input.source) === target) throw new Error("Source must differ from runtime overrides.")
12 const marker = path.join(directory, "legacy-settings-migrated.json")
13 if (existsSync(marker)) return { status: "already-migrated", fields: [] as string[], skipped: [] as string[] }
14 const source = readSettingsFile(input.source), migrated: EngineSettings = {}
15 const fields: string[] = [], skipped: string[] = []
16 function candidate(patch: Record<string, unknown>, label: string) {
17 try { validateSettingsPatch(patch); Object.assign(migrated, mergeSettings(migrated, patch)); fields.push(label) }
18 catch { skipped.push(label) }
19 }
20 for (const key of ["model", "small_model", "disabled_providers"]) if (source[key] !== undefined) candidate({ [key]: source[key] }, key)
21 for (const [id, agent] of Object.entries(source.agent ?? {})) for (const key of ["model", "disable"]) if (agent[key] !== undefined) candidate({ agent: { [id]: { [key]: agent[key] } } }, `agent.${id}.${key}`)
22 const skills = source.permission?.skill
23 if (typeof skills === "object" && skills !== null && !Array.isArray(skills)) for (const [id, action] of Object.entries(skills)) if (id !== "*") candidate({ permission: { skill: { [id]: action } } }, `permission.skill.${id}`)
24 for (const [id, provider] of Object.entries(source.provider ?? {})) {
25 for (const key of ["name", "npm"]) if (provider[key] !== undefined) candidate({ provider: { [id]: { [key]: provider[key] } } }, `provider.${id}.${key}`)
26 if (typeof provider.options === "object" && provider.options !== null) for (const [key, value] of Object.entries(provider.options)) candidate({ provider: { [id]: { options: { [key]: value } } } }, `provider.${id}.options.${key}`)
27 if (typeof provider.models === "object" && provider.models !== null) for (const [idModel, model] of Object.entries(provider.models)) if (model && typeof model === "object" && "name" in model) candidate({ provider: { [id]: { models: { [idModel]: { name: model.name } } } } }, `provider.${id}.models.${idModel}.name`)
28 }
29 if (!input.apply) return { status: "dry-run", fields, skipped }
30 const overrides = mergeSettings(migrated, readSettingsFile(target)) // existing local choices win
31 validateSettingsPatch(overrides)
32 mkdirSync(directory, { recursive: true, mode: 0o700 }); chmodSync(directory, 0o700)
33 const backup = path.join(directory, "legacy-settings.backup.json")
34 if (!existsSync(backup)) { copyFileSync(input.source, backup, 1); chmodSync(backup, 0o600) }
35 writeEngineOverrides(overrides, target)
36 if (JSON.stringify(readSettingsFile(target)) !== JSON.stringify(overrides)) throw new Error("Runtime settings readback failed; migration marker was not written.")
37 writeFileSync(marker, JSON.stringify({ version: 1 }) + "\n", { flag: "wx", mode: 0o600 })
38 return { status: "migrated", fields, skipped }
39}
40if (import.meta.main) {
41 try {
42 const args = process.argv.slice(2)
43 if (args.some((arg, index) => !["--source", "--runtime", "--apply"].includes(arg) && !["--source", "--runtime"].includes(args[index - 1]))) throw new Error("Unsupported argument")
44 const source = args[args.indexOf("--source") + 1], runtime = args[args.indexOf("--runtime") + 1]
45 if (!args.includes("--source") || !args.includes("--runtime") || !source || !runtime) throw new Error("Explicit --source and --runtime required")
46 console.log(JSON.stringify(migrateSettings({ source, runtime, apply: args.includes("--apply") })))
47 } catch { console.error("Settings migration failed. Check explicit paths and supported fields; no source values are logged."); process.exitCode = 1 }
48}
49