diff --git a/README.md b/README.md index 0d41a02..5a5b23a 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ archives are vendored under `vendor/zennotes/` with their source identity and checksums (`manifest.json`), and `package-lock.json` pins the complete install. A clean checkout installs them with `npm ci`, without a source clone or sibling repository. The vendored set is the core release -[core-2.71.1-core.h77db37926ce01390](https://github.com/ZenNotes/zennotes/releases/tag/core-2.71.1-core.h77db37926ce01390) -(desktop commit `ae288166`, clean tree). +[core-2.72.0-core.h2a3462a340c755ce](https://github.com/ZenNotes/zennotes/releases/tag/core-2.72.0-core.h2a3462a340c755ce) +(desktop commit `3792c985`, clean tree). `npm run boundaries:check` verifies the pins, installed versions, singleton React/CodeMirror peers, and public export usage. `npm run core:adopt -- ` diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 589293a..0560a1f 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -608,12 +608,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ZenWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -737,12 +737,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -759,12 +759,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -779,12 +779,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -801,12 +801,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ZenWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -839,12 +839,12 @@ CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 44; DEVELOPMENT_TEAM = WYY7PK57DM; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.27.0; + MARKETING_VERSION = 1.28.0; PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/package-lock.json b/package-lock.json index e2caeff..96ec97a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.4.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.16", "function-plot": "^1.25.3", @@ -716,6 +716,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -732,6 +733,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -748,6 +750,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -764,6 +767,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -780,6 +784,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -796,6 +801,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -812,6 +818,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -828,6 +835,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -844,6 +852,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -860,6 +869,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -876,6 +886,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -892,6 +903,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -908,6 +920,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -924,6 +937,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -940,6 +954,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -956,6 +971,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -972,6 +988,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -988,6 +1005,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1004,6 +1022,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1020,6 +1039,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1036,6 +1056,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1052,6 +1073,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1068,6 +1090,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1084,6 +1107,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1100,6 +1124,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1116,6 +1141,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2874,6 +2900,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2890,6 +2917,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2906,6 +2934,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2922,6 +2951,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2938,6 +2968,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2954,6 +2985,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2970,6 +3002,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2986,6 +3019,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3002,6 +3036,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3018,6 +3053,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3034,6 +3070,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3050,6 +3087,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3066,6 +3104,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3082,6 +3121,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3098,6 +3138,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3444,7 +3485,7 @@ "version": "22.20.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3625,9 +3666,9 @@ } }, "node_modules/@zennotes/app-core": { - "version": "2.71.1-core.h77db37926ce01390", - "resolved": "file:vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz", - "integrity": "sha512-W2S8q+nBtoxxVGNm2X+/UA3eji+YU+VB1/8fCQ0Z6BVuY6G9uUaPqkr0qqYvj2hI1RoIhVe/H0ViQJhz823NQw==", + "version": "2.72.0-core.h2a3462a340c755ce", + "resolved": "file:vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz", + "integrity": "sha512-AJsVKaxRiG76yCZkij1NaDG2dGhk781Lpy4PyDdjhE0SaDtth8rHxVxS+er9XHqlwKuE3dsld/gWGR5nNJIaJw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -3657,8 +3698,8 @@ "@replit/codemirror-vim": "^6.3.0", "@terrastruct/d2": "0.1.33", "@xyflow/react": "^12.11.2", - "@zennotes/bridge-contract": "2.71.1-boundaries.h72cca974932e1356", - "@zennotes/shared-domain": "2.71.1-boundaries.h72cca974932e1356", + "@zennotes/bridge-contract": "2.72.0-boundaries.h69adf6c7c8b0ddc5", + "@zennotes/shared-domain": "2.72.0-boundaries.h69adf6c7c8b0ddc5", "dompurify": "^3.3.4", "function-plot": "^1.25.3", "gray-matter": "^4.0.3", @@ -3702,19 +3743,19 @@ } }, "node_modules/@zennotes/bridge-contract": { - "version": "2.71.1-boundaries.h72cca974932e1356", - "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz", - "integrity": "sha512-oMkkXDcpChKcusWBHl1tt/RUfo/dXfBTNhyhZRy8pqgJorLg9/mI410Xc+7H5Qwo76CiiqY6WA3H9kTV5MdWlw==", + "version": "2.72.0-boundaries.h69adf6c7c8b0ddc5", + "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "integrity": "sha512-qUgAkVGpsWHEKZ50yYvoocfL+HVhzvGiKAnD9QT6oGbOrQhRH13E/jTuRDmvzKNBG8D20YjpE55e8eA/b++bEw==", "license": "MIT" }, "node_modules/@zennotes/shared-domain": { - "version": "2.71.1-boundaries.h72cca974932e1356", - "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz", - "integrity": "sha512-9y1b9YX9kv0pSlOcEndb1tQ0/2ydMq+z1octYCctOG5TERCrN85e6WnoR9Sq5XaqNo/YTj3VN9yvBh37bx1KBg==", + "version": "2.72.0-boundaries.h69adf6c7c8b0ddc5", + "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "integrity": "sha512-hANY/7Ehbol6ZjyHYUmVTBmSqqEhwF0qmb32wDhJ8+jVKsuCg9zU3bakgAKODnv7KJzBzFCx+DdXEbH9u6RtGA==", "license": "MIT", "dependencies": { "@scure/bip39": "2.4.0", - "@zennotes/bridge-contract": "2.71.1-boundaries.h72cca974932e1356", + "@zennotes/bridge-contract": "2.72.0-boundaries.h69adf6c7c8b0ddc5", "hash-wasm": "4.12.0", "lz-string": "^1.5.0" } @@ -5009,7 +5050,7 @@ "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5250,6 +5291,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5852,7 +5894,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -6022,6 +6064,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6042,6 +6085,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6062,6 +6106,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6082,6 +6127,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6102,6 +6148,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6122,6 +6169,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6142,6 +6190,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6162,6 +6211,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6182,6 +6232,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6202,6 +6253,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6222,6 +6274,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9003,7 +9056,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unified": { diff --git a/package.json b/package.json index 2c2217c..16fbcc4 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.4.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.16", "function-plot": "^1.25.3", diff --git a/src/bridge/tasks-folders.test.ts b/src/bridge/tasks-folders.test.ts new file mode 100644 index 0000000..0746050 --- /dev/null +++ b/src/bridge/tasks-folders.test.ts @@ -0,0 +1,129 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +// The vault's Tasks folder lists from vault.json, read by the on-device +// scanners: `tasks.excludedFolders` (#458) keeps folders out, and +// `tasks.includedFolders`, the Task folders (#962), narrows Tasks to the notes +// inside them, the excluded list still applying within. Same rule as desktop +// main, the Go server and the Go CLI. + +Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: { getItem: () => null } }) +const { MobileVault } = await loadMobileModule(['./src/bridge/vault-fs']) + +const SETTINGS = '.zennotes/vault.json' + +class MemoryFs { + files = new Map() + directories = new Set() + async statVerified(path: string) { + return this.files.has(path) ? 'file' : this.directories.has(path) || [...this.files.keys()].some(key => key.startsWith(`${path}/`)) ? 'directory' : null + } + async exists(path: string) { return await this.statVerified(path) !== null } + async mkdir(path: string) { this.directories.add(path) } + async readText(path: string) { if (!this.files.has(path)) throw new Error('ENOENT'); return this.files.get(path)! } + async readTextOrNull(path: string) { return this.files.get(path) ?? null } + async writeText(path: string, body: string) { this.files.set(path, body) } + async deleteFile(path: string) { this.files.delete(path) } + async rename(from: string, to: string) { + assert.ok(await this.exists(from)); assert.equal(await this.exists(to), false) + for (const [key, body] of [...this.files]) if (key === from || key.startsWith(`${from}/`)) { + this.files.set(to + key.slice(from.length), body); this.files.delete(key) + } + for (const key of [...this.directories]) if (key === from || key.startsWith(`${from}/`)) { + this.directories.add(to + key.slice(from.length)); this.directories.delete(key) + } + } +} + +function fixture(tasks: unknown) { + const fs = new MemoryFs(), vault = new MobileVault('Fixture') + fs.files.set(SETTINGS, JSON.stringify({ primaryNotesLocation: 'inbox', ...(tasks === undefined ? {} : { tasks }) })) + fs.files.set('inbox/Tasks/Plan.md', '# Plan\n\n- [ ] in the Task folder\n') + fs.files.set('inbox/Tasks/Done/Old.md', '# Old\n\n- [ ] in a left-out folder inside it\n') + fs.files.set('inbox/Tasksheet.md', '# Tasksheet\n\n- [ ] beside it, sharing a name prefix\n') + fs.files.set('inbox/Projects/Site.md', '# Site\n\n- [ ] outside the Task folder\n') + fs.files.set('trash/Gone.md', '# Gone\n\n- [ ] in the trash\n') + Object.assign(vault, { + fs, + invalidateMeta: () => {}, + // The scanners walk listNotes(); answer from the memory files so a rename + // shows up in the next scan. + listNotes: async () => [...fs.files.keys()] + .filter(path => path.endsWith('.md')) + .map(path => ({ + path, + title: path.split('/').pop()!.replace(/\.md$/, ''), + folder: path.startsWith('trash/') ? 'trash' : 'inbox' + })) + }) + return { fs, vault } +} + +const scanned = async (vault: any): Promise => + [...new Set((await vault.scanTasks()).map((task: { sourcePath: string }) => task.sourcePath))].sort() + +const rescanned = async (vault: any, fs: MemoryFs): Promise => { + const out: string[] = [] + for (const path of [...fs.files.keys()].filter(path => path.endsWith('.md')).sort()) { + if ((await vault.scanTasksForPath(path)).length > 0) out.push(path) + } + return out +} + +test('Task folders narrow Tasks to the notes inside them, and the excluded list still applies within', async () => { + const { fs, vault } = fixture({ includedFolders: ['inbox/Tasks'], excludedFolders: ['inbox/Tasks/Done'] }) + assert.deepEqual(await scanned(vault), ['inbox/Tasks/Plan.md']) + // A single-note rescan agrees, or an edit would bring a left-out note back. + assert.deepEqual(await rescanned(vault, fs), ['inbox/Tasks/Plan.md']) +}) + +test('no Task folders means the whole vault, less the excluded folders and the trash', async () => { + const { fs, vault } = fixture({ excludedFolders: ['inbox/Projects'] }) + const expected = ['inbox/Tasks/Done/Old.md', 'inbox/Tasks/Plan.md', 'inbox/Tasksheet.md'] + assert.deepEqual(await scanned(vault), expected) + assert.deepEqual(await rescanned(vault, fs), expected) +}) + +test('a vault.json without Tasks settings lists every live note', async () => { + const { vault } = fixture(undefined) + assert.deepEqual(await scanned(vault), [ + 'inbox/Projects/Site.md', 'inbox/Tasks/Done/Old.md', 'inbox/Tasks/Plan.md', 'inbox/Tasksheet.md' + ]) +}) + +test('the folder lists are validated on read, and a save from this device keeps the Task folders', async () => { + const { fs, vault } = fixture({ + includedFolders: ['inbox/Tasks/', '../escape', '', 'inbox/Tasks', 7], + excludedFolders: 'not a list', + mode: 'tagged' + }) + const settings = await vault.getVaultSettings() + assert.deepEqual(settings.tasks, { mode: 'tagged', includedFolders: ['inbox/Tasks'] }) + await vault.setVaultSettings({ ...settings, favorites: ['inbox/Tasks/Plan.md'] }) + const written = JSON.parse(fs.files.get(SETTINGS)!) + assert.deepEqual(written.tasks, { mode: 'tagged', includedFolders: ['inbox/Tasks'] }) + assert.deepEqual(written.favorites, ['inbox/Tasks/Plan.md']) +}) + +test('lists that end up empty leave no tasks stub in vault.json', async () => { + const { fs, vault } = fixture({ includedFolders: [], excludedFolders: ['', '..'] }) + const settings = await vault.getVaultSettings() + assert.equal('tasks' in settings, false) + await vault.setVaultSettings(settings) + assert.equal('tasks' in JSON.parse(fs.files.get(SETTINGS)!), false) +}) + +test('renaming a folder repoints both lists, so the Task folder keeps its tasks', async () => { + const { fs, vault } = fixture({ includedFolders: ['inbox/Tasks', 'inbox/Projects'], excludedFolders: ['inbox/Tasks/Done'] }) + await vault.renameFolder('inbox', 'Tasks', 'Todo') + assert.deepEqual(JSON.parse(fs.files.get(SETTINGS)!).tasks, { + excludedFolders: ['inbox/Todo/Done'], + includedFolders: ['inbox/Todo', 'inbox/Projects'] + }) + assert.deepEqual(await scanned(vault), ['inbox/Projects/Site.md', 'inbox/Todo/Plan.md']) + // A folder that is neither listed nor inside a listed one leaves the lists alone. + fs.files.set('inbox/Notes/Loose.md', '# Loose\n') + await vault.renameFolder('inbox', 'Notes', 'Kept') + assert.deepEqual(JSON.parse(fs.files.get(SETTINGS)!).tasks.includedFolders, ['inbox/Todo', 'inbox/Projects']) +}) diff --git a/src/bridge/vault-fs.ts b/src/bridge/vault-fs.ts index 7b9f551..0006d4e 100644 --- a/src/bridge/vault-fs.ts +++ b/src/bridge/vault-fs.ts @@ -30,7 +30,9 @@ import { normalizeNoteComments } from '@zennotes/shared-domain/note-comments' import { AGENT_EDITS_DIR, agentEditSidecarPath } from '@zennotes/shared-domain/agent-edits' import { isPathExcludedFromTasks, - normalizeTasksExcludedFolders + normalizeTasksExcludedFolders, + normalizeTasksIncludedFolders, + rewriteTasksFoldersForRename } from '@zennotes/shared-domain/tasks-excluded-folders' import { pastedImageFilename } from '@zennotes/shared-domain/pasted-image' import { randomUUID } from './uuid' @@ -146,6 +148,27 @@ function warnAboutDiscardedFolderPaths(parsed: unknown, settings: VaultSettings) } } +/** + * The Tasks folder lists validated the way desktop's normalizeTasksSettings + * validates them: the folders left out of Tasks (#458) and the Task folders + * it is narrowed to (#962). A hand-edited list that is not an array would + * otherwise reach a folder rename and refuse it. An empty list is left out, + * other keys under `tasks` pass through, and undefined comes back when + * nothing is left, so vault.json carries no empty stub. + */ +function normalizeTasksSettings(raw: unknown): VaultSettings['tasks'] | undefined { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined + const { excludedFolders, includedFolders, ...rest } = raw as Record + const excluded = normalizeTasksExcludedFolders(excludedFolders) + const included = normalizeTasksIncludedFolders(includedFolders) + const out = { + ...rest, + ...(excluded.length > 0 ? { excludedFolders: excluded } : {}), + ...(included.length > 0 ? { includedFolders: included } : {}) + } + return Object.keys(out).length > 0 ? out : undefined +} + export class MobileVault { readonly name: string readonly fs: NativeFs @@ -322,6 +345,9 @@ export class MobileVault { // sync erases what the user taught it. Phones never run Harper themselves. const harper = normalizeHarperVaultState(obj.harper) if (harper) out.harper = harper + const tasks = normalizeTasksSettings(obj.tasks) + if (tasks) out.tasks = tasks + else delete out.tasks return out } @@ -971,7 +997,11 @@ export class MobileVault { await this.setVaultSettings({ ...settings, folderIcons: rekey(settings.folderIcons as Record) as VaultSettings['folderIcons'], - folderColors: rekey(settings.folderColors as Record) as VaultSettings['folderColors'] + folderColors: rekey(settings.folderColors as Record) as VaultSettings['folderColors'], + // The Tasks folder lists name on-disk paths, so they follow the folder + // too, as on desktop: a renamed Task folder (#962) left behind would + // match nothing and empty Tasks. + tasks: rewriteTasksFoldersForRename(settings.tasks, oldRel, newRel) }) } catch (error) { try { @@ -1203,18 +1233,28 @@ export class MobileVault { return fileTask ? [fileTask, ...inline] : inline } + /** The vault's Tasks folder lists from vault.json: the folders left out of + * Tasks (#458) and the Task folders it is narrowed to (#962), empty + * meaning the whole vault. */ + private async tasksFolderLists(): Promise<{ excluded: string[]; included: string[] }> { + const tasks = (await this.getVaultSettings()).tasks + return { + excluded: normalizeTasksExcludedFolders(tasks?.excludedFolders), + included: normalizeTasksIncludedFolders(tasks?.includedFolders) + } + } + async scanTasks(): Promise { - // Vault-level exclusions (#458) travel in vault.json, so a folder Adib's - // desktop excluded stays excluded here; the frontmatter `tasks:` opt-out - // comes free from the shared parsers. - const excluded = normalizeTasksExcludedFolders( - (await this.getVaultSettings()).tasks?.excludedFolders - ) + // The Tasks folder lists travel in vault.json, so a folder the desktop + // left out (#458), or the Task folders it narrowed Tasks to (#962), apply + // here too; the frontmatter `tasks:` opt-out comes free from the shared + // parsers. + const { excluded, included } = await this.tasksFolderLists() const notes = await this.listNotes() const out: VaultTask[] = [] for (const note of notes) { if (note.folder === 'trash') continue - if (isPathExcludedFromTasks(note.path, excluded)) continue + if (isPathExcludedFromTasks(note.path, excluded, included)) continue if (!isMarkdownPath(note.path)) continue const body = await this.fs.readTextOrNull(note.path) if (body === null) continue @@ -1229,12 +1269,10 @@ export class MobileVault { const rel = resolveSafeRel(relPath) const folder = await this.folderOf(rel) if (!folder || folder === 'trash') return [] - // Same exclusion the full scan applies (#458), or a single-note rescan - // would resurrect an excluded folder's tasks on every edit. - const excluded = normalizeTasksExcludedFolders( - (await this.getVaultSettings()).tasks?.excludedFolders - ) - if (isPathExcludedFromTasks(rel, excluded)) return [] + // Same folder lists the full scan applies (#458, #962), or a single-note + // rescan would resurrect a left-out folder's tasks on every edit. + const { excluded, included } = await this.tasksFolderLists() + if (isPathExcludedFromTasks(rel, excluded, included)) return [] if (!isMarkdownPath(rel)) return [] const body = await this.fs.readTextOrNull(rel) if (body === null) return [] diff --git a/vendor/zennotes/manifest.json b/vendor/zennotes/manifest.json index 7ce7d0d..244df8d 100644 --- a/vendor/zennotes/manifest.json +++ b/vendor/zennotes/manifest.json @@ -1,12 +1,12 @@ { "name": "@zennotes/app-core", - "version": "2.71.1-core.h77db37926ce01390", - "file": "zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz", - "sha256": "039080f1af8a99f9c21619303ad6ffa77610033b537d2739ce35db0fc81e454b", - "integrity": "sha512-W2S8q+nBtoxxVGNm2X+/UA3eji+YU+VB1/8fCQ0Z6BVuY6G9uUaPqkr0qqYvj2hI1RoIhVe/H0ViQJhz823NQw==", - "sourceCommit": "ae28816697a8bed4c402f95af25aa81216b809b9", + "version": "2.72.0-core.h2a3462a340c755ce", + "file": "zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz", + "sha256": "7610c8a6e338e1e6c152d87ac98ce7f3fbfd48b5655139538c376cfde00a41f7", + "integrity": "sha512-AJsVKaxRiG76yCZkij1NaDG2dGhk781Lpy4PyDdjhE0SaDtth8rHxVxS+er9XHqlwKuE3dsld/gWGR5nNJIaJw==", + "sourceCommit": "3792c985eeb5c0d5f60c20be34898397901b5ae3", "workingTreeDirty": false, - "sourceLockSha256": "53a5012f6fff7b02cdef16837664f00824e48db6f49834b2a6cbfaae2807eb48", + "sourceLockSha256": "af5b3d3e6058dd7e2c911aa297cc24b5da8e1d1b635b636ceb3c49da31f25963", "toolchain": { "node": "v22.23.3", "typescript": "5.9.3", @@ -15,20 +15,20 @@ "dependencies": [ { "name": "@zennotes/bridge-contract", - "version": "2.71.1-boundaries.h72cca974932e1356", - "file": "zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz", - "sha256": "dfaaae6d4999ed826583addb3c6ecc6234433dc12957d438db13e30b6c265afa", - "integrity": "sha512-oMkkXDcpChKcusWBHl1tt/RUfo/dXfBTNhyhZRy8pqgJorLg9/mI410Xc+7H5Qwo76CiiqY6WA3H9kTV5MdWlw==", - "sourceCommit": "ae28816697a8bed4c402f95af25aa81216b809b9", + "version": "2.72.0-boundaries.h69adf6c7c8b0ddc5", + "file": "zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "sha256": "f312dd5390f68aff04ed567e0cca49dcaf7b8323026018fc345d20af2011539a", + "integrity": "sha512-qUgAkVGpsWHEKZ50yYvoocfL+HVhzvGiKAnD9QT6oGbOrQhRH13E/jTuRDmvzKNBG8D20YjpE55e8eA/b++bEw==", + "sourceCommit": "3792c985eeb5c0d5f60c20be34898397901b5ae3", "workingTreeDirty": false }, { "name": "@zennotes/shared-domain", - "version": "2.71.1-boundaries.h72cca974932e1356", - "file": "zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz", - "sha256": "69422b980aace9ad358c0580012970c13f969cc2ca0d73af91e8243e10131f33", - "integrity": "sha512-9y1b9YX9kv0pSlOcEndb1tQ0/2ydMq+z1octYCctOG5TERCrN85e6WnoR9Sq5XaqNo/YTj3VN9yvBh37bx1KBg==", - "sourceCommit": "ae28816697a8bed4c402f95af25aa81216b809b9", + "version": "2.72.0-boundaries.h69adf6c7c8b0ddc5", + "file": "zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz", + "sha256": "c8a8e41bc26200dd001cc77ed02872c1c730e447e76645465fdcea584ca53e19", + "integrity": "sha512-hANY/7Ehbol6ZjyHYUmVTBmSqqEhwF0qmb32wDhJ8+jVKsuCg9zU3bakgAKODnv7KJzBzFCx+DdXEbH9u6RtGA==", + "sourceCommit": "3792c985eeb5c0d5f60c20be34898397901b5ae3", "workingTreeDirty": false } ] diff --git a/vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz b/vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz deleted file mode 100644 index e3ae111..0000000 Binary files a/vendor/zennotes/zennotes-app-core-2.71.1-core.h77db37926ce01390.tgz and /dev/null differ diff --git a/vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz b/vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz new file mode 100644 index 0000000..d68263f Binary files /dev/null and b/vendor/zennotes/zennotes-app-core-2.72.0-core.h2a3462a340c755ce.tgz differ diff --git a/vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz b/vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz deleted file mode 100644 index 37fd7c3..0000000 Binary files a/vendor/zennotes/zennotes-bridge-contract-2.71.1-boundaries.h72cca974932e1356.tgz and /dev/null differ diff --git a/vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz b/vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz new file mode 100644 index 0000000..24d0295 Binary files /dev/null and b/vendor/zennotes/zennotes-bridge-contract-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz differ diff --git a/vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz b/vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz deleted file mode 100644 index 42b321a..0000000 Binary files a/vendor/zennotes/zennotes-shared-domain-2.71.1-boundaries.h72cca974932e1356.tgz and /dev/null differ diff --git a/vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz b/vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz new file mode 100644 index 0000000..fc4f549 Binary files /dev/null and b/vendor/zennotes/zennotes-shared-domain-2.72.0-boundaries.h69adf6c7c8b0ddc5.tgz differ