rework framework to something much more sane

This commit is contained in:
Emil Lerch 2026-05-18 17:39:19 -07:00
parent 8e4dfffc3f
commit 4e6ae0ba51
Signed by: lobo
GPG key ID: A7B62D657EF764F8
22 changed files with 597 additions and 601 deletions

View file

@ -7,23 +7,24 @@ const Money = @import("../Money.zig");
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "analysis";
pub const group: framework.Group = .portfolio;
pub const synopsis: []const u8 = "Show portfolio breakdowns by asset class, sector, geo, account, tax type";
pub const help: []const u8 =
\\Usage: zfin analysis
\\
\\Show portfolio analysis: equities/fixed-income split, plus
\\block-bar breakdowns by asset class, sector, geographic
\\region, account, and tax type. Reads classifications from
\\`metadata.srf` and account tax types from `accounts.srf`
\\(both in the same directory as the portfolio file).
\\
\\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
\\classifications, then edit by hand.
\\
;
pub const meta: framework.Meta = .{
.name = "analysis",
.group = .portfolio,
.synopsis = "Show portfolio breakdowns by asset class, sector, geo, account, tax type",
.help =
\\Usage: zfin analysis
\\
\\Show portfolio analysis: equities/fixed-income split, plus
\\block-bar breakdowns by asset class, sector, geographic
\\region, account, and tax type. Reads classifications from
\\`metadata.srf` and account tax types from `accounts.srf`
\\(both in the same directory as the portfolio file).
\\
\\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
\\classifications, then edit by hand.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -2250,32 +2250,33 @@ pub const ParsedArgs = struct {
stale_days: u32 = default_stale_days,
};
pub const meta = struct {
pub const name: []const u8 = "audit";
pub const group: framework.Group = .hygiene;
pub const synopsis: []const u8 = "Reconcile portfolio against brokerage exports + portfolio hygiene check";
pub const help: []const u8 =
\\Usage: zfin audit [opts]
\\
\\Two modes in one command:
\\
\\ Flagless: run the portfolio hygiene check — surfaces stale
\\ manual prices, account-cadence violations, and brokerage-file
\\ candidates discovered automatically.
\\
\\ With brokerage flags: reconcile the portfolio against the
\\ given export and report discrepancies.
\\
\\Options:
\\ --verbose Show full reconciliation output even when clean
\\ --stale-days <N> Manual price staleness threshold (default 3)
\\ --fidelity <CSV> Fidelity positions CSV export
\\ ("All accounts" → Positions tab → Download)
\\ --schwab <CSV> Schwab per-account positions CSV export
\\ --schwab-summary Schwab account summary; copy from accounts
\\ summary page, paste to stdin, then ^D
\\
;
pub const meta: framework.Meta = .{
.name = "audit",
.group = .hygiene,
.synopsis = "Reconcile portfolio against brokerage exports + portfolio hygiene check",
.help =
\\Usage: zfin audit [opts]
\\
\\Two modes in one command:
\\
\\ Flagless: run the portfolio hygiene check — surfaces stale
\\ manual prices, account-cadence violations, and brokerage-file
\\ candidates discovered automatically.
\\
\\ With brokerage flags: reconcile the portfolio against the
\\ given export and report discrepancies.
\\
\\Options:
\\ --verbose Show full reconciliation output even when clean
\\ --stale-days <N> Manual price staleness threshold (default 3)
\\ --fidelity <CSV> Fidelity positions CSV export
\\ ("All accounts" → Positions tab → Download)
\\ --schwab <CSV> Schwab per-account positions CSV export
\\ --schwab-summary Schwab account summary; copy from accounts
\\ summary page, paste to stdin, then ^D
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -13,25 +13,26 @@ pub const ParsedArgs = struct {
sub: Subcommand,
};
pub const meta = struct {
pub const name: []const u8 = "cache";
pub const group: framework.Group = .infra;
pub const synopsis: []const u8 = "Inspect or clear the local provider-data cache";
pub const help: []const u8 =
\\Usage: zfin cache <stats|clear>
\\
\\Subcommands:
\\ stats List every cached symbol with per-data-type size,
\\ age, and freshness state. Stale entries (past TTL)
\\ are flagged. Includes the cusip_tickers.srf file
\\ if present.
\\ clear Delete every file under the cache directory.
\\ No confirmation; the next provider call will
\\ re-fetch everything.
\\
\\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`.
\\
;
pub const meta: framework.Meta = .{
.name = "cache",
.group = .infra,
.synopsis = "Inspect or clear the local provider-data cache",
.help =
\\Usage: zfin cache <stats|clear>
\\
\\Subcommands:
\\ stats List every cached symbol with per-data-type size,
\\ age, and freshness state. Stale entries (past TTL)
\\ are flagged. Includes the cusip_tickers.srf file
\\ if present.
\\ clear Delete every file under the cache directory.
\\ No confirmation; the next provider call will
\\ re-fetch everything.
\\
\\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -87,44 +87,45 @@ pub const ParsedArgs = struct {
commit_after: ?git.CommitSpec = null,
};
pub const meta = struct {
pub const name: []const u8 = "compare";
pub const group: framework.Group = .timeseries;
pub const synopsis: []const u8 = "Compare portfolio at two points in time (or vs. today)";
pub const help: []const u8 =
\\Usage:
\\ zfin compare <DATE> compare DATE vs. live
\\ zfin compare <DATE1> <DATE2> compare two historical dates
\\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE]
\\ [--commit-before SPEC] [--commit-after SPEC]
\\ [--projections [--no-events]]
\\
\\Compare two points in time for the portfolio: liquid totals,
\\per-symbol price moves, attribution / contributions diff. The
\\positional happy-path is what most users want; the named
\\overrides exist for the cases where the snapshot date and the
\\git-commit-at-that-date diverge (e.g. you committed two days
\\after a Sunday review).
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working.
\\
\\Options:
\\ --projections Include projected-return + SWR@99%
\\ deltas. Adds ~1-2s per endpoint
\\ (Monte Carlo SWR search).
\\ --no-events (with --projections) exclude life
\\ events from the projection.
\\ --snapshot-before <DATE> Override the before-side snapshot.
\\ --snapshot-after <DATE> Override the after-side snapshot;
\\ accepts `live` for the current
\\ portfolio.
\\ --commit-before <SPEC> Pin the before-side commit for the
\\ attribution / contributions block.
\\ --commit-after <SPEC> Pin the after-side commit; accepts
\\ `working` for the working copy.
\\
;
pub const meta: framework.Meta = .{
.name = "compare",
.group = .timeseries,
.synopsis = "Compare portfolio at two points in time (or vs. today)",
.help =
\\Usage:
\\ zfin compare <DATE> compare DATE vs. live
\\ zfin compare <DATE1> <DATE2> compare two historical dates
\\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE]
\\ [--commit-before SPEC] [--commit-after SPEC]
\\ [--projections [--no-events]]
\\
\\Compare two points in time for the portfolio: liquid totals,
\\per-symbol price moves, attribution / contributions diff. The
\\positional happy-path is what most users want; the named
\\overrides exist for the cases where the snapshot date and the
\\git-commit-at-that-date diverge (e.g. you committed two days
\\after a Sunday review).
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working.
\\
\\Options:
\\ --projections Include projected-return + SWR@99%
\\ deltas. Adds ~1-2s per endpoint
\\ (Monte Carlo SWR search).
\\ --no-events (with --projections) exclude life
\\ events from the projection.
\\ --snapshot-before <DATE> Override the before-side snapshot.
\\ --snapshot-after <DATE> Override the after-side snapshot;
\\ accepts `live` for the current
\\ portfolio.
\\ --commit-before <SPEC> Pin the before-side commit for the
\\ attribution / contributions block.
\\ --commit-after <SPEC> Pin the after-side commit; accepts
\\ `working` for the working copy.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -187,44 +187,45 @@ pub const ParsedArgs = struct {
after: ?git.CommitSpec = null,
};
pub const meta = struct {
pub const name: []const u8 = "contributions";
pub const group: framework.Group = .timeseries;
pub const synopsis: []const u8 = "Show money added since the last recorded state in git";
pub const help: []const u8 =
\\Usage: zfin contributions [opts]
\\
\\Show contributions, withdrawals, and lot-level changes between
\\two points in the portfolio's git history. Four modes:
\\
\\ No flags (default):
\\ dirty working tree: HEAD vs working copy
\\ clean working tree: HEAD~1 vs HEAD (review last commit)
\\ --since <DATE> commit-at-or-before(DATE) vs HEAD (or
\\ working copy when dirty)
\\ --since <D1> --until <D2> commit-at-or-before(D1) vs
\\ commit-at-or-before(D2)
\\ --until <DATE> alone rejected; window is ambiguous
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\
\\Options:
\\ --since <DATE> Earliest side (resolves to commit-at-
\\ or-before).
\\ --until <DATE> Latest side. Pair with --since.
\\ --commit-before <SPEC> Pin the before commit directly. Same
\\ grammar as --commit-after, minus
\\ `working`. Useful when you committed
\\ after your review date.
\\ --commit-after <SPEC> Pin the after commit. SPEC accepts
\\ YYYY-MM-DD, relative (1W/1M/1Q/1Y),
\\ HEAD, HEAD~N, hex SHA, or `working`
\\ for the working copy.
\\
\\--since and --commit-before describe the same axis; pass at most
\\one. Same for --until and --commit-after.
\\
;
pub const meta: framework.Meta = .{
.name = "contributions",
.group = .timeseries,
.synopsis = "Show money added since the last recorded state in git",
.help =
\\Usage: zfin contributions [opts]
\\
\\Show contributions, withdrawals, and lot-level changes between
\\two points in the portfolio's git history. Four modes:
\\
\\ No flags (default):
\\ dirty working tree: HEAD vs working copy
\\ clean working tree: HEAD~1 vs HEAD (review last commit)
\\ --since <DATE> commit-at-or-before(DATE) vs HEAD (or
\\ working copy when dirty)
\\ --since <D1> --until <D2> commit-at-or-before(D1) vs
\\ commit-at-or-before(D2)
\\ --until <DATE> alone rejected; window is ambiguous
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\
\\Options:
\\ --since <DATE> Earliest side (resolves to commit-at-
\\ or-before).
\\ --until <DATE> Latest side. Pair with --since.
\\ --commit-before <SPEC> Pin the before commit directly. Same
\\ grammar as --commit-after, minus
\\ `working`. Useful when you committed
\\ after your review date.
\\ --commit-after <SPEC> Pin the after commit. SPEC accepts
\\ YYYY-MM-DD, relative (1W/1M/1Q/1Y),
\\ HEAD, HEAD~N, hex SHA, or `working`
\\ for the working copy.
\\
\\--since and --commit-before describe the same axis; pass at most
\\one. Same for --until and --commit-after.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -8,23 +8,23 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "divs";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show dividend history (with TTM yield) for a symbol";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin divs <SYMBOL>
\\
\\Show the dividend history for a symbol from Polygon.io. Cached
\\for 14 days. The TTM (trailing-twelve-month) total + yield are
\\computed against the current Yahoo quote when available.
\\
\\Examples:
\\ zfin divs VTI # quarterly distributions + yield
\\ zfin divs T # historical AT&T dividends
\\
;
pub const meta: framework.Meta = .{
.name = "divs",
.group = .symbol_lookup,
.synopsis = "Show dividend history (with TTM yield) for a symbol",
.uppercase_first_arg = true,
.help =
\\Usage: zfin divs <SYMBOL>
\\
\\Show the dividend history for a symbol from Polygon.io. Cached
\\for 14 days. The TTM (trailing-twelve-month) total + yield are
\\computed against the current Yahoo quote when available.
\\
\\Examples:
\\ zfin divs VTI # quarterly distributions + yield
\\ zfin divs T # historical AT&T dividends
\\
,
};
comptime {

View file

@ -8,29 +8,29 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "earnings";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show earnings history (with EPS surprise) and upcoming events";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin earnings <SYMBOL>
\\
\\Show the earnings history (estimate vs. actual + surprise %)
\\and any scheduled future events for a symbol from Financial
\\Modeling Prep. Cached for 30 days; the cache is also smart-
\\refreshed when a past event is missing its `actual` field
\\(catches "results just released" cases without waiting for
\\TTL expiry).
\\
\\Output is sorted newest-first; pipe through `| tail` for
\\oldest-first.
\\
\\Examples:
\\ zfin earnings NVDA
\\ zfin earnings AAPL | head -8 # last two years of quarters
\\
;
pub const meta: framework.Meta = .{
.name = "earnings",
.group = .symbol_lookup,
.synopsis = "Show earnings history (with EPS surprise) and upcoming events",
.uppercase_first_arg = true,
.help =
\\Usage: zfin earnings <SYMBOL>
\\
\\Show the earnings history (estimate vs. actual + surprise %)
\\and any scheduled future events for a symbol from Financial
\\Modeling Prep. Cached for 30 days; the cache is also smart-
\\refreshed when a past event is missing its `actual` field
\\(catches "results just released" cases without waiting for
\\TTL expiry).
\\
\\Output is sorted newest-first; pipe through `| tail` for
\\oldest-first.
\\
\\Examples:
\\ zfin earnings NVDA
\\ zfin earnings AAPL | head -8 # last two years of quarters
\\
,
};
comptime {

View file

@ -12,34 +12,35 @@ pub const ParsedArgs = struct {
arg: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "enrich";
pub const group: framework.Group = .hygiene;
pub const synopsis: []const u8 = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)";
pub const help: []const u8 =
\\Usage: zfin enrich <FILE|SYMBOL>
\\
\\Bootstrap a `metadata.srf` classification file from Alpha
\\Vantage's OVERVIEW endpoint. Two modes:
\\
\\ - File mode (path or `*.srf` suffix): enrich every stock
\\ symbol in the portfolio. Output is a complete SRF file
\\ written to stdout — redirect into metadata.srf and
\\ edit by hand for accuracy.
\\ - Symbol mode (anything else): enrich a single symbol and
\\ emit one appendable SRF line. Useful for adding to an
\\ existing metadata.srf without rerunning the whole file.
\\
\\Caveats: Alpha Vantage's free tier is 25 requests/day. The
\\OVERVIEW data is US-domicile-biased — international ETFs
\\classify as `geo::US`. Always review the output before
\\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY.
\\
\\Examples:
\\ zfin enrich portfolio.srf > metadata.srf # whole portfolio
\\ zfin enrich AAPL >> metadata.srf # single symbol append
\\
;
pub const meta: framework.Meta = .{
.name = "enrich",
.group = .hygiene,
.synopsis = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)",
.help =
\\Usage: zfin enrich <FILE|SYMBOL>
\\
\\Bootstrap a `metadata.srf` classification file from Alpha
\\Vantage's OVERVIEW endpoint. Two modes:
\\
\\ - File mode (path or `*.srf` suffix): enrich every stock
\\ symbol in the portfolio. Output is a complete SRF file
\\ written to stdout — redirect into metadata.srf and
\\ edit by hand for accuracy.
\\ - Symbol mode (anything else): enrich a single symbol and
\\ emit one appendable SRF line. Useful for adding to an
\\ existing metadata.srf without rerunning the whole file.
\\
\\Caveats: Alpha Vantage's free tier is 25 requests/day. The
\\OVERVIEW data is US-domicile-biased — international ETFs
\\classify as `geo::US`. Always review the output before
\\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY.
\\
\\Examples:
\\ zfin enrich portfolio.srf > metadata.srf # whole portfolio
\\ zfin enrich AAPL >> metadata.srf # single symbol append
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -8,24 +8,24 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "etf";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show ETF profile (holdings, sectors, expense ratio)";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin etf <SYMBOL>
\\
\\Show the ETF profile (expense ratio, AUM, dividend yield,
\\sector allocation, top holdings) for a fund symbol from
\\Alpha Vantage. Cached for 30 days. Leveraged funds are
\\flagged in red.
\\
\\Examples:
\\ zfin etf VTI # broad market index
\\ zfin etf TQQQ # leveraged (warning surfaced)
\\
;
pub const meta: framework.Meta = .{
.name = "etf",
.group = .symbol_lookup,
.synopsis = "Show ETF profile (holdings, sectors, expense ratio)",
.uppercase_first_arg = true,
.help =
\\Usage: zfin etf <SYMBOL>
\\
\\Show the ETF profile (expense ratio, AUM, dividend yield,
\\sector allocation, top holdings) for a fund symbol from
\\Alpha Vantage. Cached for 30 days. Leveraged funds are
\\flagged in red.
\\
\\Examples:
\\ zfin etf VTI # broad market index
\\ zfin etf TQQQ # leveraged (warning surfaced)
\\
,
};
comptime {

View file

@ -11,11 +11,12 @@
//! ```zig
//! pub const ParsedArgs = ...; // struct or tagged union
//!
//! pub const meta = struct {
//! pub const name: []const u8 = "perf";
//! pub const group: framework.Group = .symbol_lookup;
//! pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns";
//! pub const help: []const u8 = ...; // multi-line; rendered by `--help`
//! pub const meta: framework.Meta = .{
//! .name = "perf",
//! .group = .symbol_lookup,
//! .synopsis = "Show 1y/3y/5y/10y trailing returns",
//! .help = "...", // multi-line; rendered by `--help`
//! .uppercase_first_arg = true,
//! };
//!
//! pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs;
@ -72,6 +73,58 @@ pub const Group = enum {
}
};
// Meta
/// Per-command metadata. Every command module declares
/// `pub const meta: framework.Meta = .{ ... };` with all fields
/// populated. The compiler enforces the contract via the field
/// types; `validateCommandModule` reduces to a single
/// `expectDeclWithType` check on the declaration's type.
///
/// Fields are deliberately required (no defaults) so command
/// authors think about each one rather than relying on framework
/// defaults. In particular, `uppercase_first_arg` deserves an
/// explicit decision per command see field doc-comment.
pub const Meta = struct {
/// User-facing subcommand name. Must match the field name used
/// for this command in the `command_modules` registry literal
/// (the dispatcher matches by `f.name`).
name: []const u8,
/// Workflow-grouping tag for the `zfin help` sectioned output.
/// See `Group` for the canonical taxonomy.
group: Group,
/// One-line description rendered next to the command name in
/// `zfin help`.
synopsis: []const u8,
/// Multi-line help text rendered by `zfin <cmd> --help`. The
/// dispatcher emits this verbatim; include the usage line,
/// flag descriptions, and any caveats. Trailing newline is
/// added automatically if missing.
help: []const u8,
/// Whether to uppercase `cmd_args[0]` before calling
/// `parseArgs`. Symbol-taking commands (perf, quote, divs,
/// splits, options, earnings, etf, history-symbol-mode,
/// lookup) set this to `true` so users can type
/// `zfin perf aapl` and have it normalized to `AAPL`.
/// Non-symbol commands (cache, version, portfolio, ...) set
/// it to `false`.
///
/// The dispatcher's `normalizeFirstArg` honors a "skip when
/// arg starts with `-`" rule, so flag-leading invocations
/// like `zfin history --since 2026-01-01` pass through
/// unchanged regardless of this setting.
///
/// No default every command author must make an explicit
/// choice. This is metadata about command shape, not an
/// optional opt-in; the `false` answer is just as load-bearing
/// as `true`.
uppercase_first_arg: bool,
};
// Refresh policy
/// Cache-freshness policy for the invocation. Defined here ahead of
@ -218,62 +271,22 @@ pub fn validateCommandModule(comptime Module: type) void {
@compileError("Command module `" ++ mod_name ++ "` is missing `pub const ParsedArgs = ...;` " ++
"(use `pub const ParsedArgs = struct {};` if the command takes no arguments)");
}
if (!@hasDecl(Module, "meta")) {
@compileError("Command module `" ++ mod_name ++ "` is missing `pub const meta = struct { ... };` " ++
"(see src/commands/framework.zig for the full contract)");
}
const meta_decl = Module.meta;
const ParsedArgs = Module.ParsedArgs;
// meta fields
// meta
// One check covers all five fields. The compiler enforces
// field types via `Meta`'s declaration; missing fields
// surface as Zig's native "missing field" comptime errors
// pointing at the call site.
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"name",
[]const u8,
"pub const name: []const u8 = \"...\";",
Module,
"meta",
Meta,
"pub const meta: framework.Meta = .{ .name = \"...\", .group = ..., .synopsis = \"...\", .help = \"...\", .uppercase_first_arg = ... };",
);
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"group",
Group,
"pub const group: framework.Group = .symbol_lookup;",
);
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"synopsis",
[]const u8,
"pub const synopsis: []const u8 = \"one-line description for `zfin help`\";",
);
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"help",
[]const u8,
"pub const help: []const u8 = \"multi-line text for `zfin <cmd> --help`\";",
);
// Optional: `uppercase_first_arg`. Symbol-taking commands
// (perf, quote, divs, ...) set this to `true` so the
// dispatcher uppercases `cmd_args[0]` before calling
// `parseArgs`. When omitted, defaults to false.
if (@hasDecl(meta_decl, "uppercase_first_arg")) {
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"uppercase_first_arg",
bool,
"pub const uppercase_first_arg: bool = true; // (optional, defaults to false)",
);
}
// parseArgs
validator.expectFnInferredError(
@ -301,16 +314,6 @@ pub fn validateCommandModule(comptime Module: type) void {
// Help rendering
/// Whether a command module opts into having its first positional
/// argument uppercased before `parseArgs` runs. Symbol-taking
/// commands set `meta.uppercase_first_arg = true` so users can type
/// `zfin perf aapl` and have it normalize to `AAPL`. Honors the
/// "skip when the arg is a flag" rule (i.e. starts with `-`).
pub fn uppercasesFirstArg(comptime Module: type) bool {
if (!@hasDecl(Module.meta, "uppercase_first_arg")) return false;
return Module.meta.uppercase_first_arg;
}
/// If `cmd_args[0]` exists, is non-empty, and isn't a flag, return
/// a copy with the first byte-string uppercased. Otherwise return
/// `cmd_args` unchanged. The new slice is owned by `allocator`
@ -405,11 +408,12 @@ const testing = std.testing;
const ProbeModule = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "probe";
pub const group: Group = .infra;
pub const synopsis: []const u8 = "test probe";
pub const help: []const u8 = "Usage: zfin probe\n";
pub const meta: Meta = .{
.name = "probe",
.group = .infra,
.synopsis = "test probe",
.help = "Usage: zfin probe\n",
.uppercase_first_arg = false,
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
@ -453,11 +457,12 @@ test "printCommandHelp: writes meta.help and ensures trailing newline" {
test "printCommandHelp: appends newline when meta.help lacks one" {
const NoNewline = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "x";
pub const group: Group = .infra;
pub const synopsis: []const u8 = "x";
pub const help: []const u8 = "no trailing newline";
pub const meta: Meta = .{
.name = "x",
.group = .infra,
.synopsis = "x",
.help = "no trailing newline",
.uppercase_first_arg = false,
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -470,29 +475,6 @@ test "printCommandHelp: appends newline when meta.help lacks one" {
try testing.expectEqualStrings("no trailing newline\n", w.buffered());
}
test "uppercasesFirstArg: defaults to false when meta omits the field" {
try testing.expect(!uppercasesFirstArg(ProbeModule));
}
test "uppercasesFirstArg: honors meta.uppercase_first_arg when present" {
const M = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "m";
pub const group: Group = .symbol_lookup;
pub const synopsis: []const u8 = "m";
pub const help: []const u8 = "m";
pub const uppercase_first_arg: bool = true;
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
}
pub fn run(_: *RunCtx, _: ParsedArgs) !void {}
};
comptime validateCommandModule(M);
try testing.expect(uppercasesFirstArg(M));
}
test "normalizeFirstArg: empty args returns slice unchanged" {
const empty: []const []const u8 = &.{};
const out = try normalizeFirstArg(testing.allocator, empty);
@ -530,11 +512,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
const FakeRegistry = struct {
const ProbeA = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "alpha";
pub const group: Group = .symbol_lookup;
pub const synopsis: []const u8 = "alpha synopsis";
pub const help: []const u8 = "alpha help\n";
pub const meta: Meta = .{
.name = "alpha",
.group = .symbol_lookup,
.synopsis = "alpha synopsis",
.help = "alpha help\n",
.uppercase_first_arg = false,
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -543,11 +526,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
};
const ProbeB = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "beta";
pub const group: Group = .infra;
pub const synopsis: []const u8 = "beta synopsis";
pub const help: []const u8 = "beta help\n";
pub const meta: Meta = .{
.name = "beta",
.group = .infra,
.synopsis = "beta synopsis",
.help = "beta help\n",
.uppercase_first_arg = false,
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -584,11 +568,12 @@ test "printGroupedUsage: omits empty groups" {
const FakeRegistry = struct {
const Probe = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "only";
pub const group: Group = .infra;
pub const synopsis: []const u8 = "only synopsis";
pub const help: []const u8 = "h\n";
pub const meta: Meta = .{
.name = "only",
.group = .infra,
.synopsis = "only synopsis",
.help = "h\n",
.uppercase_first_arg = false,
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};

View file

@ -55,34 +55,34 @@ pub const ParsedArgs = union(enum) {
portfolio: PortfolioOpts,
};
pub const meta = struct {
pub const name: []const u8 = "history";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Price history (symbol) or portfolio value timeline";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage:
\\ zfin history <SYMBOL> # last 30 days of candles
\\ zfin history [flags] # portfolio-value timeline
\\
\\Two modes in one command. Symbol mode (positional symbol)
\\shows the last 30 trading days of candles for that symbol.
\\Portfolio mode (no positional, optionally with flags) reads
\\`history/*-portfolio.srf` snapshots and renders rolling-windows
\\returns + a braille chart + a recent-snapshots table.
\\
\\Portfolio-mode flags:
\\ --since <DATE> earliest as-of date (inclusive)
\\ --until <DATE> latest as-of date (inclusive)
\\ --metric <name> liquid (default), illiquid, or net_worth
\\ --resolution <name> daily | weekly | monthly | auto
\\ (auto: daily ≤90d, weekly ≤730d, else monthly)
\\ --limit <N> cap recent-snapshots table to N rows (default 40)
\\ --rebuild-rollup regenerate history/rollup.srf and exit
\\
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
\\
;
pub const meta: framework.Meta = .{
.name = "history",
.group = .symbol_lookup,
.synopsis = "Price history (symbol) or portfolio value timeline",
.uppercase_first_arg = true,
.help =
\\Usage:
\\ zfin history <SYMBOL> # last 30 days of candles
\\ zfin history [flags] # portfolio-value timeline
\\
\\Two modes in one command. Symbol mode (positional symbol)
\\shows the last 30 trading days of candles for that symbol.
\\Portfolio mode (no positional, optionally with flags) reads
\\`history/*-portfolio.srf` snapshots and renders rolling-windows
\\returns + a braille chart + a recent-snapshots table.
\\
\\Portfolio-mode flags:
\\ --since <DATE> earliest as-of date (inclusive)
\\ --until <DATE> latest as-of date (inclusive)
\\ --metric <name> liquid (default), illiquid, or net_worth
\\ --resolution <name> daily | weekly | monthly | auto
\\ (auto: daily ≤90d, weekly ≤730d, else monthly)
\\ --limit <N> cap recent-snapshots table to N rows (default 40)
\\ --rebuild-rollup regenerate history/rollup.srf and exit
\\
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
\\
,
};
comptime {

View file

@ -8,27 +8,27 @@ pub const ParsedArgs = struct {
cusip: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "lookup";
pub const group: framework.Group = .hygiene;
pub const synopsis: []const u8 = "Look up a CUSIP to ticker via OpenFIGI";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin lookup <CUSIP>
\\
\\Look up a CUSIP (9-char alphanumeric security identifier) to
\\its ticker via the OpenFIGI API. Successful results are cached
\\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises
\\the rate limit but the unauthenticated tier works for low
\\volume.
\\
\\Mutual funds frequently have no OpenFIGI coverage; the
\\command surfaces that and suggests a manual portfolio entry.
\\
\\Examples:
\\ zfin lookup 037833100 # → AAPL
\\
;
pub const meta: framework.Meta = .{
.name = "lookup",
.group = .hygiene,
.synopsis = "Look up a CUSIP to ticker via OpenFIGI",
.uppercase_first_arg = true,
.help =
\\Usage: zfin lookup <CUSIP>
\\
\\Look up a CUSIP (9-char alphanumeric security identifier) to
\\its ticker via the OpenFIGI API. Successful results are cached
\\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises
\\the rate limit but the unauthenticated tier works for low
\\volume.
\\
\\Mutual funds frequently have no OpenFIGI coverage; the
\\command surfaces that and suggests a manual portfolio entry.
\\
\\Examples:
\\ zfin lookup 037833100 # → AAPL
\\
,
};
comptime {

View file

@ -36,31 +36,32 @@ pub const ParsedArgs = struct {
real: bool = false,
};
pub const meta = struct {
pub const name: []const u8 = "milestones";
pub const group: framework.Group = .portfolio;
pub const synopsis: []const u8 = "Show portfolio threshold crossings (each $1M, doublings, etc.)";
pub const help: []const u8 =
\\Usage: zfin milestones --step <expr> [--real]
\\
\\Find the dates the portfolio first reached each of a
\\configured set of thresholds. Two threshold modes:
\\
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
\\ Relative multiplier: 2x / 2X / 1.5x
\\
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
\\
\\Options:
\\ --step <expr> Threshold step (required).
\\ --real Deflate the series to the last full Shiller
\\ year before detecting crossings (CPI-adjusted).
\\ Default is nominal.
\\
\\Note: crossing dates are "first observed at," bounded by the
\\source series cadence (typically weekly).
\\
;
pub const meta: framework.Meta = .{
.name = "milestones",
.group = .portfolio,
.synopsis = "Show portfolio threshold crossings (each $1M, doublings, etc.)",
.help =
\\Usage: zfin milestones --step <expr> [--real]
\\
\\Find the dates the portfolio first reached each of a
\\configured set of thresholds. Two threshold modes:
\\
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
\\ Relative multiplier: 2x / 2X / 1.5x
\\
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
\\
\\Options:
\\ --step <expr> Threshold step (required).
\\ --real Deflate the series to the last full Shiller
\\ year before detecting crossings (CPI-adjusted).
\\ Default is nominal.
\\
\\Note: crossing dates are "first observed at," bounded by the
\\source series cadence (typically weekly).
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -12,27 +12,27 @@ pub const ParsedArgs = struct {
ntm: usize = 8,
};
pub const meta = struct {
pub const name: []const u8 = "options";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show options chain (all expirations) for a symbol";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin options <SYMBOL> [--ntm <N>]
\\
\\Show the options chain (all expirations) for a symbol from
\\CBOE. Cached for 1 hour. The nearest monthly expiration is
\\auto-expanded with ±N strikes near the money; other
\\expirations are listed collapsed.
\\
\\Options:
\\ --ntm <N> Show ±N strikes near the money (default: 8)
\\
\\Examples:
\\ zfin options SPY
\\ zfin options AAPL --ntm 12
\\
;
pub const meta: framework.Meta = .{
.name = "options",
.group = .symbol_lookup,
.synopsis = "Show options chain (all expirations) for a symbol",
.uppercase_first_arg = true,
.help =
\\Usage: zfin options <SYMBOL> [--ntm <N>]
\\
\\Show the options chain (all expirations) for a symbol from
\\CBOE. Cached for 1 hour. The nearest monthly expiration is
\\auto-expanded with ±N strikes near the money; other
\\expirations are listed collapsed.
\\
\\Options:
\\ --ntm <N> Show ±N strikes near the money (default: 8)
\\
\\Examples:
\\ zfin options SPY
\\ zfin options AAPL --ntm 12
\\
,
};
comptime {

View file

@ -9,30 +9,30 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "perf";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin perf <SYMBOL>
\\
\\Show Morningstar-style trailing returns for a symbol — 1Y,
\\3Y, 5Y, 10Y price-only and total-return CAGR plus risk
\\metrics (Sharpe, max drawdown, vol). Total returns require
\\POLYGON_API_KEY (for dividend history); price-only
\\returns work without it.
\\
\\Two return tables are produced:
\\ - As-of: returns through the latest cached close.
\\ - Month-end: returns through the most recent calendar
\\ month-end (matches how mutual funds quote their stats).
\\
\\Examples:
\\ zfin perf VTI # total-market index, 30+ years of data
\\ zfin perf NVDA # individual stock
\\
;
pub const meta: framework.Meta = .{
.name = "perf",
.group = .symbol_lookup,
.synopsis = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)",
.uppercase_first_arg = true,
.help =
\\Usage: zfin perf <SYMBOL>
\\
\\Show Morningstar-style trailing returns for a symbol — 1Y,
\\3Y, 5Y, 10Y price-only and total-return CAGR plus risk
\\metrics (Sharpe, max drawdown, vol). Total returns require
\\POLYGON_API_KEY (for dividend history); price-only
\\returns work without it.
\\
\\Two return tables are produced:
\\ - As-of: returns through the latest cached close.
\\ - Month-end: returns through the most recent calendar
\\ month-end (matches how mutual funds quote their stats).
\\
\\Examples:
\\ zfin perf VTI # total-market index, 30+ years of data
\\ zfin perf NVDA # individual stock
\\
,
};
comptime {

View file

@ -13,27 +13,28 @@ pub const ParsedArgs = struct {
force_refresh: bool = false,
};
pub const meta = struct {
pub const name: []const u8 = "portfolio";
pub const group: framework.Group = .portfolio;
pub const synopsis: []const u8 = "Load and analyze the portfolio (positions + valuations + watchlist)";
pub const help: []const u8 =
\\Usage: zfin portfolio [--refresh]
\\
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
\\prices in parallel (server sync where ZFIN_SERVER is set,
\\else providers), and print the position table + valuations
\\+ historical-snapshot mini-tables. The watchlist (if
\\`watchlist.srf` exists) is appended to the price-load step
\\so its quotes show alongside.
\\
\\Options:
\\ --refresh Force a re-fetch of every symbol's candles,
\\ bypassing the per-symbol TTL freshness check.
\\ Useful when you suspect cached data is wrong
\\ or after rotating providers.
\\
;
pub const meta: framework.Meta = .{
.name = "portfolio",
.group = .portfolio,
.synopsis = "Load and analyze the portfolio (positions + valuations + watchlist)",
.help =
\\Usage: zfin portfolio [--refresh]
\\
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
\\prices in parallel (server sync where ZFIN_SERVER is set,
\\else providers), and print the position table + valuations
\\+ historical-snapshot mini-tables. The watchlist (if
\\`watchlist.srf` exists) is appended to the price-load step
\\so its quotes show alongside.
\\
\\Options:
\\ --refresh Force a re-fetch of every symbol's candles,
\\ bypassing the per-symbol TTL freshness check.
\\ Useful when you suspect cached data is wrong
\\ or after rotating providers.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -65,48 +65,49 @@ pub const CompareArgs = struct {
as_of: ?Date = null,
};
pub const meta = struct {
pub const name: []const u8 = "projections";
pub const group: framework.Group = .portfolio;
pub const synopsis: []const u8 = "Retirement projections, benchmark comparison, percentile bands";
pub const help: []const u8 =
\\Usage: zfin projections [opts]
\\
\\Default mode: percentile-bands view of the portfolio's
\\projected value over the configured horizon (`projections.srf`),
\\plus benchmark comparison (SPY/AGG) and safe-withdrawal
\\dollars at multiple horizons / confidence levels.
\\
\\Three alternate sub-modes (mutually exclusive):
\\ --vs <DATE> Side-by-side compare with a
\\ historical snapshot's projection.
\\ --convergence Plot the model's predicted
\\ retirement date over time as data
\\ accumulated.
\\ --return-backtest Plot expected_return claim over
\\ time alongside realized forward
\\ CAGR. Pair with `--real` for
\\ CPI-adjusted dollars.
\\
\\Options:
\\ --no-events Exclude life events from the
\\ simulation (baseline view).
\\ --as-of <DATE> Compute against a historical
\\ snapshot. Auto-snaps to the
\\ nearest-earlier snapshot.
\\ --overlay-actuals Plot realized portfolio trajectory
\\ from --as-of through today on top
\\ of the percentile bands. Requires
\\ --as-of. Ignored under --vs.
\\ --vs <DATE> (see above)
\\ --convergence (see above)
\\ --return-backtest (see above)
\\ --real With --return-backtest, render in
\\ CPI-adjusted dollars.
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\
;
pub const meta: framework.Meta = .{
.name = "projections",
.group = .portfolio,
.synopsis = "Retirement projections, benchmark comparison, percentile bands",
.help =
\\Usage: zfin projections [opts]
\\
\\Default mode: percentile-bands view of the portfolio's
\\projected value over the configured horizon (`projections.srf`),
\\plus benchmark comparison (SPY/AGG) and safe-withdrawal
\\dollars at multiple horizons / confidence levels.
\\
\\Three alternate sub-modes (mutually exclusive):
\\ --vs <DATE> Side-by-side compare with a
\\ historical snapshot's projection.
\\ --convergence Plot the model's predicted
\\ retirement date over time as data
\\ accumulated.
\\ --return-backtest Plot expected_return claim over
\\ time alongside realized forward
\\ CAGR. Pair with `--real` for
\\ CPI-adjusted dollars.
\\
\\Options:
\\ --no-events Exclude life events from the
\\ simulation (baseline view).
\\ --as-of <DATE> Compute against a historical
\\ snapshot. Auto-snaps to the
\\ nearest-earlier snapshot.
\\ --overlay-actuals Plot realized portfolio trajectory
\\ from --as-of through today on top
\\ of the percentile bands. Requires
\\ --as-of. Ignored under --vs.
\\ --vs <DATE> (see above)
\\ --convergence (see above)
\\ --return-backtest (see above)
\\ --real With --return-backtest, render in
\\ CPI-adjusted dollars.
\\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -9,27 +9,27 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "quote";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show latest quote with chart and 20-day history";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin quote <SYMBOL>
\\
\\Show the latest real-time quote for a symbol (Yahoo / TwelveData)
\\plus a braille price chart of the last 60 candles and a table
\\of the last 20 trading days.
\\
\\If real-time fetch fails, falls back to the cached close. The
\\Yahoo path is free and unauthenticated; TwelveData requires
\\TWELVEDATA_API_KEY.
\\
\\Examples:
\\ zfin quote AAPL
\\ zfin quote spy # symbols are case-insensitive
\\
;
pub const meta: framework.Meta = .{
.name = "quote",
.group = .symbol_lookup,
.synopsis = "Show latest quote with chart and 20-day history",
.uppercase_first_arg = true,
.help =
\\Usage: zfin quote <SYMBOL>
\\
\\Show the latest real-time quote for a symbol (Yahoo / TwelveData)
\\plus a braille price chart of the last 60 candles and a table
\\of the last 20 trading days.
\\
\\If real-time fetch fails, falls back to the cached close. The
\\Yahoo path is free and unauthenticated; TwelveData requires
\\TWELVEDATA_API_KEY.
\\
\\Examples:
\\ zfin quote AAPL
\\ zfin quote spy # symbols are case-insensitive
\\
,
};
comptime {

View file

@ -64,37 +64,38 @@ pub const ParsedArgs = struct {
as_of_override: ?Date = null,
};
pub const meta = struct {
pub const name: []const u8 = "snapshot";
pub const group: framework.Group = .timeseries;
pub const synopsis: []const u8 = "Write a daily portfolio snapshot to history/";
pub const help: []const u8 =
\\Usage: zfin snapshot [opts]
\\
\\Compute a portfolio snapshot for today (or a historical date with
\\`--as-of`) and write it as a discriminated SRF file under
\\`history/<as_of_date>-portfolio.srf`. The output records start with
\\`kind::<meta|total|tax_type|account|lot>` so readers can demux on
\\that field.
\\
\\Default mode: refresh the candle cache for held symbols, derive
\\`as_of_date` from the mode of cached candle dates, look up
\\close-on-or-before for each lot, and write atomically.
\\
\\Options:
\\ --force overwrite existing snapshot for as_of_date
\\ --dry-run compute + print to stdout, do not write
\\ --out <path> override output path
\\ --as-of <DATE> write a snapshot for a historical date
\\ (uses git to recover portfolio state and
\\ candle cache for pricing). DATE accepts
\\ YYYY-MM-DD or relative shortcuts
\\ (1W/1M/1Q/1Y).
\\
\\If `history/<as_of_date>-portfolio.srf` already exists and `--force`
\\wasn't passed, the run skips with a stderr message.
\\
;
pub const meta: framework.Meta = .{
.name = "snapshot",
.group = .timeseries,
.synopsis = "Write a daily portfolio snapshot to history/",
.help =
\\Usage: zfin snapshot [opts]
\\
\\Compute a portfolio snapshot for today (or a historical date with
\\`--as-of`) and write it as a discriminated SRF file under
\\`history/<as_of_date>-portfolio.srf`. The output records start with
\\`kind::<meta|total|tax_type|account|lot>` so readers can demux on
\\that field.
\\
\\Default mode: refresh the candle cache for held symbols, derive
\\`as_of_date` from the mode of cached candle dates, look up
\\close-on-or-before for each lot, and write atomically.
\\
\\Options:
\\ --force overwrite existing snapshot for as_of_date
\\ --dry-run compute + print to stdout, do not write
\\ --out <path> override output path
\\ --as-of <DATE> write a snapshot for a historical date
\\ (uses git to recover portfolio state and
\\ candle cache for pricing). DATE accepts
\\ YYYY-MM-DD or relative shortcuts
\\ (1W/1M/1Q/1Y).
\\
\\If `history/<as_of_date>-portfolio.srf` already exists and `--force`
\\wasn't passed, the run skips with a stderr message.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -8,23 +8,23 @@ pub const ParsedArgs = struct {
symbol: []const u8,
};
pub const meta = struct {
pub const name: []const u8 = "splits";
pub const group: framework.Group = .symbol_lookup;
pub const synopsis: []const u8 = "Show split history for a symbol";
pub const uppercase_first_arg: bool = true;
pub const help: []const u8 =
\\Usage: zfin splits <SYMBOL>
\\
\\Show the split history for a symbol from Polygon.io. Cached
\\for 14 days; subsequent calls within that window serve the
\\cached data without an API call.
\\
\\Examples:
\\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09)
\\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\
;
pub const meta: framework.Meta = .{
.name = "splits",
.group = .symbol_lookup,
.synopsis = "Show split history for a symbol",
.uppercase_first_arg = true,
.help =
\\Usage: zfin splits <SYMBOL>
\\
\\Show the split history for a symbol from Polygon.io. Cached
\\for 14 days; subsequent calls within that window serve the
\\cached data without an API call.
\\
\\Examples:
\\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09)
\\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\
,
};
comptime {

View file

@ -19,19 +19,20 @@ pub const ParsedArgs = struct {
verbose: bool = false,
};
pub const meta = struct {
pub const name: []const u8 = "version";
pub const group: framework.Group = .infra;
pub const synopsis: []const u8 = "Show zfin version and build info";
pub const help: []const u8 =
\\Usage: zfin version [-v|--verbose]
\\
\\Print zfin's version + build date. With `--verbose`/`-v`, also
\\prints the Zig compiler version, build mode, build target,
\\resolved ZFIN_HOME, and cache directory — useful for bug
\\reports.
\\
;
pub const meta: framework.Meta = .{
.name = "version",
.group = .infra,
.synopsis = "Show zfin version and build info",
.help =
\\Usage: zfin version [-v|--verbose]
\\
\\Print zfin's version + build date. With `--verbose`/`-v`, also
\\prints the Zig compiler version, build mode, build target,
\\resolved ZFIN_HOME, and cache directory — useful for bug
\\reports.
\\
,
.uppercase_first_arg = false,
};
comptime {

View file

@ -331,7 +331,7 @@ fn runCli(init: std.process.Init) !u8 {
.color = color,
.out = out,
};
const dispatched_args = if (comptime cmd_framework.uppercasesFirstArg(Module))
const dispatched_args = if (comptime Module.meta.uppercase_first_arg)
try cmd_framework.normalizeFirstArg(allocator, cmd_args)
else
cmd_args;