Files
paperclip/cli/src/commands/configure.ts
Forgotten 5b983ca4d3 feat(cli): add deployment mode prompts, auth bootstrap-ceo command, and doctor check
Extend server setup prompts with deployment mode (local_trusted vs
authenticated), exposure (private vs public), bind host, and auth config.
Add auth bootstrap-ceo command that creates a one-time invite URL for the
initial instance admin. Add deployment-auth-check to doctor diagnostics.
Register the new command in the CLI entry point.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:40:59 -06:00

184 lines
5.3 KiB
TypeScript

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 {
resolveDefaultEmbeddedPostgresDir,
resolveDefaultLogsDir,
resolvePaperclipInstanceId,
} from "../config/home.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,
},
logging: {
mode: "file",
logDir: resolveDefaultLogsDir(instanceId),
},
server: {
deploymentMode: "local_trusted",
exposure: "private",
host: "127.0.0.1",
port: 3100,
serveUi: true,
},
auth: {
baseUrlMode: "auto",
},
storage: defaultStorageConfig(),
secrets: defaultSecretsConfig(),
};
}
export async function configure(opts: {
config?: string;
section?: string;
}): Promise<void> {
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 `paperclip 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();
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();
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.");
}