rework framework to something much more sane

This commit is contained in:
Emil Lerch 2026-05-18 17:39:19 -07:00
parent 3b02c97153
commit 825419c7c3
22 changed files with 597 additions and 601 deletions

View file

@ -7,11 +7,11 @@ const Money = @import("../Money.zig");
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "analysis"; .name = "analysis",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Show portfolio breakdowns by asset class, sector, geo, account, tax type"; .synopsis = "Show portfolio breakdowns by asset class, sector, geo, account, tax type",
pub const help: []const u8 = .help =
\\Usage: zfin analysis \\Usage: zfin analysis
\\ \\
\\Show portfolio analysis: equities/fixed-income split, plus \\Show portfolio analysis: equities/fixed-income split, plus
@ -23,7 +23,8 @@ pub const meta = struct {
\\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap \\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
\\classifications, then edit by hand. \\classifications, then edit by hand.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -2250,11 +2250,11 @@ pub const ParsedArgs = struct {
stale_days: u32 = default_stale_days, stale_days: u32 = default_stale_days,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "audit"; .name = "audit",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Reconcile portfolio against brokerage exports + portfolio hygiene check"; .synopsis = "Reconcile portfolio against brokerage exports + portfolio hygiene check",
pub const help: []const u8 = .help =
\\Usage: zfin audit [opts] \\Usage: zfin audit [opts]
\\ \\
\\Two modes in one command: \\Two modes in one command:
@ -2275,7 +2275,8 @@ pub const meta = struct {
\\ --schwab-summary Schwab account summary; copy from accounts \\ --schwab-summary Schwab account summary; copy from accounts
\\ summary page, paste to stdin, then ^D \\ summary page, paste to stdin, then ^D
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -13,11 +13,11 @@ pub const ParsedArgs = struct {
sub: Subcommand, sub: Subcommand,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "cache"; .name = "cache",
pub const group: framework.Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "Inspect or clear the local provider-data cache"; .synopsis = "Inspect or clear the local provider-data cache",
pub const help: []const u8 = .help =
\\Usage: zfin cache <stats|clear> \\Usage: zfin cache <stats|clear>
\\ \\
\\Subcommands: \\Subcommands:
@ -31,7 +31,8 @@ pub const meta = struct {
\\ \\
\\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`. \\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -87,11 +87,11 @@ pub const ParsedArgs = struct {
commit_after: ?git.CommitSpec = null, commit_after: ?git.CommitSpec = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "compare"; .name = "compare",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Compare portfolio at two points in time (or vs. today)"; .synopsis = "Compare portfolio at two points in time (or vs. today)",
pub const help: []const u8 = .help =
\\Usage: \\Usage:
\\ zfin compare <DATE> compare DATE vs. live \\ zfin compare <DATE> compare DATE vs. live
\\ zfin compare <DATE1> <DATE2> compare two historical dates \\ zfin compare <DATE1> <DATE2> compare two historical dates
@ -124,7 +124,8 @@ pub const meta = struct {
\\ --commit-after <SPEC> Pin the after-side commit; accepts \\ --commit-after <SPEC> Pin the after-side commit; accepts
\\ `working` for the working copy. \\ `working` for the working copy.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -187,11 +187,11 @@ pub const ParsedArgs = struct {
after: ?git.CommitSpec = null, after: ?git.CommitSpec = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "contributions"; .name = "contributions",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Show money added since the last recorded state in git"; .synopsis = "Show money added since the last recorded state in git",
pub const help: []const u8 = .help =
\\Usage: zfin contributions [opts] \\Usage: zfin contributions [opts]
\\ \\
\\Show contributions, withdrawals, and lot-level changes between \\Show contributions, withdrawals, and lot-level changes between
@ -224,7 +224,8 @@ pub const meta = struct {
\\--since and --commit-before describe the same axis; pass at most \\--since and --commit-before describe the same axis; pass at most
\\one. Same for --until and --commit-after. \\one. Same for --until and --commit-after.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,12 +8,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "divs"; .name = "divs",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show dividend history (with TTM yield) for a symbol"; .synopsis = "Show dividend history (with TTM yield) for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin divs <SYMBOL> \\Usage: zfin divs <SYMBOL>
\\ \\
\\Show the dividend history for a symbol from Polygon.io. Cached \\Show the dividend history for a symbol from Polygon.io. Cached
@ -24,7 +24,7 @@ pub const meta = struct {
\\ zfin divs VTI # quarterly distributions + yield \\ zfin divs VTI # quarterly distributions + yield
\\ zfin divs T # historical AT&T dividends \\ zfin divs T # historical AT&T dividends
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -8,12 +8,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "earnings"; .name = "earnings",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show earnings history (with EPS surprise) and upcoming events"; .synopsis = "Show earnings history (with EPS surprise) and upcoming events",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin earnings <SYMBOL> \\Usage: zfin earnings <SYMBOL>
\\ \\
\\Show the earnings history (estimate vs. actual + surprise %) \\Show the earnings history (estimate vs. actual + surprise %)
@ -30,7 +30,7 @@ pub const meta = struct {
\\ zfin earnings NVDA \\ zfin earnings NVDA
\\ zfin earnings AAPL | head -8 # last two years of quarters \\ zfin earnings AAPL | head -8 # last two years of quarters
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -12,11 +12,11 @@ pub const ParsedArgs = struct {
arg: []const u8, arg: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "enrich"; .name = "enrich",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)"; .synopsis = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)",
pub const help: []const u8 = .help =
\\Usage: zfin enrich <FILE|SYMBOL> \\Usage: zfin enrich <FILE|SYMBOL>
\\ \\
\\Bootstrap a `metadata.srf` classification file from Alpha \\Bootstrap a `metadata.srf` classification file from Alpha
@ -39,7 +39,8 @@ pub const meta = struct {
\\ zfin enrich portfolio.srf > metadata.srf # whole portfolio \\ zfin enrich portfolio.srf > metadata.srf # whole portfolio
\\ zfin enrich AAPL >> metadata.srf # single symbol append \\ zfin enrich AAPL >> metadata.srf # single symbol append
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,12 +8,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "etf"; .name = "etf",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show ETF profile (holdings, sectors, expense ratio)"; .synopsis = "Show ETF profile (holdings, sectors, expense ratio)",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin etf <SYMBOL> \\Usage: zfin etf <SYMBOL>
\\ \\
\\Show the ETF profile (expense ratio, AUM, dividend yield, \\Show the ETF profile (expense ratio, AUM, dividend yield,
@ -25,7 +25,7 @@ pub const meta = struct {
\\ zfin etf VTI # broad market index \\ zfin etf VTI # broad market index
\\ zfin etf TQQQ # leveraged (warning surfaced) \\ zfin etf TQQQ # leveraged (warning surfaced)
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -11,11 +11,12 @@
//! ```zig //! ```zig
//! pub const ParsedArgs = ...; // struct or tagged union //! pub const ParsedArgs = ...; // struct or tagged union
//! //!
//! pub const meta = struct { //! pub const meta: framework.Meta = .{
//! pub const name: []const u8 = "perf"; //! .name = "perf",
//! pub const group: framework.Group = .symbol_lookup; //! .group = .symbol_lookup,
//! pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns"; //! .synopsis = "Show 1y/3y/5y/10y trailing returns",
//! pub const help: []const u8 = ...; // multi-line; rendered by `--help` //! .help = "...", // multi-line; rendered by `--help`
//! .uppercase_first_arg = true,
//! }; //! };
//! //!
//! pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs; //! 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 ──────────────────────────────────────────── // ── Refresh policy ────────────────────────────────────────────
/// Cache-freshness policy for the invocation. Defined here ahead of /// 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 = ...;` " ++ @compileError("Command module `" ++ mod_name ++ "` is missing `pub const ParsedArgs = ...;` " ++
"(use `pub const ParsedArgs = struct {};` if the command takes no arguments)"); "(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; 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( validator.expectDeclWithType(
"Command module", "Command module",
mod_name, mod_name,
meta_decl, Module,
"name", "meta",
[]const u8, Meta,
"pub const name: []const u8 = \"...\";", "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 ────────────────────────────────────────── // ── parseArgs ──────────────────────────────────────────
validator.expectFnInferredError( validator.expectFnInferredError(
@ -301,16 +314,6 @@ pub fn validateCommandModule(comptime Module: type) void {
// ── Help rendering ──────────────────────────────────────────── // ── 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 /// If `cmd_args[0]` exists, is non-empty, and isn't a flag, return
/// a copy with the first byte-string uppercased. Otherwise return /// a copy with the first byte-string uppercased. Otherwise return
/// `cmd_args` unchanged. The new slice is owned by `allocator` /// `cmd_args` unchanged. The new slice is owned by `allocator`
@ -405,11 +408,12 @@ const testing = std.testing;
const ProbeModule = struct { const ProbeModule = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "probe"; .name = "probe",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "test probe"; .synopsis = "test probe",
pub const help: []const u8 = "Usage: zfin probe\n"; .help = "Usage: zfin probe\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { 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" { test "printCommandHelp: appends newline when meta.help lacks one" {
const NoNewline = struct { const NoNewline = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "x"; .name = "x",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "x"; .synopsis = "x",
pub const help: []const u8 = "no trailing newline"; .help = "no trailing newline",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -470,29 +475,6 @@ test "printCommandHelp: appends newline when meta.help lacks one" {
try testing.expectEqualStrings("no trailing newline\n", w.buffered()); 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" { test "normalizeFirstArg: empty args returns slice unchanged" {
const empty: []const []const u8 = &.{}; const empty: []const []const u8 = &.{};
const out = try normalizeFirstArg(testing.allocator, empty); const out = try normalizeFirstArg(testing.allocator, empty);
@ -530,11 +512,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
const FakeRegistry = struct { const FakeRegistry = struct {
const ProbeA = struct { const ProbeA = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "alpha"; .name = "alpha",
pub const group: Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "alpha synopsis"; .synopsis = "alpha synopsis",
pub const help: []const u8 = "alpha help\n"; .help = "alpha help\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -543,11 +526,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
}; };
const ProbeB = struct { const ProbeB = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "beta"; .name = "beta",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "beta synopsis"; .synopsis = "beta synopsis",
pub const help: []const u8 = "beta help\n"; .help = "beta help\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -584,11 +568,12 @@ test "printGroupedUsage: omits empty groups" {
const FakeRegistry = struct { const FakeRegistry = struct {
const Probe = struct { const Probe = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "only"; .name = "only",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "only synopsis"; .synopsis = "only synopsis",
pub const help: []const u8 = "h\n"; .help = "h\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};

View file

@ -55,12 +55,12 @@ pub const ParsedArgs = union(enum) {
portfolio: PortfolioOpts, portfolio: PortfolioOpts,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "history"; .name = "history",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Price history (symbol) or portfolio value timeline"; .synopsis = "Price history (symbol) or portfolio value timeline",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: \\Usage:
\\ zfin history <SYMBOL> # last 30 days of candles \\ zfin history <SYMBOL> # last 30 days of candles
\\ zfin history [flags] # portfolio-value timeline \\ zfin history [flags] # portfolio-value timeline
@ -82,7 +82,7 @@ pub const meta = struct {
\\ \\
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y). \\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -8,12 +8,12 @@ pub const ParsedArgs = struct {
cusip: []const u8, cusip: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "lookup"; .name = "lookup",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Look up a CUSIP to ticker via OpenFIGI"; .synopsis = "Look up a CUSIP to ticker via OpenFIGI",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin lookup <CUSIP> \\Usage: zfin lookup <CUSIP>
\\ \\
\\Look up a CUSIP (9-char alphanumeric security identifier) to \\Look up a CUSIP (9-char alphanumeric security identifier) to
@ -28,7 +28,7 @@ pub const meta = struct {
\\Examples: \\Examples:
\\ zfin lookup 037833100 # → AAPL \\ zfin lookup 037833100 # → AAPL
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -36,11 +36,11 @@ pub const ParsedArgs = struct {
real: bool = false, real: bool = false,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "milestones"; .name = "milestones",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Show portfolio threshold crossings (each $1M, doublings, etc.)"; .synopsis = "Show portfolio threshold crossings (each $1M, doublings, etc.)",
pub const help: []const u8 = .help =
\\Usage: zfin milestones --step <expr> [--real] \\Usage: zfin milestones --step <expr> [--real]
\\ \\
\\Find the dates the portfolio first reached each of a \\Find the dates the portfolio first reached each of a
@ -60,7 +60,8 @@ pub const meta = struct {
\\Note: crossing dates are "first observed at," bounded by the \\Note: crossing dates are "first observed at," bounded by the
\\source series cadence (typically weekly). \\source series cadence (typically weekly).
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -12,12 +12,12 @@ pub const ParsedArgs = struct {
ntm: usize = 8, ntm: usize = 8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "options"; .name = "options",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show options chain (all expirations) for a symbol"; .synopsis = "Show options chain (all expirations) for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin options <SYMBOL> [--ntm <N>] \\Usage: zfin options <SYMBOL> [--ntm <N>]
\\ \\
\\Show the options chain (all expirations) for a symbol from \\Show the options chain (all expirations) for a symbol from
@ -32,7 +32,7 @@ pub const meta = struct {
\\ zfin options SPY \\ zfin options SPY
\\ zfin options AAPL --ntm 12 \\ zfin options AAPL --ntm 12
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -9,12 +9,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "perf"; .name = "perf",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)"; .synopsis = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin perf <SYMBOL> \\Usage: zfin perf <SYMBOL>
\\ \\
\\Show Morningstar-style trailing returns for a symbol — 1Y, \\Show Morningstar-style trailing returns for a symbol — 1Y,
@ -32,7 +32,7 @@ pub const meta = struct {
\\ zfin perf VTI # total-market index, 30+ years of data \\ zfin perf VTI # total-market index, 30+ years of data
\\ zfin perf NVDA # individual stock \\ zfin perf NVDA # individual stock
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -13,11 +13,11 @@ pub const ParsedArgs = struct {
force_refresh: bool = false, force_refresh: bool = false,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "portfolio"; .name = "portfolio",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Load and analyze the portfolio (positions + valuations + watchlist)"; .synopsis = "Load and analyze the portfolio (positions + valuations + watchlist)",
pub const help: []const u8 = .help =
\\Usage: zfin portfolio [--refresh] \\Usage: zfin portfolio [--refresh]
\\ \\
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol \\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
@ -33,7 +33,8 @@ pub const meta = struct {
\\ Useful when you suspect cached data is wrong \\ Useful when you suspect cached data is wrong
\\ or after rotating providers. \\ or after rotating providers.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -65,11 +65,11 @@ pub const CompareArgs = struct {
as_of: ?Date = null, as_of: ?Date = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "projections"; .name = "projections",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Retirement projections, benchmark comparison, percentile bands"; .synopsis = "Retirement projections, benchmark comparison, percentile bands",
pub const help: []const u8 = .help =
\\Usage: zfin projections [opts] \\Usage: zfin projections [opts]
\\ \\
\\Default mode: percentile-bands view of the portfolio's \\Default mode: percentile-bands view of the portfolio's
@ -106,7 +106,8 @@ pub const meta = struct {
\\ \\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -9,12 +9,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "quote"; .name = "quote",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show latest quote with chart and 20-day history"; .synopsis = "Show latest quote with chart and 20-day history",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin quote <SYMBOL> \\Usage: zfin quote <SYMBOL>
\\ \\
\\Show the latest real-time quote for a symbol (Yahoo / TwelveData) \\Show the latest real-time quote for a symbol (Yahoo / TwelveData)
@ -29,7 +29,7 @@ pub const meta = struct {
\\ zfin quote AAPL \\ zfin quote AAPL
\\ zfin quote spy # symbols are case-insensitive \\ zfin quote spy # symbols are case-insensitive
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -64,11 +64,11 @@ pub const ParsedArgs = struct {
as_of_override: ?Date = null, as_of_override: ?Date = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "snapshot"; .name = "snapshot",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Write a daily portfolio snapshot to history/"; .synopsis = "Write a daily portfolio snapshot to history/",
pub const help: []const u8 = .help =
\\Usage: zfin snapshot [opts] \\Usage: zfin snapshot [opts]
\\ \\
\\Compute a portfolio snapshot for today (or a historical date with \\Compute a portfolio snapshot for today (or a historical date with
@ -94,7 +94,8 @@ pub const meta = struct {
\\If `history/<as_of_date>-portfolio.srf` already exists and `--force` \\If `history/<as_of_date>-portfolio.srf` already exists and `--force`
\\wasn't passed, the run skips with a stderr message. \\wasn't passed, the run skips with a stderr message.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,12 +8,12 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "splits"; .name = "splits",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show split history for a symbol"; .synopsis = "Show split history for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin splits <SYMBOL> \\Usage: zfin splits <SYMBOL>
\\ \\
\\Show the split history for a symbol from Polygon.io. Cached \\Show the split history for a symbol from Polygon.io. Cached
@ -24,7 +24,7 @@ pub const meta = struct {
\\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09) \\ 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), ... \\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\ \\
; ,
}; };
comptime { comptime {

View file

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

View file

@ -331,7 +331,7 @@ fn runCli(init: std.process.Init) !u8 {
.color = color, .color = color,
.out = out, .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) try cmd_framework.normalizeFirstArg(allocator, cmd_args)
else else
cmd_args; cmd_args;