Merged
Conversation
Desktop, web, and mobile currently depend on private renderer internals and source checkouts, while server builds depend on the frontend build tree. This change introduces explicit public editor APIs, portable packages, and verified browser artifacts so each host can build against a deliberate dependency boundary. ### Review groups 1. **Contracts and domain:** move portable types into the bridge contract, preserve compatibility exports, and share exact-byte behavior fixtures. 2. **Editor APIs and lifecycle:** expose navigation, editor commands, attachments, notes, folders, databases, tasks, settings, and immutable host snapshots. Coordinate saves, file mutations, workspace transitions, and native relocation/rollback. 3. **Package distribution:** build immutable core/contract/domain archives, preserve singleton React/CodeMirror dependencies, and verify external consumers on Vite 6 and 8. 4. **Server and viewer artifacts:** support Go-only API builds and verified embedded web imports; restore a maintained read-only public viewer with safe Markdown fallback and restricted interactive plots. 5. **Extraction preparation:** provide Go repository, Docker, Nix, and draft-release templates for the later `ZenNotes/znserver` cutover. The current server source and distribution owner remain active until that cutover. This branch starts from current `main` after v2.50.4, including its verified AUR/Homebrew/Nix metadata. The asset-reference fix is reconciled with the workspace reservation so pending edits and vault switches cannot interrupt a rename/move rewrite.
… CodeQL The standalone package check installed the packed archives with npm's offline mode, which only works when the npm cache already holds registry metadata for every dependency range. npm ci caches tarballs, never that metadata, so every fresh CI runner failed to resolve lz-string. The check now prefers the cache but allows the metadata fetch. On Windows the packers wrote include and exclude globs with backslashes, and tsc treats those as patterns that match nothing. Generated tsconfig paths now use forward slashes on every platform. Behind that failure waited another: Windows checkouts convert the contract fixtures to CRLF, and the TypeScript, Go server, and TUI consumers hash those bytes against recorded provenance. The fixture directories now check out with LF everywhere. CodeQL flagged eight items in the large diff. The wikilink resolvers trim slashes with a linear scan instead of a backtracking regex, the browser harness only dispatches CDP events to registered listeners, and store.ts drops four unused imports and an unreferenced helper. Validation: the shared-package check fails on an empty npm cache before and passes after; the Windows glob is reproduced with path.win32; the CRLF hash mismatch is reproduced arithmetically against the pinned digests; root typecheck, every workspace suite with the turbo cache bypassed, the isolated package check, and the Chrome harness pass with these changes.
…ture The regression test added with the CodeQL cleanup built its own two-note array without the title and folder fields that NoteRef requires, so the app-core typecheck failed on every platform once the earlier steps passed. Vitest ran the test without complaint because it does not typecheck. Reuse the module-level fixture, which already holds both notes the test needs.
The installed-editor check ran on a GitHub runner for the first time and stopped at "editor: timed out" with nothing else in the log. The harness already collected page errors, failed requests, helper output, screenshots and DOM dumps, but wrote them into a throwaway temporary directory and never printed them, so the failure was unreadable from CI. On failure the harness now prints the collected errors, failed requests, the tail of the server, preview and Chrome output, and a compact page state. A small collector copies the screenshots and JSON dumps out of the consumer directory so the Ubuntu job can upload them as an artifact whether the step passes or fails. The first note open pulls the editor, store and Markdown chunks on a cold runner, so it now gets the same sixty seconds the lazy Mermaid and Typst renders already had. A real rendering failure still fails, with reasons. Validation: the harness still passes locally against real Chrome; the collector gathers fifteen files from a previous local run.
…nd match Windows paths in the rollback test The Ubuntu runner reached the sidebar before the workspace finished restoring, so the harness's first openNote landed during the transition and the public navigation guard dropped it by design: no error, no editor, and sixty seconds later the page was still Home with the note index loaded. The harness now waits for the shell's workspaceRestored signal, the same readiness the later host-command section already required, before opening its first note. The evidence upload and page dump from the previous run are what made this visible. The Empty Trash rollback test refuses the comment-tree move by matching a rename source that ends in .zennotes/comments/trash. Windows joins that path with backslashes, so the refusal never fired, the transaction succeeded, and the assertion that it rejects failed on that runner alone. Compare the normalized path so the refusal fires on every platform. Validation: the browser harness passes locally against real Chrome and in a Debian container with Chromium; the vault suite passes locally; macOS and Linux arm64 already passed the full build on the previous run.
… the delayed cache write before tests remove a vault The previous run showed the same restoration race at the harness's next navigation: after loading the host page it opened a note while the workspace was still restoring, and the public navigation guard dropped the call. Every scripted navigation now goes through one readiness helper that waits for the shell's workspaceRestored signal before it opens a note. On Windows the vault package failed in cleanup rather than in a test: ListNotes schedules a note-meta cache snapshot one second later, and the slow runner reached t.TempDir removal while that goroutine was creating files, so Windows refused to delete the directory. The same race exists on main; it only surfaced here because the runner happened to line up with the delay. The vault now counts its delayed writers and exposes Close to drain them, and the package's tests keep the writer off through TestMain so no test can race its own cleanup. A test that opts back in drains with Close. Validation: the browser harness passes locally against real Chrome; go vet and the full server suite pass in the branch worktree.
The self-hosted Go server now lives in its own repository, https://github.com/ZenNotes/znserver. Its history was extracted from apps/server with the full commit trail, its first release (v2.50.4) is published with checksummed binaries, and the adibhanna/zennotes Docker image is built and pushed by that repository's publisher (2.50.5 was the first image from the new channel). This commit removes the copy that stayed here and points everything that still built the server from source at the published release instead. What goes: apps/server, the root Dockerfile and .dockerignore, the docker-publish workflow, the server Nix package and its vendorHash, the server-repository templates, the extraction rehearsal script, and the scripts that built or staged the server (build-go-server, prepare-server-web-dist, run-go-server-test). CI no longer installs Go, runs the server job, or checks the Go fixture copies, because those copies are checked in the server's own CI. What replaces it: tooling/server-release.json pins the znserver release (tag plus per-platform SHA-256 from its SHA256SUMS), and tooling/scripts/server-binary.mjs resolves a server binary in a fixed order: an explicit ZENNOTES_SERVER_BINARY, a ZENNOTES_SERVER_DIR checkout built with Go, or the pinned release downloaded into dist/server-binaries and verified against the pinned digest before it is ever executed. A download whose checksum does not match is refused rather than run. dev:server, dev:web-stack, and the app-core browser harness use that resolver, so browser work needs neither Go nor a server checkout. The perf harness deliberately does not fall back to the pinned release: a perf run measures the local web bundle, and the release embeds the pinned artifact instead. It takes a znserver checkout (syncing the local web dist in, then building with -tags=embed_web) or an explicit binary, and fails with the two options spelled out otherwise. sync-web-dist and the contract-fixture sync take the checkout as their destination for the same reason; the web-dist lock now lives next to apps/web/dist, the tree it guards. Docker Compose runs the published image (adibhanna/zennotes:latest by default, IMAGE=... to pin) instead of building one; make rebuild pulls, make nuke removes the pulled image. README, guide, CONTRIBUTING, the packaging docs, the architecture notes, and the synced-copy comments in the TypeScript sources point at the new repository. The extraction rehearsal and cutover documents keep their content with a status banner recording that the move is done. Not changed: the server's own release process (bump release.json in znserver, run its release workflow, then its Docker publisher), the web artifact pipeline that znserver consumes, and the moving of the Docker `latest` tag, which happens with the next server release.
The web artifact workflow still ran check:contract-fixtures. That script now compares the shared fixtures against a ZenNotes/znserver checkout, and the runner has none, so the step failed before the artifact was built. The Go copies are checked by the server's own CI against the same bytes, and the TypeScript consumers keep verifying the fixtures through test:run, so the step goes rather than gaining a checkout it does not need.
The desktop's `zen` command has been a Node script bundled with the app. The Go CLI in ZenNotes/tui is the one that keeps moving, so the desktop now carries the machinery to ship that binary instead: terminal-release.json pins a release by tag and per-platform checksum, after-pack.js stages the verified binary through tooling/scripts/terminal-artifact.mjs, build/zen launches it, and cli-install and terminal-runtime install and repair the shortcut around it. A release of `null` stages nothing and keeps the Node fallback, and that is how this ships: the first pin follows the Go CLI v0.2.0 release, so nothing changes for users in this version. Notes gain app-owned creation metadata so the desktop, MCP and Go CLI agree on creation dates instead of trusting filesystem birth times. The desktop and MCP move, rename and delete the metadata with its note, refuse a sidecar or metadata directory that links outside the vault, and fail a save before touching Markdown when the metadata is malformed. Cloud sync carries the sidecars as vault content, which is why the Laravel allowlist for the metadata suffix must be deployed before this reaches users. Settings gets the CLI section, the bridge and IPC carry the install request and status, help documents the command, CI runs the terminal tooling tests, and docs/specs/desktop-cli-tui-migration.md records the migration. Fixes: none by number; groundwork for the desktop-to-Go CLI migration.
On Arch the in-app update ran pacman through pkexec, and pkexec can fall back to a textual agent on the controlling terminal. An app started from a window-manager binding has no terminal to show it on, so the install never finished and the Relaunch button did nothing (#790). The updater now has an explicit installing phase, requires a graphical polkit agent instead of the internal fallback, and reads the exit code honestly: 126 means the user dismissed the authorization dialog, 127 means no agent could run at all. Each case gets its own message and a recovery path from Details > About. A manual check, download or install owns its result once the user starts it, so a background check cannot overwrite it. Packaging: the launcher is the lowercase build/zennotes, fpm declares the provides and conflicts, and the AUR package conflicts with the old ZenNotes name so the two cannot coexist. Fixes #790.
A Cloud vault deleted on the server stayed "up to date" in the client, and sync kept reporting success against something that no longer existed (#791). The client had no notion of a vault that is gone as opposed to a request that merely failed. cloud-vault-availability in shared-domain classifies the server's answers. Only the authenticated vault-level confirmation retires the remote association; a missing item, revision or upload is a sync error, never proof of deletion. A retired vault shows as such, auto-sync stops, and the Cloud settings panel offers unlink and discovers the retirement even while it stays mounted. The desktop host service and filesystem layer carry the same states, with tests on every layer. Fixes #791.
Visual-block edits changed only the first row of the rectangle (#792). CodeMirror-Vim keeps the block's full rectangle in vim.sel and issues one selection per row, but CM6 silently keeps only the primary selection unless the multiple-selections facet is enabled. The Vim extension now enables it, and the block highlighter paints one inclusive range per logical row so short rows still show the whole rectangle. A jsdom suite drives the real Vim adapter through block insert, append and change, applying ordinary CM6 text transactions since jsdom cannot type into contenteditable. Fixes #792.
The handoff document for the ecosystem boundary work, with the review-scope pointers, the runtime validation summary, the znserver extraction and the monorepo cutover, plus one sentence in the ecosystem plan pointing at it.
The self-hosted server sends `script-src 'self'` in its Content Security Policy, and the service-worker registration lived in an inline script in apps/web/index.html. Browsers refused that script silently, so production deployments never registered sw.js and the offline shell never installed; only the Vite dev server, which sends no policy, ever ran it. The registration moves into the bundle as register-service-worker.ts, called from the web entry. Behavior is otherwise unchanged: it still skips file: origins, still waits for load, and still uses the relative sw.js URL so a prefixed deployment registers for its own scope.
CodeQL flagged two check-then-use sequences in the terminal runtime: the launcher was lstat'ed and then read by path, and the installed binary was stat'ed for its mode and then read by path for its digest. Between the two calls another process could swap the file, so the property that was checked was not the property of the bytes that were used. Both now go through a single open handle. The launcher opens with O_NOFOLLOW, so a symlink is refused at open time (ELOOP) instead of being detected and then followed, and the type check and the marker check read from that same descriptor. The binary's mode and its hashed bytes come from one handle too. Behavior for the cases the tests cover is unchanged: no launcher is fine, a foreign file is refused, a verified install is retained.
The Windows build job failed on two suites that arrived with the later work. The stale-shortcut repair tests in cli-install.test.ts exercise AppImage mount paths and macOS bundle links for a feature the app does not offer on Windows, where the test's POSIX path is rewritten to a drive path and CLI install refuses outright. The .pacman install tests fake a Linux platform but drive the updater through POSIX shell doubles for the download and pkexec, which cannot run on the Windows runner, so every case stopped at "available" instead of "downloaded". Both suites now skip on win32 at the describe level, matching the per-test skips the CLI file already uses. They still run on macOS and Linux, which is where the behavior exists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release branch for ZenNotes 2.51.0. Contains, in order: the ecosystem boundary refactor (#789), the server cutover to ZenNotes/znserver (#795), and the later work landed as separate commits on top:
Feat(cli): run the desktop command through a pinned Go binary(groundwork; ships withrelease: null, so the bundled Node engine stays until Go CLI v0.2.0 is pinned)Fix(updater): show pacman installs through to relaunch(.pacmanUpdater Relaunch Button Has No Effect #790)Fix(cloud): retire a vault the server has deleted(Client Continues to Display Deleted Cloud Vault as Up to Date #791)Fix(vim): apply block edits to every row(Visual rectangular block edits not working as in vim #792)Docs: record the boundary handoffFix(web): register the service worker from the bundle(the server's CSP blocked the inline registration)Release: align desktop and shared packages at 2.51.0Verified on this branch: typecheck;
test:run(app-core 2371, desktop 849); terminal, web-dist lock and web artifact node tests;build(the builtindex.htmlhas no inline script and the bundle registerssw.js). The packaged-app CDP launch check and the desktop and web smokes run next; the tag follows them.Ordering: website PR ZenNotes/website#26 (Cloud creation-metadata allowlist) deploys before this reaches users.