Jacob Schmidt f06c1d15bb refactor: encrypt fragments in SQLCipher DB, organize services into domain modules
- Move standalone fragment storage from unencrypted SQLite to the existing
  encrypted SQLCipher database (journal_cache.db)
- Add IDatabaseSessionService/DatabaseSessionService for shared encrypted
  connection management after authentication
- Update fragments table schema: nullable entry_id, add guid column
- Reorganize flat Services/ directory (28 files) into 9 domain modules:
  Ai, Config, Database, Entries, Fragments, Logging, Sidecar, Speech, Vault
- Update all namespace declarations and using statements across all projects
- Update REFACTORING_SUMMARY.md with all changes

Co-Authored-By: Warp <agent@warp.dev>
2026-02-23 21:58:45 -06:00

382 lines
12 KiB
C#

using System.Text;
using Journal.Core.Dtos;
using Journal.Core.Services.Config;
using Journal.Core.Services.Entries;
using Journal.Core.Services.Vault;
namespace Journal.Core.Services.Sidecar;
public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchService entrySearch, IJournalConfigService config)
{
private readonly IVaultStorageService _vaultStorage = vaultStorage;
private readonly IEntrySearchService _entrySearch = entrySearch;
private readonly IJournalConfigService _config = config;
public async Task<int> RunAsync(string[] args, Entry entry)
{
ArgumentNullException.ThrowIfNull(args);
ArgumentNullException.ThrowIfNull(entry);
if (args.Length == 0)
{
await entry.RunAsync();
return 0;
}
if (IsHelp(args[0]))
{
PrintUsage();
return 0;
}
if (string.Equals(args[0], "vault", StringComparison.OrdinalIgnoreCase))
return RunVaultCommand([.. args.Skip(1)]);
if (string.Equals(args[0], "search", StringComparison.OrdinalIgnoreCase))
return RunSearchCommand([.. args.Skip(1)]);
Console.Error.WriteLine($"Unknown command: {args[0]}");
PrintUsage();
return 2;
}
public int RunVaultCommand(string[] args)
{
ArgumentNullException.ThrowIfNull(args);
if (args.Length == 0 || IsHelp(args[0]))
{
PrintVaultUsage();
return 2;
}
var action = args[0].Trim().ToLowerInvariant();
if (action is not ("load" or "save"))
{
Console.Error.WriteLine($"Unknown vault action: {args[0]}");
PrintVaultUsage();
return 2;
}
if (!TryParseVaultOptions([.. args.Skip(1)], out var options, out var parseError))
{
Console.Error.WriteLine(parseError);
PrintVaultUsage();
return 2;
}
var password = options.Password;
if (string.IsNullOrWhiteSpace(password))
password = PromptPassword();
if (string.IsNullOrWhiteSpace(password))
{
Console.Error.WriteLine("Vault password cannot be empty.");
return 2;
}
var (vaultDirectory, dataDirectory) = ResolveDirectories(options.VaultDirectory, options.DataDirectory);
try
{
if (action == "load")
{
var ok = _vaultStorage.LoadAllVaults(password, vaultDirectory, dataDirectory);
if (!ok)
{
Console.Error.WriteLine("Incorrect password.");
return 1;
}
Console.WriteLine($"Vault loaded. Decrypted files are in {dataDirectory}");
return 0;
}
_vaultStorage.RebuildAllVaults(password, vaultDirectory, dataDirectory);
Console.WriteLine($"Vault saved from decrypted files in {dataDirectory}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Vault command failed: {ex.Message}");
return 1;
}
}
public int RunSearchCommand(string[] args)
{
ArgumentNullException.ThrowIfNull(args);
if (args.Length > 0 && IsHelp(args[0]))
{
PrintSearchUsage();
return 0;
}
if (!TryParseSearchOptions(args, out var options, out var parseError))
{
Console.Error.WriteLine(parseError);
PrintSearchUsage();
return 2;
}
var (_, dataDirectory) = ResolveDirectories(vaultOverride: null, options.DataDirectory);
if (!Directory.Exists(dataDirectory) || Directory.GetFiles(dataDirectory, "*.md").Length == 0)
{
Console.WriteLine("No decrypted journal entries found. Please load the vault first: journal vault load");
return 0;
}
try
{
var request = new EntrySearchRequestDto(
DataDirectory: dataDirectory,
Query: options.Query,
Section: options.Section,
StartDate: options.StartDate,
EndDate: options.EndDate,
Tags: options.Tags,
Types: options.Types,
Checked: options.Checked,
Unchecked: options.Unchecked);
var results = _entrySearch.SearchEntriesAsync(request).GetAwaiter().GetResult();
if (results.Count == 0)
{
Console.WriteLine("No entries found matching the criteria.");
return 0;
}
foreach (var result in results)
{
Console.WriteLine($"--- {result.Date} ---");
Console.WriteLine(result.RawContent);
Console.WriteLine();
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Search command failed: {ex.Message}");
return 1;
}
}
private static bool TryParseVaultOptions(string[] args, out VaultOptions options, out string error)
{
var parsed = new VaultOptions();
for (var i = 0; i < args.Length; i++)
{
var token = args[i];
if (IsHelp(token))
{
options = parsed;
error = "";
return false;
}
if (i + 1 >= args.Length)
{
options = parsed;
error = $"Missing value for option '{token}'.";
return false;
}
var value = args[i + 1];
switch (token)
{
case "--password":
case "-p":
parsed.Password = value;
break;
case "--vault-dir":
parsed.VaultDirectory = value;
break;
case "--data-dir":
parsed.DataDirectory = value;
break;
default:
options = parsed;
error = $"Unknown option '{token}'.";
return false;
}
i++;
}
options = parsed;
error = "";
return true;
}
private static bool TryParseSearchOptions(string[] args, out SearchOptions options, out string error)
{
var parsed = new SearchOptions();
for (var i = 0; i < args.Length; i++)
{
var token = args[i];
if (IsHelp(token))
{
options = parsed;
error = "";
return false;
}
if (!token.StartsWith("-", StringComparison.Ordinal))
{
if (parsed.Query is null)
{
parsed.Query = token;
continue;
}
options = parsed;
error = $"Unexpected positional argument '{token}'.";
return false;
}
if (i + 1 >= args.Length)
{
options = parsed;
error = $"Missing value for option '{token}'.";
return false;
}
var value = args[i + 1];
switch (token)
{
case "--data-dir":
parsed.DataDirectory = value;
break;
case "--tag":
case "-t":
parsed.Tags.Add(value);
break;
case "--type":
case "-y":
parsed.Types.Add(value);
break;
case "--start-date":
case "-s":
parsed.StartDate = value;
break;
case "--end-date":
case "-e":
parsed.EndDate = value;
break;
case "--section":
case "-sec":
parsed.Section = value;
break;
case "--checked":
case "-chk":
parsed.Checked.Add(value);
break;
case "--unchecked":
case "-uchk":
parsed.Unchecked.Add(value);
break;
default:
options = parsed;
error = $"Unknown option '{token}'.";
return false;
}
i++;
}
options = parsed;
error = "";
return true;
}
private (string VaultDirectory, string DataDirectory) ResolveDirectories(string? vaultOverride, string? dataOverride)
{
var envVault = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR");
var envData = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR");
var defaults = _config.Current;
var vault = FirstNonEmpty(vaultOverride, envVault) ?? defaults.VaultDirectory;
var data = FirstNonEmpty(dataOverride, envData) ?? defaults.DataDirectory;
return (Path.GetFullPath(vault), Path.GetFullPath(data));
}
private static string? FirstNonEmpty(params string?[] values) =>
values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
private static string PromptPassword()
{
if (Console.IsInputRedirected)
return Console.ReadLine() ?? "";
Console.Write("Vault password: ");
var builder = new StringBuilder();
while (true)
{
var keyInfo = Console.ReadKey(intercept: true);
if (keyInfo.Key == ConsoleKey.Enter)
{
Console.WriteLine();
break;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (builder.Length > 0)
builder.Length--;
continue;
}
if (!char.IsControl(keyInfo.KeyChar))
builder.Append(keyInfo.KeyChar);
}
return builder.ToString();
}
private static bool IsHelp(string token) =>
string.Equals(token, "--help", StringComparison.OrdinalIgnoreCase) ||
string.Equals(token, "-h", StringComparison.OrdinalIgnoreCase) ||
string.Equals(token, "help", StringComparison.OrdinalIgnoreCase);
private static void PrintUsage()
{
Console.WriteLine("Usage:");
Console.WriteLine(" Journal.Sidecar # sidecar stdin/stdout mode");
Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>] [--data-dir <path>]");
Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>] [--data-dir <path>]");
Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>] [--data-dir <path>]");
}
private static void PrintVaultUsage()
{
Console.WriteLine("Vault usage:");
Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>] [--data-dir <path>]");
Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>] [--data-dir <path>]");
}
private static void PrintSearchUsage()
{
Console.WriteLine("Search usage:");
Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>] [--data-dir <path>]");
}
private sealed class VaultOptions
{
public string? Password { get; set; }
public string? VaultDirectory { get; set; }
public string? DataDirectory { get; set; }
}
private sealed class SearchOptions
{
public string? Query { get; set; }
public string? DataDirectory { get; set; }
public string? StartDate { get; set; }
public string? EndDate { get; set; }
public string? Section { get; set; }
public List<string> Tags { get; } = [];
public List<string> Types { get; } = [];
public List<string> Checked { get; } = [];
public List<string> Unchecked { get; } = [];
}
}