mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
Merge branch 'pr-11005' into pap16587-combined
This commit is contained in:
commit
ead4149b1a
16 files changed
+940
-43
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { readConfigFile } from "../config-file.js";
|
||||
|
||||
const ORIGINAL_PAPERCLIP_CONFIG = process.env.PAPERCLIP_CONFIG;
|
||||
@@ -42,6 +42,7 @@ describe("readConfigFile", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
if (ORIGINAL_PAPERCLIP_CONFIG === undefined) {
|
||||
delete process.env.PAPERCLIP_CONFIG;
|
||||
} else {
|
||||
@@ -89,4 +90,25 @@ describe("readConfigFile", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("warns about likely misspellings without stripping them", () => {
|
||||
const config = {
|
||||
...(minimalConfig() as Record<string, unknown>),
|
||||
server: {
|
||||
ports: 3200,
|
||||
},
|
||||
};
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
writeConfig(configPath, config);
|
||||
|
||||
expect(readConfigFile()).toMatchObject({
|
||||
server: {
|
||||
port: 3100,
|
||||
ports: 3200,
|
||||
},
|
||||
});
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"Unknown config key server.ports; did you mean server.port? It will be preserved.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -981,6 +982,83 @@ describe("worktree config repair", () => {
|
||||
expect(writtenConfig.auth.publicBaseUrl).toBe("https://paperclip.example");
|
||||
});
|
||||
|
||||
it("preserves top-level and nested config extensions while persisting runtime ports", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-config-extensions-"));
|
||||
const worktreeRoot = path.join(tempRoot, "config-extensions");
|
||||
const paperclipDir = path.join(worktreeRoot, ".paperclip");
|
||||
const configPath = path.join(paperclipDir, "config.json");
|
||||
const isolatedHome = path.join(tempRoot, ".paperclip-worktrees");
|
||||
const instanceRoot = path.join(isolatedHome, "instances", "config-extensions");
|
||||
const base = buildIsolatedConfig(instanceRoot, 3101, 54331);
|
||||
const config = {
|
||||
...base,
|
||||
topLevelExtension: { enabled: true },
|
||||
database: {
|
||||
...base.database,
|
||||
backup: {
|
||||
...base.database.backup,
|
||||
backupExtension: "keep",
|
||||
},
|
||||
},
|
||||
server: {
|
||||
...base.server,
|
||||
serverExtension: "keep",
|
||||
},
|
||||
storage: {
|
||||
...base.storage,
|
||||
localDisk: {
|
||||
...base.storage.localDisk,
|
||||
driverExtension: "keep",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await fs.mkdir(paperclipDir, { recursive: true });
|
||||
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
await fs.writeFile(
|
||||
path.join(paperclipDir, ".env"),
|
||||
["# Paperclip environment variables", "PAPERCLIP_IN_WORKTREE=true", ""].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
process.chdir(worktreeRoot);
|
||||
process.env.PAPERCLIP_IN_WORKTREE = "true";
|
||||
process.env.PAPERCLIP_WORKTREE_NAME = "config-extensions";
|
||||
process.env.PAPERCLIP_HOME = isolatedHome;
|
||||
process.env.PAPERCLIP_INSTANCE_ID = "config-extensions";
|
||||
process.env.PAPERCLIP_CONFIG = configPath;
|
||||
delete process.env.PORT;
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const open = vi.spyOn(fsSync, "openSync");
|
||||
const sync = vi.spyOn(fsSync, "fsyncSync");
|
||||
maybePersistWorktreeRuntimePorts({ serverPort: 3103, databasePort: 54335 });
|
||||
|
||||
expect(open).toHaveBeenCalledWith(paperclipDir, "r");
|
||||
expect(sync).toHaveBeenCalled();
|
||||
|
||||
const writtenConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
|
||||
expect(writtenConfig).toMatchObject({
|
||||
topLevelExtension: { enabled: true },
|
||||
database: {
|
||||
embeddedPostgresPort: 54335,
|
||||
backup: { backupExtension: "keep" },
|
||||
},
|
||||
server: {
|
||||
port: 3103,
|
||||
serverExtension: "keep",
|
||||
},
|
||||
storage: {
|
||||
localDisk: { driverExtension: "keep" },
|
||||
},
|
||||
});
|
||||
|
||||
const stableTime = new Date("2020-01-01T00:00:00.000Z");
|
||||
await fs.utimes(configPath, stableTime, stableTime);
|
||||
maybePersistWorktreeRuntimePorts({ serverPort: 3103, databasePort: 54335 });
|
||||
expect((await fs.stat(configPath)).mtimeMs).toBe(stableTime.getTime());
|
||||
});
|
||||
|
||||
it("can update the in-memory config when auth URL already includes a port", () => {
|
||||
const { config, changed } = applyRuntimePortSelectionToConfig(
|
||||
buildLegacyConfig("/tmp/shared", "http://my-host.ts.net:3100"),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import { paperclipConfigSchema, type PaperclipConfig } from "@paperclipai/shared";
|
||||
import {
|
||||
findPaperclipConfigKeyWarnings,
|
||||
paperclipConfigSchema,
|
||||
type PaperclipConfig,
|
||||
} from "@paperclipai/shared";
|
||||
import { ZodError } from "zod";
|
||||
import { resolvePaperclipConfigPath } from "./paths.js";
|
||||
|
||||
@@ -26,7 +30,13 @@ export function readConfigFile(): PaperclipConfig | null {
|
||||
}
|
||||
|
||||
try {
|
||||
return paperclipConfigSchema.parse(raw);
|
||||
const config = paperclipConfigSchema.parse(raw);
|
||||
for (const warning of findPaperclipConfigKeyWarnings(config)) {
|
||||
console.warn(
|
||||
`Unknown config key ${warning.path}; did you mean ${warning.suggestion}? It will be preserved.`,
|
||||
);
|
||||
}
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
throw new Error(`Invalid Paperclip config at ${configPath}: ${formatConfigValidationError(error)}`);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { PaperclipConfig } from "@paperclipai/shared";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import {
|
||||
mergePaperclipConfig,
|
||||
paperclipConfigSchema,
|
||||
type PaperclipConfig,
|
||||
} from "@paperclipai/shared";
|
||||
import { resolvePaperclipConfigPath, resolvePaperclipEnvPath } from "./paths.js";
|
||||
|
||||
function nonEmpty(value: string | null | undefined): string | null {
|
||||
@@ -305,9 +310,60 @@ function resolveWorktreeRuntimeContext(
|
||||
};
|
||||
}
|
||||
|
||||
function writeConfigFile(configPath: string, config: PaperclipConfig): void {
|
||||
function atomicWriteFile(filePath: string, contents: string): void {
|
||||
let attempt = 0;
|
||||
|
||||
while (true) {
|
||||
const temporaryPath = `${filePath}.tmp-${process.pid}-${attempt}`;
|
||||
attempt += 1;
|
||||
let fileDescriptor: number | null = null;
|
||||
try {
|
||||
fileDescriptor = fs.openSync(temporaryPath, "wx", 0o600);
|
||||
fs.writeFileSync(fileDescriptor, contents, "utf8");
|
||||
fs.fsyncSync(fileDescriptor);
|
||||
fs.closeSync(fileDescriptor);
|
||||
fileDescriptor = null;
|
||||
fs.renameSync(temporaryPath, filePath);
|
||||
let directoryDescriptor: number | null = null;
|
||||
try {
|
||||
directoryDescriptor = fs.openSync(path.dirname(filePath), "r");
|
||||
fs.fsyncSync(directoryDescriptor);
|
||||
} catch (error) {
|
||||
const code = error instanceof Error && "code" in error ? error.code : null;
|
||||
if (
|
||||
process.platform !== "win32" ||
|
||||
!["EACCES", "EINVAL", "EISDIR", "ENOTSUP", "EPERM"].includes(String(code))
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (directoryDescriptor !== null) fs.closeSync(directoryDescriptor);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (fileDescriptor !== null) fs.closeSync(fileDescriptor);
|
||||
fs.rmSync(temporaryPath, { force: true });
|
||||
const code = error instanceof Error && "code" in error ? error.code : null;
|
||||
if (code === "EEXIST") continue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfigFile(configPath: string, config: PaperclipConfig): boolean {
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
||||
const update = paperclipConfigSchema.parse(config);
|
||||
const source = fs.existsSync(configPath)
|
||||
? paperclipConfigSchema.parse(JSON.parse(fs.readFileSync(configPath, "utf8")))
|
||||
: null;
|
||||
const nextConfig = source
|
||||
? paperclipConfigSchema.parse(mergePaperclipConfig(source, update))
|
||||
: update;
|
||||
|
||||
if (source && isDeepStrictEqual(source, nextConfig)) return false;
|
||||
|
||||
atomicWriteFile(configPath, JSON.stringify(nextConfig, null, 2) + "\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveRepoManagedWorktreesRoot(worktreeRoot: string): string | null {
|
||||
|
||||
Reference in new issue
Block a user