Refactor: separate entrypoint and main,

Fix: readlng binary file
This commit is contained in:
vorotamoroz
2026-03-12 19:41:10 +09:00
parent d4aedf59f3
commit 822d957976
8 changed files with 95 additions and 37 deletions

View File

@@ -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

View 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]);
});
});

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env node
import { main } from "./main";
main().catch((error) => {
console.error(`[Fatal Error]`, error);
process.exit(1);
});

View File

@@ -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);
});
}

View File

@@ -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

View File

@@ -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",
},

Submodule src/lib updated: 4346ead9c8...d94c9b3ed7

View File

@@ -3,7 +3,25 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## -- Unreleased2 --
## Unnamed 12th March, 2026
12th March, 2026
### Fixed
- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time).
### Internal behaviour change (or fix)
- Journal Replicator now yields true after the replication is done.
### CLI
- Add more tests.
- Object Storage support has also been confirmed (and fixed) in CLI.
- Yes, we have finally managed to 'get one file'.
## Unnamed 11th March, 2026
11th March, 2026 (second commit).
@@ -23,7 +41,7 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
### New something
- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless, and a dedicated version.
## -- Unreleased --
## Unnamed 11th March, 2026
11th March, 2026