mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-16 06:12:30 +00:00
refactor (okay, i know that this cannot be merged ; too large diff. I will pick some cheery).
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
// Copy package.json dependencies and devDependencies from the repo root to the target sub-apps package.json, and set their versions to match the repo root version with a suffix.
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const repoRoot = __dirname;
|
||||||
|
const repoPackageJsonPath = path.join(repoRoot, "package.json");
|
||||||
|
const repoPackageJson = JSON.parse(fs.readFileSync(repoPackageJsonPath, "utf-8"));
|
||||||
|
const devDependenciesToCopy = repoPackageJson.devDependencies || {};
|
||||||
|
const dependenciesToCopy = repoPackageJson.dependencies || {};
|
||||||
|
|
||||||
|
const TARGET_APPS = ["cli", "webapp", "webpeer"];
|
||||||
|
|
||||||
|
for (const app of TARGET_APPS) {
|
||||||
|
const appDir = path.join(repoRoot, "src", "apps", app);
|
||||||
|
const packageJsonPath = path.join(appDir, "package.json");
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||||
|
|
||||||
|
packageJson.dependencies = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...dependenciesToCopy,
|
||||||
|
};
|
||||||
|
packageJson.devDependencies = {
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
...devDependenciesToCopy,
|
||||||
|
};
|
||||||
|
packageJson.version = `${repoPackageJson.version}-${app}`;
|
||||||
|
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4), "utf-8");
|
||||||
|
console.log(`Applied package.json dependencies and version from repo root to ${app} package.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nApplied dependencies and version to all target applications.");
|
||||||
|
console.log(
|
||||||
|
"Please do not forget to pick dependencies that are actually needed in the target package.json, and remove the ones that are not needed."
|
||||||
|
);
|
||||||
+110
-34
@@ -1,23 +1,74 @@
|
|||||||
import tsParser from "@typescript-eslint/parser";
|
const restrictedGlobalsOptions = [
|
||||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
{
|
||||||
import svelteParser from "svelte-eslint-parser";
|
name: "app",
|
||||||
|
message: "Avoid using the global app object. Instead use the reference provided by your plugin instance.",
|
||||||
|
},
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
name: "fetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `fetch` for network requests in Obsidian.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "localStorage",
|
||||||
|
message:
|
||||||
|
"Prefer `App#saveLocalStorage` / `App#loadLocalStorage` functions to write / read localStorage data that's unique to a vault.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const restrictedImportsOptions = [
|
||||||
|
{
|
||||||
|
name: "axios",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `axios`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "superagent",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `superagent`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "got",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `got`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ofetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `ofetch`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ky",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `ky`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-fetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `node-fetch`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "moment",
|
||||||
|
message: "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const warnWhileDev = "off";
|
const warnWhileDev = "off";
|
||||||
|
|
||||||
export const tsBaseRules = {
|
/**
|
||||||
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const baseRules = {
|
||||||
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-unused-labels": "off",
|
"no-unused-labels": "off",
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"require-await": "off",
|
"require-await": "off",
|
||||||
// -- TypeScript specific rules
|
// -- TypeScript specific rules (Gradual adoption of stricter rules, currently set to 'warn' for a while).
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||||
|
// -- TypeScript specific rules
|
||||||
|
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
||||||
|
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
||||||
|
// Rules is now set to 'off' for a while.
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
// -- Reasonable rules.
|
||||||
"@typescript-eslint/no-deprecated": warnWhileDev,
|
"@typescript-eslint/no-deprecated": warnWhileDev,
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
@@ -30,38 +81,63 @@ export const tsBaseRules = {
|
|||||||
// -- General rules
|
// -- General rules
|
||||||
"no-async-promise-executor": warnWhileDev,
|
"no-async-promise-executor": warnWhileDev,
|
||||||
"no-constant-condition": ["error", { checkLoops: false }],
|
"no-constant-condition": ["error", { checkLoops: false }],
|
||||||
|
// -- Disabled rules
|
||||||
|
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
||||||
"no-undef": "off",
|
"no-undef": "off",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tsBaseConfig = {
|
/**
|
||||||
files: ["**/*.ts"],
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
plugins: {
|
*/
|
||||||
"@typescript-eslint": tsPlugin,
|
export const obsidianRules = {
|
||||||
},
|
// -- Obsidian rules
|
||||||
languageOptions: {
|
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
|
||||||
parser: tsParser,
|
"obsidianmd/no-unsupported-api": warnWhileDev,
|
||||||
parserOptions: {
|
|
||||||
project: "./tsconfig.json",
|
|
||||||
rootDir: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: tsBaseRules,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const svelteBaseConfig = {
|
// -- Plugin specific overrides
|
||||||
files: ["**/*.svelte"],
|
"obsidianmd/rule-custom-message": "off",
|
||||||
plugins: {
|
"obsidianmd/ui/sentence-case": "off",
|
||||||
"@typescript-eslint": tsPlugin,
|
"obsidianmd/no-plugin-as-component": "off",
|
||||||
},
|
|
||||||
languageOptions: {
|
// -- Temporary overrides for migration
|
||||||
parser: svelteParser,
|
"obsidianmd/no-static-styles-assignment": "off",
|
||||||
parserOptions: {
|
};
|
||||||
parser: tsParser,
|
/**
|
||||||
extraFileExtensions: [".svelte"],
|
* @type {(base:string) => import("eslint").Linter.RulesRecord}
|
||||||
rootDir: "./",
|
*/
|
||||||
|
export const ImportAliasRules = (base) => ({
|
||||||
|
"@dword-design/import-alias/prefer-alias": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
aliasForSubpaths: true,
|
||||||
|
alias: {
|
||||||
|
"@": `${base}/src`,
|
||||||
|
"@lib": `${base}/src/lib/src`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
rules: {
|
});
|
||||||
"no-unused-vars": "off",
|
/**
|
||||||
},
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const CommunityReviewRecommendedRules = {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-prototype-bultins": "off",
|
||||||
|
"no-self-compare": "warn",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"no-implicit-globals": "error",
|
||||||
|
"no-console": "off", // overridden by obsidianmd/rule-custom-message
|
||||||
|
"no-restricted-globals": ["error", ...restrictedGlobalsOptions],
|
||||||
|
"no-restricted-imports": ["error", ...restrictedImportsOptions],
|
||||||
|
"no-alert": "error",
|
||||||
|
"no-undef": "error",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-deprecated": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
|
||||||
|
// "import/no-nodejs-modules": "off",
|
||||||
|
// "import/no-extraneous-dependencies": "error",
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-41
@@ -5,7 +5,7 @@ import { defineConfig, globalIgnores } from "eslint/config";
|
|||||||
import * as sveltePlugin from "eslint-plugin-svelte";
|
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||||
import svelteParser from "svelte-eslint-parser";
|
import svelteParser from "svelte-eslint-parser";
|
||||||
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||||
import { tsBaseConfig, svelteBaseConfig } from "./eslint.config.common.mjs";
|
import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs";
|
||||||
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
@@ -58,59 +58,45 @@ export default defineConfig([
|
|||||||
...obsidianmd.configs.recommended,
|
...obsidianmd.configs.recommended,
|
||||||
importAlias.configs.recommended,
|
importAlias.configs.recommended,
|
||||||
{
|
{
|
||||||
...tsBaseConfig,
|
files: ["**/*.ts"],
|
||||||
|
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...tsBaseConfig.languageOptions,
|
|
||||||
globals: { ...globals.browser, PouchDB: "readonly" },
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "./",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
linterOptions: {
|
||||||
...tsBaseConfig.plugins,
|
reportUnusedDisableDirectives: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...tsBaseConfig.rules,
|
...baseRules,
|
||||||
// -- Obsidian rules
|
...obsidianRules,
|
||||||
"obsidianmd/no-unsupported-api": warnWhileDev,
|
|
||||||
"obsidianmd/rule-custom-message": "off",
|
|
||||||
"obsidianmd/ui/sentence-case": "off",
|
|
||||||
"obsidianmd/no-plugin-as-component": "off",
|
|
||||||
"obsidianmd/no-static-styles-assignment": "off",
|
|
||||||
|
|
||||||
// -- Project specific rules
|
// -- Project specific rules
|
||||||
"@dword-design/import-alias/prefer-alias": [
|
...ImportAliasRules("."),
|
||||||
"error",
|
|
||||||
{
|
|
||||||
aliasForSubpaths: true,
|
|
||||||
alias: {
|
|
||||||
"@": "./src",
|
|
||||||
"@lib": "./src/lib/src",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...svelteBaseConfig,
|
files: ["**/*.svelte"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...svelteBaseConfig.languageOptions,
|
|
||||||
globals: { ...globals.browser, PouchDB: "readonly" },
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
},
|
parser: svelteParser,
|
||||||
plugins: {
|
parserOptions: {
|
||||||
...svelteBaseConfig.plugins,
|
parser: tsParser,
|
||||||
|
extraFileExtensions: [".svelte"],
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "./",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...svelteBaseConfig.rules,
|
// no-unused-vars:
|
||||||
"obsidianmd/no-plugin-as-component": "off",
|
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
|
||||||
"obsidianmd/ui/sentence-case": "off",
|
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
||||||
"@dword-design/import-alias/prefer-alias": [
|
"no-unused-vars": "off",
|
||||||
"error",
|
...obsidianRules,
|
||||||
{
|
...ImportAliasRules("."),
|
||||||
aliasForSubpaths: true,
|
|
||||||
alias: {
|
|
||||||
"@": "./src",
|
|
||||||
"@lib": "./src/lib/src",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Generated
+163
-4
@@ -16624,22 +16624,181 @@
|
|||||||
},
|
},
|
||||||
"src/apps/webapp": {
|
"src/apps/webapp": {
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"version": "0.0.1",
|
"version": "0.25.74-webapp",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.3.10",
|
||||||
|
"@smithy/md5-js": "^4.2.9",
|
||||||
|
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||||
|
"@smithy/protocol-http": "^5.3.9",
|
||||||
|
"@smithy/querystring-builder": "^4.2.9",
|
||||||
|
"@smithy/types": "^4.14.3",
|
||||||
|
"@smithy/util-retry": "^4.4.5",
|
||||||
|
"@trystero-p2p/nostr": "^0.24.0",
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
|
"octagonal-wheels": "^0.1.46",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"qrcode-generator": "^1.4.4",
|
||||||
|
"werift": "^0.23.0",
|
||||||
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||||
|
"@eslint/js": "^9.39.3",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
|
"@types/deno": "^2.5.0",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/micromatch": "^4.0.10",
|
||||||
|
"@types/node": "^24.10.13",
|
||||||
|
"@types/pouchdb": "^6.4.2",
|
||||||
|
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||||
|
"@types/pouchdb-adapter-idb": "^6.1.7",
|
||||||
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
|
"@types/pouchdb-core": "^7.0.15",
|
||||||
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
|
"@types/transform-pouch": "^1.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||||
|
"@typescript-eslint/parser": "8.61.0",
|
||||||
|
"@vitest/browser": "^4.1.8",
|
||||||
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"esbuild": "0.25.0",
|
||||||
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
|
"esbuild-svelte": "^0.9.4",
|
||||||
|
"eslint": "^9.39.3",
|
||||||
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
|
"pouchdb-adapter-idb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-errors": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
|
"pouchdb-merge": "^9.0.0",
|
||||||
|
"pouchdb-replication": "^9.0.0",
|
||||||
|
"pouchdb-utils": "^9.0.0",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
|
"svelte": "5.41.1",
|
||||||
|
"svelte-check": "^4.4.3",
|
||||||
|
"svelte-eslint-parser": "^1.8.0",
|
||||||
|
"svelte-preprocess": "^6.0.3",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"transform-pouch": "^2.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"typescript-eslint": "^8.61.0",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-istanbul": "^8.0.0",
|
||||||
|
"vitest": "^4.1.8",
|
||||||
|
"webdriverio": "^9.27.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/apps/webpeer": {
|
"src/apps/webpeer": {
|
||||||
"version": "0.0.0",
|
"version": "0.25.74-webpeer",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.3.10",
|
||||||
|
"@smithy/md5-js": "^4.2.9",
|
||||||
|
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||||
|
"@smithy/protocol-http": "^5.3.9",
|
||||||
|
"@smithy/querystring-builder": "^4.2.9",
|
||||||
|
"@smithy/types": "^4.14.3",
|
||||||
|
"@smithy/util-retry": "^4.4.5",
|
||||||
|
"@trystero-p2p/nostr": "^0.24.0",
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
|
"octagonal-wheels": "^0.1.46",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"qrcode-generator": "^1.4.4",
|
||||||
|
"werift": "^0.23.0",
|
||||||
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||||
|
"@eslint/js": "^9.39.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tsconfig/svelte": "^5.0.8",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
|
"@types/deno": "^2.5.0",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/micromatch": "^4.0.10",
|
||||||
|
"@types/node": "^24.10.13",
|
||||||
|
"@types/pouchdb": "^6.4.2",
|
||||||
|
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||||
|
"@types/pouchdb-adapter-idb": "^6.1.7",
|
||||||
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
|
"@types/pouchdb-core": "^7.0.15",
|
||||||
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
|
"@types/transform-pouch": "^1.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||||
|
"@typescript-eslint/parser": "8.61.0",
|
||||||
|
"@vitest/browser": "^4.1.8",
|
||||||
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"esbuild": "0.25.0",
|
||||||
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
|
"esbuild-svelte": "^0.9.4",
|
||||||
|
"eslint": "^9.39.3",
|
||||||
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
|
"pouchdb-adapter-idb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-errors": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
|
"pouchdb-merge": "^9.0.0",
|
||||||
|
"pouchdb-replication": "^9.0.0",
|
||||||
|
"pouchdb-utils": "^9.0.0",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"svelte": "5.41.1",
|
"svelte": "5.41.1",
|
||||||
"svelte-check": "^4.4.3",
|
"svelte-check": "^4.4.3",
|
||||||
|
"svelte-eslint-parser": "^1.8.0",
|
||||||
|
"svelte-preprocess": "^6.0.3",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"transform-pouch": "^2.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"typescript-eslint": "^8.61.0",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-istanbul": "^8.0.0",
|
||||||
|
"vitest": "^4.1.8",
|
||||||
|
"webdriverio": "^9.27.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ cd obsidian-livesync
|
|||||||
# If you already cloned without submodules, run this once instead
|
# If you already cloned without submodules, run this once instead
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
# Install dependencies from the repository root
|
# Install dependencies (now the CLI is a on the same monorepo, shared dependencies will be hoisted to the root node_modules)
|
||||||
|
cd src/apps/cli
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Build the CLI from its package directory
|
# Build the CLI from its package directory
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as path from "path";
|
|||||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/models/fileaccess.type";
|
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/models/fileaccess.type";
|
||||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||||
|
import type { FilePathWithPrefix } from "@lib/common/models/db.type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversion adapter implementation for Node.js
|
* Conversion adapter implementation for Node.js
|
||||||
@@ -22,7 +23,7 @@ export class NodeConversionAdapter implements IConversionAdapter<NodeFile, NodeF
|
|||||||
path: folder.path,
|
path: folder.path,
|
||||||
isFolder: true,
|
isFolder: true,
|
||||||
children: [],
|
children: [],
|
||||||
parent: path.dirname(folder.path) as any,
|
parent: path.dirname(folder.path) as FilePathWithPrefix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
|
|||||||
}
|
}
|
||||||
|
|
||||||
private normalisePath(p: FilePath | string): string {
|
private normalisePath(p: FilePath | string): string {
|
||||||
return this.path.normalisePath(p as string);
|
return this.path.normalisePath(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
|
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
|
||||||
@@ -77,7 +77,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
|
|||||||
}
|
}
|
||||||
|
|
||||||
async statFromNative(file: NodeFile): Promise<UXStat> {
|
async statFromNative(file: NodeFile): Promise<UXStat> {
|
||||||
return file.stat;
|
return await Promise.resolve(file.stat);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reconcileInternalFile(p: string): Promise<void> {
|
async reconcileInternalFile(p: string): Promise<void> {
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import type { NodeFile, NodeFolder } from "./NodeTypes";
|
|||||||
* Type guard adapter implementation for Node.js
|
* Type guard adapter implementation for Node.js
|
||||||
*/
|
*/
|
||||||
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
|
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
|
||||||
isFile(file: any): file is NodeFile {
|
isFile(file: unknown): file is NodeFile {
|
||||||
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
|
return !!(
|
||||||
|
file &&
|
||||||
|
typeof file === "object" &&
|
||||||
|
"path" in file &&
|
||||||
|
"stat" in file &&
|
||||||
|
!("isFolder" in file && file.isFolder === true)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFolder(item: any): item is NodeFolder {
|
isFolder(item: unknown): item is NodeFolder {
|
||||||
return item && typeof item === "object" && "path" in item && item.isFolder === true;
|
return !!(item && typeof item === "object" && "path" in item && "isFolder" in item && item.isFolder === true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import * as fs from "fs/promises";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import type { UXDataWriteOptions } from "@lib/common/models/fileaccess.type";
|
import type { UXDataWriteOptions } from "@lib/common/models/fileaccess.type";
|
||||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||||
|
import type { FilePath } from "@lib/common/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vault adapter implementation for Node.js
|
* Vault adapter implementation for Node.js
|
||||||
@@ -70,7 +71,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
|||||||
|
|
||||||
const stat = await fs.stat(fullPath);
|
const stat = await fs.stat(fullPath);
|
||||||
return {
|
return {
|
||||||
path: p as any,
|
path: p as FilePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
mtime: Math.floor(stat.mtimeMs),
|
mtime: Math.floor(stat.mtimeMs),
|
||||||
@@ -93,7 +94,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
|||||||
|
|
||||||
const stat = await fs.stat(fullPath);
|
const stat = await fs.stat(fullPath);
|
||||||
return {
|
return {
|
||||||
path: p as any,
|
path: p as FilePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
mtime: Math.floor(stat.mtimeMs),
|
mtime: Math.floor(stat.mtimeMs),
|
||||||
@@ -118,7 +119,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
|||||||
await this.delete(file, force);
|
await this.delete(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(name: string, ...data: any[]): any {
|
trigger(name: string, ...data: unknown[]): unknown {
|
||||||
// No-op in CLI version (no event system)
|
// No-op in CLI version (no event system)
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { P2P_DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
|
import { P2P_DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
|
||||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
type CLIP2PPeer = {
|
type CLIP2PPeer = {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -19,7 +19,7 @@ export function parseTimeoutSeconds(value: string, commandName: string): number
|
|||||||
return timeoutSec;
|
return timeoutSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
|
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, never>) {
|
||||||
const settings = core.services.setting.currentSettings();
|
const settings = core.services.setting.currentSettings();
|
||||||
if (!settings.P2P_Enabled) {
|
if (!settings.P2P_Enabled) {
|
||||||
throw new Error("P2P is disabled in settings (P2P_Enabled=false)");
|
throw new Error("P2P is disabled in settings (P2P_Enabled=false)");
|
||||||
@@ -31,7 +31,7 @@ function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
|
|||||||
settings.P2P_IsHeadless = true;
|
settings.P2P_IsHeadless = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
|
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||||
validateP2PSettings(core);
|
validateP2PSettings(core);
|
||||||
const replicator = await core.services.replicator.getNewReplicator();
|
const replicator = await core.services.replicator.getNewReplicator();
|
||||||
if (!replicator) {
|
if (!replicator) {
|
||||||
@@ -50,7 +50,7 @@ function getSortedPeers(replicator: LiveSyncTrysteroReplicator): CLIP2PPeer[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function collectPeers(
|
export async function collectPeers(
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
timeoutSec: number
|
timeoutSec: number
|
||||||
): Promise<CLIP2PPeer[]> {
|
): Promise<CLIP2PPeer[]> {
|
||||||
const replicator = await createReplicator(core);
|
const replicator = await createReplicator(core);
|
||||||
@@ -79,7 +79,7 @@ function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undef
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function syncWithPeer(
|
export async function syncWithPeer(
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
peerToken: string,
|
peerToken: string,
|
||||||
timeoutSec: number
|
timeoutSec: number
|
||||||
): Promise<CLIP2PPeer> {
|
): Promise<CLIP2PPeer> {
|
||||||
@@ -105,11 +105,11 @@ export async function syncWithPeer(
|
|||||||
|
|
||||||
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
||||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||||
throw pullResult.error;
|
throw pullResult.error as Error;
|
||||||
}
|
}
|
||||||
const pushResult = (await replicator.requestSynchroniseToPeer(targetPeer.peerId)) as any;
|
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
||||||
if (!pushResult || pushResult.ok !== true) {
|
if (!pushResult || pushResult.ok !== true) {
|
||||||
throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync");
|
throw (pushResult?.error as Error) ?? new Error("P2P sync failed while requesting remote sync");
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetPeer;
|
return targetPeer;
|
||||||
@@ -118,7 +118,7 @@ export async function syncWithPeer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
|
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||||
const replicator = await createReplicator(core);
|
const replicator = await createReplicator(core);
|
||||||
await replicator.open();
|
await replicator.open();
|
||||||
return replicator;
|
return replicator;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import * as path from "path";
|
|||||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
||||||
import type { CLICommandContext, CLIOptions } from "./types";
|
import type { CLICommandContext, CLIOptions } from "./types";
|
||||||
import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelativePath } from "./utils";
|
import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelativePath } from "./utils";
|
||||||
|
import type { EntryMilestoneInfo } from "@lib/common/models/db.definition";
|
||||||
|
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||||
|
import type { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
|
||||||
|
|
||||||
function redactConnectionString(uri: string): string {
|
function redactConnectionString(uri: string): string {
|
||||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||||
@@ -35,16 +38,20 @@ async function verifyRemoteState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let milestone: any;
|
let milestone: EntryMilestoneInfo | null | false = null;
|
||||||
if (settings.remoteType === REMOTE_COUCHDB) {
|
if (settings.remoteType === REMOTE_COUCHDB) {
|
||||||
const dbRet = await (replicator as any).connectRemoteCouchDBWithSetting(settings, false, true);
|
const dbRet = await (replicator as LiveSyncCouchDBReplicator).connectRemoteCouchDBWithSetting(
|
||||||
|
settings,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
if (typeof dbRet === "string") {
|
if (typeof dbRet === "string") {
|
||||||
process.stderr.write(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
|
process.stderr.write(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
milestone = await dbRet.db.get(MILESTONE_DOCID);
|
milestone = await dbRet.db.get(MILESTONE_DOCID);
|
||||||
} else if (settings.remoteType === REMOTE_MINIO) {
|
} else if (settings.remoteType === REMOTE_MINIO) {
|
||||||
milestone = await (replicator as any).client.downloadJson("_00000000-milestone.json");
|
milestone = await (replicator as LiveSyncJournalReplicator).client.downloadJson("_00000000-milestone.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (milestone) {
|
if (milestone) {
|
||||||
@@ -59,8 +66,10 @@ async function verifyRemoteState(
|
|||||||
process.stderr.write("[Verification] Milestone document not found on remote.\n");
|
process.stderr.write("[Verification] Milestone document not found on remote.\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
process.stderr.write(`[Verification] Failed to fetch milestone document: ${e?.message || e}\n`);
|
process.stderr.write(
|
||||||
|
`[Verification] Failed to fetch milestone document: ${e instanceof Error ? e.message : String(e)}\n`
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +80,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
|
|
||||||
await core.services.control.activated;
|
await core.services.control.activated;
|
||||||
if (options.command === "daemon") {
|
if (options.command === "daemon") {
|
||||||
const log = (msg: unknown) => console.error(`[Daemon] ${msg}`);
|
const log = (msg: unknown) => console.error(`[Daemon] ${msg instanceof Error ? msg.message : String(msg)}`);
|
||||||
|
|
||||||
// Skip the config mismatch dialog — the daemon cannot resolve it interactively
|
// Skip the config mismatch dialog — the daemon cannot resolve it interactively
|
||||||
// and the default "Dismiss" action would block replication. The daemon should
|
// and the default "Dismiss" action would block replication. The daemon should
|
||||||
@@ -90,7 +99,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
// 2. Mirror scan to reconcile PouchDB ↔ local filesystem.
|
// 2. Mirror scan to reconcile PouchDB ↔ local filesystem.
|
||||||
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
||||||
log("Running mirror scan...");
|
log("Running mirror scan...");
|
||||||
const scanOk = await performFullScan(core as any, log, errorManager, false, true);
|
const scanOk = await performFullScan(core, log, errorManager, false, true);
|
||||||
if (!scanOk) {
|
if (!scanOk) {
|
||||||
console.error("[Daemon] Mirror scan failed, cannot continue");
|
console.error("[Daemon] Mirror scan failed, cannot continue");
|
||||||
return false;
|
return false;
|
||||||
@@ -147,12 +156,12 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollTimer = setTimeout(poll, currentIntervalMs);
|
pollTimer = setTimeout(poll as unknown as () => void, currentIntervalMs);
|
||||||
};
|
};
|
||||||
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll, currentIntervalMs);
|
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll as unknown as () => void, currentIntervalMs);
|
||||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||||
clearTimeout(pollTimer);
|
clearTimeout(pollTimer);
|
||||||
return true;
|
return await Promise.resolve(true);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log("LiveSync mode: restoring sync settings and starting _changes feed");
|
log("LiveSync mode: restoring sync settings and starting _changes feed");
|
||||||
@@ -198,7 +207,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
}
|
}
|
||||||
const timeoutSec = parseTimeoutSeconds(options.commandArgs[0], "p2p-peers");
|
const timeoutSec = parseTimeoutSeconds(options.commandArgs[0], "p2p-peers");
|
||||||
console.error(`[Command] p2p-peers timeout=${timeoutSec}s`);
|
console.error(`[Command] p2p-peers timeout=${timeoutSec}s`);
|
||||||
const peers = await collectPeers(core as any, timeoutSec);
|
const peers = await collectPeers(core, timeoutSec);
|
||||||
if (peers.length > 0) {
|
if (peers.length > 0) {
|
||||||
process.stdout.write(peers.map((peer) => `[peer]\t${peer.peerId}\t${peer.name}`).join("\n") + "\n");
|
process.stdout.write(peers.map((peer) => `[peer]\t${peer.peerId}\t${peer.name}`).join("\n") + "\n");
|
||||||
}
|
}
|
||||||
@@ -215,14 +224,14 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
}
|
}
|
||||||
const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "p2p-sync");
|
const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "p2p-sync");
|
||||||
console.error(`[Command] p2p-sync peer=${peerToken} timeout=${timeoutSec}s`);
|
console.error(`[Command] p2p-sync peer=${peerToken} timeout=${timeoutSec}s`);
|
||||||
const peer = await syncWithPeer(core as any, peerToken, timeoutSec);
|
const peer = await syncWithPeer(core, peerToken, timeoutSec);
|
||||||
console.error(`[Done] P2P sync completed with ${peer.name} (${peer.peerId})`);
|
console.error(`[Done] P2P sync completed with ${peer.name} (${peer.peerId})`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.command === "p2p-host") {
|
if (options.command === "p2p-host") {
|
||||||
console.error("[Command] p2p-host");
|
console.error("[Command] p2p-host");
|
||||||
await openP2PHost(core as any);
|
await openP2PHost(core);
|
||||||
console.error("[Ready] P2P host is running. Press Ctrl+C to stop.");
|
console.error("[Ready] P2P host is running. Press Ctrl+C to stop.");
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
return true;
|
return true;
|
||||||
@@ -435,9 +444,10 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (docPath !== targetPath) continue;
|
if (docPath !== targetPath) continue;
|
||||||
|
|
||||||
const filename = path.basename(docPath);
|
const filename = path.basename(docPath);
|
||||||
const conflictsText = (doc._conflicts?.length ?? 0) > 0 ? doc._conflicts.join("\n ") : "N/A";
|
const conflicts = doc._conflicts ?? [];
|
||||||
|
const conflictsText = conflicts.length > 0 ? conflicts.join("\n ") : "N/A";
|
||||||
const children = "children" in doc ? doc.children : [];
|
const children = "children" in doc ? doc.children : [];
|
||||||
const rawDoc = await core.services.database.localDatabase.getRaw<any>(doc._id, {
|
const rawDoc = await core.services.database.localDatabase.getRaw(doc._id, {
|
||||||
revs_info: true,
|
revs_info: true,
|
||||||
});
|
});
|
||||||
const pastRevisions = (rawDoc._revs_info ?? [])
|
const pastRevisions = (rawDoc._revs_info ?? [])
|
||||||
@@ -491,6 +501,10 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
process.stderr.write(`[Info] File not found: ${targetPath}\n`);
|
process.stderr.write(`[Info] File not found: ${targetPath}\n`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!currentMeta._rev) {
|
||||||
|
process.stderr.write(`[Info] Current revision not found for ${targetPath}\n`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const conflicts = await core.serviceModules.databaseFileAccess.getConflictedRevs(targetPath);
|
const conflicts = await core.serviceModules.databaseFileAccess.getConflictedRevs(targetPath);
|
||||||
const candidateRevisions = [currentMeta._rev, ...conflicts];
|
const candidateRevisions = [currentMeta._rev, ...conflicts];
|
||||||
@@ -520,9 +534,9 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
|
|
||||||
if (options.command === "mirror") {
|
if (options.command === "mirror") {
|
||||||
console.error("[Command] mirror");
|
console.error("[Command] mirror");
|
||||||
const log = (msg: unknown) => console.error(`[Mirror] ${msg}`);
|
const log = (msg: unknown) => console.error(`[Mirror] ${msg instanceof Error ? msg.message : String(msg)}`);
|
||||||
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
|
||||||
return await performFullScan(core as any, log, errorManager, false, true);
|
return await performFullScan(core, log, errorManager, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.command === "remote-add") {
|
if (options.command === "remote-add") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
|
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
|
||||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
export type CLICommand =
|
export type CLICommand =
|
||||||
| "daemon"
|
| "daemon"
|
||||||
| "sync"
|
| "sync"
|
||||||
@@ -46,7 +46,7 @@ export interface CLIOptions {
|
|||||||
export interface CLICommandContext {
|
export interface CLICommandContext {
|
||||||
databasePath: string;
|
databasePath: string;
|
||||||
vaultPath: string;
|
vaultPath: string;
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
core: LiveSyncBaseCore<ServiceContext, never>;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
originalSyncSettings: Pick<
|
originalSyncSettings: Pick<
|
||||||
ObsidianLiveSyncSettings,
|
ObsidianLiveSyncSettings,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
// Polyfill WebRTC in Node.js environment for CLI app.
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import * as polyfill from "werift";
|
import * as polyfill from "werift";
|
||||||
import { main } from "./main";
|
import { main } from "./main";
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,49 @@
|
|||||||
import { tsBaseConfig } from "../../../eslint.config.common.mjs";
|
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||||
import globals from "globals";
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
// import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
|
// const obsidianRules = obsidianmd.configs.recommended.find((config) => config.rules)?.rules || {};
|
||||||
|
// console.dir(obsidianRules);
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
"dist",
|
"dist",
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"test",
|
"test",
|
||||||
"testdeno"
|
"testdeno",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/test.ts",
|
||||||
|
"**/tests.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.mjs",
|
||||||
]),
|
]),
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
importAlias.configs.recommended,
|
||||||
{
|
{
|
||||||
...tsBaseConfig,
|
files: ["**/*.ts"],
|
||||||
|
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...tsBaseConfig.languageOptions,
|
globals: { ...globals.node, PouchDB: "readonly" },
|
||||||
globals: { ...globals.node },
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "../../../",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: false,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...baseRules,
|
||||||
|
// ...obsidianRules,
|
||||||
|
// -- Project specific rules
|
||||||
|
...ImportAliasRules("../../../"),
|
||||||
|
// cli specific rules
|
||||||
|
"@typescript-eslint/no-this-alias": "off", // This rule is often inconvenient in CLI code where `this` is commonly used in various contexts, including callbacks and class methods.
|
||||||
|
...CommunityReviewRecommendedRules,
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import PouchDB from "pouchdb-core";
|
import PouchDB from "pouchdb-core";
|
||||||
|
|
||||||
import HttpPouch from "pouchdb-adapter-http";
|
import HttpPouch from "pouchdb-adapter-http";
|
||||||
@@ -85,7 +86,6 @@ PouchDB.prototype.purgeMulti = adapterFun(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const tasks = docs.map(
|
const tasks = docs.map(
|
||||||
(param) => () =>
|
(param) => () =>
|
||||||
|
|||||||
+25
-19
@@ -4,10 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
|
import * as fsSync from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
||||||
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
||||||
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
|
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
||||||
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
|
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
|
||||||
import type { LOG_LEVEL } from "@lib/common/logger";
|
import type { LOG_LEVEL } from "@lib/common/logger";
|
||||||
@@ -178,11 +179,13 @@ export function parseArgs(): CLIOptions {
|
|||||||
interval = n;
|
interval = n;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// fallthrough
|
||||||
case "--debug":
|
case "--debug":
|
||||||
|
// @ts-ignore - fallthrough intended for debug implying verbose
|
||||||
case "-d":
|
case "-d":
|
||||||
// debugging automatically enables verbose logging, as it is intended for debugging issues.
|
// debugging automatically enables verbose logging, as it is intended for debugging issues.
|
||||||
debug = true;
|
debug = true;
|
||||||
// falls through
|
// fallthrough
|
||||||
case "--verbose":
|
case "--verbose":
|
||||||
case "-v":
|
case "-v":
|
||||||
verbose = true;
|
verbose = true;
|
||||||
@@ -193,6 +196,7 @@ export function parseArgs(): CLIOptions {
|
|||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
if (!databasePath) {
|
if (!databasePath) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||||
command = token as CLICommand;
|
command = token as CLICommand;
|
||||||
break;
|
break;
|
||||||
@@ -204,6 +208,7 @@ export function parseArgs(): CLIOptions {
|
|||||||
databasePath = token;
|
databasePath = token;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||||
command = token as CLICommand;
|
command = token as CLICommand;
|
||||||
break;
|
break;
|
||||||
@@ -248,8 +253,8 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
|||||||
try {
|
try {
|
||||||
await fs.stat(targetPath);
|
await fs.stat(targetPath);
|
||||||
throw new Error(`Settings file already exists: ${targetPath} (use --force to overwrite)`);
|
throw new Error(`Settings file already exists: ${targetPath} (use --force to overwrite)`);
|
||||||
} catch (ex: any) {
|
} catch (ex: unknown) {
|
||||||
if (!(ex && ex?.code === "ENOENT")) {
|
if (!(ex && (ex as { code?: string }).code === "ENOENT")) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +309,7 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve database path
|
// Resolve database path
|
||||||
const databasePath = path.resolve(options.databasePath!);
|
const databasePath = path.resolve(options.databasePath || "");
|
||||||
// Check if database directory exists
|
// Check if database directory exists
|
||||||
try {
|
try {
|
||||||
const stat = await fs.stat(databasePath);
|
const stat = await fs.stat(databasePath);
|
||||||
@@ -312,7 +317,7 @@ export async function main() {
|
|||||||
console.error(`Error: ${databasePath} is not a directory`);
|
console.error(`Error: ${databasePath} is not a directory`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(`Error: Database directory ${databasePath} does not exist`);
|
console.error(`Error: Database directory ${databasePath} does not exist`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -333,7 +338,7 @@ export async function main() {
|
|||||||
? path.resolve(options.commandArgs[0])
|
? path.resolve(options.commandArgs[0])
|
||||||
: options.vaultPath
|
: options.vaultPath
|
||||||
? path.resolve(options.vaultPath)
|
? path.resolve(options.vaultPath)
|
||||||
: databasePath!;
|
: databasePath;
|
||||||
|
|
||||||
// Check if vault directory exists
|
// Check if vault directory exists
|
||||||
try {
|
try {
|
||||||
@@ -342,7 +347,7 @@ export async function main() {
|
|||||||
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -380,7 +385,7 @@ export async function main() {
|
|||||||
levelStr = "Urgent";
|
levelStr = "Urgent";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
levelStr = `${level}`;
|
levelStr = `${level as unknown as string}`;
|
||||||
}
|
}
|
||||||
const prefix = `(${levelStr})`;
|
const prefix = `(${levelStr})`;
|
||||||
if (level <= LOG_LEVEL_INFO) {
|
if (level <= LOG_LEVEL_INFO) {
|
||||||
@@ -424,7 +429,7 @@ export async function main() {
|
|||||||
// Force disable IndexedDB adapter in CLI environment
|
// Force disable IndexedDB adapter in CLI environment
|
||||||
data.useIndexedDBAdapter = false;
|
data.useIndexedDBAdapter = false;
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch {
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
console.error(`[Settings] File not found, using defaults`);
|
console.error(`[Settings] File not found, using defaults`);
|
||||||
}
|
}
|
||||||
@@ -436,13 +441,14 @@ export async function main() {
|
|||||||
// Create LiveSync core
|
// Create LiveSync core
|
||||||
const core = new LiveSyncBaseCore(
|
const core = new LiveSyncBaseCore(
|
||||||
serviceHubInstance,
|
serviceHubInstance,
|
||||||
(core: LiveSyncBaseCore<NodeServiceContext, any>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
|
(core: LiveSyncBaseCore<NodeServiceContext, never>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
|
||||||
return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled);
|
return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled);
|
||||||
},
|
},
|
||||||
(core) => [],
|
(core) => [],
|
||||||
() => [], // No add-ons
|
() => [] as never[], // No add-ons
|
||||||
(core) => {
|
(core) => {
|
||||||
// Register P2P replicator feature.
|
// Register P2P replicator feature.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const _replicator = useP2PReplicatorFeature(core);
|
const _replicator = useP2PReplicatorFeature(core);
|
||||||
// Add target filter to prevent internal files are handled
|
// Add target filter to prevent internal files are handled
|
||||||
core.services.vault.isTargetFile.addHandler(async (target) => {
|
core.services.vault.isTargetFile.addHandler(async (target) => {
|
||||||
@@ -463,12 +469,12 @@ export async function main() {
|
|||||||
if (ignoreRules) {
|
if (ignoreRules) {
|
||||||
const rules = ignoreRules;
|
const rules = ignoreRules;
|
||||||
core.services.vault.isTargetFile.addHandler(async (target) => {
|
core.services.vault.isTargetFile.addHandler(async (target) => {
|
||||||
|
await Promise.resolve();
|
||||||
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
|
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
|
||||||
if (rules.shouldIgnore(targetPath)) {
|
if (rules.shouldIgnore(targetPath)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// undefined = pass through to next handler in chain
|
return true;
|
||||||
return undefined;
|
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,13 +494,13 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||||
|
|
||||||
// Save the settings file before any lifecycle events can mutate and persist them.
|
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||||
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||||
// various code paths persist the clobbered state to disk. We restore on shutdown.
|
// various code paths persist the clobbered state to disk. We restore on shutdown.
|
||||||
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!);
|
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null);
|
||||||
|
|
||||||
// Restore settings file on any exit to undo lifecycle mutations.
|
// Restore settings file on any exit to undo lifecycle mutations.
|
||||||
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
|
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
|
||||||
@@ -502,8 +508,8 @@ export async function main() {
|
|||||||
if (settingsBackup) {
|
if (settingsBackup) {
|
||||||
const tmpPath = settingsPath + ".tmp";
|
const tmpPath = settingsPath + ".tmp";
|
||||||
try {
|
try {
|
||||||
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
|
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||||
require("fs").renameSync(tmpPath, settingsPath);
|
fsSync.renameSync(tmpPath, settingsPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Settings] Failed to restore settings on exit:", err);
|
console.error("[Settings] Failed to restore settings on exit:", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,30 @@ import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
|||||||
import type { Stats } from "fs";
|
import type { Stats } from "fs";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import type { NodeFile, NodeFolder } from "../adapters/NodeTypes";
|
import type { NodeFile, NodeFolder } from "@cli/adapters/NodeTypes";
|
||||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
import type { IgnoreRules } from "@cli/serviceModules/IgnoreRules";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific type guard adapter
|
* CLI-specific type guard adapter
|
||||||
*/
|
*/
|
||||||
class CLITypeGuardAdapter implements IStorageEventTypeGuardAdapter<NodeFile, NodeFolder> {
|
class CLITypeGuardAdapter implements IStorageEventTypeGuardAdapter<NodeFile, NodeFolder> {
|
||||||
isFile(file: any): file is NodeFile {
|
isFile(file: unknown): file is NodeFile {
|
||||||
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
|
return !!(
|
||||||
|
file &&
|
||||||
|
typeof file === "object" &&
|
||||||
|
"path" in file &&
|
||||||
|
"stat" in file &&
|
||||||
|
!(file as { isFolder?: boolean }).isFolder
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFolder(item: any): item is NodeFolder {
|
isFolder(item: unknown): item is NodeFolder {
|
||||||
return item && typeof item === "object" && "path" in item && item.isFolder === true;
|
return !!(
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"path" in item &&
|
||||||
|
(item as { isFolder?: boolean }).isFolder === true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
import type { IgnoreRules } from "@cli/serviceModules/IgnoreRules";
|
||||||
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
||||||
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
|
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"package:apply-repo": "node scripts/apply-package.mjs",
|
"package:apply-repo": "node ../../../apply-package.mjs",
|
||||||
"prebuild": "node scripts/check-submodule.mjs",
|
"prebuild": "node scripts/check-submodule.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copy package.json dependencies and devDependencies from the repo root to the cli package.json, and set the version to match the repo root version with a -cli suffix.
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import process from "node:process";
|
|
||||||
|
|
||||||
const cliDir = process.cwd();
|
|
||||||
const repoRoot = path.resolve(cliDir, "../../..");
|
|
||||||
const repoPackageJsonPath = path.join(repoRoot, "package.json");
|
|
||||||
const repoPackageJson = JSON.parse(fs.readFileSync(repoPackageJsonPath, "utf-8"));
|
|
||||||
const devDependenciesToCopy = repoPackageJson.devDependencies || {};
|
|
||||||
const dependenciesToCopy = repoPackageJson.dependencies || {};
|
|
||||||
const packageJsonPath = path.join(cliDir, "package.json");
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
||||||
packageJson.dependencies = {
|
|
||||||
...packageJson.dependencies,
|
|
||||||
...dependenciesToCopy,
|
|
||||||
};
|
|
||||||
packageJson.devDependencies = {
|
|
||||||
...packageJson.devDependencies,
|
|
||||||
...devDependenciesToCopy,
|
|
||||||
};
|
|
||||||
packageJson.version = `${repoPackageJson.version}-cli`;
|
|
||||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4), "utf-8");
|
|
||||||
|
|
||||||
console.log("Applied package.json dependencies and version from repo root to cli package.json");
|
|
||||||
console.log(
|
|
||||||
"Please do not forget to pick dependencies that are actually needed in the cli package.json, and remove the ones that are not needed."
|
|
||||||
);
|
|
||||||
@@ -3,9 +3,9 @@ import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
|
|||||||
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
|
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
||||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
|
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||||
import { StorageEventManagerCLI } from "../managers/StorageEventManagerCLI";
|
import { StorageEventManagerCLI } from "@cli/managers/StorageEventManagerCLI";
|
||||||
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
||||||
import { FileAccessCLI } from "./FileAccessCLI";
|
import { FileAccessCLI } from "./FileAccessCLI";
|
||||||
import type { IgnoreRules } from "./IgnoreRules";
|
import type { IgnoreRules } from "./IgnoreRules";
|
||||||
@@ -22,7 +22,7 @@ import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
|
|||||||
*/
|
*/
|
||||||
export function initialiseServiceModulesCLI(
|
export function initialiseServiceModulesCLI(
|
||||||
basePath: string,
|
basePath: string,
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
services: InjectableServiceHub<ServiceContext>,
|
services: InjectableServiceHub<ServiceContext>,
|
||||||
ignoreRules?: IgnoreRules,
|
ignoreRules?: IgnoreRules,
|
||||||
watchEnabled: boolean = false
|
watchEnabled: boolean = false
|
||||||
@@ -81,7 +81,7 @@ export function initialiseServiceModulesCLI(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// File handler (platform-independent)
|
// File handler (platform-independent)
|
||||||
const fileHandler = new (ServiceFileHandler as any)({
|
const fileHandler = new ServiceFileHandler({
|
||||||
API: services.API,
|
API: services.API,
|
||||||
databaseFileAccess: databaseFileAccess,
|
databaseFileAccess: databaseFileAccess,
|
||||||
conflict: services.conflict,
|
conflict: services.conflict,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
import { NodeFileSystemAdapter } from "@cli/adapters/NodeFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific implementation of FileAccessBase
|
* CLI-specific implementation of FileAccessBase
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
import { NodeFileSystemAdapter } from "@cli/adapters/NodeFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific implementation of ServiceFileAccess
|
* CLI-specific implementation of ServiceFileAccess
|
||||||
|
|||||||
@@ -115,23 +115,25 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(key: IDBValidKey): Promise<T> {
|
async get<T>(key: IDBValidKey): Promise<T> {
|
||||||
return this.data.get(this.asKeyString(key)) as T;
|
return await Promise.resolve(this.data.get(this.asKeyString(key)) as T);
|
||||||
}
|
}
|
||||||
|
|
||||||
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
|
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
|
||||||
this.data.set(this.asKeyString(key), value);
|
this.data.set(this.asKeyString(key), value);
|
||||||
this.flush();
|
this.flush();
|
||||||
return key;
|
return await Promise.resolve(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async del(key: IDBValidKey): Promise<void> {
|
async del(key: IDBValidKey): Promise<void> {
|
||||||
this.data.delete(this.asKeyString(key));
|
this.data.delete(this.asKeyString(key));
|
||||||
this.flush();
|
this.flush();
|
||||||
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
this.data.clear();
|
this.data.clear();
|
||||||
this.flush();
|
this.flush();
|
||||||
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isIDBKeyRangeLike(value: unknown): value is { lower?: IDBValidKey; upper?: IDBValidKey } {
|
private isIDBKeyRangeLike(value: unknown): value is { lower?: IDBValidKey; upper?: IDBValidKey } {
|
||||||
@@ -143,10 +145,13 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
let filtered = allKeys;
|
let filtered = allKeys;
|
||||||
if (typeof query !== "undefined") {
|
if (typeof query !== "undefined") {
|
||||||
if (this.isIDBKeyRangeLike(query)) {
|
if (this.isIDBKeyRangeLike(query)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
const lower = query.lower?.toString() ?? "";
|
const lower = query.lower?.toString() ?? "";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
const upper = query.upper?.toString() ?? "\uffff";
|
const upper = query.upper?.toString() ?? "\uffff";
|
||||||
filtered = filtered.filter((key) => key >= lower && key <= upper);
|
filtered = filtered.filter((key) => key >= lower && key <= upper);
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
const exact = query.toString();
|
const exact = query.toString();
|
||||||
filtered = filtered.filter((key) => key === exact);
|
filtered = filtered.filter((key) => key === exact);
|
||||||
}
|
}
|
||||||
@@ -154,16 +159,18 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
if (typeof count === "number") {
|
if (typeof count === "number") {
|
||||||
filtered = filtered.slice(0, count);
|
filtered = filtered.slice(0, count);
|
||||||
}
|
}
|
||||||
return filtered;
|
return await Promise.resolve(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
this.flush();
|
this.flush();
|
||||||
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
async destroy(): Promise<void> {
|
||||||
this.data.clear();
|
this.data.clear();
|
||||||
nodeFs.rmSync(this.filePath, { force: true });
|
nodeFs.rmSync(this.filePath, { force: true });
|
||||||
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +185,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
|||||||
implements IKeyValueDBService
|
implements IKeyValueDBService
|
||||||
{
|
{
|
||||||
private _kvDB: KeyValueDatabase | undefined;
|
private _kvDB: KeyValueDatabase | undefined;
|
||||||
private _simpleStore: SimpleStore<any> | undefined;
|
private _simpleStore: SimpleStore<unknown> | undefined;
|
||||||
private filePath: string;
|
private filePath: string;
|
||||||
private _log = createInstanceLogFunction("NodeKeyValueDBService");
|
private _log = createInstanceLogFunction("NodeKeyValueDBService");
|
||||||
|
|
||||||
@@ -210,11 +217,11 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
|||||||
private async openKeyValueDB(): Promise<boolean> {
|
private async openKeyValueDB(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this._kvDB = new NodeFileKeyValueDatabase(this.filePath);
|
this._kvDB = new NodeFileKeyValueDatabase(this.filePath);
|
||||||
return true;
|
return await Promise.resolve(true);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this._log("Failed to open Node key-value database", LOG_LEVEL_NOTICE);
|
this._log("Failed to open Node key-value database", LOG_LEVEL_NOTICE);
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return await Promise.resolve(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +255,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
|||||||
if (!(await this.openKeyValueDB())) {
|
if (!(await this.openKeyValueDB())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this._simpleStore = this.openSimpleStore<any>("os");
|
this._simpleStore = this.openSimpleStore<unknown>("os");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,13 +271,14 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
|||||||
get: async (key: string): Promise<T> => {
|
get: async (key: string): Promise<T> => {
|
||||||
return await getDB().get(`${prefix}${key}`);
|
return await getDB().get(`${prefix}${key}`);
|
||||||
},
|
},
|
||||||
set: async (key: string, value: any): Promise<void> => {
|
set: async (key: string, value: T): Promise<void> => {
|
||||||
await getDB().set(`${prefix}${key}`, value);
|
await getDB().set(`${prefix}${key}`, value);
|
||||||
},
|
},
|
||||||
delete: async (key: string): Promise<void> => {
|
delete: async (key: string): Promise<void> => {
|
||||||
await getDB().del(`${prefix}${key}`);
|
await getDB().del(`${prefix}${key}`);
|
||||||
},
|
},
|
||||||
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
|
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString());
|
const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString());
|
||||||
const lower = `${prefix}${from ?? ""}`;
|
const lower = `${prefix}${from ?? ""}`;
|
||||||
const upper = `${prefix}${to ?? "\uffff"}`;
|
const upper = `${prefix}${to ?? "\uffff"}`;
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ function createNodeLocalStorageShim(): LocalStorageShape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ensureGlobalNodeLocalStorage() {
|
export function ensureGlobalNodeLocalStorage() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") {
|
if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(globalThis as any).localStorage = createNodeLocalStorageShim();
|
(globalThis as any).localStorage = createNodeLocalStorageShim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,10 +197,12 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
|
|||||||
path,
|
path,
|
||||||
API,
|
API,
|
||||||
config,
|
config,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
keyValueDB: keyValueDB as any,
|
keyValueDB: keyValueDB as any,
|
||||||
control,
|
control,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
super(context, serviceInstancesToInit as any);
|
super(context, serviceInstancesToInit as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,18 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": false,
|
"strict": true,
|
||||||
// "noImplicitAny": false,
|
// "noImplicitAny": false,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
// "rootDir": "../../../",
|
||||||
// /* Path mapping */
|
/* Path mapping */
|
||||||
// "paths": {
|
"paths": {
|
||||||
// "@/*": ["../../*"],
|
"@/*": ["../../*"],
|
||||||
// "@lib/*": ["../../lib/src/*"]
|
"@lib/*": ["../../lib/src/*"],
|
||||||
// }
|
"@cli/*": ["./*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules", "dist", "test", "testdeno"]
|
"exclude": ["node_modules", "dist", "test", "testdeno"]
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default defineConfig({
|
|||||||
fflate: resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"),
|
fflate: resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"),
|
||||||
"@": resolve(__dirname, "../../"),
|
"@": resolve(__dirname, "../../"),
|
||||||
"@lib": resolve(__dirname, "../../lib/src"),
|
"@lib": resolve(__dirname, "../../lib/src"),
|
||||||
|
"@cli": resolve(__dirname, "../../apps/cli"),
|
||||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||||
|
import type { FilePathWithPrefix } from "@lib/common/models/db.type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversion adapter implementation for FileSystem API
|
* Conversion adapter implementation for FileSystem API
|
||||||
@@ -28,7 +29,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
|
|||||||
path: folder.path,
|
path: folder.path,
|
||||||
isFolder: true,
|
isFolder: true,
|
||||||
children: [],
|
children: [],
|
||||||
parent: parentPath as any,
|
parent: parentPath as FilePathWithPrefix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private normalisePath(path: FilePath | string): string {
|
private normalisePath(path: FilePath | string): string {
|
||||||
return this.path.normalisePath(path as string);
|
return this.path.normalisePath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get file handle for a given path
|
* Get file handle for a given path
|
||||||
*/
|
*/
|
||||||
private async getFileHandleByPath(p: FilePath | string): Promise<FileSystemFileHandle | null> {
|
private async getFileHandleByPath(p: FilePath | string): Promise<FileSystemFileHandle | null> {
|
||||||
const pathStr = p as string;
|
const pathStr = p;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cached = this.handleCache.get(pathStr);
|
const cached = this.handleCache.get(pathStr);
|
||||||
@@ -173,7 +173,9 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use AsyncIterator instead of .values() for better compatibility
|
// Use AsyncIterator instead of .values() for better compatibility
|
||||||
for await (const [name, entry] of (currentHandle as any).entries()) {
|
for await (const [name, entry] of (
|
||||||
|
currentHandle as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }
|
||||||
|
).entries()) {
|
||||||
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
||||||
|
|
||||||
if (entry.kind === "directory") {
|
if (entry.kind === "directory") {
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
|
|||||||
const folders: string[] = [];
|
const folders: string[] = [];
|
||||||
|
|
||||||
// Use AsyncIterator instead of .values() for better compatibility
|
// Use AsyncIterator instead of .values() for better compatibility
|
||||||
for await (const [name, entry] of (dirHandle as any).entries()) {
|
for await (const [name, entry] of (
|
||||||
|
dirHandle as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }
|
||||||
|
).entries()) {
|
||||||
const entryPath = basePath ? `${basePath}/${name}` : name;
|
const entryPath = basePath ? `${basePath}/${name}` : name;
|
||||||
|
|
||||||
if (entry.kind === "directory") {
|
if (entry.kind === "directory") {
|
||||||
|
|||||||
@@ -5,13 +5,25 @@ import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
|||||||
* Type guard adapter implementation for FileSystem API
|
* Type guard adapter implementation for FileSystem API
|
||||||
*/
|
*/
|
||||||
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||||
isFile(file: any): file is FSAPIFile {
|
isFile(file: unknown): file is FSAPIFile {
|
||||||
return (
|
return !!(
|
||||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
file &&
|
||||||
|
typeof file === "object" &&
|
||||||
|
"path" in file &&
|
||||||
|
"stat" in file &&
|
||||||
|
"handle" in file &&
|
||||||
|
!("isFolder" in file && file.isFolder === true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFolder(item: any): item is FSAPIFolder {
|
isFolder(item: unknown): item is FSAPIFolder {
|
||||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
return !!(
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"path" in item &&
|
||||||
|
"isFolder" in item &&
|
||||||
|
item.isFolder === true &&
|
||||||
|
"handle" in item
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
|||||||
await this.delete(file, force);
|
await this.delete(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(name: string, ...data: any[]): any {
|
trigger(name: string, ...data: unknown[]): unknown {
|
||||||
// No-op in webapp version (no event system yet)
|
// No-op in webapp version (no event system yet)
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,20 +120,18 @@ async function initializeVaultSelector(): Promise<void> {
|
|||||||
await renderHistoryList();
|
await renderHistoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", () => {
|
||||||
try {
|
initializeVaultSelector().catch((error) => {
|
||||||
await initializeVaultSelector();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to initialize vault selector:", error);
|
console.error("Failed to initialize vault selector:", error);
|
||||||
setStatus("error", `Initialization failed: ${String(error)}`);
|
setStatus("error", `Initialization failed: ${String(error)}`);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
void app?.shutdown();
|
void app?.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
(window as any).livesyncApp = {
|
(window as unknown as { livesyncApp: unknown }).livesyncApp = {
|
||||||
getApp: () => app,
|
getApp: () => app,
|
||||||
historyStore,
|
historyStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
import { tsBaseConfig } from "../../../eslint.config.common.mjs";
|
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||||
import globals from "globals";
|
import tsParser from "@typescript-eslint/parser";
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
"dist",
|
"dist",
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"test"
|
"test",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/test.ts",
|
||||||
|
"**/tests.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.mjs",
|
||||||
|
"vite.config.ts",
|
||||||
|
"playwright.config.ts",
|
||||||
]),
|
]),
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
importAlias.configs.recommended,
|
||||||
{
|
{
|
||||||
...tsBaseConfig,
|
files: ["**/*.ts"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...tsBaseConfig.languageOptions,
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
globals: { ...globals.browser },
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "../../../",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: false,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...baseRules,
|
||||||
|
...ImportAliasRules("../../../"),
|
||||||
|
...CommunityReviewRecommendedRules,
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
+16
-12
@@ -50,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
|
|||||||
|
|
||||||
class LiveSyncWebApp {
|
class LiveSyncWebApp {
|
||||||
private rootHandle: FileSystemDirectoryHandle;
|
private rootHandle: FileSystemDirectoryHandle;
|
||||||
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
|
private core: LiveSyncBaseCore<ServiceContext, never> | null = null;
|
||||||
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
|
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
|
||||||
|
|
||||||
constructor(rootHandle: FileSystemDirectoryHandle) {
|
constructor(rootHandle: FileSystemDirectoryHandle) {
|
||||||
@@ -98,17 +98,19 @@ class LiveSyncWebApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// App lifecycle handlers
|
// App lifecycle handlers
|
||||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
|
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
|
||||||
console.log("[AppLifecycle] Restart requested");
|
void (async () => {
|
||||||
await this.shutdown();
|
console.log("[AppLifecycle] Restart requested");
|
||||||
await this.initialize();
|
await this.shutdown();
|
||||||
setTimeout(() => {
|
await this.initialize();
|
||||||
window.location.reload();
|
setTimeout(() => {
|
||||||
}, 1000);
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create LiveSync core
|
// Create LiveSync core
|
||||||
this.core = new LiveSyncBaseCore(
|
this.core = new LiveSyncBaseCore<ServiceContext, never>(
|
||||||
this.serviceHub,
|
this.serviceHub,
|
||||||
(core, serviceHub) => {
|
(core, serviceHub) => {
|
||||||
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
|
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
|
||||||
@@ -128,7 +130,7 @@ class LiveSyncWebApp {
|
|||||||
// new ModuleReplicatorP2P(core), // Register P2P replicator for CLI (useP2PReplicator is not used here)
|
// new ModuleReplicatorP2P(core), // Register P2P replicator for CLI (useP2PReplicator is not used here)
|
||||||
new SetupManager(core),
|
new SetupManager(core),
|
||||||
],
|
],
|
||||||
() => [], // No add-ons
|
() => [] as never[], // No add-ons
|
||||||
(core) => {
|
(core) => {
|
||||||
useOfflineScanner(core);
|
useOfflineScanner(core);
|
||||||
useRedFlagFeatures(core);
|
useRedFlagFeatures(core);
|
||||||
@@ -206,7 +208,8 @@ class LiveSyncWebApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan the directory to populate file cache
|
// Scan the directory to populate file cache
|
||||||
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
|
const fileAccess = (this.core as unknown as { _serviceModules?: any })._serviceModules?.storageAccess
|
||||||
|
?.vaultAccess;
|
||||||
if (fileAccess?.fsapiAdapter) {
|
if (fileAccess?.fsapiAdapter) {
|
||||||
console.log("[Scanning] Scanning vault directory...");
|
console.log("[Scanning] Scanning vault directory...");
|
||||||
await fileAccess.fsapiAdapter.scanDirectory();
|
await fileAccess.fsapiAdapter.scanDirectory();
|
||||||
@@ -224,7 +227,8 @@ class LiveSyncWebApp {
|
|||||||
console.log("[Shutdown] Shutting down...");
|
console.log("[Shutdown] Shutting down...");
|
||||||
|
|
||||||
// Stop file watching
|
// Stop file watching
|
||||||
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
|
const storageEventManager = (this.core as unknown as { _serviceModules?: any })._serviceModules
|
||||||
|
?.storageAccess?.storageEventManager;
|
||||||
if (storageEventManager?.cleanup) {
|
if (storageEventManager?.cleanup) {
|
||||||
await storageEventManager.cleanup();
|
await storageEventManager.cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,32 @@ import type {
|
|||||||
IStorageEventWatchHandlers,
|
IStorageEventWatchHandlers,
|
||||||
} from "@lib/managers/adapters";
|
} from "@lib/managers/adapters";
|
||||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||||
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
|
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific type guard adapter
|
* FileSystem API-specific type guard adapter
|
||||||
*/
|
*/
|
||||||
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||||
isFile(file: any): file is FSAPIFile {
|
isFile(file: unknown): file is FSAPIFile {
|
||||||
return (
|
return !!(
|
||||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
file &&
|
||||||
|
typeof file === "object" &&
|
||||||
|
"path" in file &&
|
||||||
|
"stat" in file &&
|
||||||
|
"handle" in file &&
|
||||||
|
!("isFolder" in file && file.isFolder === true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFolder(item: any): item is FSAPIFolder {
|
isFolder(item: unknown): item is FSAPIFolder {
|
||||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
return !!(
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"path" in item &&
|
||||||
|
"isFolder" in item &&
|
||||||
|
item.isFolder === true &&
|
||||||
|
"handle" in item
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,22 +155,40 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
|
|||||||
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
|
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
|
||||||
*/
|
*/
|
||||||
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||||
private observer: any = null; // FileSystemObserver type
|
private observer: {
|
||||||
|
observe: (handle: FileSystemDirectoryHandle, options: { recursive: boolean }) => Promise<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||||
|
|
||||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||||
// Use FileSystemObserver if available (Chrome 124+)
|
// Use FileSystemObserver if available (Chrome 124+)
|
||||||
if (typeof (window as any).FileSystemObserver === "undefined") {
|
const win = window as unknown as {
|
||||||
|
FileSystemObserver?: new (
|
||||||
|
callback: (
|
||||||
|
records: {
|
||||||
|
root: FileSystemDirectoryHandle;
|
||||||
|
changedHandle: FileSystemFileHandle;
|
||||||
|
relativePathComponents: string[];
|
||||||
|
type: string;
|
||||||
|
}[]
|
||||||
|
) => Promise<void>
|
||||||
|
) => {
|
||||||
|
observe: (handle: FileSystemDirectoryHandle, options: { recursive: boolean }) => Promise<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (typeof win.FileSystemObserver === "undefined") {
|
||||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const FileSystemObserver = (window as any).FileSystemObserver;
|
const FileSystemObserver = win.FileSystemObserver;
|
||||||
|
|
||||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
this.observer = new FileSystemObserver(async (records) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const handle = record.root;
|
const handle = record.root;
|
||||||
const changedHandle = record.changedHandle;
|
const changedHandle = record.changedHandle;
|
||||||
@@ -181,7 +211,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
if (changedHandle && changedHandle.kind === "file") {
|
if (changedHandle && changedHandle.kind === "file") {
|
||||||
const file = await changedHandle.getFile();
|
const file = await changedHandle.getFile();
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mtime: file.lastModified,
|
mtime: file.lastModified,
|
||||||
@@ -192,23 +222,23 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (type === "appeared") {
|
if (type === "appeared") {
|
||||||
await handlers.onCreate(fileInfo, undefined);
|
handlers.onCreate(fileInfo, undefined);
|
||||||
} else {
|
} else {
|
||||||
await handlers.onChange(fileInfo, undefined);
|
handlers.onChange(fileInfo, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === "disappeared") {
|
} else if (type === "disappeared") {
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: 0,
|
size: 0,
|
||||||
mtime: Date.now(),
|
mtime: Date.now(),
|
||||||
ctime: Date.now(),
|
ctime: Date.now(),
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
},
|
},
|
||||||
handle: null as any,
|
handle: null as unknown,
|
||||||
};
|
};
|
||||||
await handlers.onDelete(fileInfo, undefined);
|
handlers.onDelete(fileInfo, undefined);
|
||||||
} else if (type === "moved") {
|
} else if (type === "moved") {
|
||||||
// Handle as delete + create
|
// Handle as delete + create
|
||||||
// Note: FileSystemObserver provides both old and new paths in some cases
|
// Note: FileSystemObserver provides both old and new paths in some cases
|
||||||
@@ -216,7 +246,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
if (changedHandle && changedHandle.kind === "file") {
|
if (changedHandle && changedHandle.kind === "file") {
|
||||||
const file = await changedHandle.getFile();
|
const file = await changedHandle.getFile();
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mtime: file.lastModified,
|
mtime: file.lastModified,
|
||||||
@@ -225,7 +255,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
},
|
},
|
||||||
handle: changedHandle,
|
handle: changedHandle,
|
||||||
};
|
};
|
||||||
await handlers.onChange(fileInfo, undefined);
|
handlers.onChange(fileInfo, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStora
|
|||||||
async cleanup() {
|
async cleanup() {
|
||||||
// Stop file watching
|
// Stop file watching
|
||||||
if (this.fsapiAdapter?.watch) {
|
if (this.fsapiAdapter?.watch) {
|
||||||
await (this.fsapiAdapter.watch as any).stopWatch?.();
|
await (this.fsapiAdapter.watch as unknown as { stopWatch?: () => Promise<void> }).stopWatch?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.25.74-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": {
|
||||||
@@ -11,11 +11,93 @@
|
|||||||
"run:docker": "docker run -p 8002:80 livesync-webapp",
|
"run:docker": "docker run -p 8002:80 livesync-webapp",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"lint": "eslint --cache ."
|
"lint": "eslint --cache .",
|
||||||
|
"package:apply-repo": "node ../../../apply-package.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.3.10",
|
||||||
|
"@smithy/md5-js": "^4.2.9",
|
||||||
|
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||||
|
"@smithy/protocol-http": "^5.3.9",
|
||||||
|
"@smithy/querystring-builder": "^4.2.9",
|
||||||
|
"@smithy/types": "^4.14.3",
|
||||||
|
"@smithy/util-retry": "^4.4.5",
|
||||||
|
"@trystero-p2p/nostr": "^0.24.0",
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
|
"octagonal-wheels": "^0.1.46",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"qrcode-generator": "^1.4.4",
|
||||||
|
"werift": "^0.23.0",
|
||||||
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||||
|
"@eslint/js": "^9.39.3",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
|
"@types/deno": "^2.5.0",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/micromatch": "^4.0.10",
|
||||||
|
"@types/node": "^24.10.13",
|
||||||
|
"@types/pouchdb": "^6.4.2",
|
||||||
|
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||||
|
"@types/pouchdb-adapter-idb": "^6.1.7",
|
||||||
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
|
"@types/pouchdb-core": "^7.0.15",
|
||||||
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
|
"@types/transform-pouch": "^1.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||||
|
"@typescript-eslint/parser": "8.61.0",
|
||||||
|
"@vitest/browser": "^4.1.8",
|
||||||
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"esbuild": "0.25.0",
|
||||||
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
|
"esbuild-svelte": "^0.9.4",
|
||||||
|
"eslint": "^9.39.3",
|
||||||
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
|
"pouchdb-adapter-idb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-errors": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
|
"pouchdb-merge": "^9.0.0",
|
||||||
|
"pouchdb-replication": "^9.0.0",
|
||||||
|
"pouchdb-utils": "^9.0.0",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
|
"svelte": "5.41.1",
|
||||||
|
"svelte-check": "^4.4.3",
|
||||||
|
"svelte-eslint-parser": "^1.8.0",
|
||||||
|
"svelte-preprocess": "^6.0.3",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"transform-pouch": "^2.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript-eslint": "^8.61.0",
|
||||||
|
"vite-plugin-istanbul": "^8.0.0",
|
||||||
|
"vitest": "^4.1.8",
|
||||||
|
"webdriverio": "^9.27.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
|||||||
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
||||||
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
||||||
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
||||||
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
|
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
|
||||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
|||||||
*/
|
*/
|
||||||
export function initialiseServiceModulesFSAPI(
|
export function initialiseServiceModulesFSAPI(
|
||||||
rootHandle: FileSystemDirectoryHandle,
|
rootHandle: FileSystemDirectoryHandle,
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
services: InjectableServiceHub<ServiceContext>
|
services: InjectableServiceHub<ServiceContext>
|
||||||
): ServiceModules {
|
): ServiceModules {
|
||||||
const storageAccessManager = new StorageAccessManager();
|
const storageAccessManager = new StorageAccessManager();
|
||||||
@@ -67,7 +67,7 @@ export function initialiseServiceModulesFSAPI(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// File handler (platform-independent)
|
// File handler (platform-independent)
|
||||||
const fileHandler = new (ServiceFileHandler as any)({
|
const fileHandler = new ServiceFileHandler({
|
||||||
API: services.API,
|
API: services.API,
|
||||||
databaseFileAccess: databaseFileAccess,
|
databaseFileAccess: databaseFileAccess,
|
||||||
conflict: services.conflict,
|
conflict: services.conflict,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific implementation of FileAccessBase
|
* FileSystem API-specific implementation of FileAccessBase
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific implementation of ServiceFileAccess
|
* FileSystem API-specific implementation of ServiceFileAccess
|
||||||
|
|||||||
@@ -27,11 +27,45 @@ function stripPrefix(raw: string): string {
|
|||||||
return raw.replace(/^[^:]+:/, "");
|
return raw.replace(/^[^:]+:/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TestCore {
|
||||||
|
services: {
|
||||||
|
replication: {
|
||||||
|
databaseQueueCount?: { value: number };
|
||||||
|
storageApplyingCount?: { value: number };
|
||||||
|
replicate: (force: boolean) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
fileProcessing: {
|
||||||
|
totalQueued?: { value: number };
|
||||||
|
batched?: { value: number };
|
||||||
|
processing?: { value: number };
|
||||||
|
};
|
||||||
|
database: {
|
||||||
|
localDatabase: {
|
||||||
|
findAllNormalDocs: (options: { conflicts: boolean }) => AsyncIterable<{
|
||||||
|
_deleted?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
path?: string;
|
||||||
|
_rev?: string;
|
||||||
|
_conflicts?: string[];
|
||||||
|
size?: number;
|
||||||
|
mtime?: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
serviceModules: {
|
||||||
|
databaseFileAccess: {
|
||||||
|
storeContent: (path: string, content: string) => Promise<boolean>;
|
||||||
|
delete: (path: string) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll every 300 ms until all known processing queues are drained, or until
|
* Poll every 300 ms until all known processing queues are drained, or until
|
||||||
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
|
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
|
||||||
*/
|
*/
|
||||||
async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
async function waitForIdle(core: TestCore, timeoutMs = 60_000): Promise<void> {
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const q =
|
const q =
|
||||||
@@ -46,8 +80,8 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
|||||||
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCore(): any {
|
function getCore(): TestCore {
|
||||||
const core = (app as any)?.core;
|
const core = (app as unknown as { core: TestCore })?.core;
|
||||||
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
||||||
return core;
|
return core;
|
||||||
}
|
}
|
||||||
@@ -140,17 +174,14 @@ const livesyncTest: LiveSyncTestAPI = {
|
|||||||
|
|
||||||
async putFile(vaultPath: string, content: string): Promise<boolean> {
|
async putFile(vaultPath: string, content: string): Promise<boolean> {
|
||||||
const core = getCore();
|
const core = getCore();
|
||||||
const result = await core.serviceModules.databaseFileAccess.storeContent(
|
const result = await core.serviceModules.databaseFileAccess.storeContent(vaultPath, content);
|
||||||
vaultPath as FilePathWithPrefix,
|
|
||||||
content
|
|
||||||
);
|
|
||||||
await waitForIdle(core);
|
await waitForIdle(core);
|
||||||
return result !== false;
|
return result !== false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteFile(vaultPath: string): Promise<boolean> {
|
async deleteFile(vaultPath: string): Promise<boolean> {
|
||||||
const core = getCore();
|
const core = getCore();
|
||||||
const result = await core.serviceModules.databaseFileAccess.delete(vaultPath as FilePathWithPrefix);
|
const result = await core.serviceModules.databaseFileAccess.delete(vaultPath);
|
||||||
await waitForIdle(core);
|
await waitForIdle(core);
|
||||||
return result !== false;
|
return result !== false;
|
||||||
},
|
},
|
||||||
@@ -200,4 +231,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Expose on window for Playwright page.evaluate() calls.
|
// Expose on window for Playwright page.evaluate() calls.
|
||||||
(window as any).livesyncTest = livesyncTest;
|
(window as unknown as { livesyncTest: unknown }).livesyncTest = livesyncTest;
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ function randomId(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
||||||
const h = handle as any;
|
const h = handle as unknown as {
|
||||||
|
queryPermission?: (options: { mode: "read" | "readwrite" }) => Promise<PermissionState>;
|
||||||
|
requestPermission?: (options: { mode: "read" | "readwrite" }) => Promise<PermissionState>;
|
||||||
|
};
|
||||||
if (typeof h.queryPermission === "function") {
|
if (typeof h.queryPermission === "function") {
|
||||||
const queried = await h.queryPermission({ mode: "readwrite" });
|
const queried = await h.queryPermission({ mode: "readwrite" });
|
||||||
if (queried === "granted") {
|
if (queried === "granted") {
|
||||||
@@ -89,11 +92,15 @@ export class VaultHistoryStore {
|
|||||||
|
|
||||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||||
return this.withStore("readonly", async (store) => {
|
return this.withStore("readonly", async (store) => {
|
||||||
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
|
const keys = await this.requestAsPromise(store.getAllKeys());
|
||||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
||||||
const items: VaultHistoryItem[] = [];
|
const items: VaultHistoryItem[] = [];
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = String(keys[i]);
|
const keyVal = keys[i];
|
||||||
|
if (typeof keyVal !== "string" && typeof keyVal !== "number") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = String(keyVal);
|
||||||
const id = parseVaultId(key);
|
const id = parseVaultId(key);
|
||||||
const value = values[i] as Partial<VaultHistoryValue> | undefined;
|
const value = values[i] as Partial<VaultHistoryValue> | undefined;
|
||||||
if (!id || !value || !value.handle || !value.name) {
|
if (!id || !value || !value.handle || !value.name) {
|
||||||
@@ -170,7 +177,14 @@ export class VaultHistoryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||||
const picker = (window as any).showDirectoryPicker;
|
const picker = (
|
||||||
|
window as unknown as {
|
||||||
|
showDirectoryPicker?: (options?: {
|
||||||
|
mode?: "read" | "readwrite";
|
||||||
|
startIn?: "documents" | "desktop" | "downloads" | "music" | "pictures" | "videos";
|
||||||
|
}) => Promise<FileSystemDirectoryHandle>;
|
||||||
|
}
|
||||||
|
).showDirectoryPicker;
|
||||||
if (typeof picker !== "function") {
|
if (typeof picker !== "function") {
|
||||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,61 @@
|
|||||||
import { tsBaseConfig, svelteBaseConfig } from "../../../eslint.config.common.mjs";
|
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||||
import globals from "globals";
|
import tsParser from "@typescript-eslint/parser";
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
import * as sveltePlugin from "eslint-plugin-svelte";
|
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||||
|
import svelteParser from "svelte-eslint-parser";
|
||||||
|
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
"dist",
|
"dist",
|
||||||
"node_modules"
|
"node_modules",
|
||||||
|
"vite.config.ts",
|
||||||
|
"svelte.config.js",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.mjs",
|
||||||
]),
|
]),
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
...sveltePlugin.configs["flat/base"],
|
...sveltePlugin.configs["flat/base"],
|
||||||
|
importAlias.configs.recommended,
|
||||||
{
|
{
|
||||||
...tsBaseConfig,
|
|
||||||
files: ["src/**/*.ts"],
|
files: ["src/**/*.ts"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...tsBaseConfig.languageOptions,
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
globals: { ...globals.browser },
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.app.json",
|
||||||
|
rootDir: "../../../",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: false,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...baseRules,
|
||||||
|
...ImportAliasRules("../../../"),
|
||||||
|
...CommunityReviewRecommendedRules,
|
||||||
|
"no-restricted-globals": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...svelteBaseConfig,
|
|
||||||
files: ["src/**/*.svelte"],
|
files: ["src/**/*.svelte"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...svelteBaseConfig.languageOptions,
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
globals: { ...globals.browser },
|
parser: svelteParser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
extraFileExtensions: [".svelte"],
|
||||||
|
project: "./tsconfig.app.json",
|
||||||
|
rootDir: "../../../",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
...ImportAliasRules("../../../"),
|
||||||
|
...CommunityReviewRecommendedRules,
|
||||||
|
"no-restricted-globals": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webpeer",
|
"name": "webpeer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.25.74-webpeer",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,9 +10,32 @@
|
|||||||
"run:docker": "docker run -p 8001:80 livesync-webpeer",
|
"run:docker": "docker run -p 8001:80 livesync-webpeer",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
||||||
"lint": "eslint --cache src"
|
"lint": "eslint --cache src",
|
||||||
|
"package:apply-repo": "node ../../../apply-package.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.3.10",
|
||||||
|
"@smithy/md5-js": "^4.2.9",
|
||||||
|
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||||
|
"@smithy/protocol-http": "^5.3.9",
|
||||||
|
"@smithy/querystring-builder": "^4.2.9",
|
||||||
|
"@smithy/types": "^4.14.3",
|
||||||
|
"@smithy/util-retry": "^4.4.5",
|
||||||
|
"@trystero-p2p/nostr": "^0.24.0",
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"markdown-it": "^14.1.1",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
|
"octagonal-wheels": "^0.1.46",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"qrcode-generator": "^1.4.4",
|
||||||
|
"werift": "^0.23.0",
|
||||||
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
@@ -20,7 +43,61 @@
|
|||||||
"svelte": "5.41.1",
|
"svelte": "5.41.1",
|
||||||
"svelte-check": "^4.4.3",
|
"svelte-check": "^4.4.3",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||||
|
"@eslint/js": "^9.39.3",
|
||||||
|
"@types/deno": "^2.5.0",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/micromatch": "^4.0.10",
|
||||||
|
"@types/node": "^24.10.13",
|
||||||
|
"@types/pouchdb": "^6.4.2",
|
||||||
|
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||||
|
"@types/pouchdb-adapter-idb": "^6.1.7",
|
||||||
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
|
"@types/pouchdb-core": "^7.0.15",
|
||||||
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
|
"@types/transform-pouch": "^1.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||||
|
"@typescript-eslint/parser": "8.61.0",
|
||||||
|
"@vitest/browser": "^4.1.8",
|
||||||
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"esbuild": "0.25.0",
|
||||||
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
|
"esbuild-svelte": "^0.9.4",
|
||||||
|
"eslint": "^9.39.3",
|
||||||
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
|
"pouchdb-adapter-idb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-errors": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
|
"pouchdb-merge": "^9.0.0",
|
||||||
|
"pouchdb-replication": "^9.0.0",
|
||||||
|
"pouchdb-utils": "^9.0.0",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
|
"svelte-eslint-parser": "^1.8.0",
|
||||||
|
"svelte-preprocess": "^6.0.3",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"transform-pouch": "^2.0.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript-eslint": "^8.61.0",
|
||||||
|
"vite-plugin-istanbul": "^8.0.0",
|
||||||
|
"vitest": "^4.1.8",
|
||||||
|
"webdriverio": "^9.27.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ import {
|
|||||||
import { eventHub } from "@lib/hub/hub";
|
import { eventHub } from "@lib/hub/hub";
|
||||||
|
|
||||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||||
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
|
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger";
|
||||||
import { storeP2PStatusLine } from "./CommandsShim";
|
import { storeP2PStatusLine } from "./CommandsShim";
|
||||||
import {
|
import {
|
||||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||||
type PeerStatus,
|
type PeerStatus,
|
||||||
type PluginShim,
|
type PluginShim,
|
||||||
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||||
import { P2PLogCollector, type P2PReplicatorBase, useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
|
||||||
|
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase";
|
||||||
|
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||||
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||||
@@ -26,6 +28,7 @@ import { BrowserServiceHub } from "@lib/services/BrowserServices";
|
|||||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
|
||||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
||||||
|
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||||
import { Menu } from "@lib/services/implements/browser/Menu";
|
import { Menu } from "@lib/services/implements/browser/Menu";
|
||||||
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
||||||
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
|
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
|
||||||
@@ -63,7 +66,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
}
|
}
|
||||||
return this.db;
|
return this.db;
|
||||||
}
|
}
|
||||||
_simpleStore!: SimpleStore<any>;
|
_simpleStore!: SimpleStore<unknown>;
|
||||||
|
|
||||||
async closeDB() {
|
async closeDB() {
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
@@ -80,7 +83,20 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
replicator,
|
replicator,
|
||||||
p2pLogCollector,
|
p2pLogCollector,
|
||||||
storeP2PStatusLine: p2pStatusLine,
|
storeP2PStatusLine: p2pStatusLine,
|
||||||
} = useP2PReplicator({ services: this.services } as any);
|
} = useP2PReplicator({ services: this.services } as unknown as NecessaryServices<
|
||||||
|
| "API"
|
||||||
|
| "appLifecycle"
|
||||||
|
| "setting"
|
||||||
|
| "vault"
|
||||||
|
| "database"
|
||||||
|
| "databaseEvents"
|
||||||
|
| "keyValueDB"
|
||||||
|
| "replication"
|
||||||
|
| "config"
|
||||||
|
| "UI"
|
||||||
|
| "replicator",
|
||||||
|
never
|
||||||
|
>);
|
||||||
this._liveSyncReplicator = replicator;
|
this._liveSyncReplicator = replicator;
|
||||||
this.p2pLogCollector = p2pLogCollector;
|
this.p2pLogCollector = p2pLogCollector;
|
||||||
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||||
@@ -95,15 +111,15 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
|
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
|
||||||
() => "p2p-livesync-web-peer"
|
() => "p2p-livesync-web-peer"
|
||||||
);
|
);
|
||||||
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
|
const repStore = SimpleStoreIDBv2.open<unknown>("p2p-livesync-web-peer");
|
||||||
this._simpleStore = repStore;
|
this._simpleStore = repStore;
|
||||||
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
|
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
|
||||||
this.services.setting.settings = _settings as any;
|
this.services.setting.settings = _settings;
|
||||||
(this.services.setting as InjectableSettingService<any>).saveData.setHandler(async (data) => {
|
(this.services.setting as InjectableSettingService<ServiceContext>).saveData.setHandler(async (data) => {
|
||||||
await repStore.set("settings", data);
|
await repStore.set("settings", data);
|
||||||
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
|
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
|
||||||
});
|
});
|
||||||
(this.services.setting as InjectableSettingService<any>).loadData.setHandler(async () => {
|
(this.services.setting as InjectableSettingService<ServiceContext>).loadData.setHandler(async () => {
|
||||||
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
|
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
|
||||||
return settings;
|
return settings;
|
||||||
});
|
});
|
||||||
@@ -145,7 +161,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log(msg: any, level?: any): void {
|
_log(msg: unknown, level?: LOG_LEVEL): void {
|
||||||
Logger(msg, level);
|
Logger(msg, level);
|
||||||
}
|
}
|
||||||
_notice(msg: string, key?: string): void {
|
_notice(msg: string, key?: string): void {
|
||||||
@@ -154,7 +170,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
getSettings(): P2PSyncSetting {
|
getSettings(): P2PSyncSetting {
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
simpleStore(): SimpleStore<any> {
|
simpleStore(): SimpleStore<unknown> {
|
||||||
return this._simpleStore;
|
return this._simpleStore;
|
||||||
}
|
}
|
||||||
handleReplicatedDocuments(_docs: EntryDoc[]): Promise<boolean> {
|
handleReplicatedDocuments(_docs: EntryDoc[]): Promise<boolean> {
|
||||||
@@ -278,7 +294,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
}
|
}
|
||||||
await this.services.setting.applyExternalSettings(remoteConfig, true);
|
await this.services.setting.applyExternalSettings(remoteConfig, true);
|
||||||
if (yn !== DROP) {
|
if (yn !== DROP) {
|
||||||
await this.plugin.core.services.appLifecycle.scheduleRestart();
|
this.plugin.core.services.appLifecycle.scheduleRestart();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||||
|
|||||||
@@ -5,18 +5,20 @@
|
|||||||
import { cmdSyncShim } from "./P2PReplicatorShim";
|
import { cmdSyncShim } from "./P2PReplicatorShim";
|
||||||
import { eventHub } from "@lib/hub/hub";
|
import { eventHub } from "@lib/hub/hub";
|
||||||
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
|
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
|
||||||
|
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
|
|
||||||
let synchronised = $state(cmdSyncShim.init());
|
let synchronised = $state(cmdSyncShim.init());
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
eventHub.emitEvent(EVENT_LAYOUT_READY);
|
eventHub.emitEvent(EVENT_LAYOUT_READY);
|
||||||
return () => {
|
return () => {
|
||||||
synchronised.then((e) => e.close());
|
void synchronised.then((e) => e.close());
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let elP: HTMLDivElement;
|
let elP: HTMLDivElement;
|
||||||
logs.subscribe((log) => {
|
logs.subscribe((log) => {
|
||||||
tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
|
void tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
|
||||||
});
|
});
|
||||||
let statusLine = $state("");
|
let statusLine = $state("");
|
||||||
storeP2PStatusLine.subscribe((status) => {
|
storeP2PStatusLine.subscribe((status) => {
|
||||||
@@ -27,7 +29,10 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{#await synchronised then cmdSync}
|
{#await synchronised then cmdSync}
|
||||||
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
|
<P2PReplicatorPane
|
||||||
|
cmdSync={cmdSync as unknown as LiveSyncTrysteroReplicator}
|
||||||
|
core={cmdSync.plugin.core as unknown as LiveSyncBaseCore}
|
||||||
|
></P2PReplicatorPane>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
const context = getDialogContext();
|
const context = getDialogContext();
|
||||||
|
|
||||||
async function testUI() {
|
async function testUI() {
|
||||||
const confirm = await context.services.confirm;
|
const confirm = context.services.confirm;
|
||||||
const ret = await confirm.askString("Your name", "What is your name?", "John Doe", false);
|
const ret = await confirm.askString("Your name", "What is your name?", "John Doe", false);
|
||||||
result = ret;
|
result = ret;
|
||||||
}
|
}
|
||||||
let resultPassword = $state<string | boolean>("");
|
let resultPassword = $state<string | boolean>("");
|
||||||
async function testPassword() {
|
async function testPassword() {
|
||||||
const confirm = await context.services.confirm;
|
const confirm = context.services.confirm;
|
||||||
const ret = await confirm.askString("passphrase", "?", "anythingonlyyouknow", true);
|
const ret = await confirm.askString("passphrase", "?", "anythingonlyyouknow", true);
|
||||||
resultPassword = ret;
|
resultPassword = ret;
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
.onClick(async () => {})
|
.onClick(async () => {})
|
||||||
.setIcon(mark);
|
.setIcon(mark);
|
||||||
});
|
});
|
||||||
m.showAtPosition({ x: event.x, y: event.y });
|
void m.showAtPosition({ x: event.x, y: event.y });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceRoot": "../",
|
"sourceRoot": "../",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
@@ -15,11 +15,13 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["../../*"],
|
"@/*": ["../../*"],
|
||||||
"@lib/*": ["../../lib/src/*"]
|
"@lib/*": ["../../lib/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@
|
|||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
"@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"]
|
"@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"],
|
||||||
|
"@cli/*": ["./src/apps/cli/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts", "**/*.svelte"],
|
"include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts", "**/*.svelte"],
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
"@lib": path.resolve(__dirname, "./src/lib/src"),
|
"@lib": path.resolve(__dirname, "./src/lib/src"),
|
||||||
|
"@cli": path.resolve(__dirname, "./src/apps/cli"),
|
||||||
src: path.resolve(__dirname, "./src"),
|
src: path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user