introduce ZFIN_HOME
This commit is contained in:
parent
d9ce5b7082
commit
249b5ec2c4
5 changed files with 102 additions and 13 deletions
|
|
@ -139,7 +139,7 @@ Tests use `std.testing.allocator` (which detects leaks) and are structured as un
|
|||
|
||||
- **The `color` parameter flows through everything.** CLI commands accept a `color: bool` parameter. Don't use ANSI escapes unconditionally — always gate on the `color` flag.
|
||||
|
||||
- **Portfolio auto-detection.** Both CLI and TUI auto-load `portfolio.srf` from cwd if no explicit path is given. `watchlist.srf` is similarly auto-detected.
|
||||
- **Portfolio auto-detection.** Both CLI and TUI auto-load `portfolio.srf` from cwd if no explicit path is given. If not found in cwd, falls back to `$ZFIN_HOME/portfolio.srf`. `watchlist.srf` and `.env` follow the same cascade. `metadata.srf` and `accounts.srf` are loaded from the same directory as the resolved portfolio file.
|
||||
|
||||
- **Server sync is optional.** The `ZFIN_SERVER` env var enables parallel cache syncing from a remote zfin-server instance. All server sync code silently no-ops when the URL is null.
|
||||
|
||||
|
|
|
|||
|
|
@ -1173,6 +1173,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, args: []const [
|
|||
var schwab_csv: ?[]const u8 = null;
|
||||
var schwab_summary = false;
|
||||
var portfolio_path: []const u8 = "portfolio.srf";
|
||||
var explicit_portfolio = false;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
|
|
@ -1187,11 +1188,21 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, args: []const [
|
|||
} else if ((std.mem.eql(u8, args[i], "--portfolio") or std.mem.eql(u8, args[i], "-p")) and i + 1 < args.len) {
|
||||
i += 1;
|
||||
portfolio_path = args[i];
|
||||
explicit_portfolio = true;
|
||||
} else if (std.mem.eql(u8, args[i], "--no-color")) {
|
||||
// handled globally
|
||||
}
|
||||
}
|
||||
|
||||
var resolved_pf: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_pf) |r| r.deinit(allocator);
|
||||
if (!explicit_portfolio) {
|
||||
if (svc.config.resolveUserFile(allocator, portfolio_path)) |r| {
|
||||
resolved_pf = r;
|
||||
portfolio_path = r.path;
|
||||
}
|
||||
}
|
||||
|
||||
if (fidelity_csv == null and schwab_csv == null and !schwab_summary) {
|
||||
try cli.stderrPrint(
|
||||
\\Usage: zfin audit [options] [-p <portfolio.srf>]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub const Config = struct {
|
|||
server_url: ?[]const u8 = null,
|
||||
cache_dir: []const u8,
|
||||
cache_dir_owned: bool = false, // true when cache_dir was allocated via path.join
|
||||
zfin_home: ?[]const u8 = null,
|
||||
allocator: ?std.mem.Allocator = null,
|
||||
/// Raw .env file contents (keys/values in env_map point into this).
|
||||
env_buf: ?[]const u8 = null,
|
||||
|
|
@ -33,6 +34,22 @@ pub const Config = struct {
|
|||
self.env_map = parseEnvFile(allocator, buf);
|
||||
}
|
||||
|
||||
self.zfin_home = self.resolve("ZFIN_HOME");
|
||||
|
||||
// Try loading .env file from ZFIN_HOME as well (cwd .env takes priority)
|
||||
if (self.env_buf == null) {
|
||||
if (self.zfin_home) |home| {
|
||||
const env_path = std.fs.path.join(allocator, &.{ home, ".env" }) catch null;
|
||||
if (env_path) |p| {
|
||||
defer allocator.free(p);
|
||||
self.env_buf = std.fs.cwd().readFileAlloc(allocator, p, 4096) catch null;
|
||||
if (self.env_buf) |buf| {
|
||||
self.env_map = parseEnvFile(allocator, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.twelvedata_key = self.resolve("TWELVEDATA_API_KEY");
|
||||
self.polygon_key = self.resolve("POLYGON_API_KEY");
|
||||
self.finnhub_key = self.resolve("FINNHUB_API_KEY");
|
||||
|
|
@ -73,6 +90,33 @@ pub const Config = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub const ResolvedPath = struct {
|
||||
path: []const u8,
|
||||
owned: bool,
|
||||
|
||||
pub fn deinit(self: ResolvedPath, allocator: std.mem.Allocator) void {
|
||||
if (self.owned) allocator.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
/// Resolve a user file, trying cwd first then ZFIN_HOME.
|
||||
/// Returns the path to use; caller must call `deinit()` on the result.
|
||||
pub fn resolveUserFile(self: Config, allocator: std.mem.Allocator, rel_path: []const u8) ?ResolvedPath {
|
||||
if (std.fs.cwd().access(rel_path, .{})) |_| {
|
||||
return .{ .path = rel_path, .owned = false };
|
||||
} else |_| {}
|
||||
|
||||
if (self.zfin_home) |home| {
|
||||
const full = std.fs.path.join(allocator, &.{ home, rel_path }) catch return null;
|
||||
if (std.fs.cwd().access(full, .{})) |_| {
|
||||
return .{ .path = full, .owned = true };
|
||||
} else |_| {
|
||||
allocator.free(full);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn hasAnyKey(self: Config) bool {
|
||||
return self.twelvedata_key != null or
|
||||
self.polygon_key != null or
|
||||
|
|
|
|||
40
src/main.zig
40
src/main.zig
|
|
@ -39,7 +39,7 @@ const usage =
|
|||
\\ --ntm <N> Show +/- N strikes near the money (default: 8)
|
||||
\\
|
||||
\\Portfolio command options:
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\ If no file is given, searches current directory then ZFIN_HOME.
|
||||
\\ -w, --watchlist <FILE> Watchlist file
|
||||
\\ --refresh Force refresh (ignore cache, re-fetch all prices)
|
||||
\\
|
||||
|
|
@ -52,7 +52,7 @@ const usage =
|
|||
\\Analysis command:
|
||||
\\ Reads metadata.srf (classification) and accounts.srf (tax types)
|
||||
\\ from the same directory as the portfolio file.
|
||||
\\ If no file is given, defaults to portfolio.srf in the current directory.
|
||||
\\ If no file is given, searches current directory then ZFIN_HOME.
|
||||
\\
|
||||
\\Environment Variables:
|
||||
\\ TWELVEDATA_API_KEY Twelve Data API key (primary: prices)
|
||||
|
|
@ -61,6 +61,7 @@ const usage =
|
|||
\\ ALPHAVANTAGE_API_KEY Alpha Vantage API key (ETF profiles)
|
||||
\\ OPENFIGI_API_KEY OpenFIGI API key (CUSIP lookup, optional)
|
||||
\\ ZFIN_CACHE_DIR Cache directory (default: ~/.cache/zfin)
|
||||
\\ ZFIN_HOME User file directory (portfolio, watchlist, .env)
|
||||
\\ NO_COLOR Disable colored output (https://no-color.org)
|
||||
\\
|
||||
;
|
||||
|
|
@ -182,26 +183,41 @@ pub fn main() !u8 {
|
|||
} else if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse -w/--watchlist and --refresh flags; file path is first non-flag arg (default: portfolio.srf)
|
||||
var watchlist_path: ?[]const u8 = null;
|
||||
var explicit_watchlist = false;
|
||||
var force_refresh = false;
|
||||
var file_path: []const u8 = "portfolio.srf";
|
||||
var explicit_file = false;
|
||||
var pi: usize = 2;
|
||||
while (pi < args.len) : (pi += 1) {
|
||||
if ((std.mem.eql(u8, args[pi], "--watchlist") or std.mem.eql(u8, args[pi], "-w")) and pi + 1 < args.len) {
|
||||
pi += 1;
|
||||
watchlist_path = args[pi];
|
||||
explicit_watchlist = true;
|
||||
} else if (std.mem.eql(u8, args[pi], "--refresh")) {
|
||||
force_refresh = true;
|
||||
} else if (std.mem.eql(u8, args[pi], "--no-color")) {
|
||||
// already handled globally
|
||||
} else {
|
||||
file_path = args[pi];
|
||||
explicit_file = true;
|
||||
}
|
||||
}
|
||||
// Auto-detect watchlist.srf in cwd if not explicitly provided (same as TUI)
|
||||
if (watchlist_path == null) {
|
||||
if (std.fs.cwd().access("watchlist.srf", .{})) |_| {
|
||||
watchlist_path = "watchlist.srf";
|
||||
} else |_| {}
|
||||
// Resolve default file paths via ZFIN_HOME when not explicitly provided
|
||||
var resolved_pf: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_pf) |r| r.deinit(allocator);
|
||||
if (!explicit_file) {
|
||||
if (config.resolveUserFile(allocator, file_path)) |r| {
|
||||
resolved_pf = r;
|
||||
file_path = r.path;
|
||||
}
|
||||
}
|
||||
var resolved_wl: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_wl) |r| r.deinit(allocator);
|
||||
if (!explicit_watchlist and watchlist_path == null) {
|
||||
if (config.resolveUserFile(allocator, "watchlist.srf")) |r| {
|
||||
resolved_wl = r;
|
||||
watchlist_path = r.path;
|
||||
}
|
||||
}
|
||||
try commands.portfolio.run(allocator, &svc, file_path, watchlist_path, force_refresh, color, out);
|
||||
} else if (std.mem.eql(u8, command, "lookup")) {
|
||||
|
|
@ -227,12 +243,22 @@ pub fn main() !u8 {
|
|||
} else if (std.mem.eql(u8, command, "analysis")) {
|
||||
// File path is first non-flag arg (default: portfolio.srf)
|
||||
var analysis_file: []const u8 = "portfolio.srf";
|
||||
var explicit_analysis = false;
|
||||
for (args[2..]) |arg| {
|
||||
if (!std.mem.startsWith(u8, arg, "--")) {
|
||||
analysis_file = arg;
|
||||
explicit_analysis = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var resolved_af: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_af) |r| r.deinit(allocator);
|
||||
if (!explicit_analysis) {
|
||||
if (config.resolveUserFile(allocator, analysis_file)) |r| {
|
||||
resolved_af = r;
|
||||
analysis_file = r.path;
|
||||
}
|
||||
}
|
||||
try commands.analysis.run(allocator, &svc, analysis_file, color, out);
|
||||
} else {
|
||||
try cli.stderrPrint("Unknown command. Run 'zfin help' for usage.\n");
|
||||
|
|
|
|||
18
src/tui.zig
18
src/tui.zig
|
|
@ -2043,10 +2043,13 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
|||
}
|
||||
}
|
||||
|
||||
var resolved_pf: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_pf) |r| r.deinit(allocator);
|
||||
if (portfolio_path == null and !has_explicit_symbol) {
|
||||
if (std.fs.cwd().access("portfolio.srf", .{})) |_| {
|
||||
portfolio_path = "portfolio.srf";
|
||||
} else |_| {}
|
||||
if (config.resolveUserFile(allocator, "portfolio.srf")) |r| {
|
||||
resolved_pf = r;
|
||||
portfolio_path = r.path;
|
||||
}
|
||||
}
|
||||
|
||||
var keymap = blk: {
|
||||
|
|
@ -2097,10 +2100,15 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
|||
}
|
||||
}
|
||||
|
||||
var resolved_wl: ?zfin.Config.ResolvedPath = null;
|
||||
defer if (resolved_wl) |r| r.deinit(allocator);
|
||||
if (!skip_watchlist) {
|
||||
const wl_path = watchlist_path orelse blk: {
|
||||
std.fs.cwd().access("watchlist.srf", .{}) catch break :blk null;
|
||||
break :blk @as(?[]const u8, "watchlist.srf");
|
||||
if (config.resolveUserFile(allocator, "watchlist.srf")) |r| {
|
||||
resolved_wl = r;
|
||||
break :blk @as(?[]const u8, r.path);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
if (wl_path) |path| {
|
||||
app_inst.watchlist = loadWatchlist(allocator, path);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue