cascade-cms-sync
Disclaimer: cascade-cms-sync is an independent, community-developed extension. It is not an official Hannon Hill Cascade CMS product and is not affiliated with, endorsed by, or supported by Hannon Hill.
cascade-cms-sync connects a local VS Code folder to a Cascade CMS site and pulls specific folders you choose to sync (/_cms and /_files by default, plus _system-assets/). Inside those synced folders, it downloads editable text-based files only (Velocity .vm formats, template .xml files, .css, .js, .html, .json, and .xml), skipping images and binary files. Outside those synced folders, it downloads only folders that contain pages or blocks, saving each page as a read-only .page file and each block as a read-only .block file (both highlighted as XML in VS Code) so you can see the <system-data-structure> XML data populating each page and block. You can then diff and push your text-file edits inside the synced folders straight back to Cascade CMS from VS Code, while .page and .block files remain strictly read-only.
Background
Developing Velocity formats (.vm), templates (.xml), frontend text assets (.css, .js, .html, .xml, .json), and system definitions for Cascade CMS typically introduces three daily friction points:
- Constant editor-to-browser context switching: Editing code in VS Code and copying or uploading changes back and forth into the Cascade CMS web interface is slow, error-prone, and offers no side-by-side diff before overwriting live CMS assets.
- No local baseline or conflict detection: Without a local manifest tracking what was last pulled from Cascade CMS, it is easy to overwrite a teammate's newer CMS edits or lose track of which local files have unpushed changes.
- No local view of the data populating CMS pages and blocks: Writing Velocity scripts against
$currentPage.getStructuredDataNode(), getStructuredDataNodes(), getChild(), getChildren(), or $_XPathTool requires knowing exact Data Definition field identifiers, group hierarchy, and block XML structure, which otherwise means hunting through pages and blocks in the Cascade CMS browser UI.
cascade-cms-sync brings the entire Cascade CMS development workflow directly into the native VS Code File Explorer:
- Native Explorer Badges, Editor Title Bar Actions & Docked
Ready to Push Queue: Decorates files in the standard VS Code Explorer (Ctrl+Shift+E) with live sync badges (↑ Modified, + New, R Renamed, ! Conflict, RO Read-Only Page/Block XML), adds 1-click Push, Pull, Diff with CMS, and Check Relationships buttons to the active editor tab and Explorer right-click menu, and docks a compact Cascade CMS: Ready to Push queue right inside the Explorer sidebar.
- Native Node.js
cascade-restapi.js Client with Pre-Push .snapshots/: Uses a built-in CommonJS Cascade REST API client (cascade-restapi.js) with AbortSignal cancellation, concurrent processQueue workers, dynamic per-profile credential injection from context.secrets, and automatic pre-push JSON backups written to <localRootPath>/.snapshots/.
- Default
/_cms + /_files + _system-assets/ + Site-Wide Read-Only Page XML (.page) & Block XML (.block) Scope: Defaults to pulling /_cms, /_files, and editable _system-assets/ alongside all site page assets serialized as <path>.page and all site block assets serialized as <path>.block <system-data-structure> XML reference files (associated with VS Code's xml language mode) so you can quickly see the data and field identifiers populating each page and block locally, while strictly enforcing that .page and .block files are read-only and only pushable assets inside pulled folders can ever be pushed.
Features
| Feature |
Details |
Native Explorer Badges (CascadeFileDecorationProvider) |
Displays live badges and theme colors on every file and parent folder in the native VS Code Explorer: ↑ (Modified, ready to push), + (New local file), R (Renamed), ! (Conflict), and RO (Read-Only Page/Block XML). |
| Editor Tab Bar & Explorer Context Actions |
Adds 1-click Diff with CMS, Pull This Asset, Push This Asset, and Check Asset Relationships buttons to editor/title and the explorer/context right-click menu, plus a Cascade CMS: Ready to Push panel docked inside the Explorer sidebar and a live Status Bar connection indicator. |
| Clean-Slate Connection Wizard |
Starts with zero pre-populated URLs or keys (cascadeSync.connect): select or enter a Cascade CMS URL, store the API key in the OS keychain (context.secrets), fetch available sites via listSites, and bind a local folder. |
Default /_cms, /_files, _system-assets & Page/Block XML Pull |
Automatically defaults to pulling /_cms and /_files (Velocity formats .vm, templates .xml, text files, and folders), editable _system-assets/ (content-types/*.json, config-sets/*.json, data-definitions/*.xml, shared-fields/*.xml), plus all site page assets (includePages: true) and all site block assets (includeBlocks: true), converting structured data nodes (convertPageToReferenceXml, convertBlockToReferenceXml) into read-only <path>.page and <path>.block XML files that match Cascade's XML view so you can quickly inspect the data and field identifiers populating any page or block. |
Two-Way _system-assets/ Sync (JSON + XML) |
Saves Content Types (_system-assets/content-types/*.json) and Configuration Sets (_system-assets/config-sets/*.json) as full Cascade JSON (preserving every configuration, destination, and ID while automatically stripping stale *Id fields when a *Path changes on Push), and Data Definitions (_system-assets/data-definitions/*.xml) and Shared Fields (_system-assets/shared-fields/*.xml) as formatted <system-data-structure> XML. |
| Strict Push Boundaries |
Enforces "you can only push what you pull": page and block assets (requestType: "page" | "block", readOnly: true) are locked from Push, and discoverLocalEntries / preparePushPlan only allow pushable assets inside the configured syncFolders and _system-assets/. |
SHA-256 sync-manifest.json Baseline Tracking |
Uses deterministic line-ending normalization (normalizeLineEndings), SHA-256 hex checksums (hashText), payload canonicalization (getSynchronizedPayload, stableStringify), and sync:no-push marker detection (/sync:\s*no-push/i) to track three-way changes between local files and Cascade CMS. |
Native VS Code Diff (vscode.diff) |
Registers CmsVirtualDocumentProvider (cascade-sync-cms:) to compare live or baseline CMS payloads on the left against the editable local file on the right. |
Pre-Push .snapshots/ Backups |
Before any update or rename Push executes (executePushPlan), writes a timestamped backup JSON (sync-<site>_backup_YYYY-MM-DD_HH-MM-SS.json) to <localRootPath>/.snapshots/, which is automatically excluded from future Pushes and workspace search. |
| Sync Dashboard Webview |
Full-featured Webview panel (openSyncDashboard) for Dry Run previews, status filtering (Ready, Conflicts, Warnings, Skipped), bulk conflict resolution (Resolve All), and scope configuration. |
Installation
Option 1: Install from VSIX
- Open VS Code.
- Open the Extensions view (
Ctrl+Shift+X).
- Click the
... menu in the top-right corner of the Extensions sidebar and choose Install from VSIX....
- Select
cascade-cms-sync-1.0.0.vsix.
Option 2: Package from Source
cd cascade-cms-sync
npx @vscode/vsce package
Authentication & Step-by-Step Setup
- Click the Cascade CMS Sync icon in the VS Code Activity Bar.
- Click Connect to Cascade CMS (or run
Cascade Sync: Connect / Switch Site & URL from the Command Palette).
- Select or Enter CMS URL: Enter your Cascade CMS URL (for example,
https://cms.example.edu/). Previously used URLs are saved locally in context.globalState for quick switching.
- Enter API Key: If no API key is stored in
context.secrets for that URL, enter your Cascade CMS REST API key. Keys are stored in your OS keychain via VS Code SecretStorage.
- Select Site: The extension calls
listSites via cascade-restapi.js and presents a filterable list of sites on that instance.
- Select Local Sync Folder: Bind the site to your current workspace folder or browse for a local folder on disk, then click Pull Now or Dry Run Pull.
Usage
Automatic Workspace Tracking
- Live
Ready to Push Updates (onDidSaveTextDocument & FileSystemWatcher): Saving, creating, renaming, or deleting any file inside the active localRootPath automatically triggers scanLocalChanges() against sync-manifest.json and updates both sidebar trees.
- Dirty Buffer Auto-Save (
saveDirtyDocumentsInRoot): Running Pull or Push automatically saves any dirty editor tabs inside localRootPath so unsaved in-memory edits are never skipped or overwritten silently.
- Workspace Search & Watcher Exclusions (
configureWorkspaceExclusions): Automatically adds **/sync-manifest.json and **/.snapshots/** to workspace search.exclude and files.watcherExclude.
Interactive Entry Points
- Native File Explorer (
Ctrl+Shift+E): Every file and folder displays live CascadeFileDecorationProvider badges (↑ Modified, + New, R Renamed, ! Conflict, RO Read-Only Page/Block XML). Right-click any file or folder in the Explorer to Diff with CMS, Snapshot Versions (Compare / Revert)..., Push This Asset to CMS, Pull This Asset from CMS, or Check Asset Relationships.
- Active Editor Title Bar (
editor/title): When editing a synced file, use the top-right editor tab buttons to Diff with CMS, Snapshot Versions (Compare / Revert)..., Pull This Asset from CMS, Push This Asset to CMS (automatically hidden on read-only .page and .block files), or Check Asset Relationships.
- Docked
Cascade CMS: Ready to Push Panel & Status Bar: View only modified/new/conflicted files ready to push inside the Explorer sidebar (or click the bottom Status Bar item $(cloud) Cascade: <site>) to trigger Pull, Push All, Browse Pre-Push Snapshots, or Open Sync Dashboard.
- Sync Dashboard Webview (
Cascade Sync: Open Sync Dashboard): Click the dashboard icon in the Ready to Push title bar to run Dry Run previews, compare and revert Pre-Push Snapshots, inspect Asset Relationships, manage saved URLs/keys/folders, and batch-resolve conflicts.
Commands Reference
| Command |
ID |
UI Location |
Description |
| Connect / Switch Site & URL |
cascadeSync.connect |
Sidebar Title / Welcome |
Select or enter a Cascade CMS URL, configure API key in SecretStorage, pick a site from listSites, and bind a local folder. |
| Configure Pull Scope (Folders & Types) |
cascadeSync.configureScope |
Connection Header / Dashboard |
Customize pulled syncFolders (default /_cms, /_files), toggle all site page XML and block XML references, or filter pulled asset types. |
| Pull from Cascade CMS |
cascadeSync.pull |
Ready to Push Title Bar |
Pull folders, Velocity formats (.vm), templates (.xml), text files, _system-assets/, and read-only Page XML (.page) and Block XML (.block) files from Cascade CMS into the local folder. |
| Dry Run Pull (Preview) |
cascadeSync.pullDryRun |
Command Palette / Dashboard |
Preview all Pull actions (Write, Update, Rename, Preserve, Block) without modifying local files. |
| Push All Ready to Cascade CMS |
cascadeSync.push |
Ready to Push Title Bar |
Save dirty buffers, create a pre-push .snapshots/ backup, and push modified/new/renamed pushable files to Cascade CMS. |
| Dry Run Push / Verify with CMS |
cascadeSync.pushDryRun |
Ready to Push Title Bar |
Compare local changes against live Cascade CMS assets (readAsset) and preview Push operations and conflicts. |
| Open Sync Dashboard |
cascadeSync.openDashboard |
Ready to Push Title Bar |
Open the full Sync Dashboard Webview panel for dry-run tables, search filtering, and batch conflict resolution. |
| Refresh Local Changes |
cascadeSync.refresh |
Ready to Push Title Bar |
Rescan localRootPath against sync-manifest.json and refresh Explorer badges and the Ready to Push tree. |
| Open File |
cascadeSync.openFile |
Inline Row Action |
Open the local file in the VS Code code editor. |
| Diff with CMS |
cascadeSync.diffItem |
Inline Row Action |
Open native vscode.diff comparing the CMS payload on the left against the local file on the right. |
| Snapshot Versions (Compare / Revert)... |
cascadeSync.snapshotVersions |
Editor Title / Explorer / Row |
Pick any pre-push .snapshots/*.json version of the selected file to compare side-by-side (vscode.diff) against the current local file, revert the local file, or revert and push to CMS. |
Browse Pre-Push Snapshots (.snapshots)... |
cascadeSync.manageSnapshots |
Sidebar Title / Dashboard |
Browse all .snapshots/sync-<site>_backup_*.json batches by asset version or timestamp, compare versions with local files, or revert single files or an entire snapshot batch. |
| Push This Asset |
cascadeSync.pushSingleItem |
Inline Row Action |
Execute runSinglePush for a single pushable asset (format_SCRIPT, template, file, or _system-assets/). |
| Pull This Asset from CMS (Overwrite Local) |
cascadeSync.pullSingleItem |
Inline Row Action / Explorer |
Execute pullSingleTarget for a single asset from Cascade CMS (prompts to overwrite if modified locally). |
| Discard Local Changes / Pull from CMS |
cascadeSync.discardLocalChanges |
Ready to Push / Explorer |
Discard local edits by pulling the latest version from Cascade CMS, restoring the last pulled baseline offline, or deleting an untracked local file. |
| Check Asset Relationships |
cascadeSync.checkRelationships |
Inline Row Action / Explorer |
Inspect incoming subscribers and outgoing asset references (relationships), with 1-click local navigation and direct Cascade CMS browser links. |
| Disconnect Current Folder (Hide from Explorer) |
cascadeSync.disconnectFolder |
Activity Bar Initializer |
Uninitialize the current workspace folder so the Explorer sidebar, badges, and Status Bar are hidden. |
| Resolve Conflict |
cascadeSync.resolveConflict |
Inline Row Action |
Choose Use Local or Use CMS for a conflicted asset row. |
For day-to-day operational workflows, scoped folder vs. site-wide page pulling mechanics, manual local overrides, _system-assets/ ID/Path rules, and .snapshots/ disaster recovery, see OPERATIONAL.md.
How It Works (Architecture & Internals)
1. Native CommonJS cascade-restapi.js Client (cascade-restapi.js)
cascade-restapi.js is a zero-dependency Node.js CommonJS client for the Cascade CMS REST API v1 (/api/v1/read, /edit, /create, /move, /copy, /delete, /publish, /relationships, /search, /listSites). Before every sync operation, syncEngine.configure() injects the active profile's cmsUrl and SecretStorage API key via setCredentials(url, apiKey), wires an AbortSignal (setActiveSignal) for user-cancellable progress notifications, and writes pre-push backups directly to <localRootPath>/.snapshots/.
2. Site Discovery & Scoped Crawling (discoverSiteContent & readAndExtractFolder)
During Pull (runPull), discoverSiteContent reads the site root (readAsset({ type: "site" })) to obtain rootFolderId and recursively crawls folders (readAsset({ type: "folder" })):
- When
includePages or includeBlocks is enabled (true by default), shouldTraverseCmsPath(childPath, syncFolders, includePages || includeBlocks) traverses all site folders in memory so page assets and block assets (isBlockAssetType(child.type), matching all 6 Cascade block_* WSDL subtypes) anywhere in the site are discovered, while format_SCRIPT, template, file, and folder records are restricted to syncFolders (/_cms and /_files by default). Outside syncFolders, local directories (such as /_blocks or /about) are created on disk only when writeLocalText writes a <page>.page or <block>.block file inside them; empty folders or folders containing only non-reference assets (images, PDFs, files, symlinks) are never created locally.
- When both
includePages and includeBlocks are disabled, traversal is pruned strictly to shouldTraverseCmsPath(childPath, syncFolders, false).
3. Read-Only Page & Block XML Serialization (convertPageToReferenceXml, readBlockAsset, convertBlockToReferenceXml & renderStructuredDataNodeXml)
Cascade's REST API returns a page's Data Definition values as nested JSON arrays (page.structuredData.structuredDataNodes), whereas inside Cascade CMS a page's XML view presents that content as <system-data-structure> XML where every tag name is the field's Identifier. When readPullRecords reads a page asset (readAsset({ type: "page", id })), convertPageToReferenceXml serializes page.structuredData.structuredDataNodes into an indented <system-data-structure> XML document saved with a .page extension (registered with VS Code's xml language mode for full XML syntax highlighting and folding) so you can immediately see the data populating each page and verify exact field identifiers and group paths for Cascade API calls ($currentPage.getStructuredDataNode(), getStructuredDataNodes(), getChild(), getChildren()), as well as XML structure when a region uses an Index Block with $_XPathTool:
- Group nodes recurse through child
structuredDataNodes.
- Multi-value delimited text values (
::CONTENT-XML-CHECKBOX:: and ::CONTENT-XML-SELECTOR::) split into multiple <value> elements.
- Asset reference nodes (
page, file, block, symlink, page,file,symlink) emit their <path> and type attributes.
- Every resulting record is marked with
requestType: "page", localPath: "<cmsPath>.page", and readOnly: true.
Similarly, when readPullRecords reads a block asset via readBlockAsset (resolving Cascade's 6 WSDL block subtypes in BLOCK_API_TYPES: block_XHTML_DATADEFINITION, block_XML, block_TEXT, block_INDEX, block_FEED, and block_TWITTER_FEED), convertBlockToReferenceXml handles all concrete block payload keys: xhtmlDataDefinitionBlock (structured data or xhtml, same serialization as pages), xmlBlock (raw XML content), textBlock (plain text wrapped in <text>), and indexBlock/feedBlock/twitterFeedBlock (configuration fields summarized as XML leaf tags). Every block record is marked with requestType: "block", localPath: "<cmsPath>.block", and readOnly: true.
4. Baseline Checksums & Three-Way Conflict Detection (comparePullRecords & preparePushPlan)
Both Pull and Push compute SHA-256 hex checksums (hashText) over LF-normalized text (normalizeLineEndings) and compare three states: localChecksum, cmsChecksum, and manifestRecord.baseChecksum:
| Direction |
Local vs Base |
CMS vs Base |
Resulting Action & Status |
| Pull |
Unchanged |
Changed |
Update (ready, writes CMS content to disk) |
| Pull |
Changed |
Unchanged |
Preserve (warning, preserves local edits) |
| Pull |
Changed |
Changed |
Block (conflict, requires user choice: Pull from CMS or Keep Local) |
| Push |
Changed |
Unchanged |
Update (ready, pushes local content via editAsset) |
| Push |
Unchanged |
Changed |
Preserve CMS (warning, skips Push so newer CMS state is not overwritten) |
| Push |
Changed |
Changed |
Block (conflict, requires user choice: Use Local or Keep CMS) |
5. Pre-Push .snapshots/ Backup, Version Compare & Revert (executePushPlan, listSnapshots & revertSnapshotEntryToLocal)
Before executePushPlan modifies or renames any asset in Cascade CMS, it collects the live cmsPayload of every target asset and calls the Node createSnapshot override, writing <localRootPath>/.snapshots/sync-<site>_backup_YYYY-MM-DD_HH-MM-SS.json. Executable rows are sorted by comparePushOrder (folder and *container assets first, then format_SCRIPT/template/datadefinition/sharedfield, then pageconfigurationset, then contenttype), dispatched sequentially through processQueue (delayMs: 100, batchSize: 1), and re-read from Cascade CMS (refreshManifestAfterPush) to update sync-manifest.json. At any time afterward, listSnapshots parses .snapshots/*.json payloads back into local file content (serializePayloadForDiff) so you can compare any historical pre-push version against the current local file (vscode.diff), revert a single file on disk (revertSnapshotEntryToLocal), revert and immediately push to Cascade CMS, or restore an entire snapshot batch from the Pre-Push Snapshots tab in the Sync Dashboard.
Directory Structure
<localRootPath>/
├── .snapshots/
│ └── sync-mysite_backup_2026-10-30.json # Automatic pre-push CMS asset backups (ignored by Push)
├── _cms/
│ ├── formats/
│ │ └── header-nav.vm # Velocity script format (format_SCRIPT, pushable)
│ └── templates/
│ └── default.xml # Cascade template XML (template, pushable)
├── _files/
│ ├── css/
│ │ └── site.css # Text file asset (file, pushable)
│ └── js/
│ └── app.js # Text file asset (file, pushable)
├── _system-assets/
│ ├── content-types/
│ │ └── Global/Article.json # Content Type (contenttype, full JSON, pushable)
│ ├── config-sets/
│ │ └── Global/Default.json # Configuration Set (pageconfigurationset, full JSON, pushable)
│ ├── data-definitions/
│ │ └── Global/Article.xml # Data Definition (datadefinition, XML, pushable)
│ └── shared-fields/
│ └── Global/Hero.xml # Shared Field (sharedfield, XML, pushable)
├── about/
│ └── index.page # Pulled Page structured data XML (page, read-only, XML syntax highlighted)
├── _blocks/
│ └── footer.block # Pulled Block structured data XML outside syncFolders (block, read-only, XML syntax highlighted)
├── _cms/blocks/
│ └── sidebar.block # Pulled Block structured data XML inside _cms (block, read-only, XML syntax highlighted)
├── index.page # Root Page structured data XML (page, read-only, XML syntax highlighted)
└── sync-manifest.json # Schema v1 baseline manifest (excluded from VS Code search)
License
MIT
| |