Files
paperclip/cli/src/config/env.ts

126 lines
3.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { randomBytes } from "node:crypto";
import { config as loadDotenv, parse as parseEnvFileContents } from "dotenv";
import { resolveConfigPath } from "./store.js";
const JWT_SECRET_ENV_KEY = "PAPERCLIP_AGENT_JWT_SECRET";
function resolveEnvFilePath(configPath?: string) {
return path.resolve(path.dirname(resolveConfigPath(configPath)), ".env");
}
const loadedEnvFiles = new Set<string>();
function isNonEmpty(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function parseEnvFile(contents: string) {
try {
return parseEnvFileContents(contents);
} catch {
return {};
}
}
function formatEnvValue(value: string): string {
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) {
return value;
}
return JSON.stringify(value);
}
function renderEnvFile(entries: Record<string, string>) {
const lines = [
"# Paperclip environment variables",
"# Generated by Paperclip CLI commands",
...Object.entries(entries).map(([key, value]) => `${key}=${formatEnvValue(value)}`),
"",
];
return lines.join("\n");
}
export function resolvePaperclipEnvFile(configPath?: string): string {
return resolveEnvFilePath(configPath);
}
export function resolveAgentJwtEnvFile(configPath?: string): string {
return resolveEnvFilePath(configPath);
}
export function loadPaperclipEnvFile(configPath?: string): void {
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
}
export function loadAgentJwtEnvFile(filePath = resolveEnvFilePath()): void {
if (loadedEnvFiles.has(filePath)) return;
if (!fs.existsSync(filePath)) return;
loadedEnvFiles.add(filePath);
loadDotenv({ path: filePath, override: false, quiet: true });
}
export function readAgentJwtSecretFromEnv(configPath?: string): string | null {
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
const raw = process.env[JWT_SECRET_ENV_KEY];
return isNonEmpty(raw) ? raw!.trim() : null;
}
export function readAgentJwtSecretFromEnvFile(filePath = resolveEnvFilePath()): string | null {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf-8");
const values = parseEnvFile(raw);
const value = values[JWT_SECRET_ENV_KEY];
return isNonEmpty(value) ? value!.trim() : null;
}
export function ensureAgentJwtSecret(configPath?: string): { secret: string; created: boolean } {
const existingEnv = readAgentJwtSecretFromEnv(configPath);
if (existingEnv) {
return { secret: existingEnv, created: false };
}
const envFilePath = resolveEnvFilePath(configPath);
const existingFile = readAgentJwtSecretFromEnvFile(envFilePath);
const secret = existingFile ?? randomBytes(32).toString("hex");
const created = !existingFile;
if (!existingFile) {
writeAgentJwtEnv(secret, envFilePath);
}
return { secret, created };
}
export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void {
mergePaperclipEnvEntries({ [JWT_SECRET_ENV_KEY]: secret }, filePath);
}
export function readPaperclipEnvEntries(filePath = resolveEnvFilePath()): Record<string, string> {
if (!fs.existsSync(filePath)) return {};
return parseEnvFile(fs.readFileSync(filePath, "utf-8"));
}
export function writePaperclipEnvEntries(entries: Record<string, string>, filePath = resolveEnvFilePath()): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, renderEnvFile(entries), {
mode: 0o600,
});
}
export function mergePaperclipEnvEntries(
entries: Record<string, string>,
filePath = resolveEnvFilePath(),
): Record<string, string> {
const current = readPaperclipEnvEntries(filePath);
const next = {
...current,
...Object.fromEntries(
Object.entries(entries).filter(([, value]) => typeof value === "string" && value.trim().length > 0),
),
};
writePaperclipEnvEntries(next, filePath);
return next;
}