How `update-system.mjs` Manages Version Checking, Apply, Dismiss, and Rollback in santifer/career-ops

The update-system.mjs script in santifer/career-ops provides four CLI commands—check, apply, dismiss, and rollback—that safely update the system layer without touching user data, using semantic version comparison, backup branches, and a hidden marker file for suppression.

The update-system.mjs script serves as the safe auto-updater for the career-ops repository. It operates exclusively on the system layer—mode files, scripts, and templates—while strictly protecting any user-layer data. This article breaks down exactly how the script handles version checking, applying updates, dismissing notifications, and rolling back changes, based on the source implementation in santifer/career-ops.


Version Checking in update-system.mjs

The check command determines whether a newer version exists upstream through a three-step process.

Reading Local and Remote Versions

localVersion() reads the VERSION file at the repository root and normalizes its contents:

function localVersion() {
  const vPath = join(ROOT, 'VERSION');
  return existsSync(vPath) ? parseVersionFile(readFileSync(vPath, 'utf-8')) : '0.0.0';
}

(source: lines 53-63 of update-system.mjs)

For the remote version, the script fetches the latest commit via git fetch and reads the upstream VERSION from FETCH_HEAD using the RAW_VERSION_URL constant.

Semantic Version Comparison

compareVersions(a, b) implements standard semver logic:

function compareVersions(a, b) {
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pa[i] || 0) < (pb[i] || 0)) return -1;
    if ((pa[i] || 0) > (pb[i] || 0)) return 1;
  }
  return 0;
}

(source: lines 64-71 of update-system.mjs)

If the remote version is greater, the script outputs {"status": "update-available", ...}; otherwise, it returns {"status": "up-to-date"}.


Applying Updates Safely

The apply command implements a multi-layer safety protocol before modifying any files.

Pre-Update Safety Checks

  1. Build user-layer path list – effectiveUserPaths() combines the built-in USER_PATHS array with any additional entries from config/local-paths.txt
  2. Detect conflicts – userLayerViolations() aborts if any SYSTEM_PATHS entry overlaps with user-layer paths
  3. Flag local modifications – locallyModifiedSystemFiles() identifies system files the user has edited; these receive .bak preservation copies

Executing the Update

The script performs a raw checkout (no merge) for each system path:

git checkout FETCH_HEAD -- <path>

After checkout, it stages changes, commits with a message including the backup branch name, and creates a timestamped backup branch via updateBackupBranchName(). The .update-dismissed marker is removed if present.


Dismissing Update Checks

The dismiss command suppresses automatic update notifications until explicitly re-enabled:

function dismiss() {
  writeFileSync(join(ROOT, '.update-dismissed'), new Date().toISOString());
  console.log('Update check dismissed. Run "node update-system.mjs check" or say "check for updates" to re-enable.');
}

(source: lines 2308-2310 of update-system.mjs)

When .update-dismissed exists, subsequent check commands return {"status": "dismissed"} without network activity.


Rolling Back Updates

Rollback restores the pre-update state using backup branches created during apply.

Backup Branch Naming Convention

updateBackupBranchName() generates recoverable branch names:

function updateBackupBranchName(version, date = new Date()) {
  const stamp = date.toISOString()
    .replace(/[-:]/g, '')
    .replace(/\.\d{3}Z$/, 'Z');
  return `backup-pre-update-${version}-${stamp}`;
}

Rollback Execution Process

  1. newestBackupBranch() parses git branch output to find the most recent backup
  2. The identified branch is checked out to restore original system files
  3. The backup branch is deleted after successful restoration
  4. Files introduced by the rolled-back update are removed from the working tree

Practical Usage Examples

Command Purpose Example Output
node update-system.mjs check Query for available updates {"status": "update-available", "local": "1.8.0", "remote": "1.9.0"}
node update-system.mjs apply Apply the latest update safely Success message with backup branch name
node update-system.mjs apply --force Overwrite local system file edits Same as above, skipping modification checks
node update-system.mjs dismiss Suppress future update prompts Update check dismissed...
node update-system.mjs rollback Revert to pre-update state Restoration confirmation

Key Files and Constants in update-system.mjs

File/Constant Role
update-system.mjs Core auto-updater implementation with all four commands
VERSION Local semantic version identifier
.update-dismissed Hidden marker file disabling automatic checks
SYSTEM_PATHS Whitelist of system-layer paths safe to overwrite
USER_PATHS Protected user-layer paths that block updates on conflict
config/local-paths.txt Optional extension for fork-specific protected paths

Summary

  • Version checking in update-system.mjs compares local VERSION against remote using strict semver logic
  • Applying updates validates against user-layer conflicts, preserves modified files, creates recoverable backups, and performs raw checkouts
  • Dismissing checks writes a .update-dismissed marker that short-circuits future check commands
  • Rolling back locates the newest backup-pre-update-* branch and restores that state cleanly

Frequently Asked Questions

How does update-system.mjs prevent overwriting my data?

The script builds a protection list via effectiveUserPaths() combining USER_PATHS and config/local-paths.txt. userLayerViolations() aborts the update if any SYSTEM_PATHS entry conflicts with these protected paths. Additionally, locallyModifiedSystemFiles() creates .bak copies of system files you've edited before checkout.

Can I force an update if I've modified system files?

Yes. Run node update-system.mjs apply --force to bypass the local modification check. Without --force, the script preserves your edits as .bak files and aborts.

How long does a dismissed update stay suppressed?

Indefinitely. The .update-dismissed marker persists until you explicitly run node update-system.mjs check or remove the file manually. There is no automatic expiration.

Where are rollback backups stored?

Backup branches remain in your local git repository following the pattern backup-pre-update-{version}-{timestamp}. These branches are local-only and deleted after successful rollback.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →