mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Repair stale worktree provision configs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3a15727dc7
commit
85351a2c9a
2 files changed
+173
-1
No files matched your search
@@ -80,6 +80,84 @@ paperclipai_command_available() {
|
||||
return 1
|
||||
}
|
||||
|
||||
existing_worktree_config_is_usable() {
|
||||
WORKTREE_CONFIG_PATH="$worktree_config_path" \
|
||||
WORKTREE_ENV_PATH="$worktree_env_path" \
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
function expandHomePrefix(value) {
|
||||
if (!value) return value;
|
||||
if (value === "~") return os.homedir();
|
||||
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseEnvFile(contents) {
|
||||
const entries = {};
|
||||
for (const rawLine of contents.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const match = rawLine.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
|
||||
if (!match) continue;
|
||||
const [, key, rawValue] = match;
|
||||
const value = rawValue.trim();
|
||||
if (
|
||||
(value.startsWith("\"") && value.endsWith("\"")) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
entries[key] = value.slice(1, -1);
|
||||
continue;
|
||||
}
|
||||
entries[key] = value.replace(/\s+#.*$/, "").trim();
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function fail(reason) {
|
||||
console.error(reason);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configPath = path.resolve(process.env.WORKTREE_CONFIG_PATH);
|
||||
const envPath = path.resolve(process.env.WORKTREE_ENV_PATH);
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
const env = parseEnvFile(fs.readFileSync(envPath, "utf8"));
|
||||
const envConfigPath = expandHomePrefix(env.PAPERCLIP_CONFIG);
|
||||
if (envConfigPath && path.resolve(envConfigPath) !== configPath) {
|
||||
fail(`existing worktree env points at ${envConfigPath}, not ${configPath}`);
|
||||
}
|
||||
|
||||
const homeDir = expandHomePrefix(env.PAPERCLIP_HOME);
|
||||
const instanceId = env.PAPERCLIP_INSTANCE_ID;
|
||||
if (!homeDir || !instanceId) {
|
||||
fail("existing worktree env is missing PAPERCLIP_HOME or PAPERCLIP_INSTANCE_ID");
|
||||
}
|
||||
if (!fs.existsSync(homeDir)) {
|
||||
fail(`existing worktree home does not exist on this host: ${homeDir}`);
|
||||
}
|
||||
|
||||
const instanceRoot = path.resolve(homeDir, "instances", instanceId);
|
||||
const runtimePaths = [
|
||||
config.database?.embeddedPostgresDataDir,
|
||||
config.database?.backup?.dir,
|
||||
config.logging?.logDir,
|
||||
config.storage?.localDisk?.baseDir,
|
||||
config.secrets?.localEncrypted?.keyFilePath,
|
||||
].filter((value) => typeof value === "string" && value.length > 0);
|
||||
|
||||
for (const rawValue of runtimePaths) {
|
||||
const resolved = path.resolve(expandHomePrefix(rawValue));
|
||||
const relative = path.relative(instanceRoot, resolved);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
fail(`existing worktree config path is outside ${instanceRoot}: ${resolved}`);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_fallback_worktree_config() {
|
||||
WORKTREE_NAME="$worktree_name" \
|
||||
BASE_CWD="$base_cwd" \
|
||||
@@ -332,9 +410,12 @@ main().catch((error) => {
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ -e "$worktree_config_path" && -e "$worktree_env_path" ]]; then
|
||||
if [[ -e "$worktree_config_path" && -e "$worktree_env_path" ]] && existing_worktree_config_is_usable; then
|
||||
echo "Reusing existing isolated Paperclip worktree config at $worktree_config_path" >&2
|
||||
else
|
||||
if [[ -e "$worktree_config_path" || -e "$worktree_env_path" ]]; then
|
||||
echo "Existing isolated Paperclip worktree config is stale for this host; regenerating." >&2
|
||||
fi
|
||||
if paperclipai_command_available; then
|
||||
run_isolated_worktree_init
|
||||
else
|
||||
|
||||
@@ -1536,6 +1536,97 @@ describe("realizeExecutionWorkspace", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("regenerates stale worktree config that points at another host", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-stale-config-"));
|
||||
const baseRoot = path.join(tempRoot, "base");
|
||||
const worktreeRoot = path.join(tempRoot, "worktree");
|
||||
const fakeBin = path.join(tempRoot, "bin");
|
||||
const fakePnpmPath = path.join(fakeBin, "pnpm");
|
||||
const scriptPath = path.join(worktreeRoot, "provision-worktree.sh");
|
||||
const paperclipDir = path.join(worktreeRoot, ".paperclip");
|
||||
|
||||
try {
|
||||
await fs.mkdir(baseRoot, { recursive: true });
|
||||
await fs.mkdir(paperclipDir, { recursive: true });
|
||||
await fs.mkdir(fakeBin, { recursive: true });
|
||||
await fs.copyFile(provisionWorktreeScriptPath, scriptPath);
|
||||
await fs.chmod(scriptPath, 0o755);
|
||||
await fs.writeFile(
|
||||
path.join(paperclipDir, "config.json"),
|
||||
JSON.stringify({
|
||||
database: {
|
||||
mode: "embedded-postgres",
|
||||
embeddedPostgresDataDir: "/Users/example/.paperclip-worktrees/instances/stale/db",
|
||||
},
|
||||
logging: {
|
||||
mode: "file",
|
||||
logDir: "/Users/example/.paperclip-worktrees/instances/stale/logs",
|
||||
},
|
||||
storage: {
|
||||
provider: "local_disk",
|
||||
localDisk: {
|
||||
baseDir: "/Users/example/.paperclip-worktrees/instances/stale/data/storage",
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
provider: "local_encrypted",
|
||||
localEncrypted: {
|
||||
keyFilePath: "/Users/example/.paperclip-worktrees/instances/stale/secrets/master.key",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(paperclipDir, ".env"),
|
||||
[
|
||||
"PAPERCLIP_HOME=/Users/example/.paperclip-worktrees",
|
||||
"PAPERCLIP_INSTANCE_ID=stale",
|
||||
`PAPERCLIP_CONFIG=/Users/example/paperclip/${path.basename(worktreeRoot)}/.paperclip/config.json`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
fakePnpmPath,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
"if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"--help\" ]; then",
|
||||
" exit 0",
|
||||
"fi",
|
||||
"if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"worktree\" ] && [ \"$3\" = \"init\" ]; then",
|
||||
" mkdir -p \"$PWD/.paperclip\"",
|
||||
" printf '%s\\n' '{\"database\":{\"embeddedPostgresDataDir\":\"'$PWD'/.paperclip/runtime/db\"}}' > \"$PWD/.paperclip/config.json\"",
|
||||
" printf '%s\\n' \"PAPERCLIP_HOME=$PWD/.paperclip/runtime\" \"PAPERCLIP_INSTANCE_ID=healthy\" \"PAPERCLIP_CONFIG=$PWD/.paperclip/config.json\" > \"$PWD/.paperclip/.env\"",
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await fs.chmod(fakePnpmPath, 0o755);
|
||||
|
||||
const result = await execFileAsync(scriptPath, [], {
|
||||
cwd: worktreeRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
PAPERCLIP_WORKSPACE_BASE_CWD: baseRoot,
|
||||
PAPERCLIP_WORKSPACE_CWD: worktreeRoot,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.stderr).toContain("Existing isolated Paperclip worktree config is stale for this host; regenerating.");
|
||||
await expect(fs.readFile(path.join(paperclipDir, ".env"), "utf8")).resolves.toContain(
|
||||
`PAPERCLIP_CONFIG=${worktreeRoot}/.paperclip/config.json`,
|
||||
);
|
||||
await expect(fs.readFile(path.join(paperclipDir, "config.json"), "utf8")).resolves.toContain(worktreeRoot);
|
||||
} finally {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("retries worktree-local pnpm install without a frozen lockfile when the lockfile is outdated", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-outdated-lockfile-"));
|
||||
const baseRoot = path.join(tempRoot, "base");
|
||||
|
||||
Reference in new issue
Block a user