Spaces:
Paused
Paused
| import * as p from "@clack/prompts"; | |
| import pc from "picocolors"; | |
| import { readConfig, writeConfig, configExists, resolveConfigPath } from "../config/store.js"; | |
| import type { PaperclipConfig } from "../config/schema.js"; | |
| import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js"; | |
| import { promptDatabase } from "../prompts/database.js"; | |
| import { promptLlm } from "../prompts/llm.js"; | |
| import { promptLogging } from "../prompts/logging.js"; | |
| import { defaultSecretsConfig, promptSecrets } from "../prompts/secrets.js"; | |
| import { defaultStorageConfig, promptStorage } from "../prompts/storage.js"; | |
| import { promptServer } from "../prompts/server.js"; | |
| import { | |
| resolveDefaultBackupDir, | |
| resolveDefaultEmbeddedPostgresDir, | |
| resolveDefaultLogsDir, | |
| resolvePaperclipInstanceId, | |
| } from "../config/home.js"; | |
| import { printPaperclipCliBanner } from "../utils/banner.js"; | |
| type Section = "llm" | "database" | "logging" | "server" | "storage" | "secrets"; | |
| const SECTION_LABELS: Record<Section, string> = { | |
| llm: "LLM Provider", | |
| database: "Database", | |
| logging: "Logging", | |
| server: "Server", | |
| storage: "Storage", | |
| secrets: "Secrets", | |
| }; | |
| function defaultConfig(): PaperclipConfig { | |
| const instanceId = resolvePaperclipInstanceId(); | |
| return { | |
| $meta: { | |
| version: 1, | |
| updatedAt: new Date().toISOString(), | |
| source: "configure", | |
| }, | |
| database: { | |
| mode: "embedded-postgres", | |
| embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId), | |
| embeddedPostgresPort: 54329, | |
| backup: { | |
| enabled: true, | |
| intervalMinutes: 60, | |
| retentionDays: 30, | |
| dir: resolveDefaultBackupDir(instanceId), | |
| }, | |
| }, | |
| logging: { | |
| mode: "file", | |
| logDir: resolveDefaultLogsDir(instanceId), | |
| }, | |
| server: { | |
| deploymentMode: "local_trusted", | |
| exposure: "private", | |
| host: "127.0.0.1", | |
| port: 3100, | |
| allowedHostnames: [], | |
| serveUi: true, | |
| }, | |
| auth: { | |
| baseUrlMode: "auto", | |
| disableSignUp: false, | |
| }, | |
| storage: defaultStorageConfig(), | |
| secrets: defaultSecretsConfig(), | |
| }; | |
| } | |
| export async function configure(opts: { | |
| config?: string; | |
| section?: string; | |
| }): Promise<void> { | |
| printPaperclipCliBanner(); | |
| p.intro(pc.bgCyan(pc.black(" paperclip configure "))); | |
| const configPath = resolveConfigPath(opts.config); | |
| if (!configExists(opts.config)) { | |
| p.log.error("No config file found. Run `paperclipai onboard` first."); | |
| p.outro(""); | |
| return; | |
| } | |
| let config: PaperclipConfig; | |
| try { | |
| config = readConfig(opts.config) ?? defaultConfig(); | |
| } catch (err) { | |
| p.log.message( | |
| pc.yellow( | |
| `Existing config is invalid. Loading defaults so you can repair it now.\n${err instanceof Error ? err.message : String(err)}`, | |
| ), | |
| ); | |
| config = defaultConfig(); | |
| } | |
| let section: Section | undefined = opts.section as Section | undefined; | |
| if (section && !SECTION_LABELS[section]) { | |
| p.log.error(`Unknown section: ${section}. Choose from: ${Object.keys(SECTION_LABELS).join(", ")}`); | |
| p.outro(""); | |
| return; | |
| } | |
| // Section selection loop | |
| let continueLoop = true; | |
| while (continueLoop) { | |
| if (!section) { | |
| const choice = await p.select({ | |
| message: "Which section do you want to configure?", | |
| options: Object.entries(SECTION_LABELS).map(([value, label]) => ({ | |
| value: value as Section, | |
| label, | |
| })), | |
| }); | |
| if (p.isCancel(choice)) { | |
| p.cancel("Configuration cancelled."); | |
| return; | |
| } | |
| section = choice; | |
| } | |
| p.log.step(pc.bold(SECTION_LABELS[section])); | |
| switch (section) { | |
| case "database": | |
| config.database = await promptDatabase(config.database); | |
| break; | |
| case "llm": { | |
| const llm = await promptLlm(); | |
| if (llm) { | |
| config.llm = llm; | |
| } else { | |
| delete config.llm; | |
| } | |
| break; | |
| } | |
| case "logging": | |
| config.logging = await promptLogging(); | |
| break; | |
| case "server": | |
| { | |
| const { server, auth } = await promptServer({ | |
| currentServer: config.server, | |
| currentAuth: config.auth, | |
| }); | |
| config.server = server; | |
| config.auth = auth; | |
| } | |
| break; | |
| case "storage": | |
| config.storage = await promptStorage(config.storage); | |
| break; | |
| case "secrets": | |
| config.secrets = await promptSecrets(config.secrets); | |
| { | |
| const keyResult = ensureLocalSecretsKeyFile(config, configPath); | |
| if (keyResult.status === "created") { | |
| p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`); | |
| } else if (keyResult.status === "existing") { | |
| p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`)); | |
| } else if (keyResult.status === "skipped_provider") { | |
| p.log.message(pc.dim("Skipping local key file management for non-local provider")); | |
| } else { | |
| p.log.message(pc.dim("Skipping local key file management because PAPERCLIP_SECRETS_MASTER_KEY is set")); | |
| } | |
| } | |
| break; | |
| } | |
| config.$meta.updatedAt = new Date().toISOString(); | |
| config.$meta.source = "configure"; | |
| writeConfig(config, opts.config); | |
| p.log.success(`${SECTION_LABELS[section]} configuration updated.`); | |
| // If section was provided via CLI flag, don't loop | |
| if (opts.section) { | |
| continueLoop = false; | |
| } else { | |
| const another = await p.confirm({ | |
| message: "Configure another section?", | |
| initialValue: false, | |
| }); | |
| if (p.isCancel(another) || !another) { | |
| continueLoop = false; | |
| } else { | |
| section = undefined; // Reset to show picker again | |
| } | |
| } | |
| } | |
| p.outro("Configuration saved."); | |
| } | |