mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-01 08:17:07 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4419a837c |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"minAppVersion": "1.7.2",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
Generated
+48
-48
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"src/apps/cli",
|
"src/apps/cli",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"@smithy/types": "^4.14.3",
|
"@smithy/types": "^4.14.3",
|
||||||
"@smithy/util-retry": "^4.4.5",
|
"@smithy/util-retry": "^4.4.5",
|
||||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||||
"@vrtmrz/livesync-commonlib": "0.1.3",
|
"@vrtmrz/livesync-commonlib": "0.1.0",
|
||||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||||
"@vrtmrz/ui-interactions": "0.1.2",
|
"@vrtmrz/ui-interactions": "0.1.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
@@ -1887,9 +1887,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1968,9 +1968,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4775,9 +4775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vrtmrz/livesync-commonlib": {
|
"node_modules/@vrtmrz/livesync-commonlib": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0.tgz",
|
||||||
"integrity": "sha512-M6+mOlf4R60pf6mMuQdIlOoeIjOlDQn/hIRjELCkAwAcJXo5V/l8LfBCujeuvVk9FOzO3S0Q4WET9VaDHDxosw==",
|
"integrity": "sha512-rdzEubzLStioanE67pps2XSGZ2UGyMIyeEoKsdPztQLLW8wM05PWApOPbJujIydHcDr2hFanbDFe89Lk7Mn4XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.808.0",
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
@@ -4896,9 +4896,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/config/node_modules/brace-expansion": {
|
"node_modules/@wdio/config/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5264,9 +5264,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5734,9 +5734,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-module-resolver/node_modules/brace-expansion": {
|
"node_modules/babel-plugin-module-resolver/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5986,9 +5986,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -7673,9 +7673,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7770,9 +7770,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7836,9 +7836,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8013,9 +8013,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8215,9 +8215,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9052,9 +9052,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/globby/node_modules/brace-expansion": {
|
"node_modules/globby/node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -12668,9 +12668,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -15924,7 +15924,7 @@
|
|||||||
},
|
},
|
||||||
"src/apps/cli": {
|
"src/apps/cli": {
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"version": "1.0.4-cli",
|
"version": "1.0.2-cli",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"minimatch": "^10.2.5",
|
"minimatch": "^10.2.5",
|
||||||
@@ -15949,7 +15949,7 @@
|
|||||||
},
|
},
|
||||||
"src/apps/webapp": {
|
"src/apps/webapp": {
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"version": "1.0.4-webapp",
|
"version": "1.0.2-webapp",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.52"
|
"octagonal-wheels": "^0.1.52"
|
||||||
},
|
},
|
||||||
@@ -15961,7 +15961,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/apps/webpeer": {
|
"src/apps/webpeer": {
|
||||||
"version": "1.0.4-webpeer",
|
"version": "1.0.2-webpeer",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.52"
|
"octagonal-wheels": "^0.1.52"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
"@smithy/types": "^4.14.3",
|
"@smithy/types": "^4.14.3",
|
||||||
"@smithy/util-retry": "^4.4.5",
|
"@smithy/util-retry": "^4.4.5",
|
||||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||||
"@vrtmrz/livesync-commonlib": "0.1.3",
|
"@vrtmrz/livesync-commonlib": "0.1.0",
|
||||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||||
"@vrtmrz/ui-interactions": "0.1.2",
|
"@vrtmrz/ui-interactions": "0.1.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ livesync-cli [database-path] [command] [args...]
|
|||||||
- `--vault <path>` / `-V <path>`: (daemon/mirror only) Path to the vault directory containing `.md` files.
|
- `--vault <path>` / `-V <path>`: (daemon/mirror only) Path to the vault directory containing `.md` files.
|
||||||
- Allows the PouchDB database directory and the actual vault directory to be different locations.
|
- Allows the PouchDB database directory and the actual vault directory to be different locations.
|
||||||
- For `mirror` command, the positional `[vault-path]` argument takes precedence over `--vault`.
|
- For `mirror` command, the positional `[vault-path]` argument takes precedence over `--vault`.
|
||||||
- `--write-settings`: Write setting migrations and other lasting changes after the command succeeds.
|
|
||||||
- `init-settings` writes its target file. `setup`, `remote-add`, `remote-rm`, `remote-set`, and `remote-activate` write their settings changes without this option.
|
|
||||||
- All remaining commands leave the settings file unchanged by default.
|
|
||||||
- Temporary values used to suspend synchronisation or select a remote for one command are never written.
|
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
@@ -337,7 +333,6 @@ Options:
|
|||||||
--debug, -d Enable debug logging (includes verbose)
|
--debug, -d Enable debug logging (includes verbose)
|
||||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||||
--vault <path>, -V <path> (daemon/mirror) Path to vault directory, decoupled from database-path
|
--vault <path>, -V <path> (daemon/mirror) Path to vault directory, decoupled from database-path
|
||||||
--write-settings Write setting changes after a successful command
|
|
||||||
--help, -h Show this help message
|
--help, -h Show this help message
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export interface CLIOptions {
|
|||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
writeSettings?: boolean;
|
|
||||||
command: CLICommand;
|
command: CLICommand;
|
||||||
commandArgs: string[];
|
commandArgs: string[];
|
||||||
interval?: number;
|
interval?: number;
|
||||||
|
|||||||
+29
-116
@@ -25,20 +25,10 @@ import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_b
|
|||||||
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
||||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
|
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
|
||||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
|
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
|
||||||
import { createNodeStandardIo, fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
|
import { createNodeStandardIo, fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
|
||||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||||
import { writeStderrLine, writeStdoutLine } from "./cliOutput";
|
import { writeStderrLine, writeStdoutLine } from "./cliOutput";
|
||||||
import { createDefaultCliSettings } from "./cliSettingsDefaults";
|
import { createDefaultCliSettings } from "./cliSettingsDefaults";
|
||||||
import {
|
|
||||||
applyStoredSetting,
|
|
||||||
changedSettingKeys,
|
|
||||||
CLI_RUNTIME_ONLY_SETTING_KEYS,
|
|
||||||
cloneSettings,
|
|
||||||
isSettingsWriteCommand,
|
|
||||||
preserveStoredSetting,
|
|
||||||
reconcileDurableSettings,
|
|
||||||
settingsEqual,
|
|
||||||
} from "./settingsPersistence";
|
|
||||||
|
|
||||||
const SETTINGS_FILE = ".livesync/settings.json";
|
const SETTINGS_FILE = ".livesync/settings.json";
|
||||||
ensureGlobalNodeLocalStorage();
|
ensureGlobalNodeLocalStorage();
|
||||||
@@ -102,7 +92,6 @@ Options:
|
|||||||
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
|
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
|
||||||
(defaults to database-path; allows separate PouchDB and vault dirs)
|
(defaults to database-path; allows separate PouchDB and vault dirs)
|
||||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||||
--write-settings Write setting changes after a successful command
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
livesync-cli ./my-database Run daemon (LiveSync mode)
|
livesync-cli ./my-database Run daemon (LiveSync mode)
|
||||||
@@ -152,7 +141,6 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
|||||||
let verbose = false;
|
let verbose = false;
|
||||||
let debug = false;
|
let debug = false;
|
||||||
let force = false;
|
let force = false;
|
||||||
let writeSettings = false;
|
|
||||||
let interval: number | undefined;
|
let interval: number | undefined;
|
||||||
let command: CLICommand = "daemon";
|
let command: CLICommand = "daemon";
|
||||||
const commandArgs: string[] = [];
|
const commandArgs: string[] = [];
|
||||||
@@ -209,9 +197,6 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
|||||||
case "-f":
|
case "-f":
|
||||||
force = true;
|
force = true;
|
||||||
break;
|
break;
|
||||||
case "--write-settings":
|
|
||||||
writeSettings = true;
|
|
||||||
break;
|
|
||||||
default: {
|
default: {
|
||||||
if (!databasePath) {
|
if (!databasePath) {
|
||||||
if (command === "daemon" && isCLICommand(token)) {
|
if (command === "daemon" && isCLICommand(token)) {
|
||||||
@@ -252,7 +237,6 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
|||||||
verbose,
|
verbose,
|
||||||
debug,
|
debug,
|
||||||
force,
|
force,
|
||||||
writeSettings,
|
|
||||||
command,
|
command,
|
||||||
commandArgs,
|
commandArgs,
|
||||||
interval,
|
interval,
|
||||||
@@ -427,26 +411,16 @@ export async function main(
|
|||||||
|
|
||||||
// Setup settings handlers
|
// Setup settings handlers
|
||||||
const settingService = serviceHubInstance.setting;
|
const settingService = serviceHubInstance.setting;
|
||||||
const originalSettingsText = await fs.readFile(settingsPath, "utf-8").catch(() => undefined);
|
|
||||||
let latestPreparedSettingsText: string | undefined;
|
|
||||||
let preparedSettingsRevision = 0;
|
|
||||||
let commandIsRunning = false;
|
|
||||||
let commandPreparedSettingsTexts: string[] = [];
|
|
||||||
|
|
||||||
(settingService as InjectableSettingService<NodeServiceContext>).saveData.setHandler(
|
(settingService as InjectableSettingService<NodeServiceContext>).saveData.setHandler(
|
||||||
async (data: ObsidianLiveSyncSettings) => {
|
async (data: ObsidianLiveSyncSettings) => {
|
||||||
try {
|
try {
|
||||||
latestPreparedSettingsText = JSON.stringify(data, null, 2);
|
await fs.writeFile(settingsPath, JSON.stringify(data, null, 2), "utf-8");
|
||||||
preparedSettingsRevision++;
|
|
||||||
if (commandIsRunning) {
|
|
||||||
commandPreparedSettingsTexts.push(latestPreparedSettingsText);
|
|
||||||
}
|
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
writeStderrLine(standardIo, `[Settings] Prepared an update for ${settingsPath}`);
|
writeStderrLine(standardIo, `[Settings] Saved to ${settingsPath}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStderrLine(standardIo, `[Settings] Failed to prepare an update:`, error);
|
writeStderrLine(standardIo, `[Settings] Failed to save:`, error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -529,21 +503,24 @@ export async function main(
|
|||||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||||
|
|
||||||
const writeSettingsAtomically = async (content: string | undefined): Promise<void> => {
|
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||||
if (content === undefined || content === originalSettingsText) return;
|
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||||
const temporaryPath = `${settingsPath}.${process.pid}.tmp`;
|
// various code paths persist the clobbered state to disk. We restore on shutdown.
|
||||||
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!);
|
||||||
try {
|
|
||||||
await fs.writeFile(temporaryPath, content, "utf-8");
|
// Restore settings file on any exit to undo lifecycle mutations.
|
||||||
await fs.rename(temporaryPath, settingsPath);
|
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
|
||||||
if (options.verbose) {
|
process.on("exit", () => {
|
||||||
writeStderrLine(standardIo, `[Settings] Saved to ${settingsPath}`);
|
if (settingsBackup) {
|
||||||
|
const tmpPath = settingsPath + ".tmp";
|
||||||
|
try {
|
||||||
|
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||||
|
fsSync.renameSync(tmpPath, settingsPath);
|
||||||
|
} catch (err) {
|
||||||
|
writeStderrLine(standardIo, "[Settings] Failed to restore settings on exit:", err);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
await fs.unlink(temporaryPath).catch(() => {});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Start the core
|
// Start the core
|
||||||
try {
|
try {
|
||||||
@@ -554,15 +531,9 @@ export async function main(
|
|||||||
writeStderrLine(standardIo, `[Error] Failed to initialize LiveSync`);
|
writeStderrLine(standardIo, `[Error] Failed to initialize LiveSync`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const settingsAfterLoadText = latestPreparedSettingsText
|
|
||||||
? preserveStoredSetting(latestPreparedSettingsText, originalSettingsText, "useIndexedDBAdapter")
|
|
||||||
: originalSettingsText;
|
|
||||||
|
|
||||||
// Capture sync settings before suspendAllSync() clobbers them.
|
// Capture sync settings before suspendAllSync() clobbers them.
|
||||||
// Used by daemon mode to restore the correct sync behaviour after the mirror scan.
|
// Used by daemon mode to restore the correct sync behaviour after the mirror scan.
|
||||||
const settingsBeforeSuspend = cloneSettings(core.services.setting.currentSettings());
|
const settingsBeforeSuspend = core.services.setting.currentSettings();
|
||||||
const durableSettingsBeforeSuspend = cloneSettings(settingsBeforeSuspend);
|
|
||||||
applyStoredSetting(durableSettingsBeforeSuspend, settingsAfterLoadText, "useIndexedDBAdapter");
|
|
||||||
const originalSyncSettings = {
|
const originalSyncSettings = {
|
||||||
liveSync: settingsBeforeSuspend.liveSync,
|
liveSync: settingsBeforeSuspend.liveSync,
|
||||||
syncOnStart: settingsBeforeSuspend.syncOnStart,
|
syncOnStart: settingsBeforeSuspend.syncOnStart,
|
||||||
@@ -573,19 +544,7 @@ export async function main(
|
|||||||
syncAfterMerge: settingsBeforeSuspend.syncAfterMerge,
|
syncAfterMerge: settingsBeforeSuspend.syncAfterMerge,
|
||||||
};
|
};
|
||||||
await core.services.setting.suspendAllSync();
|
await core.services.setting.suspendAllSync();
|
||||||
const settingsAfterSuspend = cloneSettings(core.services.setting.currentSettings());
|
|
||||||
await core.services.control.onReady();
|
await core.services.control.onReady();
|
||||||
const settingsBeforeCommand = cloneSettings(core.services.setting.currentSettings());
|
|
||||||
const transientSettingKeys = changedSettingKeys(settingsBeforeSuspend, settingsAfterSuspend);
|
|
||||||
for (const key of CLI_RUNTIME_ONLY_SETTING_KEYS) {
|
|
||||||
transientSettingKeys.add(key);
|
|
||||||
}
|
|
||||||
const durableSettingsBeforeCommand = reconcileDurableSettings({
|
|
||||||
durableBase: durableSettingsBeforeSuspend,
|
|
||||||
runtimeBaseline: settingsAfterSuspend,
|
|
||||||
runtimeCurrent: settingsBeforeCommand,
|
|
||||||
preserveKeys: transientSettingKeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
infoLog(`[Ready] LiveSync is running`);
|
infoLog(`[Ready] LiveSync is running`);
|
||||||
infoLog(`[Ready] Press Ctrl+C to stop`);
|
infoLog(`[Ready] Press Ctrl+C to stop`);
|
||||||
@@ -609,58 +568,14 @@ export async function main(
|
|||||||
infoLog("");
|
infoLog("");
|
||||||
}
|
}
|
||||||
|
|
||||||
commandPreparedSettingsTexts = [];
|
const result = await commandRunner(options, {
|
||||||
let result: boolean;
|
databasePath,
|
||||||
try {
|
vaultPath,
|
||||||
commandIsRunning = true;
|
core,
|
||||||
result = await commandRunner(options, {
|
p2pReplicator,
|
||||||
databasePath,
|
settingsPath,
|
||||||
vaultPath,
|
originalSyncSettings,
|
||||||
core,
|
});
|
||||||
p2pReplicator,
|
|
||||||
settingsPath,
|
|
||||||
originalSyncSettings,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
commandIsRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let settingsTextToCommit: string | undefined;
|
|
||||||
if (result && options.command === "setup") {
|
|
||||||
settingsTextToCommit = commandPreparedSettingsTexts[0];
|
|
||||||
if (settingsTextToCommit === undefined) {
|
|
||||||
throw new Error("The setup command completed without preparing its settings update.");
|
|
||||||
}
|
|
||||||
} else if (result && (isSettingsWriteCommand(options.command) || options.writeSettings)) {
|
|
||||||
const runtimeSettingsAfterCommand = cloneSettings(core.services.setting.currentSettings());
|
|
||||||
const durableSettingsAfterCommand = reconcileDurableSettings({
|
|
||||||
durableBase: durableSettingsBeforeCommand,
|
|
||||||
runtimeBaseline: settingsBeforeCommand,
|
|
||||||
runtimeCurrent: runtimeSettingsAfterCommand,
|
|
||||||
preserveKeys: transientSettingKeys,
|
|
||||||
command: options.command,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
isSettingsWriteCommand(options.command) ||
|
|
||||||
!settingsEqual(durableSettingsAfterCommand, durableSettingsBeforeSuspend)
|
|
||||||
) {
|
|
||||||
const runtimeSettings = cloneSettings(core.services.setting.currentSettings());
|
|
||||||
const revisionBeforeSave = preparedSettingsRevision;
|
|
||||||
try {
|
|
||||||
await core.services.setting.updateSettings(() => cloneSettings(durableSettingsAfterCommand), true);
|
|
||||||
if (preparedSettingsRevision === revisionBeforeSave || latestPreparedSettingsText === undefined) {
|
|
||||||
throw new Error("The setting service did not prepare the requested settings update.");
|
|
||||||
}
|
|
||||||
settingsTextToCommit = latestPreparedSettingsText;
|
|
||||||
} finally {
|
|
||||||
await core.services.setting.updateSettings(() => runtimeSettings, false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
settingsTextToCommit = settingsAfterLoadText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
writeStderrLine(standardIo, `[Error] Command '${options.command}' failed`);
|
writeStderrLine(standardIo, `[Error] Command '${options.command}' failed`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
@@ -669,12 +584,10 @@ export async function main(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.command === "daemon" && result) {
|
if (options.command === "daemon" && result) {
|
||||||
await writeSettingsAtomically(settingsTextToCommit);
|
|
||||||
// Keep the process running
|
// Keep the process running
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
} else {
|
} else {
|
||||||
await core.services.control.onUnload();
|
await core.services.control.onUnload();
|
||||||
await writeSettingsAtomically(settingsTextToCommit);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStderrLine(standardIo, `[Error] Failed to start:`, error);
|
writeStderrLine(standardIo, `[Error] Failed to start:`, error);
|
||||||
|
|||||||
@@ -206,13 +206,4 @@ describe("CLI parseArgs", () => {
|
|||||||
expect(parsed.command).toBe("daemon");
|
expect(parsed.command).toBe("daemon");
|
||||||
expect(parsed.interval).toBe(30);
|
expect(parsed.interval).toBe(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses --write-settings as a global option", () => {
|
|
||||||
process.argv = ["node", "livesync-cli", "./vault", "--write-settings", "ls"];
|
|
||||||
const parsed = parseArgs();
|
|
||||||
|
|
||||||
expect(parsed.command).toBe("ls");
|
|
||||||
expect(parsed.writeSettings).toBe(true);
|
|
||||||
expect(parsed.commandArgs).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.4-cli",
|
"version": "1.0.2-cli",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"buildRun": "npm run build && npm run cli --",
|
"buildRun": "npm run build && npm run cli --",
|
||||||
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
||||||
"check": "tsc -p tsconfig.json",
|
"check": "tsc -p tsconfig.json",
|
||||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/settingsPersistence.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
||||||
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
||||||
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
||||||
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
||||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
|
||||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
|
||||||
import type { CLICommand } from "./commands/types";
|
|
||||||
|
|
||||||
const SETTINGS_WRITE_COMMANDS = new Set<CLICommand>([
|
|
||||||
"setup",
|
|
||||||
"remote-add",
|
|
||||||
"remote-rm",
|
|
||||||
"remote-set",
|
|
||||||
"remote-activate",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const REMOTE_SETTINGS_WRITE_COMMANDS = new Set<CLICommand>([
|
|
||||||
"remote-add",
|
|
||||||
"remote-rm",
|
|
||||||
"remote-set",
|
|
||||||
"remote-activate",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const CLI_RUNTIME_ONLY_SETTING_KEYS = new Set<keyof ObsidianLiveSyncSettings>([
|
|
||||||
"disableCheckingConfigMismatch",
|
|
||||||
"suspendFileWatching",
|
|
||||||
"suspendParseReplicationResult",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function cloneJsonValue<T>(value: T): T {
|
|
||||||
if (value === undefined) return value;
|
|
||||||
return JSON.parse(JSON.stringify(value)) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloneSettings(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
|
||||||
return cloneJsonValue(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
function settingValuesEqual(left: unknown, right: unknown): boolean {
|
|
||||||
return JSON.stringify(left) === JSON.stringify(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function settingsEqual(left: ObsidianLiveSyncSettings, right: ObsidianLiveSyncSettings): boolean {
|
|
||||||
return settingValuesEqual(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
function settingsKeys(...settings: ObsidianLiveSyncSettings[]): Set<keyof ObsidianLiveSyncSettings> {
|
|
||||||
return new Set(settings.flatMap((value) => Object.keys(value) as Array<keyof ObsidianLiveSyncSettings>));
|
|
||||||
}
|
|
||||||
|
|
||||||
function copySetting(
|
|
||||||
target: ObsidianLiveSyncSettings,
|
|
||||||
source: ObsidianLiveSyncSettings,
|
|
||||||
key: keyof ObsidianLiveSyncSettings
|
|
||||||
): void {
|
|
||||||
const targetRecord = target as unknown as Record<string, unknown>;
|
|
||||||
const sourceRecord = source as unknown as Record<string, unknown>;
|
|
||||||
if (Object.prototype.hasOwnProperty.call(sourceRecord, key)) {
|
|
||||||
targetRecord[key] = cloneJsonValue(sourceRecord[key]);
|
|
||||||
} else {
|
|
||||||
delete targetRecord[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function remoteSettingKeys(...settings: ObsidianLiveSyncSettings[]): Set<keyof ObsidianLiveSyncSettings> {
|
|
||||||
const keys = new Set<keyof ObsidianLiveSyncSettings>([
|
|
||||||
"remoteConfigurations",
|
|
||||||
"activeConfigurationId",
|
|
||||||
"P2P_ActiveRemoteConfigurationId",
|
|
||||||
"remoteType",
|
|
||||||
]);
|
|
||||||
for (const current of settings) {
|
|
||||||
for (const configuration of Object.values(current.remoteConfigurations ?? {})) {
|
|
||||||
try {
|
|
||||||
const parsed = ConnectionStringParser.parse(configuration.uri);
|
|
||||||
for (const key of Object.keys(parsed.settings) as Array<keyof ObsidianLiveSyncSettings>) {
|
|
||||||
keys.add(key);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// The setting service reports invalid remote configurations when loading them.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changedSettingKeys(
|
|
||||||
before: ObsidianLiveSyncSettings,
|
|
||||||
after: ObsidianLiveSyncSettings
|
|
||||||
): Set<keyof ObsidianLiveSyncSettings> {
|
|
||||||
const changed = new Set<keyof ObsidianLiveSyncSettings>();
|
|
||||||
for (const key of settingsKeys(before, after)) {
|
|
||||||
if (!settingValuesEqual(before[key], after[key])) {
|
|
||||||
changed.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSettingsWriteCommand(command: CLICommand): boolean {
|
|
||||||
return SETTINGS_WRITE_COMMANDS.has(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reconcileDurableSettings(options: {
|
|
||||||
durableBase: ObsidianLiveSyncSettings;
|
|
||||||
runtimeBaseline: ObsidianLiveSyncSettings;
|
|
||||||
runtimeCurrent: ObsidianLiveSyncSettings;
|
|
||||||
preserveKeys: ReadonlySet<keyof ObsidianLiveSyncSettings>;
|
|
||||||
command?: CLICommand;
|
|
||||||
}): ObsidianLiveSyncSettings {
|
|
||||||
const durable = cloneSettings(options.durableBase);
|
|
||||||
for (const key of settingsKeys(options.runtimeBaseline, options.runtimeCurrent)) {
|
|
||||||
if (options.preserveKeys.has(key)) continue;
|
|
||||||
if (!settingValuesEqual(options.runtimeBaseline[key], options.runtimeCurrent[key])) {
|
|
||||||
copySetting(durable, options.runtimeCurrent, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.command) {
|
|
||||||
if (REMOTE_SETTINGS_WRITE_COMMANDS.has(options.command)) {
|
|
||||||
copySetting(durable, options.runtimeCurrent, "remoteConfigurations");
|
|
||||||
copySetting(durable, options.runtimeCurrent, "activeConfigurationId");
|
|
||||||
copySetting(durable, options.runtimeCurrent, "P2P_ActiveRemoteConfigurationId");
|
|
||||||
|
|
||||||
if (durable.activeConfigurationId) {
|
|
||||||
activateRemoteConfiguration(durable, durable.activeConfigurationId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Commands such as remote-status may activate a profile temporarily. Only
|
|
||||||
// the dedicated remote settings commands are allowed to retain that switch.
|
|
||||||
for (const key of remoteSettingKeys(options.durableBase, options.runtimeBaseline, options.runtimeCurrent)) {
|
|
||||||
copySetting(durable, options.durableBase, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return durable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preserveStoredSetting(
|
|
||||||
candidateText: string,
|
|
||||||
originalText: string | undefined,
|
|
||||||
key: keyof ObsidianLiveSyncSettings
|
|
||||||
): string {
|
|
||||||
if (originalText === undefined) return candidateText;
|
|
||||||
try {
|
|
||||||
const candidate = JSON.parse(candidateText) as ObsidianLiveSyncSettings;
|
|
||||||
const original = JSON.parse(originalText) as ObsidianLiveSyncSettings;
|
|
||||||
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
|
||||||
copySetting(candidate, original, key);
|
|
||||||
} else {
|
|
||||||
delete (candidate as unknown as Record<string, unknown>)[key];
|
|
||||||
}
|
|
||||||
return JSON.stringify(candidate, null, 2);
|
|
||||||
} catch {
|
|
||||||
return candidateText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyStoredSetting(
|
|
||||||
target: ObsidianLiveSyncSettings,
|
|
||||||
storedText: string | undefined,
|
|
||||||
key: keyof ObsidianLiveSyncSettings
|
|
||||||
): void {
|
|
||||||
if (storedText === undefined) return;
|
|
||||||
try {
|
|
||||||
const stored = JSON.parse(storedText) as ObsidianLiveSyncSettings;
|
|
||||||
if (Object.prototype.hasOwnProperty.call(stored, key)) {
|
|
||||||
copySetting(target, stored, key);
|
|
||||||
} else {
|
|
||||||
delete (target as unknown as Record<string, unknown>)[key];
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// The setting service owns validation and recovery of malformed files.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
||||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
|
||||||
import {
|
|
||||||
applyStoredSetting,
|
|
||||||
changedSettingKeys,
|
|
||||||
cloneSettings,
|
|
||||||
isSettingsWriteCommand,
|
|
||||||
preserveStoredSetting,
|
|
||||||
reconcileDurableSettings,
|
|
||||||
} from "./settingsPersistence";
|
|
||||||
|
|
||||||
function settings(overrides: Partial<ObsidianLiveSyncSettings> = {}): ObsidianLiveSyncSettings {
|
|
||||||
return Object.assign(cloneSettings(DEFAULT_SETTINGS), overrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("CLI settings persistence", () => {
|
|
||||||
it("identifies commands which change the settings file automatically", () => {
|
|
||||||
expect(isSettingsWriteCommand("setup")).toBe(true);
|
|
||||||
expect(isSettingsWriteCommand("remote-add")).toBe(true);
|
|
||||||
expect(isSettingsWriteCommand("remote-rm")).toBe(true);
|
|
||||||
expect(isSettingsWriteCommand("remote-set")).toBe(true);
|
|
||||||
expect(isSettingsWriteCommand("remote-activate")).toBe(true);
|
|
||||||
expect(isSettingsWriteCommand("ls")).toBe(false);
|
|
||||||
expect(isSettingsWriteCommand("remote-status")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retains lasting changes without retaining CLI suspension values", () => {
|
|
||||||
const durableBase = settings({
|
|
||||||
liveSync: true,
|
|
||||||
syncOnStart: true,
|
|
||||||
periodicReplication: true,
|
|
||||||
P2P_AutoStart: true,
|
|
||||||
settingVersion: 9,
|
|
||||||
customChunkSize: 40,
|
|
||||||
});
|
|
||||||
const runtimeBaseline = settings({
|
|
||||||
...durableBase,
|
|
||||||
liveSync: false,
|
|
||||||
syncOnStart: false,
|
|
||||||
periodicReplication: false,
|
|
||||||
P2P_AutoStart: false,
|
|
||||||
});
|
|
||||||
const runtimeCurrent = settings({
|
|
||||||
...runtimeBaseline,
|
|
||||||
settingVersion: 10,
|
|
||||||
customChunkSize: 60,
|
|
||||||
});
|
|
||||||
|
|
||||||
const reconciled = reconcileDurableSettings({
|
|
||||||
durableBase,
|
|
||||||
runtimeBaseline,
|
|
||||||
runtimeCurrent,
|
|
||||||
preserveKeys: changedSettingKeys(durableBase, runtimeBaseline),
|
|
||||||
command: "ls",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(reconciled.liveSync).toBe(true);
|
|
||||||
expect(reconciled.syncOnStart).toBe(true);
|
|
||||||
expect(reconciled.periodicReplication).toBe(true);
|
|
||||||
expect(reconciled.P2P_AutoStart).toBe(true);
|
|
||||||
expect(reconciled.settingVersion).toBe(10);
|
|
||||||
expect(reconciled.customChunkSize).toBe(60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retains a remote profile change and restores the durable sync values", () => {
|
|
||||||
const durableBase = settings({
|
|
||||||
liveSync: true,
|
|
||||||
remoteConfigurations: {},
|
|
||||||
activeConfigurationId: "",
|
|
||||||
});
|
|
||||||
const runtimeBaseline = settings({ ...durableBase, liveSync: false });
|
|
||||||
const runtimeCurrent = settings({
|
|
||||||
...runtimeBaseline,
|
|
||||||
remoteConfigurations: {
|
|
||||||
main: {
|
|
||||||
id: "main",
|
|
||||||
name: "Main",
|
|
||||||
uri: "sls+https://user:pass@example.com/?db=notes",
|
|
||||||
isEncrypted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activeConfigurationId: "main",
|
|
||||||
});
|
|
||||||
activateRemoteConfiguration(runtimeCurrent, "main");
|
|
||||||
|
|
||||||
const reconciled = reconcileDurableSettings({
|
|
||||||
durableBase,
|
|
||||||
runtimeBaseline,
|
|
||||||
runtimeCurrent,
|
|
||||||
preserveKeys: changedSettingKeys(durableBase, runtimeBaseline),
|
|
||||||
command: "remote-add",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(reconciled.liveSync).toBe(true);
|
|
||||||
expect(reconciled.activeConfigurationId).toBe("main");
|
|
||||||
expect(reconciled.remoteConfigurations.main?.name).toBe("Main");
|
|
||||||
expect(reconciled.couchDB_URI).toBe("https://example.com");
|
|
||||||
expect(reconciled.couchDB_DBNAME).toBe("notes");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not retain a remote profile selected temporarily by an operational command", () => {
|
|
||||||
const durableBase = settings({
|
|
||||||
remoteConfigurations: {
|
|
||||||
first: {
|
|
||||||
id: "first",
|
|
||||||
name: "First",
|
|
||||||
uri: "sls+https://first:pass@example.com/?db=first",
|
|
||||||
isEncrypted: false,
|
|
||||||
},
|
|
||||||
second: {
|
|
||||||
id: "second",
|
|
||||||
name: "Second",
|
|
||||||
uri: "sls+https://second:pass@example.net/?db=second",
|
|
||||||
isEncrypted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activeConfigurationId: "first",
|
|
||||||
});
|
|
||||||
activateRemoteConfiguration(durableBase, "first");
|
|
||||||
const runtimeBaseline = cloneSettings(durableBase);
|
|
||||||
const runtimeCurrent = cloneSettings(durableBase);
|
|
||||||
activateRemoteConfiguration(runtimeCurrent, "second");
|
|
||||||
|
|
||||||
const reconciled = reconcileDurableSettings({
|
|
||||||
durableBase,
|
|
||||||
runtimeBaseline,
|
|
||||||
runtimeCurrent,
|
|
||||||
preserveKeys: new Set(),
|
|
||||||
command: "remote-status",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(reconciled.activeConfigurationId).toBe("first");
|
|
||||||
expect(reconciled.couchDB_URI).toBe("https://example.com");
|
|
||||||
expect(reconciled.couchDB_USER).toBe("first");
|
|
||||||
expect(reconciled.couchDB_DBNAME).toBe("first");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves the stored adapter choice while the CLI uses its Node.js adapter", () => {
|
|
||||||
const original = JSON.stringify({ useIndexedDBAdapter: true, settingVersion: 9 });
|
|
||||||
const prepared = JSON.stringify({ useIndexedDBAdapter: false, settingVersion: 10 });
|
|
||||||
const preserved = preserveStoredSetting(prepared, original, "useIndexedDBAdapter");
|
|
||||||
const target = settings({ useIndexedDBAdapter: false });
|
|
||||||
|
|
||||||
applyStoredSetting(target, preserved, "useIndexedDBAdapter");
|
|
||||||
|
|
||||||
expect(JSON.parse(preserved).useIndexedDBAdapter).toBe(true);
|
|
||||||
expect(target.useIndexedDBAdapter).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not add the CLI adapter override to an older settings file", () => {
|
|
||||||
const original = JSON.stringify({ settingVersion: 9 });
|
|
||||||
const prepared = JSON.stringify({ useIndexedDBAdapter: false, settingVersion: 10 });
|
|
||||||
const preserved = preserveStoredSetting(prepared, original, "useIndexedDBAdapter");
|
|
||||||
const target = settings({ useIndexedDBAdapter: false });
|
|
||||||
|
|
||||||
applyStoredSetting(target, preserved, "useIndexedDBAdapter");
|
|
||||||
|
|
||||||
expect(JSON.parse(preserved)).not.toHaveProperty("useIndexedDBAdapter");
|
|
||||||
expect(target).not.toHaveProperty("useIndexedDBAdapter");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
"test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts",
|
"test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts",
|
||||||
"test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts",
|
"test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts",
|
||||||
"test:remote-commands": "deno test --env-file=.test.env -A --no-check test-remote-commands.ts",
|
"test:remote-commands": "deno test --env-file=.test.env -A --no-check test-remote-commands.ts",
|
||||||
"test:settings-writeback": "deno test -A --no-check test-settings-writeback.ts",
|
|
||||||
"test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
|
"test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
|
||||||
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
||||||
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
|
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const TASKS = [
|
const TASKS = [
|
||||||
"test:settings-writeback",
|
|
||||||
"test:setup-put-cat",
|
"test:setup-put-cat",
|
||||||
"test:mirror",
|
"test:mirror",
|
||||||
"test:daemon",
|
"test:daemon",
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { assert, assertEquals } from "@std/assert";
|
|
||||||
import { TempDir } from "./helpers/temp.ts";
|
|
||||||
import { runCli } from "./helpers/cli.ts";
|
|
||||||
import { initSettingsFile } from "./helpers/settings.ts";
|
|
||||||
|
|
||||||
async function prepareSettingsFixture(prefix: string) {
|
|
||||||
const workDir = await TempDir.create(prefix);
|
|
||||||
const settingsFile = workDir.join("settings.json");
|
|
||||||
const databaseDir = workDir.join("database");
|
|
||||||
await Deno.mkdir(databaseDir, { recursive: true });
|
|
||||||
await initSettingsFile(settingsFile);
|
|
||||||
return { workDir, settingsFile, databaseDir };
|
|
||||||
}
|
|
||||||
|
|
||||||
Deno.test("settings-changing commands persist durable settings without CLI runtime suspension", async () => {
|
|
||||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-command");
|
|
||||||
await using workDir = fixture.workDir;
|
|
||||||
const { settingsFile, databaseDir } = fixture;
|
|
||||||
|
|
||||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
settings.liveSync = true;
|
|
||||||
settings.syncOnStart = true;
|
|
||||||
settings.periodicReplication = true;
|
|
||||||
settings.P2P_Enabled = true;
|
|
||||||
settings.P2P_AutoStart = true;
|
|
||||||
settings.P2P_AutoBroadcast = true;
|
|
||||||
await Deno.writeTextFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
||||||
|
|
||||||
const result = await runCli(
|
|
||||||
databaseDir,
|
|
||||||
"--settings",
|
|
||||||
settingsFile,
|
|
||||||
"remote-add",
|
|
||||||
"test-remote",
|
|
||||||
"sls+https://user:pass@example.com/database"
|
|
||||||
);
|
|
||||||
assertEquals(result.code, 0, result.combined);
|
|
||||||
const firstRemoteId = result.stdout.trim().split("\t")[0];
|
|
||||||
assert(firstRemoteId, `remote-add did not return an ID: ${result.combined}`);
|
|
||||||
|
|
||||||
let persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
let remotes = Object.values(persisted.remoteConfigurations ?? {}) as Array<{ name?: string }>;
|
|
||||||
assert(
|
|
||||||
remotes.some((remote) => remote.name === "test-remote"),
|
|
||||||
"remote-add did not persist the new profile"
|
|
||||||
);
|
|
||||||
assertEquals(persisted.liveSync, true);
|
|
||||||
assertEquals(persisted.syncOnStart, true);
|
|
||||||
assertEquals(persisted.periodicReplication, true);
|
|
||||||
assertEquals(persisted.P2P_Enabled, true);
|
|
||||||
assertEquals(persisted.P2P_AutoStart, true);
|
|
||||||
assertEquals(persisted.P2P_AutoBroadcast, true);
|
|
||||||
|
|
||||||
const secondAdd = await runCli(
|
|
||||||
databaseDir,
|
|
||||||
"--settings",
|
|
||||||
settingsFile,
|
|
||||||
"remote-add",
|
|
||||||
"second-remote",
|
|
||||||
"sls+https://other:secret@example.net/second"
|
|
||||||
);
|
|
||||||
assertEquals(secondAdd.code, 0, secondAdd.combined);
|
|
||||||
const secondRemoteId = secondAdd.stdout.trim().split("\t")[0];
|
|
||||||
assert(secondRemoteId, `second remote-add did not return an ID: ${secondAdd.combined}`);
|
|
||||||
|
|
||||||
const activate = await runCli(databaseDir, "--settings", settingsFile, "remote-activate", secondRemoteId);
|
|
||||||
assertEquals(activate.code, 0, activate.combined);
|
|
||||||
persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
assertEquals(persisted.activeConfigurationId, secondRemoteId);
|
|
||||||
|
|
||||||
const set = await runCli(
|
|
||||||
databaseDir,
|
|
||||||
"--settings",
|
|
||||||
settingsFile,
|
|
||||||
"remote-set",
|
|
||||||
secondRemoteId,
|
|
||||||
"sls+https://replacement:secret@example.org/replaced"
|
|
||||||
);
|
|
||||||
assertEquals(set.code, 0, set.combined);
|
|
||||||
const exported = await runCli(databaseDir, "--settings", settingsFile, "remote-export", secondRemoteId);
|
|
||||||
assertEquals(exported.code, 0, exported.combined);
|
|
||||||
assert(exported.stdout.includes("replacement"), "remote-set did not persist the replacement URI");
|
|
||||||
|
|
||||||
const remove = await runCli(databaseDir, "--settings", settingsFile, "remote-rm", secondRemoteId);
|
|
||||||
assertEquals(remove.code, 0, remove.combined);
|
|
||||||
persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
remotes = Object.values(persisted.remoteConfigurations ?? {}) as Array<{ id?: string }>;
|
|
||||||
assert(!remotes.some((remote) => remote.id === secondRemoteId), "remote-rm did not persist the removal");
|
|
||||||
assertEquals(persisted.activeConfigurationId, firstRemoteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("ordinary commands keep the settings file unchanged by default", async () => {
|
|
||||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-readonly");
|
|
||||||
await using workDir = fixture.workDir;
|
|
||||||
const { settingsFile, databaseDir } = fixture;
|
|
||||||
|
|
||||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
settings.settingVersion = 9;
|
|
||||||
const original = JSON.stringify(settings, null, 2);
|
|
||||||
await Deno.writeTextFile(settingsFile, original);
|
|
||||||
|
|
||||||
const result = await runCli(databaseDir, "--settings", settingsFile, "ls");
|
|
||||||
assertEquals(result.code, 0, result.combined);
|
|
||||||
assertEquals(await Deno.readTextFile(settingsFile), original);
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("--write-settings persists durable start-up setting changes", async () => {
|
|
||||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-explicit");
|
|
||||||
await using workDir = fixture.workDir;
|
|
||||||
const { settingsFile, databaseDir } = fixture;
|
|
||||||
|
|
||||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
settings.settingVersion = 9;
|
|
||||||
delete settings.useIndexedDBAdapter;
|
|
||||||
await Deno.writeTextFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
||||||
|
|
||||||
const result = await runCli(databaseDir, "--settings", settingsFile, "--write-settings", "ls");
|
|
||||||
assertEquals(result.code, 0, result.combined);
|
|
||||||
|
|
||||||
const persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
assertEquals(persisted.settingVersion, 10);
|
|
||||||
assert(!("useIndexedDBAdapter" in persisted), "the CLI-only adapter override was written to the settings file");
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("failed settings-changing commands leave the settings file unchanged", async () => {
|
|
||||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-failure");
|
|
||||||
await using workDir = fixture.workDir;
|
|
||||||
const { settingsFile, databaseDir } = fixture;
|
|
||||||
|
|
||||||
const original = await Deno.readTextFile(settingsFile);
|
|
||||||
const result = await runCli(databaseDir, "--settings", settingsFile, "remote-rm", "missing-remote");
|
|
||||||
assert(result.code !== 0, "remote-rm unexpectedly succeeded");
|
|
||||||
assertEquals(await Deno.readTextFile(settingsFile), original);
|
|
||||||
});
|
|
||||||
@@ -41,13 +41,6 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
|||||||
setupResult.combined.includes("[Command] setup ->"),
|
setupResult.combined.includes("[Command] setup ->"),
|
||||||
`setup command did not execute expected code path\n${setupResult.combined}`
|
`setup command did not execute expected code path\n${setupResult.combined}`
|
||||||
);
|
);
|
||||||
const persistedSetup = JSON.parse(await Deno.readTextFile(settingsFile));
|
|
||||||
assertEquals(persistedSetup.isConfigured, true, "setup did not persist the configured state");
|
|
||||||
assert(
|
|
||||||
typeof persistedSetup.encryptedCouchDBConnection === "string" &&
|
|
||||||
persistedSetup.encryptedCouchDBConnection.length > 0,
|
|
||||||
"setup did not persist the encrypted connection settings"
|
|
||||||
);
|
|
||||||
|
|
||||||
const run = (...args: string[]) => runCliOrFail(vaultDir, "--settings", settingsFile, ...args);
|
const run = (...args: string[]) => runCliOrFail(vaultDir, "--settings", settingsFile, ...args);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.4-webapp",
|
"version": "1.0.2-webapp",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webpeer",
|
"name": "webpeer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.4-webpeer",
|
"version": "1.0.2-webpeer",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -147,14 +147,9 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
|||||||
: req instanceof Request && typeof req.method === "string"
|
: req instanceof Request && typeof req.method === "string"
|
||||||
? req.method
|
? req.method
|
||||||
: "GET";
|
: "GET";
|
||||||
if (typeof req !== "string") {
|
const suppliedBody = opts?.body ?? (req instanceof Request ? req.body : undefined);
|
||||||
if (opts?.body) {
|
if (suppliedBody !== undefined && suppliedBody !== null) {
|
||||||
body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).arrayBuffer();
|
body = typeof suppliedBody === "string" ? suppliedBody : await new Response(suppliedBody).arrayBuffer();
|
||||||
} else if (req.body) {
|
|
||||||
body = await new Response(req.body).arrayBuffer();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body = opts?.body as string;
|
|
||||||
}
|
}
|
||||||
const reqHeaders = new Headers(req instanceof Request ? req.headers : {});
|
const reqHeaders = new Headers(req instanceof Request ? req.headers : {});
|
||||||
|
|
||||||
@@ -192,7 +187,9 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
|||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
};
|
};
|
||||||
const r = await requestUrl({ ...requestParam, throw: false });
|
const r = await requestUrl({ ...requestParam, throw: false });
|
||||||
return new Response(r.arrayBuffer, {
|
const responseHasNoBody =
|
||||||
|
method.toUpperCase() === "HEAD" || r.status === 204 || r.status === 205 || r.status === 304;
|
||||||
|
return new Response(responseHasNoBody ? null : r.arrayBuffer, {
|
||||||
headers: r.headers,
|
headers: r.headers,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
statusText: `${r.status}`,
|
statusText: `${r.status}`,
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ const mocks = vi.hoisted(() => ({
|
|||||||
platform: {
|
platform: {
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
},
|
},
|
||||||
|
requestUrl: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/deps.ts", () => ({
|
vi.mock("@/deps.ts", () => ({
|
||||||
Platform: mocks.platform,
|
Platform: mocks.platform,
|
||||||
requestUrl: vi.fn(),
|
requestUrl: mocks.requestUrl,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/deps", () => ({
|
vi.mock("@/deps", () => ({
|
||||||
Platform: mocks.platform,
|
Platform: mocks.platform,
|
||||||
requestUrl: vi.fn(),
|
requestUrl: mocks.requestUrl,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/modules/essentialObsidian/APILib/ObsHttpHandler", () => ({
|
vi.mock("@/modules/essentialObsidian/APILib/ObsHttpHandler", () => ({
|
||||||
@@ -65,3 +66,41 @@ describe("ObsidianAPIService.showWindowOnRight", () => {
|
|||||||
expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf);
|
expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ObsidianAPIService.nativeFetch", () => {
|
||||||
|
it("normalises a typed-array body when the request URL is a string", async () => {
|
||||||
|
mocks.requestUrl.mockResolvedValue({
|
||||||
|
arrayBuffer: new Uint8Array([9, 8, 7]).buffer,
|
||||||
|
headers: { etag: '"created"' },
|
||||||
|
status: 201,
|
||||||
|
});
|
||||||
|
const source = new Uint8Array([0, 1, 2, 3, 4]);
|
||||||
|
const body = source.subarray(1, 4);
|
||||||
|
|
||||||
|
const response = await createService({}).nativeFetch("http://127.0.0.1:8088/dav/probe.bin", {
|
||||||
|
body: body as unknown as BodyInit,
|
||||||
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
const request = mocks.requestUrl.mock.calls[0][0] as { body?: unknown };
|
||||||
|
expect(request.body).toBeInstanceOf(ArrayBuffer);
|
||||||
|
expect([...new Uint8Array(request.body as ArrayBuffer)]).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("constructs a bodyless response for a successful DELETE", async () => {
|
||||||
|
mocks.requestUrl.mockResolvedValue({
|
||||||
|
arrayBuffer: new ArrayBuffer(0),
|
||||||
|
headers: {},
|
||||||
|
status: 204,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await createService({}).nativeFetch("http://127.0.0.1:8088/dav/probe.bin", {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
expect(await response.text()).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+3
-24
@@ -12,32 +12,11 @@ Earlier releases remain available in the 0.25 release history and the legacy rel
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## 1.0.4
|
### P2P and experimental browser applications
|
||||||
|
|
||||||
5th August, 2026
|
#### Improved
|
||||||
|
|
||||||
### Synchronisation and storage
|
- WebPeer now includes a separate P2P connection check which prepares a disposable encrypted Setup URI and QR code locally, tests desktop and mobile devices in separate random rooms, can reuse the unchanged QR and a recorded baseline for an optional second device in the same room, and displays browser-side WebRTC diagnostic totals without presenting them as note-synchronisation results.
|
||||||
|
|
||||||
#### Fixed
|
|
||||||
|
|
||||||
- Testing or saving a fresh remote configuration no longer tries to access the local database while constructing a replicator, avoiding 'Local database is not ready yet' failures before local database initialisation (#1064).
|
|
||||||
|
|
||||||
### Command-line tool
|
|
||||||
|
|
||||||
#### Fixed
|
|
||||||
|
|
||||||
- Successful setup and remote-configuration commands now retain their settings changes. Other commands leave the settings file unchanged unless `--write-settings` is supplied, and temporary CLI suspension values are never written (#1070).
|
|
||||||
|
|
||||||
## 1.0.3
|
|
||||||
|
|
||||||
3rd August, 2026
|
|
||||||
|
|
||||||
### Synchronisation and storage
|
|
||||||
|
|
||||||
#### Fixed
|
|
||||||
|
|
||||||
- File consistency checks no longer read older revisions after the current Vault content matches known synchronised history, avoiding unnecessary 'Missing document content' warnings from obsolete unreadable revisions.
|
|
||||||
- Remote chunk fetching now retains chunks which were returned successfully when another chunk in the same request is unavailable, so the available content can still be processed (#771).
|
|
||||||
|
|
||||||
### Interface
|
### Interface
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -14,7 +14,5 @@
|
|||||||
"1.0.0-rc.1": "1.7.2",
|
"1.0.0-rc.1": "1.7.2",
|
||||||
"1.0.0": "1.7.2",
|
"1.0.0": "1.7.2",
|
||||||
"1.0.1": "1.7.2",
|
"1.0.1": "1.7.2",
|
||||||
"1.0.2": "1.7.2",
|
"1.0.2": "1.7.2"
|
||||||
"1.0.3": "1.7.2",
|
|
||||||
"1.0.4": "1.7.2"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user