refactor (okay, i know that this cannot be merged ; too large diff. I will pick some cheery).

This commit is contained in:
vorotamoroz
2026-06-12 09:14:10 +01:00
parent b62a565c8a
commit f91b38d4ab
52 changed files with 974 additions and 317 deletions
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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",
},
},
],
}, },
}, },
]); ]);
+163 -4
View File
@@ -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"
} }
} }
} }
+2 -1
View File
@@ -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> {
+10 -4
View File
@@ -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);
} }
} }
+5 -4
View File
@@ -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;
} }
+9 -9
View File
@@ -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;
+31 -17
View File
@@ -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") {
+2 -2
View File
@@ -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,
+2
View File
@@ -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";
+38 -8
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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";
+1 -1
View File
@@ -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",
-28
View File
@@ -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 -1
View File
@@ -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
+16 -8
View File
@@ -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();
} }
} }
+2
View File
@@ -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);
} }
} }
+9 -8
View File
@@ -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"]
+1
View File
@@ -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;
} }
+4 -6
View File
@@ -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,
}; };
+32 -7
View File
@@ -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
View File
@@ -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?.();
} }
} }
} }
+86 -4
View File
@@ -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
+40 -9
View File
@@ -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;
+18 -4
View File
@@ -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");
} }
+42 -9
View File
@@ -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",
}, },
}, },
]); ]);
+81 -4
View File
@@ -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",
+27 -11
View File
@@ -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);
+8 -3
View File
@@ -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}
+3 -3
View File
@@ -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>
+4 -2
View File
@@ -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
View File
@@ -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"],
+1
View File
@@ -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"),
}, },
}, },