Merge master into canonical Runner D delivery

Resolve the migration sequence against current master while preserving the audited Runner D data migration. Type the native backend factory mock so the merged TypeScript checks retain its two-argument contract.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dev AgentandPaperclip committed 2026-08-19 21:17:29 +00:00
commit 83a1c438e0
227 files changed
+64329 -3014

No files matched your search

@@ -37,6 +37,9 @@ test.after(() => {
*/
function makeBaseWorkspace({ helpExit, initExit, ensureExit = 0 }) {
const baseCwd = makeTempDir("paperclip-provision-base-");
fs.mkdirSync(path.join(baseCwd, ".paperclip"), { recursive: true });
fs.writeFileSync(path.join(baseCwd, ".paperclip", "config.json"), "{}\n");
fs.writeFileSync(path.join(baseCwd, ".paperclip", ".env"), "PAPERCLIP_INSTANCE_ID=base-source\n");
const runnerPath = path.join(baseCwd, "cli", "node_modules", "tsx", "dist", "cli.mjs");
const entryPath = path.join(baseCwd, "cli", "src", "index.ts");
fs.mkdirSync(path.dirname(runnerPath), { recursive: true });
@@ -68,7 +71,21 @@ if (cliArgs[0] === "worktree" && cliArgs[1] === "ensure-seeded") {
process.exit(${ensureExit});
}
fs.rmSync(".paperclip/seed-pending", { force: true });
fs.writeFileSync(".paperclip/seed-complete", "{}\\n");
fs.rmSync(".paperclip/seed-complete", { force: true });
fs.writeFileSync(".paperclip/seed-manifest.json", JSON.stringify({
version: 2,
source: { instanceId: "base-source", configPath: ${JSON.stringify(path.join(baseCwd, ".paperclip", "config.json"))} },
snapshotAt: "2026-08-19T00:00:00.000Z",
seedMode: "minimal",
migrationRevision: "0142_test.sql",
targetInstanceId: "target-test",
phase: "complete",
state: "verified",
attemptId: "attempt-test",
startedAt: "2026-08-19T00:00:00.000Z",
finishedAt: "2026-08-19T00:01:00.000Z",
diagnostics: [{ phase: "complete", status: "succeeded", at: "2026-08-19T00:01:00.000Z" }],
}) + "\\n");
process.exit(0);
}
process.exit(0);
@@ -91,6 +108,8 @@ function runProvision(baseCwd, { pathPrefix } = {}) {
PAPERCLIP_WORKSPACE_BRANCH: "feature/provision-test",
PAPERCLIP_WORKTREES_DIR: worktreesHome,
PAPERCLIP_HOME: path.join(worktreesHome, "no-such-instance-home"),
PAPERCLIP_PROJECT_WORKSPACE_ID: "project-workspace-1",
PAPERCLIP_SEED_EXPECTED_COMPANY_ID: "company-1",
},
});
return { result, worktreeCwd, worktreesHome };
@@ -109,6 +128,8 @@ function runRuntimeProvision(baseCwd, worktreeCwd) {
PAPERCLIP_WORKSPACE_BRANCH: "feature/provision-runtime-test",
PAPERCLIP_WORKTREES_DIR: worktreesHome,
PAPERCLIP_HOME: path.join(worktreesHome, "no-such-instance-home"),
PAPERCLIP_PROJECT_WORKSPACE_ID: "project-workspace-1",
PAPERCLIP_COMPANY_ID: "company-1",
},
});
}
@@ -137,7 +158,10 @@ test("uses the base CLI when its import graph boots", () => {
assert.equal(result.status, 0, result.stderr);
const config = readWorktreeConfig(worktreeCwd);
assert.equal(config.$meta.source, "fake-cli");
assert.ok(fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-pending")));
assert.equal(
JSON.parse(fs.readFileSync(path.join(worktreeCwd, ".paperclip", "seed-manifest.json"), "utf8")).state,
"pending",
);
const initInvocation = readCliInvocations(baseCwd).find(
(args) => args[0] === "worktree" && args[1] === "init",
);
@@ -166,7 +190,49 @@ test("falls back to an isolated config when the base CLI cannot boot", () => {
);
const env = fs.readFileSync(path.join(worktreeCwd, ".paperclip", ".env"), "utf8");
assert.match(env, /PAPERCLIP_IN_WORKTREE=true/);
assert.ok(fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-pending")));
assert.equal(
JSON.parse(fs.readFileSync(path.join(worktreeCwd, ".paperclip", "seed-manifest.json"), "utf8")).state,
"pending",
);
});
test("reconciles deployment mode from the registered source when reusing a guest config", () => {
const baseCwd = makeBaseWorkspace({ helpExit: 1, initExit: 0 });
const { result: first, worktreeCwd, worktreesHome } = runProvision(baseCwd);
assert.equal(first.status, 0, first.stderr);
assert.equal(readWorktreeConfig(worktreeCwd).server.deploymentMode, "local_trusted");
fs.writeFileSync(
path.join(baseCwd, ".paperclip", "config.json"),
`${JSON.stringify({
server: {
deploymentMode: "authenticated",
exposure: "private",
},
}, null, 2)}\n`,
);
const second = spawnSync("bash", [script], {
cwd: worktreeCwd,
encoding: "utf8",
env: {
PATH: testPath,
HOME: os.homedir(),
PAPERCLIP_WORKSPACE_BASE_CWD: baseCwd,
PAPERCLIP_WORKSPACE_CWD: worktreeCwd,
PAPERCLIP_WORKSPACE_BRANCH: "feature/provision-test",
PAPERCLIP_WORKTREES_DIR: worktreesHome,
PAPERCLIP_HOME: path.join(worktreesHome, "no-such-instance-home"),
PAPERCLIP_PROJECT_WORKSPACE_ID: "project-workspace-1",
PAPERCLIP_SEED_EXPECTED_COMPANY_ID: "company-1",
},
});
assert.equal(second.status, 0, second.stderr);
assert.match(second.stderr, /Reusing existing isolated Paperclip worktree config/);
assert.match(second.stderr, /Reconciled isolated Paperclip worktree deployment mode/);
assert.equal(readWorktreeConfig(worktreeCwd).server.deploymentMode, "authenticated");
assert.equal(readWorktreeConfig(worktreeCwd).server.exposure, "private");
});
test("repairs an unhealthy base install under the lock and then uses the CLI", (t) => {
@@ -181,6 +247,9 @@ test("repairs an unhealthy base install under the lock and then uses the CLI", (
// The CLI's health is controlled by a flag file, and a fake `pnpm install`
// creates that flag — modeling a forced reinstall that relinks the store.
const baseCwd = makeTempDir("paperclip-provision-repair-base-");
fs.mkdirSync(path.join(baseCwd, ".paperclip"), { recursive: true });
fs.writeFileSync(path.join(baseCwd, ".paperclip", "config.json"), "{}\n");
fs.writeFileSync(path.join(baseCwd, ".paperclip", ".env"), "PAPERCLIP_INSTANCE_ID=base-source\n");
const healthFlag = path.join(baseCwd, "cli-healthy.flag");
const runnerPath = path.join(baseCwd, "cli", "node_modules", "tsx", "dist", "cli.mjs");
const entryPath = path.join(baseCwd, "cli", "src", "index.ts");
@@ -260,23 +329,72 @@ test("runtime provisioning invokes ensure-seeded once and fast-exits after succe
const first = runRuntimeProvision(baseCwd, worktreeCwd);
assert.equal(first.status, 0, first.stderr);
assert.ok(fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-complete")));
assert.equal(
JSON.parse(fs.readFileSync(path.join(worktreeCwd, ".paperclip", "seed-manifest.json"), "utf8")).state,
"verified",
);
assert.ok(!fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-pending")));
const ensureCallsAfterFirst = readCliInvocations(baseCwd)
.filter((args) => args[0] === "worktree" && args[1] === "ensure-seeded");
assert.equal(ensureCallsAfterFirst.length, 1);
assert.ok(ensureCallsAfterFirst[0].includes("--config"));
assert.ok(ensureCallsAfterFirst[0].includes("--from-config"));
assert.ok(!ensureCallsAfterFirst[0].includes("--from-config"));
const second = runRuntimeProvision(baseCwd, worktreeCwd);
assert.equal(second.status, 0, second.stderr);
assert.match(second.stderr, /already seeded; skipping/);
assert.match(second.stderr, /verified seed manifest.*skipping/);
const ensureCallsAfterSecond = readCliInvocations(baseCwd)
.filter((args) => args[0] === "worktree" && args[1] === "ensure-seeded");
assert.equal(ensureCallsAfterSecond.length, 1);
});
test("runtime provisioning seeds a worktree config that has no seed markers", () => {
const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0 });
const worktreeCwd = makeTempDir("paperclip-provision-runtime-unmarked-config-");
fs.mkdirSync(path.join(worktreeCwd, ".paperclip"), { recursive: true });
fs.writeFileSync(path.join(worktreeCwd, ".paperclip", "config.json"), "{}\n");
const result = runRuntimeProvision(baseCwd, worktreeCwd);
assert.equal(result.status, 0, result.stderr);
assert.equal(
readCliInvocations(baseCwd)
.filter((args) => args[0] === "worktree" && args[1] === "ensure-seeded").length,
1,
);
assert.equal(
JSON.parse(fs.readFileSync(path.join(worktreeCwd, ".paperclip", "seed-manifest.json"), "utf8")).state,
"verified",
);
});
test("runtime provisioning bootstraps and seeds an empty .paperclip directory", () => {
const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0 });
fs.mkdirSync(path.join(baseCwd, "scripts"), { recursive: true });
fs.copyFileSync(script, path.join(baseCwd, "scripts", "provision-worktree.sh"));
const worktreeCwd = makeTempDir("paperclip-provision-runtime-empty-state-");
fs.mkdirSync(path.join(worktreeCwd, ".paperclip"), { recursive: true });
const result = runRuntimeProvision(baseCwd, worktreeCwd);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stderr, /config is missing; running the built-in worktree provisioner/);
const invocations = readCliInvocations(baseCwd);
assert.equal(
invocations.filter((args) => args[0] === "worktree" && args[1] === "init").length,
1,
);
assert.equal(
invocations.filter((args) => args[0] === "worktree" && args[1] === "ensure-seeded").length,
1,
);
assert.equal(
JSON.parse(fs.readFileSync(path.join(worktreeCwd, ".paperclip", "seed-manifest.json"), "utf8")).state,
"verified",
);
});
test("runtime provisioning leaves seed-pending in place when ensure-seeded fails", () => {
const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0, ensureExit: 4 });
const worktreeCwd = makeTempDir("paperclip-provision-runtime-failure-");
@@ -290,3 +408,22 @@ test("runtime provisioning leaves seed-pending in place when ensure-seeded fails
assert.ok(fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-pending")));
assert.ok(!fs.existsSync(path.join(worktreeCwd, ".paperclip", "seed-complete")));
});
test("runtime provisioning does not trust a truncated verified manifest", () => {
const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0, ensureExit: 4 });
const worktreeCwd = makeTempDir("paperclip-provision-runtime-truncated-");
fs.mkdirSync(path.join(worktreeCwd, ".paperclip"), { recursive: true });
fs.writeFileSync(path.join(worktreeCwd, ".paperclip", "config.json"), "{}\n");
fs.writeFileSync(
path.join(worktreeCwd, ".paperclip", "seed-manifest.json"),
JSON.stringify({ version: 2, state: "verified" }),
);
const result = runRuntimeProvision(baseCwd, worktreeCwd);
assert.equal(result.status, 4, result.stderr);
assert.equal(
readCliInvocations(baseCwd)
.filter((args) => args[0] === "worktree" && args[1] === "ensure-seeded").length,
1,
);
});
+35 -9
View File
@@ -70,28 +70,54 @@ if ! git -C "$repo_dir" rev-parse --verify "refs/tags/${beta_tag}" >/dev/null 2>
release_fail "beta tag ${beta_tag} does not exist. Draft notes are generated from a published beta."
fi
source_sha="$(git -C "$repo_dir" rev-parse "${beta_tag}^{commit}")"
source_short="$(git -C "$repo_dir" rev-parse --short "$source_sha")"
if [ -z "$out_file" ]; then
out_file="${repo_dir}/releases/beta/v${beta_version}.md"
fi
# Range start: the newest stable tag reachable from the source commit —
# not the newest by version, which can sit on a divergent lineage (a
# stable cut from a candidate branch, or a source that predates it) and
# would produce an empty or wrong range. Before the first reachable
# stable, fall back to the nearest beta tag strictly before the source;
# with no marker at all, cover the source commit's full history.
range_start="$(git -C "$repo_dir" describe --tags --match 'v[0-9]*' --abbrev=0 "$source_sha" 2>/dev/null || true)"
range_label="$range_start"
# Range start: walk stable tags newest-first and take the first whose
# merge-base with the source is a proper ancestor of the source. A stable
# cut from a candidate branch is not itself an ancestor of master, but
# its merge-base with the source is the promoted source commit — exactly
# the point the shipped stable's content diverges from. A stable that
# already contains the source (promoting an older commit) is skipped so
# the range never collapses to empty. Before the first stable, fall back
# to the nearest beta tag strictly before the source; with no marker at
# all, cover the source commit's full history.
range_start=""
range_label=""
range_cmd_start=""
while IFS= read -r stable_tag; do
[ -n "$stable_tag" ] || continue
mb="$(git -C "$repo_dir" merge-base "$stable_tag" "$source_sha" 2>/dev/null || true)"
[ -n "$mb" ] || continue
if [ "$mb" != "$source_sha" ]; then
range_start="$mb"
if [ "$mb" = "$(git -C "$repo_dir" rev-parse "${stable_tag}^{commit}")" ]; then
range_label="$stable_tag"
range_cmd_start="$stable_tag"
else
# rev-parse --short picks an abbreviation that is unambiguous in
# this repository, unlike a fixed nine-character truncation.
mb_short="$(git -C "$repo_dir" rev-parse --short "$mb")"
range_label="${stable_tag} (merge-base ${mb_short})"
range_cmd_start="$mb_short"
fi
break
fi
done < <(git -C "$repo_dir" tag -l 'v[0-9]*' --sort=-v:refname)
if [ -z "$range_start" ]; then
range_start="$(git -C "$repo_dir" describe --tags --match 'beta/v*' --abbrev=0 "${source_sha}^" 2>/dev/null || true)"
range_label="$range_start"
range_cmd_start="$range_start"
fi
if [ -n "$range_start" ]; then
range="${range_start}..${source_sha}"
else
range="$source_sha"
range_label="the beginning of history"
range_cmd_start="the beginning of history"
release_info "No stable or prior beta tag found; drafting from full history."
fi
@@ -114,7 +140,7 @@ conventional='^(feat|fix)(\([^)]*\))?!?: '
mkdir -p "$(dirname "$out_file")"
{
printf '# Paperclip stable draft — from beta %s\n\n' "$beta_version"
printf '> Auto-generated at beta publish from `git log %s..%s`.\n' "${range_label}" "${source_sha:0:9}"
printf '> Auto-generated at beta publish from `git log %s..%s` (baseline: %s).\n' "${range_cmd_start}" "${source_short}" "${range_label}"
printf '> Edit freely during the soak: rewrite for release-notes voice,\n'
printf '> fold noise, and call out anything a self-hoster must act on.\n'
printf '> The stable promotion reads this file from master and publishes\n'
+20
View File
@@ -79,6 +79,26 @@ test("uses the nearest ancestor stable tag, not the newest by version", () => {
assert.doesNotMatch(body, /new work/);
});
test("starts from the merge-base when the last stable was cut from a candidate branch", () => {
const dir = makeFixtureRepo();
commit(dir, "feat: shipped in the stable (#1)");
git(dir, "checkout", "-q", "-b", "candidate");
commit(dir, "docs: release notes only (#2)");
git(dir, "tag", "v2026.200.0");
git(dir, "checkout", "-q", "master");
commit(dir, "feat: next-release work (#3)");
git(dir, "tag", "beta/v2026.201.0-beta.0");
// The stable tag sits on the unmerged candidate branch, so it is not an
// ancestor of the beta source. The range must start at its merge-base
// with the source (the promoted commit), not fall back past it.
const { body } = runDraft(dir, "2026.201.0-beta.0");
assert.match(body, /- feat: next-release work \(#3\)/);
assert.doesNotMatch(body, /shipped in the stable/);
assert.doesNotMatch(body, /release notes only/);
});
test("falls back to the previous beta tag when no stable tag exists", () => {
const dir = makeFixtureRepo();
commit(dir, "feat: first-train work (#1)");
+48 -22
View File
@@ -3,12 +3,9 @@ set -euo pipefail
base_cwd="${PAPERCLIP_WORKSPACE_BASE_CWD:?PAPERCLIP_WORKSPACE_BASE_CWD is required}"
worktree_cwd="${PAPERCLIP_WORKSPACE_CWD:?PAPERCLIP_WORKSPACE_CWD is required}"
paperclip_home="${PAPERCLIP_HOME:-$HOME/.paperclip}"
paperclip_instance_id="${PAPERCLIP_INSTANCE_ID:-default}"
paperclip_dir="$worktree_cwd/.paperclip"
worktree_config_path="$paperclip_dir/config.json"
seed_pending_marker_path="$paperclip_dir/seed-pending"
seed_complete_marker_path="$paperclip_dir/seed-complete"
seed_manifest_path="$paperclip_dir/seed-manifest.json"
if [[ ! -d "$base_cwd" ]]; then
echo "Base workspace does not exist: $base_cwd" >&2
@@ -20,30 +17,59 @@ if [[ ! -d "$worktree_cwd" ]]; then
exit 1
fi
if [[ -e "$seed_complete_marker_path" || ! -e "$seed_pending_marker_path" ]]; then
echo "Worktree database is already seeded; skipping runtime provisioning." >&2
exit 0
if [[ -e "$seed_manifest_path" ]]; then
seed_manifest_state="$(SEED_MANIFEST_PATH="$seed_manifest_path" node <<'EOF'
const fs = require("node:fs");
try {
const value = JSON.parse(fs.readFileSync(process.env.SEED_MANIFEST_PATH, "utf8"));
const complete = value?.version === 2
&& value?.state === "verified"
&& value?.phase === "complete"
&& typeof value?.source?.instanceId === "string" && value.source.instanceId.length > 0
&& typeof value?.source?.configPath === "string" && value.source.configPath.length > 0
&& (value?.seedMode === "minimal" || value?.seedMode === "full")
&& typeof value?.snapshotAt === "string" && value.snapshotAt.length > 0
&& typeof value?.migrationRevision === "string" && value.migrationRevision.length > 0
&& typeof value?.targetInstanceId === "string" && value.targetInstanceId.length > 0
&& typeof value?.attemptId === "string" && value.attemptId.length > 0
&& typeof value?.startedAt === "string"
&& typeof value?.finishedAt === "string"
&& Array.isArray(value?.diagnostics)
&& value.diagnostics.some((entry) => entry?.phase === "complete" && entry?.status === "succeeded" && typeof entry?.at === "string");
process.stdout.write(complete ? "verified" : "incomplete");
} catch {
process.stdout.write("invalid");
}
EOF
)"
if [[ "$seed_manifest_state" == "verified" ]]; then
echo "Worktree database has a verified seed manifest; skipping runtime provisioning." >&2
exit 0
fi
fi
if [[ ! -f "$worktree_config_path" ]]; then
echo "Worktree config does not exist: $worktree_config_path" >&2
initial_provision_script="$base_cwd/scripts/provision-worktree.sh"
if [[ ! -f "$initial_provision_script" ]]; then
echo "Worktree config does not exist and the built-in provision script is unavailable: $worktree_config_path" >&2
exit 1
fi
echo "Worktree config is missing; running the built-in worktree provisioner before database seeding." >&2
(
cd "$worktree_cwd" &&
bash "$initial_provision_script"
)
fi
if [[ ! -f "$worktree_config_path" ]]; then
echo "Worktree config still does not exist after built-in provisioning: $worktree_config_path" >&2
exit 1
fi
source_config_path="${PAPERCLIP_CONFIG:-}"
if [[ -z "$source_config_path" && ( -e "$base_cwd/.paperclip/config.json" || -L "$base_cwd/.paperclip/config.json" ) ]]; then
source_config_path="$base_cwd/.paperclip/config.json"
fi
if [[ -z "$source_config_path" ]]; then
source_config_path="$paperclip_home/instances/$paperclip_instance_id/config.json"
fi
source_config_args=(--from-config "$source_config_path")
if [[ "$source_config_path" == "$worktree_config_path" ]]; then
# A human may invoke this after sourcing `worktree env`, which points
# PAPERCLIP_CONFIG at the target. In that case the CLI reads the original
# source config from the seed-pending marker instead.
source_config_args=()
fi
# The CLI derives the source from PAPERCLIP_WORKSPACE_BASE_CWD, which the
# control plane injects from the registered project-workspace row. The seed
# manifest is diagnostic evidence only and must never choose the clone source.
source_config_args=()
base_cli_runner_path="$base_cwd/cli/node_modules/tsx/dist/cli.mjs"
base_cli_entry_path="$base_cwd/cli/src/index.ts"
+94 -15
View File
@@ -8,6 +8,7 @@ paperclip_instance_id="${PAPERCLIP_INSTANCE_ID:-default}"
paperclip_dir="$worktree_cwd/.paperclip"
worktree_config_path="$paperclip_dir/config.json"
worktree_env_path="$paperclip_dir/.env"
seed_manifest_path="$paperclip_dir/seed-manifest.json"
seed_pending_marker_path="$paperclip_dir/seed-pending"
seed_complete_marker_path="$paperclip_dir/seed-complete"
worktree_name="${PAPERCLIP_WORKSPACE_BRANCH:-$(basename "$worktree_cwd")}"
@@ -39,12 +40,16 @@ if [[ ! -d "$worktree_cwd" ]]; then
exit 1
fi
source_config_path="${PAPERCLIP_CONFIG:-}"
if [[ -z "$source_config_path" && ( -e "$base_cwd/.paperclip/config.json" || -L "$base_cwd/.paperclip/config.json" ) ]]; then
source_config_path="$base_cwd/.paperclip/config.json"
canonical_base_cwd="$(cd "$base_cwd" && pwd -P)"
source_config_path="$canonical_base_cwd/.paperclip/config.json"
if [[ ! -f "$source_config_path" || -L "$source_config_path" ]]; then
echo "Registered base project workspace has no canonical Paperclip config: $source_config_path" >&2
exit 1
fi
if [[ -z "$source_config_path" ]]; then
source_config_path="$paperclip_home/instances/$paperclip_instance_id/config.json"
canonical_source_dir="$(cd "$(dirname "$source_config_path")" && pwd -P)"
if [[ "$canonical_source_dir/config.json" != "$source_config_path" ]]; then
echo "Registered base project workspace Paperclip config uses a symlink alias: $source_config_path" >&2
exit 1
fi
source_env_path="$(dirname "$source_config_path")/.env"
@@ -237,25 +242,93 @@ for (const rawValue of runtimePaths) {
EOF
}
write_seed_pending_marker() {
SEED_PENDING_MARKER_PATH="$seed_pending_marker_path" \
SEED_COMPLETE_MARKER_PATH="$seed_complete_marker_path" \
reconcile_worktree_deployment_mode() {
SOURCE_CONFIG_PATH="$source_config_path" \
WORKTREE_CONFIG_PATH="$worktree_config_path" \
node <<'EOF'
const fs = require("node:fs");
const path = require("node:path");
const sourceConfigPath = path.resolve(process.env.SOURCE_CONFIG_PATH);
const worktreeConfigPath = path.resolve(process.env.WORKTREE_CONFIG_PATH);
const sourceConfig = JSON.parse(fs.readFileSync(sourceConfigPath, "utf8"));
const worktreeConfig = JSON.parse(fs.readFileSync(worktreeConfigPath, "utf8"));
const deploymentMode = sourceConfig?.server?.deploymentMode ?? "local_trusted";
if (deploymentMode !== "local_trusted" && deploymentMode !== "authenticated") {
throw new Error(`Registered source has unsupported server.deploymentMode: ${deploymentMode}`);
}
const exposure = deploymentMode === "local_trusted"
? "private"
: (sourceConfig?.server?.exposure ?? "private");
const currentServer = worktreeConfig?.server && typeof worktreeConfig.server === "object"
? worktreeConfig.server
: {};
if (currentServer.deploymentMode === deploymentMode && currentServer.exposure === exposure) {
process.exit(0);
}
worktreeConfig.server = {
...currentServer,
deploymentMode,
exposure,
};
if (worktreeConfig.$meta && typeof worktreeConfig.$meta === "object") {
worktreeConfig.$meta.updatedAt = new Date().toISOString();
}
const temporaryPath = `${worktreeConfigPath}.deployment-mode-${process.pid}`;
try {
fs.writeFileSync(temporaryPath, `${JSON.stringify(worktreeConfig, null, 2)}\n`, { mode: 0o600 });
fs.renameSync(temporaryPath, worktreeConfigPath);
} finally {
fs.rmSync(temporaryPath, { force: true });
}
console.error(`Reconciled isolated Paperclip worktree deployment mode from ${sourceConfigPath}: ${deploymentMode}/${exposure}`);
EOF
}
write_seed_pending_manifest() {
SEED_MANIFEST_PATH="$seed_manifest_path" \
SEED_PENDING_MARKER_PATH="$seed_pending_marker_path" \
SEED_COMPLETE_MARKER_PATH="$seed_complete_marker_path" \
SOURCE_CONFIG_PATH="$source_config_path" \
TARGET_INSTANCE_ID="$worktree_instance_id" \
node <<'EOF'
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const manifestPath = process.env.SEED_MANIFEST_PATH;
const pendingPath = process.env.SEED_PENDING_MARKER_PATH;
const completePath = process.env.SEED_COMPLETE_MARKER_PATH;
const sourceConfigPath = path.resolve(process.env.SOURCE_CONFIG_PATH);
const sourceEnvPath = path.join(path.dirname(sourceConfigPath), ".env");
let sourceInstanceId = path.basename(path.dirname(sourceConfigPath));
if (fs.existsSync(sourceEnvPath)) {
const match = fs.readFileSync(sourceEnvPath, "utf8").match(/^\s*(?:export\s+)?PAPERCLIP_INSTANCE_ID\s*=\s*["']?([^\s"'#]+)["']?/m);
if (match?.[1]) sourceInstanceId = match[1];
}
fs.rmSync(completePath, { force: true });
fs.rmSync(pendingPath, { force: true });
const at = new Date().toISOString();
fs.writeFileSync(
pendingPath,
manifestPath,
`${JSON.stringify({
version: 1,
state: "pending",
sourceConfigPath: path.resolve(process.env.SOURCE_CONFIG_PATH),
version: 2,
source: {
instanceId: sourceInstanceId,
configPath: sourceConfigPath,
},
snapshotAt: null,
seedMode: "minimal",
createdAt: new Date().toISOString(),
migrationRevision: null,
targetInstanceId: process.env.TARGET_INSTANCE_ID,
phase: "pending",
state: "pending",
attemptId: crypto.randomUUID(),
startedAt: null,
finishedAt: null,
diagnostics: [{ phase: "pending", status: "succeeded", at }],
}, null, 2)}\n`,
{ mode: 0o600 },
);
@@ -551,8 +624,14 @@ else
created_worktree_config=1
fi
if [[ "$created_worktree_config" -eq 1 && ! -e "$seed_pending_marker_path" && ! -e "$seed_complete_marker_path" ]]; then
write_seed_pending_marker
# The target config can predate a deployment-mode change on the registered
# source, and older/fallback CLI writers may default this field independently.
# Reconcile it after either create or reuse so the final guest config always
# carries the source's deployment/auth contract without replacing its database.
reconcile_worktree_deployment_mode
if [[ "$created_worktree_config" -eq 1 && ! -e "$seed_manifest_path" && ! -e "$seed_pending_marker_path" && ! -e "$seed_complete_marker_path" ]]; then
write_seed_pending_manifest
fi
list_base_node_modules_paths() {
+2 -1
View File
@@ -73,6 +73,7 @@ const serializedServerVitestArgs = [
"--no-file-parallelism",
"--maxWorkers=1",
];
const sourceOnlyVitestArgs = ["--exclude", "**/dist/**"];
function walk(dir) {
const entries = readdirSync(dir);
@@ -283,7 +284,7 @@ function runVitest(args, label) {
};
mkdirSync(env.PAPERCLIP_HOME, { recursive: true });
mkdirSync(env.TMPDIR, { recursive: true });
const result = spawnSync("pnpm", ["exec", "vitest", "run", ...args], {
const result = spawnSync("pnpm", ["exec", "vitest", "run", ...sourceOnlyVitestArgs, ...args], {
cwd: repoRoot,
env,
stdio: "inherit",