refactor(cli): convert stats command to effectCmd (#25474)

This commit is contained in:
Kit Langton
2026-05-02 18:01:06 -04:00
committed by GitHub
parent 79b6ce5db4
commit c1686c6ddc
+33 -28
View File
@@ -1,11 +1,11 @@
import type { Argv } from "yargs" import { Effect } from "effect"
import { cmd } from "./cmd" import { effectCmd } from "../effect-cmd"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { bootstrap } from "../bootstrap"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { SessionTable } from "../../session/session.sql" import { SessionTable } from "../../session/session.sql"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Instance } from "../../project/instance" import { InstanceRef } from "@/effect/instance-ref"
import { InstanceStore } from "@/project/instance-store"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
interface SessionStats { interface SessionStats {
@@ -47,11 +47,11 @@ interface SessionStats {
medianTokensPerSession: number medianTokensPerSession: number
} }
export const StatsCommand = cmd({ export const StatsCommand = effectCmd({
command: "stats", command: "stats",
describe: "show token usage and cost statistics", describe: "show token usage and cost statistics",
builder: (yargs: Argv) => { builder: (yargs) =>
return yargs yargs
.option("days", { .option("days", {
describe: "show stats for the last N days (default: all time)", describe: "show stats for the last N days (default: all time)",
type: "number", type: "number",
@@ -66,34 +66,39 @@ export const StatsCommand = cmd({
.option("project", { .option("project", {
describe: "filter by project (default: all projects, empty string: current project)", describe: "filter by project (default: all projects, empty string: current project)",
type: "string", type: "string",
}) }),
}, handler: Effect.fn("Cli.stats")(function* (args) {
handler: async (args) => { const ctx = yield* InstanceRef
await bootstrap(process.cwd(), async () => { if (!ctx) return
const stats = await aggregateSessionStats(args.days, args.project) const store = yield* InstanceStore.Service
return yield* run(args, ctx.project).pipe(Effect.ensuring(store.dispose(ctx)))
let modelLimit: number | undefined }),
if (args.models === true) {
modelLimit = Infinity
} else if (typeof args.models === "number") {
modelLimit = args.models
}
displayStats(stats, args.tools, modelLimit)
})
},
}) })
async function getCurrentProject(): Promise<Project.Info> { const run = (args: { days?: number; tools?: number; models?: unknown; project?: string }, currentProject: Project.Info) =>
return Instance.project Effect.promise(async () => {
} const stats = await aggregateSessionStats(args.days, args.project, currentProject)
let modelLimit: number | undefined
if (args.models === true) {
modelLimit = Infinity
} else if (typeof args.models === "number") {
modelLimit = args.models
}
displayStats(stats, args.tools, modelLimit)
})
async function getAllSessions(): Promise<Session.Info[]> { async function getAllSessions(): Promise<Session.Info[]> {
const rows = Database.use((db) => db.select().from(SessionTable).all()) const rows = Database.use((db) => db.select().from(SessionTable).all())
return rows.map((row) => Session.fromRow(row)) return rows.map((row) => Session.fromRow(row))
} }
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> { export async function aggregateSessionStats(
days?: number,
projectFilter?: string,
currentProject?: Project.Info,
): Promise<SessionStats> {
const sessions = await getAllSessions() const sessions = await getAllSessions()
const MS_IN_DAY = 24 * 60 * 60 * 1000 const MS_IN_DAY = 24 * 60 * 60 * 1000
@@ -117,7 +122,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
if (projectFilter !== undefined) { if (projectFilter !== undefined) {
if (projectFilter === "") { if (projectFilter === "") {
const currentProject = await getCurrentProject() if (!currentProject) throw new Error("currentProject required when projectFilter is empty string")
filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id) filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
} else { } else {
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter) filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)