fix(config): preserve env files during managed updates

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-06 12:45:09 +00:00
1 parent 2ea22d6cea
commit 31a54faef6
5 files changed
+351 -92

No files matched your search

+11 -72
View File
@@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { PaperclipConfig } from "@paperclipai/shared";
import { updateEnvFileContents, writeEnvFileAtomicallyIfChanged } from "@paperclipai/shared/env-file";
import { resolvePaperclipConfigPath, resolvePaperclipEnvPath } from "./paths.js";
function nonEmpty(value: string | null | undefined): string | null {
@@ -76,73 +77,14 @@ function readEnvEntries(envPath: string): Record<string, string> {
return parseEnvFile(fs.readFileSync(envPath, "utf8"));
}
function formatEnvEntries(entries: Record<string, string>): string {
function emptyWorktreeEnvFileContents(): string {
return [
"# Paperclip environment variables",
"# Generated by Paperclip worktree repair",
...Object.entries(entries).map(([key, value]) => `${key}=${JSON.stringify(value)}`),
"",
].join("\n");
}
function trailingEnvComment(rawValue: string): string {
let quote: "\"" | "'" | null = null;
let escaped = false;
for (let index = 0; index < rawValue.length; index += 1) {
const character = rawValue[index];
if (escaped) {
escaped = false;
continue;
}
if (quote === "\"" && character === "\\") {
escaped = true;
continue;
}
if (quote !== null) {
if (character === quote) quote = null;
continue;
}
if (character === "\"" || character === "'") {
quote = character;
continue;
}
if (character !== "#" || index === 0 || !/\s/.test(rawValue[index - 1] ?? "")) continue;
let commentStart = index;
while (commentStart > 0 && /\s/.test(rawValue[commentStart - 1] ?? "")) {
commentStart -= 1;
}
return rawValue.slice(commentStart);
}
return "";
}
function updateEnvFileContents(contents: string, entries: Record<string, string>): string {
const newline = contents.includes("\r\n") ? "\r\n" : "\n";
const missingEntries = new Map(Object.entries(entries));
const lines = contents.split(/\r?\n/).map((rawLine) => {
const match = rawLine.match(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)(.*)$/);
if (!match) return rawLine;
const [, prefix, key, separator, rawValue] = match;
const value = entries[key];
if (value === undefined) return rawLine;
missingEntries.delete(key);
return `${prefix}${key}${separator}${JSON.stringify(value)}${trailingEnvComment(rawValue)}`;
});
const insertionIndex = lines.at(-1) === "" ? lines.length - 1 : lines.length;
lines.splice(
insertionIndex,
0,
...Array.from(missingEntries, ([key, value]) => `${key}=${JSON.stringify(value)}`),
);
return lines.join(newline);
}
function isPathInside(candidatePath: string, rootPath: string): boolean {
const candidate = path.resolve(candidatePath);
const root = path.resolve(rootPath);
@@ -623,7 +565,6 @@ export function maybeRepairLegacyWorktreeConfigAndEnvFiles(): {
}
}
const existingEnvEntries = readEnvEntries(context.envPath);
const managedEnvEntries: Record<string, string> = {
PAPERCLIP_HOME: context.homeDir,
PAPERCLIP_INSTANCE_ID: context.instanceId,
@@ -636,20 +577,18 @@ export function maybeRepairLegacyWorktreeConfigAndEnvFiles(): {
process.env.PAPERCLIP_DB_BACKUP_ENABLED = "false";
const repairedEnv = Object.entries(managedEnvEntries).some(
([key, value]) => existingEnvEntries[key] !== value,
const existingContents = fs.existsSync(context.envPath)
? fs.readFileSync(context.envPath, "utf8")
: null;
const repairedContents = updateEnvFileContents(
existingContents ?? emptyWorktreeEnvFileContents(),
managedEnvEntries,
{ valueEncoding: "json" },
);
const repairedEnv = existingContents !== repairedContents;
if (repairedEnv) {
fs.mkdirSync(path.dirname(context.envPath), { recursive: true });
const existingContents = fs.existsSync(context.envPath)
? fs.readFileSync(context.envPath, "utf8")
: null;
const repairedContents =
existingContents === null
? formatEnvEntries(managedEnvEntries)
: updateEnvFileContents(existingContents, managedEnvEntries);
fs.writeFileSync(context.envPath, repairedContents, { mode: 0o600 });
writeEnvFileAtomicallyIfChanged(context.envPath, existingContents, repairedContents);
}
return { repairedConfig, repairedEnv };