mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
chore(dev): preflight workspace links and simplify worktree helpers
This commit is contained in:
@@ -573,6 +573,7 @@ describe("worktree helpers", () => {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true });
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
fs.mkdirSync(sourceRoot, { recursive: true });
|
||||
|
||||
@@ -590,6 +591,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8");
|
||||
fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8");
|
||||
fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8");
|
||||
fs.writeFileSync(
|
||||
currentPaths.envPath,
|
||||
[
|
||||
@@ -606,7 +608,6 @@ describe("worktree helpers", () => {
|
||||
|
||||
await worktreeReseedCommand({
|
||||
fromConfig: sourcePaths.configPath,
|
||||
seed: false,
|
||||
yes: true,
|
||||
});
|
||||
|
||||
@@ -628,7 +629,7 @@ describe("worktree helpers", () => {
|
||||
}
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
it("restores the current worktree config and instance data if reseed fails", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-"));
|
||||
|
||||
@@ -98,22 +98,6 @@ type WorktreeMakeOptions = WorktreeInitOptions & {
|
||||
startPoint?: string;
|
||||
};
|
||||
|
||||
type WorktreeReseedOptions = {
|
||||
fromConfig?: string;
|
||||
fromDataDir?: string;
|
||||
fromInstance?: string;
|
||||
home?: string;
|
||||
seedMode?: string;
|
||||
yes?: boolean;
|
||||
seed?: boolean;
|
||||
};
|
||||
|
||||
type WorktreeReseedBackup = {
|
||||
tempRoot: string;
|
||||
repoConfigDirBackup: string | null;
|
||||
instanceRootBackup: string | null;
|
||||
};
|
||||
|
||||
type WorktreeEnvOptions = {
|
||||
config?: string;
|
||||
json?: boolean;
|
||||
@@ -964,6 +948,8 @@ async function seedWorktreeDatabase(input: {
|
||||
input.sourceConfig.database.embeddedPostgresDataDir,
|
||||
input.sourceConfig.database.embeddedPostgresPort,
|
||||
);
|
||||
const sourceAdminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${sourceHandle.port}/postgres`;
|
||||
await ensurePostgresDatabase(sourceAdminConnectionString, "paperclip");
|
||||
}
|
||||
const sourceConnectionString = resolveSourceConnectionString(
|
||||
input.sourceConfig,
|
||||
@@ -1138,160 +1124,6 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise<vo
|
||||
await runWorktreeInit(opts);
|
||||
}
|
||||
|
||||
function hasExplicitSourceSelection(opts: {
|
||||
fromConfig?: string;
|
||||
fromDataDir?: string;
|
||||
fromInstance?: string;
|
||||
sourceConfigPathOverride?: string;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
nonEmpty(opts.fromConfig)
|
||||
|| nonEmpty(opts.fromDataDir)
|
||||
|| nonEmpty(opts.fromInstance)
|
||||
|| nonEmpty(opts.sourceConfigPathOverride),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCurrentWorktreeReseedState(opts: { home?: string } = {}) {
|
||||
const currentConfigPath = resolveConfigPath();
|
||||
if (!existsSync(currentConfigPath)) {
|
||||
throw new Error(
|
||||
"Current directory does not have a Paperclip worktree config. Run `paperclipai worktree init` here first.",
|
||||
);
|
||||
}
|
||||
const currentConfig = readConfig(currentConfigPath);
|
||||
if (!currentConfig) {
|
||||
throw new Error(`Could not read current worktree config at ${currentConfigPath}.`);
|
||||
}
|
||||
if (currentConfig.database.mode !== "embedded-postgres") {
|
||||
throw new Error("Worktree reseed only supports embedded-postgres worktree instances.");
|
||||
}
|
||||
|
||||
const currentEnvEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(currentConfigPath));
|
||||
const instanceRoot = path.dirname(currentConfig.database.embeddedPostgresDataDir);
|
||||
const derivedHomeDir = path.dirname(path.dirname(instanceRoot));
|
||||
|
||||
return {
|
||||
currentConfigPath: path.resolve(currentConfigPath),
|
||||
instanceId:
|
||||
nonEmpty(currentEnvEntries.PAPERCLIP_INSTANCE_ID)
|
||||
?? nonEmpty(path.basename(instanceRoot))
|
||||
?? sanitizeWorktreeInstanceId(path.basename(process.cwd())),
|
||||
homeDir: path.resolve(expandHomePrefix(opts.home ?? currentEnvEntries.PAPERCLIP_HOME ?? derivedHomeDir)),
|
||||
serverPort: currentConfig.server.port,
|
||||
dbPort: currentConfig.database.embeddedPostgresPort,
|
||||
worktreeName: nonEmpty(currentEnvEntries.PAPERCLIP_WORKTREE_NAME) ?? undefined,
|
||||
worktreeColor: nonEmpty(currentEnvEntries.PAPERCLIP_WORKTREE_COLOR) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function snapshotDirectory(sourcePath: string, targetPath: string): Promise<string | null> {
|
||||
if (!existsSync(sourcePath)) {
|
||||
return null;
|
||||
}
|
||||
await fsPromises.cp(sourcePath, targetPath, { recursive: true });
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
async function snapshotWorktreeReseedState(target: {
|
||||
repoConfigDir: string;
|
||||
instanceRoot: string;
|
||||
}): Promise<WorktreeReseedBackup> {
|
||||
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-reseed-backup-"));
|
||||
return {
|
||||
tempRoot,
|
||||
repoConfigDirBackup: await snapshotDirectory(
|
||||
target.repoConfigDir,
|
||||
path.resolve(tempRoot, "repo-config"),
|
||||
),
|
||||
instanceRootBackup: await snapshotDirectory(
|
||||
target.instanceRoot,
|
||||
path.resolve(tempRoot, "instance-root"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function restoreDirectoryBackup(backupPath: string | null, targetPath: string): Promise<void> {
|
||||
rmSync(targetPath, { recursive: true, force: true });
|
||||
if (!backupPath) {
|
||||
return;
|
||||
}
|
||||
await fsPromises.cp(backupPath, targetPath, { recursive: true });
|
||||
}
|
||||
|
||||
async function restoreWorktreeReseedState(
|
||||
backup: WorktreeReseedBackup,
|
||||
target: { repoConfigDir: string; instanceRoot: string },
|
||||
): Promise<void> {
|
||||
await restoreDirectoryBackup(backup.repoConfigDirBackup, target.repoConfigDir);
|
||||
await restoreDirectoryBackup(backup.instanceRootBackup, target.instanceRoot);
|
||||
}
|
||||
|
||||
export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise<void> {
|
||||
printPaperclipCliBanner();
|
||||
p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed ")));
|
||||
|
||||
if (!hasExplicitSourceSelection(opts)) {
|
||||
throw new Error(
|
||||
"Reseed requires an explicit source. Pass --from-config or --from-instance (optionally with --from-data-dir).",
|
||||
);
|
||||
}
|
||||
|
||||
const target = resolveCurrentWorktreeReseedState({ home: opts.home });
|
||||
const sourceConfigPath = resolveSourceConfigPath(opts);
|
||||
if (path.resolve(sourceConfigPath) === target.currentConfigPath) {
|
||||
throw new Error(
|
||||
"Source and target Paperclip configs are the same. Pass a different source instance/config when reseeding.",
|
||||
);
|
||||
}
|
||||
|
||||
const seedMode = opts.seedMode ?? "minimal";
|
||||
if (!isWorktreeSeedMode(seedMode)) {
|
||||
throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`);
|
||||
}
|
||||
|
||||
const confirmed = opts.yes
|
||||
? true
|
||||
: await p.confirm({
|
||||
message: `Reseed the current worktree instance (${target.instanceId}) from ${sourceConfigPath}? This overwrites only the current worktree Paperclip instance data.`,
|
||||
initialValue: false,
|
||||
});
|
||||
if (p.isCancel(confirmed) || !confirmed) {
|
||||
p.log.warn("Reseed cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPaths = resolveWorktreeLocalPaths({
|
||||
cwd: process.cwd(),
|
||||
homeDir: target.homeDir,
|
||||
instanceId: target.instanceId,
|
||||
});
|
||||
const backup = await snapshotWorktreeReseedState(targetPaths);
|
||||
|
||||
try {
|
||||
await runWorktreeInit({
|
||||
name: target.worktreeName,
|
||||
color: target.worktreeColor,
|
||||
instance: target.instanceId,
|
||||
home: target.homeDir,
|
||||
fromConfig: opts.fromConfig,
|
||||
fromDataDir: opts.fromDataDir,
|
||||
fromInstance: opts.fromInstance,
|
||||
sourceConfigPathOverride: sourceConfigPath,
|
||||
serverPort: target.serverPort,
|
||||
dbPort: target.dbPort,
|
||||
seed: opts.seed ?? true,
|
||||
seedMode,
|
||||
force: true,
|
||||
});
|
||||
} catch (error) {
|
||||
await restoreWorktreeReseedState(backup, targetPaths);
|
||||
throw error;
|
||||
} finally {
|
||||
rmSync(backup.tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function worktreeMakeCommand(nameArg: string, opts: WorktreeMakeOptions): Promise<void> {
|
||||
printPaperclipCliBanner();
|
||||
p.intro(pc.bgCyan(pc.black(" paperclipai worktree:make ")));
|
||||
@@ -2968,17 +2800,6 @@ export function registerWorktreeCommands(program: Command): void {
|
||||
.option("--json", "Print JSON instead of shell exports")
|
||||
.action(worktreeEnvCommand);
|
||||
|
||||
worktree
|
||||
.command("reseed")
|
||||
.description("Replace the current worktree instance with a fresh seed while preserving this worktree's ports and instance id")
|
||||
.option("--from-config <path>", "Source config.json to seed from")
|
||||
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
|
||||
.option("--from-instance <id>", "Source instance id when deriving the source config")
|
||||
.option("--home <path>", `Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`)
|
||||
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
|
||||
.option("--yes", "Skip the destructive confirmation prompt", false)
|
||||
.action(worktreeReseedCommand);
|
||||
|
||||
program
|
||||
.command("worktree:list")
|
||||
.description("List git worktrees visible from this repo and whether they look like Paperclip worktrees")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preflight:workspace-links": "pnpm exec tsx scripts/ensure-workspace-package-links.ts",
|
||||
"dev": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts watch",
|
||||
"dev:watch": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts watch",
|
||||
"dev:once": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts dev",
|
||||
@@ -10,10 +11,10 @@
|
||||
"dev:stop": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-service.ts stop",
|
||||
"dev:server": "pnpm --filter @paperclipai/server dev",
|
||||
"dev:ui": "pnpm --filter @paperclipai/ui dev",
|
||||
"build": "pnpm -r build",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"build": "pnpm run preflight:workspace-links && pnpm -r build",
|
||||
"typecheck": "pnpm run preflight:workspace-links && pnpm -r typecheck",
|
||||
"test": "pnpm run preflight:workspace-links && vitest",
|
||||
"test:run": "pnpm run preflight:workspace-links && vitest run",
|
||||
"db:generate": "pnpm --filter @paperclipai/db generate",
|
||||
"db:migrate": "pnpm --filter @paperclipai/db migrate",
|
||||
"secrets:migrate-inline-env": "tsx scripts/migrate-inline-env-secrets.ts",
|
||||
|
||||
@@ -44,6 +44,13 @@ function discoverWorkspacePackagePaths(rootDir: string): Map<string, string> {
|
||||
}
|
||||
|
||||
const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot);
|
||||
const workspaceDirs = Array.from(
|
||||
new Set(
|
||||
Array.from(workspacePackagePaths.values())
|
||||
.map((packagePath) => path.relative(repoRoot, packagePath))
|
||||
.filter((workspaceDir) => workspaceDir.length > 0),
|
||||
),
|
||||
).sort();
|
||||
|
||||
function findWorkspaceLinkMismatches(workspaceDir: string): WorkspaceLinkMismatch[] {
|
||||
const packageJson = readJsonFile(path.join(repoRoot, workspaceDir, "package.json"));
|
||||
@@ -100,6 +107,6 @@ async function ensureWorkspaceLinksCurrent(workspaceDir: string) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspaceDir of ["server", "ui"]) {
|
||||
for (const workspaceDir of workspaceDirs) {
|
||||
await ensureWorkspaceLinksCurrent(workspaceDir);
|
||||
}
|
||||
|
||||
65
scripts/kill-agent-browsers.sh
Executable file
65
scripts/kill-agent-browsers.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Kill all "Google Chrome for Testing" processes (agent headless browsers).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/kill-agent-browsers.sh # kill all
|
||||
# scripts/kill-agent-browsers.sh --dry # preview what would be killed
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=false
|
||||
if [[ "${1:-}" == "--dry" || "${1:-}" == "--dry-run" || "${1:-}" == "-n" ]]; then
|
||||
DRY_RUN=true
|
||||
fi
|
||||
|
||||
pids=()
|
||||
lines=()
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
pid=$(echo "$line" | awk '{print $2}')
|
||||
pids+=("$pid")
|
||||
lines+=("$line")
|
||||
done < <(ps aux | grep 'Google Chrome for Testing' | grep -v grep || true)
|
||||
|
||||
if [[ ${#pids[@]} -eq 0 ]]; then
|
||||
echo "No Google Chrome for Testing processes found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#pids[@]} Google Chrome for Testing process(es):"
|
||||
echo ""
|
||||
|
||||
for i in "${!pids[@]}"; do
|
||||
line="${lines[$i]}"
|
||||
pid=$(echo "$line" | awk '{print $2}')
|
||||
start=$(echo "$line" | awk '{print $9}')
|
||||
cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}')
|
||||
cmd=$(echo "$cmd" | sed "s|$HOME/||g")
|
||||
printf " PID %-7s started %-10s %s\n" "$pid" "$start" "$cmd"
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Dry run — re-run without --dry to kill these processes."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Sending SIGTERM..."
|
||||
for pid in "${pids[@]}"; do
|
||||
kill -TERM "$pid" 2>/dev/null && echo " signaled $pid" || echo " $pid already gone"
|
||||
done
|
||||
|
||||
sleep 2
|
||||
|
||||
for pid in "${pids[@]}"; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " $pid still alive, sending SIGKILL..."
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
Reference in New Issue
Block a user