mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-03 14:21:52 +00:00
Refactor: separate entrypoint and main,
Fix: readlng binary file
This commit is contained in:
@@ -187,9 +187,6 @@ TODO: Conflict and resolution checks for real local databases.
|
||||
- `serve`: Start CLI in server mode, exposing REST APIs for remote, and batch operations.
|
||||
- `cause-conflicted <vaultPath>`: Mark a file as conflicted without changing its content, to trigger conflict resolution in Obsidian.
|
||||
|
||||
## Current Limitations and known issues
|
||||
- Binary files are not supported yet (it seems... but I haven't tested this yet).
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Bootstrap a new headless vault
|
||||
|
||||
40
src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts
Normal file
40
src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||
|
||||
describe("NodeStorageAdapter binary I/O", () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createAdapter() {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-cli-node-storage-"));
|
||||
tempDirs.push(tempDir);
|
||||
return new NodeStorageAdapter(tempDir);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
it("writes and reads binary data without corruption", async () => {
|
||||
const adapter = await createAdapter();
|
||||
const expected = Uint8Array.from([0x00, 0x7f, 0x80, 0xff, 0x42]);
|
||||
|
||||
await adapter.writeBinary("files/blob.bin", expected.buffer.slice(0));
|
||||
const result = await adapter.readBinary("files/blob.bin");
|
||||
|
||||
expect(Array.from(new Uint8Array(result))).toEqual(Array.from(expected));
|
||||
});
|
||||
|
||||
it("returns an ArrayBuffer with the exact file length", async () => {
|
||||
const adapter = await createAdapter();
|
||||
const expected = Uint8Array.from([0x10, 0x20, 0x30]);
|
||||
|
||||
await adapter.writeBinary("files/small.bin", expected.buffer.slice(0));
|
||||
const result = await adapter.readBinary("files/small.bin");
|
||||
|
||||
expect(result.byteLength).toBe(expected.byteLength);
|
||||
expect(Array.from(new Uint8Array(result))).toEqual([0x10, 0x20, 0x30]);
|
||||
});
|
||||
});
|
||||
7
src/apps/cli/entrypoint.ts
Normal file
7
src/apps/cli/entrypoint.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { main } from "./main";
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[Fatal Error]`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Self-hosted LiveSync CLI
|
||||
* Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian
|
||||
@@ -22,7 +21,6 @@ if (!("localStorage" in globalThis)) {
|
||||
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
||||
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
|
||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
||||
@@ -201,7 +199,7 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
||||
console.log(`[Done] Created settings file: ${targetPath}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const options = parseArgs();
|
||||
const avoidStdoutNoise =
|
||||
options.command === "cat" ||
|
||||
@@ -373,21 +371,3 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run main only when invoked as the entrypoint, not when imported by tests.
|
||||
const isEntryPoint = (() => {
|
||||
const argv1 = process.argv[1];
|
||||
if (!argv1) return false;
|
||||
try {
|
||||
return import.meta.url === pathToFileURL(argv1).href;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (isEntryPoint) {
|
||||
main().catch((error) => {
|
||||
console.error(`[Fatal Error]`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,6 +134,18 @@ assert_command_fails() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_files_equal() {
|
||||
local expected_file="$1"
|
||||
local actual_file="$2"
|
||||
local message="$3"
|
||||
if ! cmp -s "$expected_file" "$actual_file"; then
|
||||
echo "[FAIL] $message" >&2
|
||||
echo "[FAIL] expected sha256: $(sha256sum "$expected_file" | awk '{print $1}')" >&2
|
||||
echo "[FAIL] actual sha256: $(sha256sum "$actual_file" | awk '{print $1}')" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sanitise_cat_stdout() {
|
||||
sed '/^\[CLIWatchAdapter\] File watching is not enabled in CLI version$/d'
|
||||
}
|
||||
@@ -295,6 +307,7 @@ TARGET_A_ONLY="e2e/a-only-info.md"
|
||||
TARGET_SYNC="e2e/sync-info.md"
|
||||
TARGET_PUSH="e2e/pushed-from-a.md"
|
||||
TARGET_PUT="e2e/put-from-a.md"
|
||||
TARGET_PUSH_BINARY="e2e/pushed-from-a.bin"
|
||||
TARGET_CONFLICT="e2e/conflict.md"
|
||||
|
||||
echo "[CASE] A puts and A can get info"
|
||||
@@ -318,18 +331,21 @@ run_cli_a push "$PUSH_SRC" "$TARGET_PUSH" >/dev/null
|
||||
printf 'put-content-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_PUT" >/dev/null
|
||||
sync_both
|
||||
run_cli_b pull "$TARGET_PUSH" "$PULL_DST" >/dev/null
|
||||
if ! cmp -s "$PUSH_SRC" "$PULL_DST"; then
|
||||
echo "[FAIL] B pull result does not match pushed source" >&2
|
||||
echo "--- source ---" >&2
|
||||
cat "$PUSH_SRC" >&2
|
||||
echo "--- pulled ---" >&2
|
||||
cat "$PULL_DST" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_files_equal "$PUSH_SRC" "$PULL_DST" "B pull result does not match pushed source"
|
||||
CAT_B_PUT="$(run_cli_b cat "$TARGET_PUT" | sanitise_cat_stdout)"
|
||||
assert_equal "put-content-$DB_SUFFIX" "$CAT_B_PUT" "B cat should return A put content"
|
||||
echo "[PASS] push/pull and put/cat across vaults"
|
||||
|
||||
echo "[CASE] A pushes binary, both sync, and B can pull identical bytes"
|
||||
PUSH_BINARY_SRC="$WORK_DIR/push-source.bin"
|
||||
PULL_BINARY_DST="$WORK_DIR/pull-destination.bin"
|
||||
head -c 4096 /dev/urandom > "$PUSH_BINARY_SRC"
|
||||
run_cli_a push "$PUSH_BINARY_SRC" "$TARGET_PUSH_BINARY" >/dev/null
|
||||
sync_both
|
||||
run_cli_b pull "$TARGET_PUSH_BINARY" "$PULL_BINARY_DST" >/dev/null
|
||||
assert_files_equal "$PUSH_BINARY_SRC" "$PULL_BINARY_DST" "B pull result does not match pushed binary source"
|
||||
echo "[PASS] binary push/pull across vaults"
|
||||
|
||||
echo "[CASE] A removes, both sync, and B can no longer cat"
|
||||
run_cli_a rm "$TARGET_PUT" >/dev/null
|
||||
sync_both
|
||||
|
||||
@@ -33,7 +33,7 @@ export default defineConfig({
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.resolve(__dirname, "main.ts"),
|
||||
index: path.resolve(__dirname, "entrypoint.ts"),
|
||||
},
|
||||
external: (id) => {
|
||||
if (defaultExternal.includes(id)) return true;
|
||||
@@ -48,7 +48,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "main.ts"),
|
||||
entry: path.resolve(__dirname, "entrypoint.ts"),
|
||||
formats: ["cjs"],
|
||||
fileName: "index",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user