finish CLI framework refactor: per-command --help, delegate help to modules, cleanup
This commit is contained in:
parent
f2a374f2b5
commit
6407ecfbb8
2 changed files with 168 additions and 232 deletions
|
|
@ -348,6 +348,54 @@ pub fn printCommandHelp(out: *std.Io.Writer, comptime Module: type) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print the grouped `zfin help` output. Walks the supplied
|
||||||
|
/// `Modules` registry (an anonymous-struct literal) and groups
|
||||||
|
/// commands by `meta.group`, rendering one section per group with
|
||||||
|
/// the group's display label as the header.
|
||||||
|
///
|
||||||
|
/// `header_text` and `footer_text` are emitted before the first
|
||||||
|
/// group and after the last respectively — the caller supplies
|
||||||
|
/// them so main.zig can keep its bespoke "Usage" line, the
|
||||||
|
/// global-options block, and the env-vars list while letting the
|
||||||
|
/// command list itself be derived from the registry.
|
||||||
|
pub fn printGroupedUsage(
|
||||||
|
out: *std.Io.Writer,
|
||||||
|
comptime Modules: type,
|
||||||
|
modules: Modules,
|
||||||
|
header_text: []const u8,
|
||||||
|
footer_text: []const u8,
|
||||||
|
) !void {
|
||||||
|
try out.writeAll(header_text);
|
||||||
|
|
||||||
|
inline for (std.meta.fields(Group)) |gf| {
|
||||||
|
const group_value: Group = @enumFromInt(gf.value);
|
||||||
|
var first_in_group = true;
|
||||||
|
inline for (std.meta.fields(Modules)) |mf| {
|
||||||
|
const Module = @field(modules, mf.name);
|
||||||
|
if (Module.meta.group == group_value) {
|
||||||
|
if (first_in_group) {
|
||||||
|
try out.writeAll("\n");
|
||||||
|
try out.writeAll(group_value.label());
|
||||||
|
try out.writeAll(":\n");
|
||||||
|
first_in_group = false;
|
||||||
|
}
|
||||||
|
try out.writeAll(" ");
|
||||||
|
try out.writeAll(mf.name);
|
||||||
|
// Pad name to 16 cols so synopses align.
|
||||||
|
if (mf.name.len < 16) {
|
||||||
|
var pad: usize = 16 - mf.name.len;
|
||||||
|
while (pad > 0) : (pad -= 1) try out.writeAll(" ");
|
||||||
|
}
|
||||||
|
try out.writeAll(" ");
|
||||||
|
try out.writeAll(Module.meta.synopsis);
|
||||||
|
try out.writeAll("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try out.writeAll(footer_text);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tests ─────────────────────────────────────────────────────
|
// ── Tests ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
@ -477,3 +525,86 @@ test "normalizeFirstArg: empty first arg is left untouched" {
|
||||||
const out = try normalizeFirstArg(testing.allocator, &args);
|
const out = try normalizeFirstArg(testing.allocator, &args);
|
||||||
try testing.expectEqualStrings("", out[0]);
|
try testing.expectEqualStrings("", out[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
pub fn run(_: *RunCtx, _: ParsedArgs) !void {}
|
||||||
|
};
|
||||||
|
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 fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
pub fn run(_: *RunCtx, _: ParsedArgs) !void {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const reg = .{
|
||||||
|
.alpha = FakeRegistry.ProbeA,
|
||||||
|
.beta = FakeRegistry.ProbeB,
|
||||||
|
};
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
|
try printGroupedUsage(&w, @TypeOf(reg), reg, "HEAD\n", "FOOT\n");
|
||||||
|
const out = w.buffered();
|
||||||
|
|
||||||
|
// Header / footer present.
|
||||||
|
try testing.expect(std.mem.startsWith(u8, out, "HEAD\n"));
|
||||||
|
try testing.expect(std.mem.endsWith(u8, out, "FOOT\n"));
|
||||||
|
|
||||||
|
// Group labels in canonical order.
|
||||||
|
const sym_idx = std.mem.indexOf(u8, out, "Per-symbol lookups:") orelse return error.MissingSymbolLookups;
|
||||||
|
const infra_idx = std.mem.indexOf(u8, out, "Infrastructure:") orelse return error.MissingInfra;
|
||||||
|
try testing.expect(sym_idx < infra_idx);
|
||||||
|
|
||||||
|
// Each command's name + synopsis present.
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "alpha") != null);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "alpha synopsis") != null);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "beta") != null);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "beta synopsis") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
pub fn run(_: *RunCtx, _: ParsedArgs) !void {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const reg = .{ .only = FakeRegistry.Probe };
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
var w: std.Io.Writer = .fixed(&buf);
|
||||||
|
try printGroupedUsage(&w, @TypeOf(reg), reg, "", "");
|
||||||
|
const out = w.buffered();
|
||||||
|
|
||||||
|
// The Infrastructure section appears...
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "Infrastructure:") != null);
|
||||||
|
// ...but Per-symbol lookups (which has no commands in this fake
|
||||||
|
// registry) does NOT.
|
||||||
|
try testing.expect(std.mem.indexOf(u8, out, "Per-symbol lookups:") == null);
|
||||||
|
}
|
||||||
|
|
|
||||||
269
src/main.zig
269
src/main.zig
|
|
@ -49,149 +49,33 @@ comptime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage =
|
const usage_header =
|
||||||
\\Usage: zfin [global options] <command> [command options]
|
\\Usage: zfin [global options] <command> [command options]
|
||||||
\\
|
\\
|
||||||
\\Commands:
|
\\Per-command help: zfin <command> --help
|
||||||
\\ interactive [opts] Launch interactive TUI
|
\\
|
||||||
\\ perf <SYMBOL> Show 1yr/3yr/5yr/10yr trailing returns (Morningstar-style)
|
;
|
||||||
\\ quote <SYMBOL> Show latest quote with chart and history
|
|
||||||
\\ history [SYMBOL|opts] Show price history (symbol) or portfolio timeline
|
const usage_footer =
|
||||||
\\ divs <SYMBOL> Show dividend history
|
\\
|
||||||
\\ splits <SYMBOL> Show split history
|
\\Other commands:
|
||||||
\\ options <SYMBOL> Show options chain (all expirations)
|
\\ interactive [opts] Launch interactive TUI
|
||||||
\\ earnings <SYMBOL> Show earnings history and upcoming
|
\\ help / --help Show this message
|
||||||
\\ etf <SYMBOL> Show ETF profile (holdings, sectors, expense ratio)
|
|
||||||
\\ portfolio Load and analyze the portfolio
|
|
||||||
\\ analysis Show portfolio analysis
|
|
||||||
\\ contributions [opts] Show money added since last commit (git-based diff)
|
|
||||||
\\ snapshot [opts] Write a daily portfolio snapshot to history/
|
|
||||||
\\ compare <DATE> [<DATE>] Compare portfolio against snapshot (one date = vs today)
|
|
||||||
\\ enrich <FILE|SYMBOL> Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)
|
|
||||||
\\ lookup <CUSIP> Look up CUSIP to ticker via OpenFIGI
|
|
||||||
\\ audit [opts] Reconcile portfolio against brokerage export
|
|
||||||
\\ projections [opts] Retirement projections and benchmark comparison
|
|
||||||
\\ milestones [opts] Show portfolio threshold crossings (e.g. each $1M, doublings)
|
|
||||||
\\ cache stats Show cache statistics
|
|
||||||
\\ cache clear Clear all cached data
|
|
||||||
\\ version [-v] Show zfin version and build info
|
|
||||||
\\
|
\\
|
||||||
\\Global options (must appear before the subcommand):
|
\\Global options (must appear before the subcommand):
|
||||||
\\ --no-color Disable colored output
|
\\ --no-color Disable colored output
|
||||||
\\ -p, --portfolio <FILE> Portfolio file (default: portfolio.srf; cwd then ZFIN_HOME)
|
\\ -p, --portfolio <FILE> Portfolio file (default: portfolio.srf;
|
||||||
\\ metadata.srf and accounts.srf are loaded from the
|
\\ cwd → ZFIN_HOME). metadata.srf and
|
||||||
\\ same directory as the resolved portfolio.
|
\\ accounts.srf are loaded from the same
|
||||||
|
\\ directory as the resolved portfolio.
|
||||||
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
\\ -w, --watchlist <FILE> Watchlist file (default: watchlist.srf)
|
||||||
\\
|
\\
|
||||||
\\Interactive command options:
|
\\Interactive command options:
|
||||||
\\ -s, --symbol <SYMBOL> Initial symbol (default: VTI)
|
\\ -s, --symbol <SYMBOL> Initial symbol (default: VTI)
|
||||||
\\ --chart <MODE> Chart graphics: auto, braille, or WxH (e.g. 1920x1080)
|
\\ --chart <MODE> Chart graphics: auto, braille, or WxH
|
||||||
\\ --default-keys Print default keybindings
|
\\ --default-keys Print default keybindings
|
||||||
\\ --default-theme Print default theme
|
\\ --default-theme Print default theme
|
||||||
\\
|
\\
|
||||||
\\Options command options:
|
|
||||||
\\ --ntm <N> Show +/- N strikes near the money (default: 8)
|
|
||||||
\\
|
|
||||||
\\Portfolio command options:
|
|
||||||
\\ --refresh Force refresh (ignore cache, re-fetch all prices)
|
|
||||||
\\
|
|
||||||
\\History command options (portfolio mode; omit SYMBOL):
|
|
||||||
\\ --since <YYYY-MM-DD> Earliest as_of_date (inclusive)
|
|
||||||
\\ --until <YYYY-MM-DD> Latest as_of_date (inclusive)
|
|
||||||
\\ --metric <name> liquid (default), illiquid, or net_worth
|
|
||||||
\\ --resolution <name> daily, weekly, monthly, or auto (default: auto)
|
|
||||||
\\ auto: daily ≤90d, weekly ≤730d, else monthly
|
|
||||||
\\ --limit <N> Max rows in the recent-snapshots table (default: 40)
|
|
||||||
\\ --rebuild-rollup (Re)write history/rollup.srf and exit
|
|
||||||
\\
|
|
||||||
\\Audit command options:
|
|
||||||
\\ (no flags) Portfolio hygiene check + auto-reconcile discovered files
|
|
||||||
\\ --verbose Show full reconciliation output even when clean
|
|
||||||
\\ --stale-days <N> Manual price staleness threshold (default: 3)
|
|
||||||
\\ --fidelity <CSV> Fidelity positions CSV export (download from "All accounts" positions tab)
|
|
||||||
\\ --schwab <CSV> Schwab per-account positions CSV export
|
|
||||||
\\ --schwab-summary Schwab account summary (copy from accounts summary page, paste to stdin)
|
|
||||||
\\
|
|
||||||
\\Analysis & Contributions commands:
|
|
||||||
\\ Both operate on the globally-specified portfolio. They also read
|
|
||||||
\\ metadata.srf and accounts.srf from the same directory.
|
|
||||||
\\ Contributions additionally requires the portfolio file to be tracked
|
|
||||||
\\ in a git repo; `git` must be on PATH.
|
|
||||||
\\
|
|
||||||
\\Contributions command options:
|
|
||||||
\\ --since <DATE> Compare against the portfolio at-or-before DATE
|
|
||||||
\\ (accepts YYYY-MM-DD or relative like 1M, 3Q, 1Y).
|
|
||||||
\\ Without --until, the "after" side is HEAD (or
|
|
||||||
\\ working copy when dirty). Default: HEAD~1..HEAD.
|
|
||||||
\\ --until <DATE> Upper bound. Pair with --since to diff two
|
|
||||||
\\ commits within a date window.
|
|
||||||
\\ --commit-before <SPEC> Pin the before commit directly. Takes precedence
|
|
||||||
\\ over --since's date-based resolution. SPEC accepts
|
|
||||||
\\ YYYY-MM-DD, relative (1W/1M/1Q/1Y), HEAD, HEAD~N,
|
|
||||||
\\ or a 7+ hex SHA. Useful when you committed after
|
|
||||||
\\ your review date and --since 1W lands on the
|
|
||||||
\\ wrong commit (try --commit-before HEAD).
|
|
||||||
\\ --commit-after <SPEC> Pin the after commit. Same grammar as
|
|
||||||
\\ --commit-before, plus `working` / `WORKING` for
|
|
||||||
\\ the filesystem working copy. Mutually exclusive
|
|
||||||
\\ with --until.
|
|
||||||
\\
|
|
||||||
\\Projections command options:
|
|
||||||
\\ --no-events Exclude life events from simulation (baseline view)
|
|
||||||
\\ --as-of <DATE|N[WMQY]> Compute against a historical snapshot instead of
|
|
||||||
\\ the live portfolio. Accepts YYYY-MM-DD, relative
|
|
||||||
\\ shortcuts (1W, 1M, 3M, 1Q, 1Y, 3Y, 5Y), or 'live'.
|
|
||||||
\\ Auto-snaps to nearest-earlier snapshot if the
|
|
||||||
\\ exact date has no snapshot file.
|
|
||||||
\\ --overlay-actuals Plot the realized portfolio trajectory from --as-of
|
|
||||||
\\ up to today on top of the projected percentile
|
|
||||||
\\ bands. Requires --as-of. The TUI projections tab
|
|
||||||
\\ is the higher-fidelity surface (press `o` after
|
|
||||||
\\ setting an as-of date with `d`). Caveat: this shows
|
|
||||||
\\ whether the model was directionally honest, NOT
|
|
||||||
\\ whether the SWR claim was accurate.
|
|
||||||
\\ --vs <DATE|N[WMQY]> Compact side-by-side comparison: projected return
|
|
||||||
\\ and safe-withdrawal @99% for live vs DATE, with
|
|
||||||
\\ deltas. Combine with --as-of to compare two
|
|
||||||
\\ historical dates (--vs = then, --as-of = now).
|
|
||||||
\\ --convergence Plot the spreadsheet's predicted retirement date
|
|
||||||
\\ over time as data accumulated. Sources data from
|
|
||||||
\\ imported_values.srf. Caveat: this evaluates the
|
|
||||||
\\ model's directional honesty, not its SWR claim.
|
|
||||||
\\ --return-backtest Plot the spreadsheet's expected_return claim over
|
|
||||||
\\ time alongside realized 1y/3y/5y forward CAGR.
|
|
||||||
\\ Sources data from imported_values.srf. Pair with
|
|
||||||
\\ --real to compare in inflation-adjusted dollars.
|
|
||||||
\\
|
|
||||||
\\Milestones command options:
|
|
||||||
\\ --step <expr> Threshold step. Required.
|
|
||||||
\\ Absolute: 1M, 1m, 1500000, 1.5M, 500K, 500k
|
|
||||||
\\ Relative: 2x, 2X, 1.5x (must be > 1.0)
|
|
||||||
\\ --real Deflate the series to the last full Shiller year
|
|
||||||
\\ before detecting crossings (CPI-adjusted).
|
|
||||||
\\
|
|
||||||
\\Compare command options:
|
|
||||||
\\ --projections Include projected return + safe-withdrawal @99%
|
|
||||||
\\ deltas between the attribution rows and the
|
|
||||||
\\ per-symbol table. Opt-in because projections cost
|
|
||||||
\\ ~1-2s per endpoint (Monte Carlo SWR search).
|
|
||||||
\\ --no-events (with --projections) Exclude life events from the
|
|
||||||
\\ underlying projection simulation. Matches the
|
|
||||||
\\ `projections --no-events` flag.
|
|
||||||
\\ --snapshot-before <DATE> Override the before-snapshot independently of
|
|
||||||
\\ the positional date. DATE accepts YYYY-MM-DD or
|
|
||||||
\\ relative (1W/1M/1Q/1Y). Defaults from positional.
|
|
||||||
\\ --snapshot-after <DATE> Override the after-snapshot. Accepts the same
|
|
||||||
\\ plus `live` for the current portfolio. Defaults
|
|
||||||
\\ from positional arg 2, else live.
|
|
||||||
\\ --commit-before <SPEC> Pin the attribution's before commit. Same SPEC
|
|
||||||
\\ grammar as the contributions flag. When the
|
|
||||||
\\ positional date's commit-at-or-before lands on
|
|
||||||
\\ the wrong commit (committed after review day),
|
|
||||||
\\ pass `HEAD` or an explicit SHA.
|
|
||||||
\\ --commit-after <SPEC> Pin the attribution's after commit. Accepts
|
|
||||||
\\ `working` / `WORKING` for the working copy.
|
|
||||||
\\
|
|
||||||
\\Environment Variables:
|
\\Environment Variables:
|
||||||
\\ TWELVEDATA_API_KEY Twelve Data API key (primary: prices)
|
\\ TWELVEDATA_API_KEY Twelve Data API key (primary: prices)
|
||||||
\\ POLYGON_API_KEY Polygon.io API key (dividends, splits)
|
\\ POLYGON_API_KEY Polygon.io API key (dividends, splits)
|
||||||
|
|
@ -204,6 +88,10 @@ const usage =
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
|
|
||||||
|
fn writeUsage(out: *std.Io.Writer) !void {
|
||||||
|
try cmd_framework.printGroupedUsage(out, @TypeOf(command_modules), command_modules, usage_header, usage_footer);
|
||||||
|
}
|
||||||
|
|
||||||
/// Parsed global options. Paths are raw (not yet resolved through ZFIN_HOME).
|
/// Parsed global options. Paths are raw (not yet resolved through ZFIN_HOME).
|
||||||
const Globals = struct {
|
const Globals = struct {
|
||||||
no_color: bool = false,
|
no_color: bool = false,
|
||||||
|
|
@ -309,7 +197,10 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
const out: *std.Io.Writer = &stdout_writer.interface;
|
const out: *std.Io.Writer = &stdout_writer.interface;
|
||||||
|
|
||||||
if (args.len < 2) {
|
if (args.len < 2) {
|
||||||
try cli.stderrPrint(io, usage);
|
var sb: [4096]u8 = undefined;
|
||||||
|
var sw = std.Io.File.stderr().writer(io, &sb);
|
||||||
|
try writeUsage(&sw.interface);
|
||||||
|
try sw.interface.flush();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,7 +209,7 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
std.mem.eql(u8, args[1], "--help") or
|
std.mem.eql(u8, args[1], "--help") or
|
||||||
std.mem.eql(u8, args[1], "-h"))
|
std.mem.eql(u8, args[1], "-h"))
|
||||||
{
|
{
|
||||||
try out.writeAll(usage);
|
try writeUsage(out);
|
||||||
try out.flush();
|
try out.flush();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +263,7 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
const color = @import("format.zig").shouldUseColor(io, init.environ_map, globals.no_color);
|
const color = @import("format.zig").shouldUseColor(io, init.environ_map, globals.no_color);
|
||||||
|
|
||||||
const command = args[globals.cursor];
|
const command = args[globals.cursor];
|
||||||
var cmd_args: []const []const u8 = @ptrCast(args[globals.cursor + 1 ..]);
|
const cmd_args: []const []const u8 = @ptrCast(args[globals.cursor + 1 ..]);
|
||||||
|
|
||||||
// Interactive TUI: long-lived, per-frame allocations benefit from a
|
// Interactive TUI: long-lived, per-frame allocations benefit from a
|
||||||
// real (non-arena) allocator. Runs against `gpa` directly.
|
// real (non-arena) allocator. Runs against `gpa` directly.
|
||||||
|
|
@ -410,13 +301,19 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
//
|
//
|
||||||
// Comptime walk over `command_modules`. Each registered command
|
// Comptime walk over `command_modules`. Each registered command
|
||||||
// owns its own flag parsing (`parseArgs`) and execution (`run`)
|
// owns its own flag parsing (`parseArgs`) and execution (`run`)
|
||||||
// — both take `*RunCtx`. As more commands migrate the legacy
|
// — both take `*RunCtx`. Per-command help (`zfin <cmd> --help`)
|
||||||
// if-else chain below shrinks; once empty (commit 17) it goes
|
// intercepts before parseArgs runs.
|
||||||
// away entirely along with the per-command symbol-uppercasing
|
|
||||||
// hack.
|
|
||||||
inline for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
inline for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
||||||
if (std.mem.eql(u8, command, f.name)) {
|
if (std.mem.eql(u8, command, f.name)) {
|
||||||
const Module = @field(command_modules, f.name);
|
const Module = @field(command_modules, f.name);
|
||||||
|
|
||||||
|
// Per-command --help / -h: print meta.help and exit 0.
|
||||||
|
if (cmd_args.len > 0 and (std.mem.eql(u8, cmd_args[0], "--help") or std.mem.eql(u8, cmd_args[0], "-h"))) {
|
||||||
|
try cmd_framework.printCommandHelp(out, Module);
|
||||||
|
try out.flush();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
var ctx: cmd_framework.RunCtx = .{
|
var ctx: cmd_framework.RunCtx = .{
|
||||||
.io = io,
|
.io = io,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
|
@ -445,75 +342,8 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize symbol argument (cmd_args[0]) to uppercase for commands
|
try cli.stderrPrint(io, "Unknown command. Run 'zfin help' for usage.\n");
|
||||||
// that take a symbol. Skip for commands whose first arg is a subcommand
|
return 1;
|
||||||
// or operand of a different kind.
|
|
||||||
const symbol_cmd =
|
|
||||||
!std.mem.eql(u8, command, "cache") and
|
|
||||||
!std.mem.eql(u8, command, "enrich") and
|
|
||||||
!std.mem.eql(u8, command, "audit") and
|
|
||||||
!std.mem.eql(u8, command, "analysis") and
|
|
||||||
!std.mem.eql(u8, command, "contributions") and
|
|
||||||
!std.mem.eql(u8, command, "portfolio") and
|
|
||||||
!std.mem.eql(u8, command, "projections") and
|
|
||||||
!std.mem.eql(u8, command, "snapshot") and
|
|
||||||
!std.mem.eql(u8, command, "compare") and
|
|
||||||
!std.mem.eql(u8, command, "version");
|
|
||||||
// Upper-case the first arg for symbol-taking commands, but skip when
|
|
||||||
// the arg is a flag (starts with '-'). This lets commands like
|
|
||||||
// `history` have both symbol mode (`zfin history VTI`) and
|
|
||||||
// flag-driven mode (`zfin history --since 2026-01-01`).
|
|
||||||
//
|
|
||||||
// Args returned by `init.minimal.args.toSlice` are `[]const [:0]const u8`
|
|
||||||
// — we can't mutate the slice. Build an owned mutable copy when the
|
|
||||||
// symbol upper-cased form differs from the raw arg.
|
|
||||||
var cmd_args_owned: ?[][]const u8 = null;
|
|
||||||
defer if (cmd_args_owned) |c| allocator.free(c);
|
|
||||||
if (symbol_cmd and cmd_args.len >= 1 and
|
|
||||||
(cmd_args[0].len == 0 or cmd_args[0][0] != '-'))
|
|
||||||
{
|
|
||||||
const upper = try allocator.dupe(u8, cmd_args[0]);
|
|
||||||
for (upper) |*c| c.* = std.ascii.toUpper(c.*);
|
|
||||||
const owned = try allocator.alloc([]const u8, cmd_args.len);
|
|
||||||
owned[0] = upper;
|
|
||||||
for (cmd_args[1..], 1..) |a, i| owned[i] = a;
|
|
||||||
cmd_args_owned = owned;
|
|
||||||
cmd_args = owned;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, command, "portfolio")) {
|
|
||||||
// Parse --refresh flag; reject any other token (including old
|
|
||||||
// positional FILE, which is now a global -p).
|
|
||||||
} else {
|
|
||||||
try cli.stderrPrint(io, "Unknown command. Run 'zfin help' for usage.\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single flush for all stdout output
|
|
||||||
try out.flush();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit a consistent "unexpected argument" error, with a hint pointing at
|
|
||||||
/// the global-flag migration. Called when a command finds an arg it doesn't
|
|
||||||
/// understand (typically a stale positional file path or a misplaced global
|
|
||||||
/// flag like `--no-color` after the subcommand).
|
|
||||||
fn reportUnexpectedArg(io: std.Io, command: []const u8, arg: []const u8) !void {
|
|
||||||
try cli.stderrPrint(io, "Error: unexpected argument to '");
|
|
||||||
try cli.stderrPrint(io, command);
|
|
||||||
try cli.stderrPrint(io, "': ");
|
|
||||||
try cli.stderrPrint(io, arg);
|
|
||||||
try cli.stderrPrint(io, "\n");
|
|
||||||
if (std.mem.eql(u8, arg, "--no-color") or
|
|
||||||
std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--portfolio") or
|
|
||||||
std.mem.eql(u8, arg, "-w") or std.mem.eql(u8, arg, "--watchlist"))
|
|
||||||
{
|
|
||||||
try cli.stderrPrint(io, "Hint: global flags must appear before the subcommand.\n");
|
|
||||||
} else {
|
|
||||||
try cli.stderrPrint(io, "Hint: the portfolio file is now a global option; use `zfin -p <FILE> ");
|
|
||||||
try cli.stderrPrint(io, command);
|
|
||||||
try cli.stderrPrint(io, "`.\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the first args[1..] token that looks like a flag we didn't handle.
|
/// Return the first args[1..] token that looks like a flag we didn't handle.
|
||||||
|
|
@ -538,21 +368,6 @@ fn globalOffender(args: []const []const u8) ?[]const u8 {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = struct {
|
|
||||||
const history = @import("commands/history.zig");
|
|
||||||
const portfolio = @import("commands/portfolio.zig");
|
|
||||||
const cache = @import("commands/cache.zig");
|
|
||||||
const analysis = @import("commands/analysis.zig");
|
|
||||||
const audit = @import("commands/audit.zig");
|
|
||||||
const enrich = @import("commands/enrich.zig");
|
|
||||||
const contributions = @import("commands/contributions.zig");
|
|
||||||
const snapshot = @import("commands/snapshot.zig");
|
|
||||||
const compare = @import("commands/compare.zig");
|
|
||||||
const version = @import("commands/version.zig");
|
|
||||||
const projections = @import("commands/projections.zig");
|
|
||||||
const milestones = @import("commands/milestones.zig");
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
test "parseGlobals: no flags, subcommand only" {
|
test "parseGlobals: no flags, subcommand only" {
|
||||||
|
|
@ -623,14 +438,4 @@ test "parseGlobals: subcommand-local flag NOT consumed as global" {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
std.testing.refAllDecls(@This());
|
std.testing.refAllDecls(@This());
|
||||||
// TEMPORARY: force test discovery for command-framework support
|
|
||||||
// modules. Nothing in main.zig sema-touches them yet, so their
|
|
||||||
// `test` blocks would otherwise be skipped. Remove these lines
|
|
||||||
// once `runCli` references the framework directly (via the
|
|
||||||
// comptime command-registry walk that replaces the legacy
|
|
||||||
// if-else dispatch chain) — at that point both TimeRange and
|
|
||||||
// framework.zig are reachable through the registry's command
|
|
||||||
// imports.
|
|
||||||
_ = @import("commands/framework.zig");
|
|
||||||
_ = @import("commands/TimeRange.zig");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue