From ffd1631b14fcf898f54bdad7084a61975cec4cb2 Mon Sep 17 00:00:00 2001 From: zvictor Date: Thu, 5 Mar 2026 16:12:17 -0300 Subject: [PATCH] `onboard` now derives defaults from env vars before writing config --- cli/src/commands/onboard.ts | 181 +++++++++++++++++++++++++++++++----- cli/src/prompts/server.ts | 9 +- 2 files changed, 167 insertions(+), 23 deletions(-) diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 050925e4..9fee5f84 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -1,5 +1,18 @@ import * as p from "@clack/prompts"; +import path from "node:path"; import pc from "picocolors"; +import { + AUTH_BASE_URL_MODES, + DEPLOYMENT_EXPOSURES, + DEPLOYMENT_MODES, + SECRET_PROVIDERS, + STORAGE_PROVIDERS, + type AuthBaseUrlMode, + type DeploymentExposure, + type DeploymentMode, + type SecretProvider, + type StorageProvider, +} from "@paperclipai/shared"; import { configExists, readConfig, resolveConfigPath, writeConfig } from "../config/store.js"; import type { PaperclipConfig } from "../config/schema.js"; import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js"; @@ -12,6 +25,7 @@ import { defaultStorageConfig, promptStorage } from "../prompts/storage.js"; import { promptServer } from "../prompts/server.js"; import { describeLocalInstancePaths, + expandHomePrefix, resolveDefaultBackupDir, resolveDefaultEmbeddedPostgresDir, resolveDefaultLogsDir, @@ -29,18 +43,109 @@ type OnboardOptions = { invokedByRun?: boolean; }; -function quickstartDefaults(): Pick { +type OnboardDefaults = Pick; + +const ONBOARD_ENV_KEYS = [ + "DATABASE_URL", + "PAPERCLIP_DB_BACKUP_ENABLED", + "PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES", + "PAPERCLIP_DB_BACKUP_RETENTION_DAYS", + "PAPERCLIP_DB_BACKUP_DIR", + "PAPERCLIP_DEPLOYMENT_MODE", + "PAPERCLIP_DEPLOYMENT_EXPOSURE", + "HOST", + "PORT", + "SERVE_UI", + "PAPERCLIP_ALLOWED_HOSTNAMES", + "PAPERCLIP_AUTH_BASE_URL_MODE", + "PAPERCLIP_AUTH_PUBLIC_BASE_URL", + "BETTER_AUTH_URL", + "PAPERCLIP_STORAGE_PROVIDER", + "PAPERCLIP_STORAGE_LOCAL_DIR", + "PAPERCLIP_STORAGE_S3_BUCKET", + "PAPERCLIP_STORAGE_S3_REGION", + "PAPERCLIP_STORAGE_S3_ENDPOINT", + "PAPERCLIP_STORAGE_S3_PREFIX", + "PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE", + "PAPERCLIP_SECRETS_PROVIDER", + "PAPERCLIP_SECRETS_STRICT_MODE", + "PAPERCLIP_SECRETS_MASTER_KEY_FILE", +] as const; + +function parseBooleanFromEnv(rawValue: string | undefined): boolean | null { + if (rawValue === undefined) return null; + return rawValue === "true"; +} + +function parseNumberFromEnv(rawValue: string | undefined): number | null { + if (!rawValue) return null; + const parsed = Number(rawValue); + if (!Number.isFinite(parsed)) return null; + return parsed; +} + +function parseEnumFromEnv(rawValue: string | undefined, allowedValues: readonly T[]): T | null { + if (!rawValue) return null; + return allowedValues.includes(rawValue as T) ? (rawValue as T) : null; +} + +function resolvePathFromEnv(rawValue: string | undefined): string | null { + if (!rawValue || rawValue.trim().length === 0) return null; + return path.resolve(expandHomePrefix(rawValue.trim())); +} + +function quickstartDefaultsFromEnv(): { defaults: OnboardDefaults; usedEnvKeys: string[] } { const instanceId = resolvePaperclipInstanceId(); - return { + const defaultStorage = defaultStorageConfig(); + const defaultSecrets = defaultSecretsConfig(); + const databaseUrl = process.env.DATABASE_URL?.trim() || undefined; + const deploymentMode = + parseEnumFromEnv(process.env.PAPERCLIP_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted"; + const deploymentExposureFromEnv = parseEnumFromEnv( + process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE, + DEPLOYMENT_EXPOSURES, + ); + const deploymentExposure = + deploymentMode === "local_trusted" ? "private" : (deploymentExposureFromEnv ?? "private"); + const authPublicBaseUrl = + (process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ?? process.env.BETTER_AUTH_URL)?.trim() || undefined; + const authBaseUrlModeFromEnv = parseEnumFromEnv( + process.env.PAPERCLIP_AUTH_BASE_URL_MODE, + AUTH_BASE_URL_MODES, + ); + const authBaseUrlMode = authBaseUrlModeFromEnv ?? (authPublicBaseUrl ? "explicit" : "auto"); + const allowedHostnamesFromEnv = process.env.PAPERCLIP_ALLOWED_HOSTNAMES + ? process.env.PAPERCLIP_ALLOWED_HOSTNAMES + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter((value) => value.length > 0) + : []; + const storageProvider = + parseEnumFromEnv(process.env.PAPERCLIP_STORAGE_PROVIDER, STORAGE_PROVIDERS) ?? + defaultStorage.provider; + const secretsProvider = + parseEnumFromEnv(process.env.PAPERCLIP_SECRETS_PROVIDER, SECRET_PROVIDERS) ?? + defaultSecrets.provider; + const databaseBackupEnabled = parseBooleanFromEnv(process.env.PAPERCLIP_DB_BACKUP_ENABLED) ?? true; + const databaseBackupIntervalMinutes = Math.max( + 1, + parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES) ?? 60, + ); + const databaseBackupRetentionDays = Math.max( + 1, + parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_RETENTION_DAYS) ?? 30, + ); + const defaults: OnboardDefaults = { database: { - mode: "embedded-postgres", + mode: databaseUrl ? "postgres" : "embedded-postgres", + ...(databaseUrl ? { connectionString: databaseUrl } : {}), embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId), embeddedPostgresPort: 54329, backup: { - enabled: true, - intervalMinutes: 60, - retentionDays: 30, - dir: resolveDefaultBackupDir(instanceId), + enabled: databaseBackupEnabled, + intervalMinutes: databaseBackupIntervalMinutes, + retentionDays: databaseBackupRetentionDays, + dir: resolvePathFromEnv(process.env.PAPERCLIP_DB_BACKUP_DIR) ?? resolveDefaultBackupDir(instanceId), }, }, logging: { @@ -48,19 +153,45 @@ function quickstartDefaults(): Pick process.env[key] !== undefined); + return { defaults, usedEnvKeys }; } export async function onboard(opts: OnboardOptions): Promise { @@ -116,6 +247,7 @@ export async function onboard(opts: OnboardOptions): Promise { } let llm: PaperclipConfig["llm"] | undefined; + const { defaults: derivedDefaults, usedEnvKeys } = quickstartDefaultsFromEnv(); let { database, logging, @@ -123,7 +255,7 @@ export async function onboard(opts: OnboardOptions): Promise { auth, storage, secrets, - } = quickstartDefaults(); + } = derivedDefaults; if (setupMode === "advanced") { p.log.step(pc.bold("Database")); @@ -191,10 +323,10 @@ export async function onboard(opts: OnboardOptions): Promise { logging = await promptLogging(); p.log.step(pc.bold("Server")); - ({ server, auth } = await promptServer()); + ({ server, auth } = await promptServer({ currentServer: server, currentAuth: auth })); p.log.step(pc.bold("Storage")); - storage = await promptStorage(defaultStorageConfig()); + storage = await promptStorage(storage); p.log.step(pc.bold("Secrets")); secrets = defaultSecretsConfig(); @@ -205,9 +337,14 @@ export async function onboard(opts: OnboardOptions): Promise { ); } else { p.log.step(pc.bold("Quickstart")); - p.log.message( - pc.dim("Using local defaults: embedded database, no LLM provider, file storage, and local encrypted secrets."), - ); + p.log.message(pc.dim("Using quickstart defaults.")); + if (usedEnvKeys.length > 0) { + p.log.message(pc.dim(`Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`)); + } else { + p.log.message( + pc.dim("No environment overrides detected: embedded database, file storage, local encrypted secrets."), + ); + } } const jwtSecret = ensureAgentJwtSecret(configPath); diff --git a/cli/src/prompts/server.ts b/cli/src/prompts/server.ts index 1b271316..c2ab4218 100644 --- a/cli/src/prompts/server.ts +++ b/cli/src/prompts/server.ts @@ -149,7 +149,14 @@ export async function promptServer(opts?: { } return { - server: { deploymentMode, exposure, host: hostStr.trim(), port, allowedHostnames, serveUi: true }, + server: { + deploymentMode, + exposure, + host: hostStr.trim(), + port, + allowedHostnames, + serveUi: currentServer?.serveUi ?? true, + }, auth, }; }