diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3582c654..61eb1a00 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/desktop", "productName": "ZenNotes", - "version": "2.58.0", + "version": "2.59.0", "description": "ZenNotes desktop shell", "private": true, "main": "./out/main/index.js", diff --git a/apps/desktop/src/main/undo-history-store.test.ts b/apps/desktop/src/main/undo-history-store.test.ts index fbd409e5..4025e0bb 100644 --- a/apps/desktop/src/main/undo-history-store.test.ts +++ b/apps/desktop/src/main/undo-history-store.test.ts @@ -5,7 +5,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { MAX_UNDO_HISTORY_AGE_MS, MAX_UNDO_HISTORY_BYTES, - MAX_UNDO_HISTORY_FILES, UNDO_HISTORY_DIR, clearUndoHistories, pruneUndoHistories, @@ -94,21 +93,26 @@ describe('pruning undo history files', () => { expect((await readdir(path.dirname(oldFile))).length).toBe(1) }) + // Checked against a small cap, not the real 400: writing 400 histories took + // over five seconds on the Windows runner, the test timed out, and vitest's + // cleanup then raced the abandoned writes into an ENOTEMPTY. it('keeps the most recently written notes when a vault has too many', async () => { + const cap = 4 const extra = 3 - for (let n = 0; n < MAX_UNDO_HISTORY_FILES + extra; n++) { + for (let n = 0; n < cap + extra; n++) { await writeUndoHistory(base, VAULT, `inbox/${n}.md`, String(n)) const file = undoHistoryFile(base, VAULT, `inbox/${n}.md`)! - const at = new Date(Date.now() - (MAX_UNDO_HISTORY_FILES + extra - n) * 1000) + const at = new Date(Date.now() - (cap + extra - n) * 1000) await utimes(file, at, at) } - await pruneUndoHistories(base) + await pruneUndoHistories(base, Date.now(), cap) expect(await readUndoHistory(base, VAULT, 'inbox/0.md')).toBeNull() expect(await readUndoHistory(base, VAULT, `inbox/${extra - 1}.md`)).toBeNull() expect(await readUndoHistory(base, VAULT, `inbox/${extra}.md`)).toBe(String(extra)) - expect(await readFile(undoHistoryFile(base, VAULT, `inbox/${MAX_UNDO_HISTORY_FILES}.md`)!, 'utf8')).toBe( - String(MAX_UNDO_HISTORY_FILES) + expect(await readFile(undoHistoryFile(base, VAULT, `inbox/${cap + extra - 1}.md`)!, 'utf8')).toBe( + String(cap + extra - 1) ) + expect(await readdir(path.dirname(undoHistoryFile(base, VAULT, 'inbox/0.md')!))).toHaveLength(cap) }) it('is fine with nothing to prune', async () => { diff --git a/apps/desktop/src/main/undo-history-store.ts b/apps/desktop/src/main/undo-history-store.ts index bc704910..1d483838 100644 --- a/apps/desktop/src/main/undo-history-store.ts +++ b/apps/desktop/src/main/undo-history-store.ts @@ -105,7 +105,11 @@ export async function clearUndoHistories(baseDir: string): Promise { * oldest ones beyond the per-vault count, and any stray partial writes. Cheap * enough to run once per launch. */ -export async function pruneUndoHistories(baseDir: string, now: number = Date.now()): Promise { +export async function pruneUndoHistories( + baseDir: string, + now: number = Date.now(), + maxFiles: number = MAX_UNDO_HISTORY_FILES +): Promise { const root = path.join(baseDir, UNDO_HISTORY_DIR) let vaults: string[] try { @@ -134,7 +138,7 @@ export async function pruneUndoHistories(baseDir: string, now: number = Date.now } } kept.sort((a, b) => b.mtimeMs - a.mtimeMs) - for (const { file } of kept.slice(MAX_UNDO_HISTORY_FILES)) { + for (const { file } of kept.slice(maxFiles)) { await fsp.rm(file, { force: true }).catch(() => undefined) } if (kept.length === 0) await fsp.rmdir(dir).catch(() => undefined) diff --git a/apps/desktop/terminal-release.json b/apps/desktop/terminal-release.json index 2b98d750..9a64982c 100644 --- a/apps/desktop/terminal-release.json +++ b/apps/desktop/terminal-release.json @@ -3,24 +3,24 @@ "release": { "repository": "ZenNotes/tui", "protocol": 1, - "version": "0.4.1", - "commit": "86e2149b94fc27587a704fc02afd25e5a42c5cc2", + "version": "0.6.0", + "commit": "5d18fc86b58f94d7ffe90cbf9767904a20f33e9e", "artifacts": { "darwin-arm64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_darwin_arm64.tar.gz", - "sha256": "3c83f847268c26279121537c807785dfab0403fa0cae248f149d09a11a2e2eda" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_darwin_arm64.tar.gz", + "sha256": "5102fce1a2ee2cf9c7358dd0eb8d929023a5675ceb693a96ad2fa50fc6936795" }, "darwin-x64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_darwin_amd64.tar.gz", - "sha256": "d541dcc3fbfd15d1f702a71ff4d629d1a1907a21d055c23df27934925e9f4557" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_darwin_amd64.tar.gz", + "sha256": "514a642c820bbbba9f93b6e29626757ba165bc8f69ee967b6304054ad65633b2" }, "linux-arm64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_linux_arm64.tar.gz", - "sha256": "55d6999163b66fc0e29a3a35a449f37175c448bb7e6e22072ee1c970769fd739" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_linux_arm64.tar.gz", + "sha256": "9c7bfb76ad248585cccd61fedea9a0e6547afd1f14cc96191799996478118142" }, "linux-x64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_linux_amd64.tar.gz", - "sha256": "42e5e64edde08f3145883ffcfeaf197d6290d966074ce0e28f054d254bb806c1" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_linux_amd64.tar.gz", + "sha256": "7e41439f78cceeb80e10a7d0caa68c7ae80986e821cca4d0f5787fdd597c426b" } } } diff --git a/apps/share-viewer/package.json b/apps/share-viewer/package.json index 72e50f98..8295db01 100644 --- a/apps/share-viewer/package.json +++ b/apps/share-viewer/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/share-viewer", "private": true, - "version": "2.58.0", + "version": "2.59.0", "type": "module", "description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website", "homepage": "https://zennotes.org", diff --git a/apps/web/package.json b/apps/web/package.json index 889cdd1c..4b483438 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/web", "private": true, - "version": "2.58.0", + "version": "2.59.0", "type": "module", "description": "ZenNotes web client for self-hosted and hosted deployments", "homepage": "https://zennotes.org", diff --git a/package-lock.json b/package-lock.json index 8a07c322..b8d43667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zennotes-monorepo", - "version": "2.58.0", + "version": "2.59.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zennotes-monorepo", - "version": "2.58.0", + "version": "2.59.0", "hasInstallScript": true, "workspaces": [ "apps/*", @@ -23,7 +23,7 @@ }, "apps/desktop": { "name": "@zennotes/desktop", - "version": "2.58.0", + "version": "2.59.0", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -874,7 +874,7 @@ }, "apps/share-viewer": { "name": "@zennotes/share-viewer", - "version": "2.58.0", + "version": "2.59.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -945,7 +945,7 @@ }, "apps/web": { "name": "@zennotes/web", - "version": "2.58.0", + "version": "2.59.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -16382,7 +16382,7 @@ }, "packages/app-core": { "name": "@zennotes/app-core", - "version": "2.58.1", + "version": "2.59.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -16469,14 +16469,14 @@ }, "packages/bridge-contract": { "name": "@zennotes/bridge-contract", - "version": "2.58.1", + "version": "2.59.0", "devDependencies": { "typescript": "^5.7.2" } }, "packages/shared-domain": { "name": "@zennotes/shared-domain", - "version": "2.58.1", + "version": "2.59.0", "dependencies": { "@zennotes/bridge-contract": "*", "lz-string": "^1.5.0" @@ -16488,7 +16488,7 @@ }, "packages/shared-ui": { "name": "@zennotes/shared-ui", - "version": "2.58.0" + "version": "2.59.0" } } } diff --git a/package.json b/package.json index 382ff024..f2e870ab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-monorepo", "private": true, - "version": "2.58.0", + "version": "2.59.0", "description": "ZenNotes monorepo for desktop, web, and self-hosted server builds", "packageManager": "npm@10.9.2", "engines": { diff --git a/packages/app-core/package.json b/packages/app-core/package.json index f3b4003a..5e373e8f 100644 --- a/packages/app-core/package.json +++ b/packages/app-core/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/app-core", "private": true, - "version": "2.58.1", + "version": "2.59.0", "type": "module", "exports": { "./main": "./src/main.tsx", diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 5ab4c081..a7a11230 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -1226,6 +1226,11 @@ export const HELP_CLI: HelpCard[] = [ body: 'Builds with the Go terminal tool include `zn tui`. Update and open ZenNotes once to upgrade an existing desktop-managed CLI; keep using the same `zn` commands. Settings shows the installed terminal version and offers Repair if an upgrade needs attention. Desktop-installed commands keep following the desktop vault, while the TUI remembers its own selection. Explicit `--vault` and `--server` flags still win. Set `ZENNOTES_WORKSPACE_SOURCE=terminal` to use the terminal default for a command, or `ZENNOTES_CLI_ENGINE=legacy` to run the previous CLI during the transition. Homebrew and manual installations stay managed by their own installer. A shortcut left behind by a moved Mac app or an old AppImage can be repaired from Settings: review the old target, replacement and backup path before choosing Repair shortcut. Note saves preserve creation dates in small files under `.zennotes/note-metadata` without changing Markdown; keep the `.zennotes` folder with vault backups. Explicit legacy rollback needs the original app resources to remain available.' }, + { + title: 'CLI 0.6.0 in desktop 2.59.0', + body: + 'On macOS and Linux, desktop 2.59.0 bundles CLI 0.6.0. Run `zn` in an interactive terminal or `zn tui` to open the editor. It includes editable settings, restored splits and sessions, comments, attachments, templates, bulk actions, and cross-note yank and paste. Use `:version` in the TUI for a persistent report and press `c` or `y` to copy it. `zn status`, `zn doctor`, `zn config`, and `zn completion` help with setup and scripts; `zn server` manages native local servers, also available through `:servers`. Run `zn update --check` to check the CLI release; a desktop-managed CLI updates with the desktop app. The full command reference is at https://zennotes.org/tui/docs.' + }, { title: 'No app required', body: diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index e8a5c0c6..853315c9 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -87,31 +87,38 @@ function installZen(overrides: Record = {}): void { }) } -// The most recent store module a test loaded. `vi.resetModules` gives every -// test a fresh module, but timers the OLD module already scheduled keep -// running: `updateNoteBody` debounces a `persistNote` at 350ms -// (pathSaveTimers), and database edits debounce a write at 400ms -// (databaseSaveTimers). A test that schedules either without awaiting it -// leaves a straggler that fires against whichever `window.zen` spy is -// installed one or two tests later; a partial mock there turns the write -// into `undefined.catch`, which is an unhandled error that fails the whole -// run (seen on the macOS runner). The cure is the afterEach below: it -// starves both timers' bail-out checks on the old store, so a stray -// persistNote finds nothing dirty and a stray database write finds no doc, -// and each returns before touching `window.zen`. -let lastLoadedStore: { useStore: { setState: (partial: object) => void } } | null = null +// Every store module the running test loaded (a relaunch test loads two). +// `vi.resetModules` gives every test a fresh module, but timers the OLD +// module already scheduled keep running: `updateNoteBody` debounces a +// `persistNote` at 350ms (pathSaveTimers), database edits debounce a write at +// 400ms (databaseSaveTimers), and opening a vault refreshes its assets 2s +// later (scheduleAssetsRefreshForVault). A test that schedules any of them +// without awaiting it leaves a straggler that fires against whichever +// `window.zen` spy is installed tests later; a partial mock there turns the +// write into `undefined.catch`, which is an unhandled error that fails the +// whole run (seen on the macOS runner). The asset refresh can also outlive +// the file: on a slow runner it fired after vitest had torn down the jsdom +// environment, and `window is not defined` failed the Windows build. The cure +// is the afterEach below: it starves every timer's bail-out check on the old +// stores, so a stray persistNote finds nothing dirty, a stray database write +// finds no doc and a stray asset refresh finds its vault closed, and each +// returns before touching `window`. +type LoadedStoreModule = { useStore: { setState: (partial: object) => void } } +const loadedStores = new Set() async function loadStore() { vi.resetModules() localStorage.clear() const mod = await import('./store') - lastLoadedStore = mod as unknown as typeof lastLoadedStore + loadedStores.add(mod as unknown as LoadedStoreModule) return mod } afterEach(() => { - lastLoadedStore?.useStore.setState({ noteDirty: {}, databases: {} }) - lastLoadedStore = null + for (const loaded of loadedStores) { + loaded.useStore.setState({ noteDirty: {}, databases: {}, vault: null }) + } + loadedStores.clear() }) type LoadedStore = Awaited>['useStore'] @@ -842,7 +849,7 @@ describe('per-note panels survive a restart (#794)', () => { async function relaunch() { vi.resetModules() const mod = await import('./store') - lastLoadedStore = mod as unknown as typeof lastLoadedStore + loadedStores.add(mod as unknown as LoadedStoreModule) return mod } diff --git a/packages/bridge-contract/package.json b/packages/bridge-contract/package.json index a8081c04..9c99e96f 100644 --- a/packages/bridge-contract/package.json +++ b/packages/bridge-contract/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/bridge-contract", "private": true, - "version": "2.58.1", + "version": "2.59.0", "type": "module", "exports": { "./bridge": "./src/bridge.ts", diff --git a/packages/shared-domain/package.json b/packages/shared-domain/package.json index cafaf523..e9ed7825 100644 --- a/packages/shared-domain/package.json +++ b/packages/shared-domain/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/shared-domain", "private": true, - "version": "2.58.1", + "version": "2.59.0", "type": "module", "exports": { "./*": "./src/*.ts" diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json index f8dc46a2..853dd11a 100644 --- a/packages/shared-ui/package.json +++ b/packages/shared-ui/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/shared-ui", "private": true, - "version": "2.58.0", + "version": "2.59.0", "type": "module", "exports": { ".": "./src/index.ts"