Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ artifacts/
screenshots/
traces/
*.log
.mstack-sync-upstream.lock*
.mstack-sync-upstream/
**/.mstack-sync-upstream-tx/
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ content still matches the previous manifest. Pass `--apply --force` after
reviewing a diff when overwriting or removing a locally changed file is
intentional.

Only one `--apply` can run against a target at a time. Writes and removals are
staged and journaled before the target is changed; an ordinary failure rolls
the whole refresh back. If the process is interrupted, the next `--apply`
recovers the unfinished transaction before rebuilding the sync plan. Dry runs
and `check-upstream` refuse to inspect a target while a sync is active or needs
recovery. The transaction prevents persistent partial refreshes, but readers
that ignore the sync lock can still observe files changing during the commit.

`--apply` writes `profiles/upstream-manifest.json`. Its hashes describe the
transformed upstream baseline, not package integrity. Files configured with
`compareContent: false`, including the adapted agents, may intentionally differ
Expand Down
17 changes: 17 additions & 0 deletions scripts/check-upstream.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { execFileSync } from "node:child_process";
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
import { fileURLToPath } from "node:url";
import {
acquireUpstreamSyncLock,
assertUpstreamSyncTargetReadable,
releaseUpstreamSyncLock,
} from "./sync-upstream-transaction.mjs";

const args = process.argv.slice(2);
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
Expand All @@ -23,6 +28,16 @@ if (args.includes("--help")) {
}

const targetRoot = resolve(valueAfter("--target") ?? repoRoot);
const syncLock = acquireUpstreamSyncLock(targetRoot, { recoverStale: false });
const releaseReadLockAtExit = () => {
try {
releaseUpstreamSyncLock(syncLock);
} catch {
// A stale read lock is recoverable by the next apply.
}
};
process.once("exit", releaseReadLockAtExit);
assertUpstreamSyncTargetReadable(targetRoot, syncLock);
const upstreamsPath = resolveInside(targetRoot, "profiles/upstreams.json", "upstream profile");
if (!existsSync(upstreamsPath)) throw new Error(`mstack upstream profile not found: ${upstreamsPath}`);
const upstreams = JSON.parse(readFileSync(upstreamsPath, "utf8"));
Expand Down Expand Up @@ -338,3 +353,5 @@ if (!existsSync(manifestPath)) {

console.log(`Matched ${expected.length} pstack skills at ${pstack.commit}.`);
if (artifactProblems && strict) process.exit(1);
releaseUpstreamSyncLock(syncLock);
process.removeListener("exit", releaseReadLockAtExit);
Loading