import { Effect } from "effect" import { join } from "node:path" import type { BaseCommandOptions } from "../utils/command" import { EpicService } from "../services/EpicService" import { createLoggers } from "../utils/effect" import { formatTable } from "../utils/table" import { getWorkViews } from "../work-view" import { parseRepositoryReferences } from "../workbase/repository-reference" import { GraphMutationService } from "../services/GraphMutationService" import { work as startWork, type StartWork } from "./work" import { resolveEpicSelector, splitSelectorArgs, } from "../workbase/item-selector" interface EpicOptions extends BaseCommandOptions { readonly subcommand?: string readonly args: readonly string[] readonly ticketUrl?: string readonly description?: string readonly clearDescription?: boolean readonly ifRevision?: string readonly repos?: readonly string[] readonly json?: boolean readonly statuses?: readonly string[] readonly repositories?: readonly string[] readonly ready?: boolean readonly blocked?: boolean readonly pr?: boolean readonly work?: boolean readonly auto?: boolean } export const epic = (options: EpicOptions, work: StartWork = startWork) => Effect.gen(function* () { const epics = yield* EpicService const mutations = yield* GraphMutationService const { log } = createLoggers(options) const cwd = options.cwd ?? process.cwd() switch (options.subcommand) { case "new": case "create": { const id = options.args[0] if (!id || !options.ticketUrl || !options.repos?.length) { return yield* Effect.fail( new Error( "Usage: agency epic create --ticket-url --repo :", ), ) } const record = yield* epics.create( id, options.ticketUrl, parseRepositoryReferences(options.repos), cwd, options.description, ) const { content: _, ...output } = record log( options.json ? JSON.stringify(output, null, 2) : `Created epic '${record.id}'`, ) if (options.subcommand === "new" && options.work) { yield* work({ epicId: record.id, auto: options.auto, cwd, inputAllowed: options.inputAllowed, silent: options.silent, verbose: options.verbose, }) } return } case "list": { const { epicRows } = yield* getWorkViews({ cwd, statuses: options.statuses, repositories: options.repositories, ready: options.ready, blocked: options.blocked, pr: options.pr, }) if (options.json) { log( JSON.stringify( epicRows.map((row) => ({ id: row.id, path: join(cwd, "epics", row.id, "EPIC.md"), revision: row.revision, data: row.data, })), null, 2, ), ) } else { log( formatTable( ["EPIC", "STATUS", "READINESS", "REPOSITORIES", "PR", "WORKTREE"], epicRows.map((row) => [ row.id, row.status, row.readiness, row.repositories, row.pr, row.worktree, ]), ), ) } return } case "show": { const { root, epicId } = yield* resolveEpicSelector( options.args[0], cwd, ) const record = yield* epics.show(epicId, root) const { content: _, ...output } = record log( options.json ? JSON.stringify(output, null, 2) : record.content.trimEnd(), ) return } case "update": { const { root, epicId: id } = yield* resolveEpicSelector( options.args[0], cwd, ) const output = yield* mutations.updateEpic( id, { description: options.clearDescription ? null : options.description, ticketUrl: options.ticketUrl, repos: options.repos === undefined ? undefined : parseRepositoryReferences(options.repos), }, root, options.ifRevision, ) log( options.json ? JSON.stringify(output, null, 2) : `Updated epic '${id}'`, ) return } case "rename": { const { selectors: [selector], operands: [newId], } = splitSelectorArgs(options.args, 1) if (!newId) { return yield* Effect.fail(new Error("New epic ID is required")) } const { root, epicId: id } = yield* resolveEpicSelector(selector, cwd) const output = yield* mutations.renameEpic( id, newId, root, options.ifRevision, ) log( options.json ? JSON.stringify(output, null, 2) : `Renamed epic '${id}' to '${newId}'`, ) return } default: return yield* Effect.fail( new Error( "Subcommand is required. Available subcommands: new, create, list, show, update, rename", ), ) } }) export const help = ` Usage: agency epic Subcommands: new Create an epic, optionally starting work create Create an epic list List epics show [epic] Show an epic update [epic] Update epic metadata rename [epic] Rename an epic and update task references An [epic] selector is an epic ID or a path to an epic document or directory. When omitted, the epic containing the current directory is used. Create options: --ticket-url External ticket URL --description Short description of the epic --repo : Read-only repository reference; repeatable --work Start work on the new epic after creating it --auto Pass --auto to work; requires --work Update options: --ticket-url Replace the external ticket URL --description Replace the description --clear-description Remove the description --repo : Replace repository references; repeatable Mutation options: --if-revision Require the target's current revision Options: --json Output results as JSON --status Filter list by status; repeatable --repository Filter list by repository; repeatable --ready Include only ready epics --blocked Include only blocked epics --pr / --no-pr Filter by recorded PR presence `