Merge branch 'pr-10980' into pap16587-combined

# Conflicts:
#	server/src/worktree-config.ts
This commit is contained in:
Dotta committed 2026-08-07 05:22:22 +00:00
commit 0ac9c6e80f
5 files changed
+368 -92

No files matched your search

+11 -72
View File
@@ -7,6 +7,7 @@ import {
paperclipConfigSchema,
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 {
@@ -81,73 +82,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);
@@ -679,7 +621,6 @@ export function maybeRepairLegacyWorktreeConfigAndEnvFiles(): {
}
}
const existingEnvEntries = readEnvEntries(context.envPath);
const managedEnvEntries: Record<string, string> = {
PAPERCLIP_HOME: context.homeDir,
PAPERCLIP_INSTANCE_ID: context.instanceId,
@@ -692,20 +633,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 };