migrate cache to new cli framework
This commit is contained in:
parent
9813d951d9
commit
722ddc0e98
2 changed files with 239 additions and 141 deletions
|
|
@ -1,11 +1,43 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zfin = @import("../root.zig");
|
const zfin = @import("../root.zig");
|
||||||
const cli = @import("common.zig");
|
const cli = @import("common.zig");
|
||||||
|
const framework = @import("framework.zig");
|
||||||
const srf = @import("srf");
|
const srf = @import("srf");
|
||||||
|
|
||||||
const Store = zfin.cache.Store;
|
const Store = zfin.cache.Store;
|
||||||
const DataType = zfin.cache.DataType;
|
const DataType = zfin.cache.DataType;
|
||||||
|
|
||||||
|
pub const Subcommand = enum { stats, clear };
|
||||||
|
|
||||||
|
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`.
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
framework.validateCommandModule(@This());
|
||||||
|
}
|
||||||
|
|
||||||
/// Data types to show in the stats table (skip candles_meta and meta — internal bookkeeping).
|
/// Data types to show in the stats table (skip candles_meta and meta — internal bookkeeping).
|
||||||
const display_types = [_]DataType{
|
const display_types = [_]DataType{
|
||||||
.candles_daily,
|
.candles_daily,
|
||||||
|
|
@ -25,134 +57,168 @@ const display_labels = [_][]const u8{
|
||||||
"etf_profile",
|
"etf_profile",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(io: std.Io, allocator: std.mem.Allocator, config: zfin.Config, subcommand: []const u8, out: *std.Io.Writer) !void {
|
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||||
if (std.mem.eql(u8, subcommand, "stats")) {
|
if (cmd_args.len < 1) {
|
||||||
// Capture wall-clock once per invocation so every "X ago" display
|
try cli.stderrPrint(ctx.io, "Error: 'cache' requires a subcommand (stats, clear)\n");
|
||||||
// line in the table is computed against the same reference point.
|
return error.MissingSubcommand;
|
||||||
const now_s = std.Io.Timestamp.now(io, .real).toSeconds();
|
}
|
||||||
try out.print("Cache directory: {s}\n\n", .{config.cache_dir});
|
if (cmd_args.len > 1) {
|
||||||
|
try cli.stderrPrint(ctx.io, "Error: 'cache' takes a single subcommand\n");
|
||||||
|
return error.UnexpectedArg;
|
||||||
|
}
|
||||||
|
const sub_str = cmd_args[0];
|
||||||
|
if (std.mem.eql(u8, sub_str, "stats")) {
|
||||||
|
return .{ .sub = .stats };
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, sub_str, "clear")) {
|
||||||
|
return .{ .sub = .clear };
|
||||||
|
}
|
||||||
|
try cli.stderrPrint(ctx.io, "Error: unknown cache subcommand '");
|
||||||
|
try cli.stderrPrint(ctx.io, sub_str);
|
||||||
|
try cli.stderrPrint(ctx.io, "'. Use 'stats' or 'clear'.\n");
|
||||||
|
return error.UnknownSubcommand;
|
||||||
|
}
|
||||||
|
|
||||||
var dir = std.Io.Dir.cwd().openDir(io, config.cache_dir, .{ .iterate = true }) catch {
|
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
try out.print(" (empty -- no cached data)\n", .{});
|
switch (parsed.sub) {
|
||||||
return;
|
.stats => try runStats(ctx),
|
||||||
};
|
.clear => try runClear(ctx),
|
||||||
defer dir.close(io);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collect and sort symbol names
|
fn runStats(ctx: *framework.RunCtx) !void {
|
||||||
var symbols: std.ArrayList([]const u8) = .empty;
|
const io = ctx.io;
|
||||||
defer {
|
const allocator = ctx.allocator;
|
||||||
for (symbols.items) |s| allocator.free(s);
|
const config = ctx.config;
|
||||||
symbols.deinit(allocator);
|
const out = ctx.out;
|
||||||
|
// Capture wall-clock once per invocation so every "X ago" display
|
||||||
|
// line in the table is computed against the same reference point.
|
||||||
|
//
|
||||||
|
// wall-clock required: human-readable "age" rendering needs the
|
||||||
|
// current time rather than the per-invocation `today`.
|
||||||
|
const now_s = std.Io.Timestamp.now(io, .real).toSeconds();
|
||||||
|
try out.print("Cache directory: {s}\n\n", .{config.cache_dir});
|
||||||
|
|
||||||
|
var dir = std.Io.Dir.cwd().openDir(io, config.cache_dir, .{ .iterate = true }) catch {
|
||||||
|
try out.print(" (empty -- no cached data)\n", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer dir.close(io);
|
||||||
|
|
||||||
|
// Collect and sort symbol names
|
||||||
|
var symbols: std.ArrayList([]const u8) = .empty;
|
||||||
|
defer {
|
||||||
|
for (symbols.items) |s| allocator.free(s);
|
||||||
|
symbols.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
var iter = dir.iterate();
|
||||||
|
while (iter.next(io) catch null) |entry| {
|
||||||
|
if (entry.kind == .directory) {
|
||||||
|
const name = allocator.dupe(u8, entry.name) catch continue;
|
||||||
|
symbols.append(allocator, name) catch {
|
||||||
|
allocator.free(name);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var iter = dir.iterate();
|
if (symbols.items.len == 0) {
|
||||||
while (iter.next(io) catch null) |entry| {
|
try out.print(" (empty -- no cached data)\n", .{});
|
||||||
if (entry.kind == .directory) {
|
return;
|
||||||
const name = allocator.dupe(u8, entry.name) catch continue;
|
}
|
||||||
symbols.append(allocator, name) catch {
|
|
||||||
allocator.free(name);
|
std.mem.sort([]const u8, symbols.items, {}, struct {
|
||||||
continue;
|
fn cmp(_: void, a: []const u8, b: []const u8) bool {
|
||||||
};
|
return std.mem.order(u8, a, b) == .lt;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}.cmp);
|
||||||
|
|
||||||
if (symbols.items.len == 0) {
|
// Track totals
|
||||||
try out.print(" (empty -- no cached data)\n", .{});
|
var total_size: u64 = 0;
|
||||||
return;
|
var total_files: usize = 0;
|
||||||
}
|
|
||||||
|
|
||||||
std.mem.sort([]const u8, symbols.items, {}, struct {
|
for (symbols.items) |symbol| {
|
||||||
fn cmp(_: void, a: []const u8, b: []const u8) bool {
|
try out.print("{s}\n", .{symbol});
|
||||||
return std.mem.order(u8, a, b) == .lt;
|
|
||||||
}
|
|
||||||
}.cmp);
|
|
||||||
|
|
||||||
// Track totals
|
// Print header
|
||||||
var total_size: u64 = 0;
|
try out.print(" {s:<14} {s:>10} {s}\n", .{ "Type", "Size", "Updated" });
|
||||||
var total_files: usize = 0;
|
try out.print(" {s:->14} {s:->10} {s:->24}\n", .{ "", "", "" });
|
||||||
|
|
||||||
for (symbols.items) |symbol| {
|
var symbol_size: u64 = 0;
|
||||||
try out.print("{s}\n", .{symbol});
|
var symbol_files: usize = 0;
|
||||||
|
|
||||||
// Print header
|
for (display_types, display_labels) |dt, label| {
|
||||||
try out.print(" {s:<14} {s:>10} {s}\n", .{ "Type", "Size", "Updated" });
|
const info = getFileInfo(io, allocator, config.cache_dir, symbol, dt);
|
||||||
try out.print(" {s:->14} {s:->10} {s:->24}\n", .{ "", "", "" });
|
if (info.exists) {
|
||||||
|
symbol_files += 1;
|
||||||
|
symbol_size += info.size;
|
||||||
|
|
||||||
var symbol_size: u64 = 0;
|
var size_buf: [10]u8 = undefined;
|
||||||
var symbol_files: usize = 0;
|
const size_str = formatSize(&size_buf, info.size);
|
||||||
|
|
||||||
for (display_types, display_labels) |dt, label| {
|
if (info.is_negative) {
|
||||||
const info = getFileInfo(io, allocator, config.cache_dir, symbol, dt);
|
try out.print(" {s:<14} {s:>10} (negative cache)\n", .{ label, size_str });
|
||||||
if (info.exists) {
|
} else if (info.created) |ts| {
|
||||||
symbol_files += 1;
|
var age_buf: [24]u8 = undefined;
|
||||||
symbol_size += info.size;
|
const age_str = formatAge(&age_buf, ts, now_s);
|
||||||
|
const thru = info.lastDate() orelse "";
|
||||||
var size_buf: [10]u8 = undefined;
|
if (info.expired) {
|
||||||
const size_str = formatSize(&size_buf, info.size);
|
if (thru.len > 0) {
|
||||||
|
try out.print(" {s:<14} {s:>10} {s} (stale, thru {s})\n", .{ label, size_str, age_str, thru });
|
||||||
if (info.is_negative) {
|
|
||||||
try out.print(" {s:<14} {s:>10} (negative cache)\n", .{ label, size_str });
|
|
||||||
} else if (info.created) |ts| {
|
|
||||||
var age_buf: [24]u8 = undefined;
|
|
||||||
const age_str = formatAge(&age_buf, ts, now_s);
|
|
||||||
const thru = info.lastDate() orelse "";
|
|
||||||
if (info.expired) {
|
|
||||||
if (thru.len > 0) {
|
|
||||||
try out.print(" {s:<14} {s:>10} {s} (stale, thru {s})\n", .{ label, size_str, age_str, thru });
|
|
||||||
} else {
|
|
||||||
try out.print(" {s:<14} {s:>10} {s} (stale)\n", .{ label, size_str, age_str });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (thru.len > 0) {
|
try out.print(" {s:<14} {s:>10} {s} (stale)\n", .{ label, size_str, age_str });
|
||||||
try out.print(" {s:<14} {s:>10} {s} (thru {s})\n", .{ label, size_str, age_str, thru });
|
|
||||||
} else {
|
|
||||||
try out.print(" {s:<14} {s:>10} {s}\n", .{ label, size_str, age_str });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try out.print(" {s:<14} {s:>10} (no timestamp)\n", .{ label, size_str });
|
if (thru.len > 0) {
|
||||||
|
try out.print(" {s:<14} {s:>10} {s} (thru {s})\n", .{ label, size_str, age_str, thru });
|
||||||
|
} else {
|
||||||
|
try out.print(" {s:<14} {s:>10} {s}\n", .{ label, size_str, age_str });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
try out.print(" {s:<14} {s:>10} (no timestamp)\n", .{ label, size_str });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also count candles_meta size (not displayed as its own row but is part of total)
|
|
||||||
const meta_info = getFileInfo(io, allocator, config.cache_dir, symbol, .candles_meta);
|
|
||||||
if (meta_info.exists) {
|
|
||||||
symbol_size += meta_info.size;
|
|
||||||
symbol_files += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (symbol_files == 0) {
|
|
||||||
try out.print(" (empty)\n", .{});
|
|
||||||
}
|
|
||||||
try out.print("\n", .{});
|
|
||||||
total_size += symbol_size;
|
|
||||||
total_files += symbol_files;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also count the top-level cusip_tickers.srf if present
|
// Also count candles_meta size (not displayed as its own row but is part of total)
|
||||||
const cusip_path = std.fs.path.join(allocator, &.{ config.cache_dir, "cusip_tickers.srf" }) catch null;
|
const meta_info = getFileInfo(io, allocator, config.cache_dir, symbol, .candles_meta);
|
||||||
if (cusip_path) |path| {
|
if (meta_info.exists) {
|
||||||
defer allocator.free(path);
|
symbol_size += meta_info.size;
|
||||||
if (std.Io.Dir.cwd().statFile(io, path, .{})) |stat| {
|
symbol_files += 1;
|
||||||
total_size += stat.size;
|
|
||||||
total_files += 1;
|
|
||||||
} else |_| {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var total_buf: [10]u8 = undefined;
|
if (symbol_files == 0) {
|
||||||
try out.print("{d} symbol(s), {d} file(s), {s} total\n", .{
|
try out.print(" (empty)\n", .{});
|
||||||
symbols.items.len,
|
}
|
||||||
total_files,
|
try out.print("\n", .{});
|
||||||
formatSize(&total_buf, total_size),
|
total_size += symbol_size;
|
||||||
});
|
total_files += symbol_files;
|
||||||
} else if (std.mem.eql(u8, subcommand, "clear")) {
|
|
||||||
var store = Store.init(io, allocator, config.cache_dir);
|
|
||||||
try store.clearAll();
|
|
||||||
try out.writeAll("Cache cleared.\n");
|
|
||||||
} else {
|
|
||||||
try cli.stderrPrint(io, "Unknown cache subcommand. Use 'stats' or 'clear'.\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also count the top-level cusip_tickers.srf if present
|
||||||
|
const cusip_path = std.fs.path.join(allocator, &.{ config.cache_dir, "cusip_tickers.srf" }) catch null;
|
||||||
|
if (cusip_path) |path| {
|
||||||
|
defer allocator.free(path);
|
||||||
|
if (std.Io.Dir.cwd().statFile(io, path, .{})) |stat| {
|
||||||
|
total_size += stat.size;
|
||||||
|
total_files += 1;
|
||||||
|
} else |_| {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var total_buf: [10]u8 = undefined;
|
||||||
|
try out.print("{d} symbol(s), {d} file(s), {s} total\n", .{
|
||||||
|
symbols.items.len,
|
||||||
|
total_files,
|
||||||
|
formatSize(&total_buf, total_size),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runClear(ctx: *framework.RunCtx) !void {
|
||||||
|
var store = Store.init(ctx.io, ctx.allocator, ctx.config.cache_dir);
|
||||||
|
try store.clearAll();
|
||||||
|
try ctx.out.writeAll("Cache cleared.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileInfo = struct {
|
const FileInfo = struct {
|
||||||
|
|
@ -253,6 +319,43 @@ fn formatAge(buf: *[24]u8, before_s: i64, after_s: i64) []const u8 {
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "parseArgs: 'stats' resolves to .stats" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"stats"};
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqual(Subcommand.stats, parsed.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: 'clear' resolves to .clear" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"clear"};
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqual(Subcommand.clear, parsed.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: missing subcommand errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{};
|
||||||
|
try std.testing.expectError(error.MissingSubcommand, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: unknown subcommand errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"bogus"};
|
||||||
|
try std.testing.expectError(error.UnknownSubcommand, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: extra args error" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{ "stats", "extra" };
|
||||||
|
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
test "formatAge: future timestamp renders 'just now'" {
|
test "formatAge: future timestamp renders 'just now'" {
|
||||||
var buf: [24]u8 = undefined;
|
var buf: [24]u8 = undefined;
|
||||||
try std.testing.expectEqualStrings("just now", formatAge(&buf, 1_700_000_100, 1_700_000_000));
|
try std.testing.expectEqualStrings("just now", formatAge(&buf, 1_700_000_100, 1_700_000_000));
|
||||||
|
|
|
||||||
67
src/main.zig
67
src/main.zig
|
|
@ -2,6 +2,37 @@ const std = @import("std");
|
||||||
const zfin = @import("root.zig");
|
const zfin = @import("root.zig");
|
||||||
const tui = @import("tui.zig");
|
const tui = @import("tui.zig");
|
||||||
const cli = @import("commands/common.zig");
|
const cli = @import("commands/common.zig");
|
||||||
|
const cmd_framework = @import("commands/framework.zig");
|
||||||
|
|
||||||
|
/// Comptime registry of CLI commands. Field name is the user-facing
|
||||||
|
/// subcommand name; value is the imported module struct. Order
|
||||||
|
/// follows the canonical group taxonomy in `framework.Group`
|
||||||
|
/// (symbol-lookup → portfolio → time-series → hygiene → infra) so
|
||||||
|
/// `zfin help` reads in workflow order. Adding a new command is one
|
||||||
|
/// edit here (after authoring the module). Validation runs at
|
||||||
|
/// comptime in the block below.
|
||||||
|
const command_modules = .{
|
||||||
|
// Per-symbol lookups
|
||||||
|
.perf = @import("commands/perf.zig"),
|
||||||
|
.quote = @import("commands/quote.zig"),
|
||||||
|
.divs = @import("commands/divs.zig"),
|
||||||
|
.splits = @import("commands/splits.zig"),
|
||||||
|
.options = @import("commands/options.zig"),
|
||||||
|
.earnings = @import("commands/earnings.zig"),
|
||||||
|
.etf = @import("commands/etf.zig"),
|
||||||
|
|
||||||
|
// Data hygiene
|
||||||
|
.lookup = @import("commands/lookup.zig"),
|
||||||
|
|
||||||
|
// Infrastructure
|
||||||
|
.cache = @import("commands/cache.zig"),
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
||||||
|
cmd_framework.validateCommandModule(@field(command_modules, f.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const usage =
|
const usage =
|
||||||
\\Usage: zfin [global options] <command> [command options]
|
\\Usage: zfin [global options] <command> [command options]
|
||||||
|
|
@ -486,12 +517,6 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
defer if (wl.resolved) |r| r.deinit(allocator);
|
defer if (wl.resolved) |r| r.deinit(allocator);
|
||||||
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
||||||
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
||||||
} else if (std.mem.eql(u8, command, "cache")) {
|
|
||||||
if (cmd_args.len < 1) {
|
|
||||||
try cli.stderrPrint(io, "Error: 'cache' requires a subcommand (stats, clear)\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
try commands.cache.run(io, allocator, config, cmd_args[0], out);
|
|
||||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||||
if (cmd_args.len < 1) {
|
if (cmd_args.len < 1) {
|
||||||
try cli.stderrPrint(io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
try cli.stderrPrint(io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||||
|
|
@ -797,36 +822,6 @@ fn globalOffender(args: []const []const u8) ?[]const u8 {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command modules ──────────────────────────────────────────
|
|
||||||
const cmd_framework = @import("commands/framework.zig");
|
|
||||||
|
|
||||||
/// Comptime registry of CLI commands that have migrated to the
|
|
||||||
/// framework contract. The dispatcher in `runCli` walks this with
|
|
||||||
/// `inline for`; the field name is the user-facing command name
|
|
||||||
/// and the value is the imported module struct. Each registered
|
|
||||||
/// module satisfies `framework.validateCommandModule` (enforced at
|
|
||||||
/// the registry's comptime block below).
|
|
||||||
///
|
|
||||||
/// During the migration window, this registry coexists with the
|
|
||||||
/// legacy if-else chain in `runCli`. Once every command is
|
|
||||||
/// registered, the legacy chain goes away (commit 17).
|
|
||||||
const command_modules = .{
|
|
||||||
.divs = @import("commands/divs.zig"),
|
|
||||||
.earnings = @import("commands/earnings.zig"),
|
|
||||||
.etf = @import("commands/etf.zig"),
|
|
||||||
.lookup = @import("commands/lookup.zig"),
|
|
||||||
.options = @import("commands/options.zig"),
|
|
||||||
.perf = @import("commands/perf.zig"),
|
|
||||||
.quote = @import("commands/quote.zig"),
|
|
||||||
.splits = @import("commands/splits.zig"),
|
|
||||||
};
|
|
||||||
|
|
||||||
comptime {
|
|
||||||
for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
|
||||||
cmd_framework.validateCommandModule(@field(command_modules, f.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const commands = struct {
|
const commands = struct {
|
||||||
const history = @import("commands/history.zig");
|
const history = @import("commands/history.zig");
|
||||||
const portfolio = @import("commands/portfolio.zig");
|
const portfolio = @import("commands/portfolio.zig");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue