cli: add configurable ignore rules and deployment artifacts

IgnoreRules (src/apps/cli/serviceModules/IgnoreRules.ts):
- Reads .livesync/ignore for user-defined glob patterns
- Applies gitignore matchBase semantics: patterns without / get **/ prefix,
  patterns ending with / get ** appended for directory contents
- Supports `import: .gitignore` directive to merge gitignore patterns
- Rejects negation patterns with a warning (not fully supportable)
- Integrated into both daemon and mirror commands via isTargetFile handler

Wiring:
- IgnoreRules loaded before LiveSyncBaseCore construction so beginWatch()
  receives rules when it fires during onLoad/onFirstInitialise
- Passed through initialiseServiceModulesCLI -> StorageEventManagerCLI ->
  CLIStorageEventManagerAdapter -> CLIWatchAdapter

Deployment:
- src/apps/cli/deploy/livesync-cli.service - systemd unit template
- src/apps/cli/deploy/install.sh - user/system install script

Testing:
- src/apps/cli/test/test-daemon-linux.sh - e2e tests for ignore rules
- src/apps/cli/serviceModules/IgnoreRules.unit.spec.ts - 15 unit tests
- src/apps/cli/commands/daemonCommand.unit.spec.ts - 7 unit tests
This commit is contained in:
Andrew Leech
2026-04-01 19:22:24 +11:00
parent e6ae516493
commit c0ad8ee15a
14 changed files with 856 additions and 64 deletions

View File

@@ -15,6 +15,7 @@ import type { Stats } from "fs";
import * as fs from "fs/promises";
import * as path from "path";
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
/**
* CLI-specific type guard adapter
@@ -96,7 +97,7 @@ class CLIConverterAdapter implements IStorageEventConverterAdapter<NodeFile> {
class CLIWatchAdapter implements IStorageEventWatchAdapter {
private _watcher: FSWatcher | undefined;
constructor(private basePath: string, private watchEnabled: boolean = false) {}
constructor(private basePath: string, private ignoreRules?: IgnoreRules, private watchEnabled: boolean = false) {}
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
return {
@@ -110,18 +111,6 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
};
}
private _toNodeFileStub(filePath: string): NodeFile {
return {
path: path.relative(this.basePath, filePath) as FilePath,
stat: {
ctime: Date.now(),
mtime: Date.now(),
size: 0,
type: "file",
},
};
}
private _toNodeFolder(dirPath: string): NodeFolder {
return {
path: path.relative(this.basePath, dirPath) as FilePath,
@@ -131,10 +120,19 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
if (!this.watchEnabled) return;
const baseIgnored: Array<RegExp | string | ((p: string) => boolean)> = [
/(^|[/\\])\./,
/(^|[/\\])[^/\\]*-livesync-v2([/\\]|$)/,
];
// Bind rules to a local const before the closure — chokidar v4 requires a
// MatchFunction, not glob strings, for custom patterns.
const rules = this.ignoreRules;
const ignored = rules
? [...baseIgnored, (p: string) => rules.shouldIgnore(path.relative(this.basePath, p))]
: baseIgnored;
const watcher = chokidarWatch(this.basePath, {
ignored: [
/(^|[/\\])\./,
],
ignored,
ignoreInitial: true,
persistent: true,
awaitWriteFinish: {
@@ -154,7 +152,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
});
watcher.on("unlink", (filePath) => {
const nodeFile = this._toNodeFileStub(filePath);
const nodeFile = this._toNodeFile(filePath, undefined);
handlers.onDelete(nodeFile);
});
@@ -199,10 +197,10 @@ export class CLIStorageEventManagerAdapter implements IStorageEventManagerAdapte
readonly status: CLIStatusAdapter;
readonly converter: CLIConverterAdapter;
constructor(basePath: string, watchEnabled: boolean = false) {
constructor(basePath: string, ignoreRules?: IgnoreRules, watchEnabled: boolean = false) {
this.typeGuard = new CLITypeGuardAdapter();
this.persistence = new CLIPersistenceAdapter(basePath);
this.watch = new CLIWatchAdapter(basePath, watchEnabled);
this.watch = new CLIWatchAdapter(basePath, ignoreRules, watchEnabled);
this.status = new CLIStatusAdapter();
this.converter = new CLIConverterAdapter();
}