From ab2d7626586fa855e9bee0c121bc279050deb988 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 31 Oct 2025 00:46:40 -0400 Subject: [PATCH] update action, iterate on logging --- agents/claude.ts | 13 ++-- package.json | 2 +- utils/cli.ts | 189 ++++++++--------------------------------------- utils/table.ts | 142 ----------------------------------- 4 files changed, 38 insertions(+), 308 deletions(-) delete mode 100644 utils/table.ts diff --git a/agents/claude.ts b/agents/claude.ts index 7fd0fea..b2f7d01 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -1,8 +1,8 @@ import * as core from "@actions/core"; import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { createMcpConfig } from "../mcp/config.ts"; -import { debugLog, isDebug } from "../utils/logging.ts"; import { log } from "../utils/cli.ts"; +import { debugLog, isDebug } from "../utils/logging.ts"; import { instructions } from "./shared.ts"; import type { Agent, AgentConfig, AgentResult } from "./types.ts"; @@ -39,8 +39,6 @@ export class ClaudeAgent implements Agent { debugLog(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`); } - core.startGroup("🔄 Run details"); - // Initialize session core.info(`🚀 Starting Claude Agent SDK session...`); @@ -63,7 +61,6 @@ export class ClaudeAgent implements Agent { } log.success("Task complete."); - core.endGroup(); return { success: true, @@ -130,8 +127,12 @@ const messageHandlers: SDKMessageHandlers = { }, result: async (data) => { if (data.subtype === "success") { - await log.table([ - [{ data: "Cost", header: true }, { data: "Input Tokens", header: true }, { data: "Output Tokens", header: true }], + await log.summaryTable([ + [ + { data: "Cost", header: true }, + { data: "Input Tokens", header: true }, + { data: "Output Tokens", header: true }, + ], [ `$${data.total_cost_usd?.toFixed(4) || "0.0000"}`, String(data.usage?.input_tokens || 0), diff --git a/package.json b/package.json index b210a6f..b21db15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.62", + "version": "0.0.63", "type": "module", "files": [ "index.js", diff --git a/utils/cli.ts b/utils/cli.ts index e6ba45e..d6ebd79 100644 --- a/utils/cli.ts +++ b/utils/cli.ts @@ -1,6 +1,5 @@ /** - * CLI output utilities for GitHub Actions and local development - * Uses GitHub Actions Job Summaries API when available for rich formatting + * CLI output utilities that work well in both local and GitHub Actions environments */ import * as core from "@actions/core"; @@ -79,7 +78,7 @@ function boxString( /** * Print a formatted box with text - * In GitHub Actions, uses markdown code blocks; locally uses box formatting + * Works well in both local and GitHub Actions environments */ function box( text: string, @@ -88,29 +87,14 @@ function box( maxWidth?: number; } ): void { - if (isGitHubActions) { - const { title } = options || {}; - let markdown = ""; - if (title) { - markdown += `### ${title}\n\n`; - } - markdown += "```\n"; - markdown += text; - markdown += "\n```\n"; - // Note: summary.write() should be called once at the end, but for immediate visibility we call it here - // In practice, you might want to batch multiple calls - core.summary.addRaw(markdown); - core.info(markdown); - } else { - log.info(boxString(text, options)); - } + core.info(boxString(text, options)); } /** - * Add a table using GitHub Actions Job Summaries API - * Falls back to formatted console table locally + * Add a table to GitHub Actions job summary (rich formatting) + * Also logs to console. Only use this once at the end of execution. */ -async function table( +async function summaryTable( rows: Array>, options?: { title?: string; @@ -118,107 +102,38 @@ async function table( ): Promise { const { title } = options || {}; + // Convert rows to format expected by Job Summaries API + const formattedRows = rows.map((row) => + row.map((cell) => { + if (typeof cell === "string") { + return { data: cell }; + } + return cell; + }) + ); + if (isGitHubActions) { const summary = core.summary; - if (title) { summary.addHeading(title); } - - // Convert rows to format expected by Job Summaries API - const formattedRows = rows.map((row) => - row.map((cell) => { - if (typeof cell === "string") { - return { data: cell }; - } - return cell; - }) - ); - summary.addTable(formattedRows); await summary.write(); - - // Also log to console for visibility in logs - const tableText = formattedRows - .map((row) => row.map((cell) => cell.data).join(" | ")) - .join("\n"); - log.info(tableText); - } else { - // Local fallback: simple console table - if (title) { - log.info(`\n${title}`); - } - const tableText = rows - .map((row) => { - const rowData = row.map((cell) => { - const text = typeof cell === "string" ? cell : cell.data; - const isHeader = typeof cell !== "string" && cell.header; - return isHeader ? `**${text}**` : text; - }); - return rowData.join(" | "); - }) - .join("\n"); - log.info(`\n${tableText}\n`); } -} -/** - * Add raw markdown to job summary (GitHub Actions) or print as info (local) - */ -async function markdown(content: string): Promise { - if (isGitHubActions) { - core.summary.addRaw(content); - await core.summary.write(); - // Also log to console - core.info(content); - } else { - log.info(content); - } -} - -/** - * Add a code block to output - */ -function codeBlock(code: string, language?: string): void { - if (isGitHubActions) { - core.summary.addCodeBlock(code, language); - core.info(`\`\`\`${language || ""}\n${code}\n\`\`\``); - } else { - log.info(`\`\`\`${language || ""}\n${code}\n\`\`\``); - } -} - -/** - * Add a link to output - */ -function link(text: string, url: string): void { - if (isGitHubActions) { - core.summary.addLink(text, url); - log.info(`[${text}](${url})`); - } else { - log.info(`[${text}](${url})`); - } -} - -/** - * Write all pending summary changes to the job summary - * Call this at the end of your action to ensure all summary content is written - */ -async function writeSummary(): Promise { - if (isGitHubActions) { - await core.summary.write(); + // Also log to console for visibility + if (title) { + core.info(`\n${title}`); } + const tableText = formattedRows.map((row) => row.map((cell) => cell.data).join(" | ")).join("\n"); + core.info(`\n${tableText}\n`); } /** * Print a separator line */ function separator(length: number = 50): void { - if (isGitHubActions) { - core.info("─".repeat(length)); - } else { - console.log("─".repeat(length)); - } + core.info("─".repeat(length)); } /** @@ -226,87 +141,43 @@ function separator(length: number = 50): void { */ export const log = { /** - * Check if running in GitHub Actions environment - */ - isGitHubActions: (): boolean => isGitHubActions, - - /** - * Print info message (GitHub Actions or console) + * Print info message */ info: (message: string): void => { - if (isGitHubActions) { - core.info(message); - } else { - console.log(message); - } + core.info(message); }, /** * Print warning message */ warning: (message: string): void => { - if (isGitHubActions) { - core.warning(message); - } else { - console.warn(`⚠️ ${message}`); - } + core.warning(message); }, /** * Print error message */ error: (message: string): void => { - if (isGitHubActions) { - core.error(message); - } else { - console.error(`❌ ${message}`); - } + core.error(message); }, /** * Print success message */ success: (message: string): void => { - const msg = `✅ ${message}`; - if (isGitHubActions) { - core.info(msg); - } else { - console.log(msg); - } + core.info(`✅ ${message}`); }, /** * Print a formatted box with text - * In GitHub Actions, uses markdown code blocks; locally uses box formatting */ box, /** - * Add a table using GitHub Actions Job Summaries API - * Falls back to formatted console table locally + * Add a table to GitHub Actions job summary (rich formatting) + * Only use this once at the end of execution */ - table, - - /** - * Add raw markdown to job summary (GitHub Actions) or print as info (local) - */ - markdown, - - /** - * Add a code block to output - */ - codeBlock, - - /** - * Add a link to output - */ - link, - - /** - * Write all pending summary changes to the job summary - * Call this at the end of your action to ensure all summary content is written - */ - writeSummary, + summaryTable, /** * Print a separator line diff --git a/utils/table.ts b/utils/table.ts deleted file mode 100644 index af28642..0000000 --- a/utils/table.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { table } from "table"; - -/** - * Print a formatted table with consistent styling - * @param rows - Array of string arrays representing table rows - * @param options - Optional table configuration - */ - -export function tableString( - rows: string[][], - options?: { - title?: string; - indent?: string; - drawHorizontalLine?: (lineIndex: number, rowCount: number) => boolean; - } -): string { - const { - title, - indent = "", - drawHorizontalLine = (lineIndex: number, rowCount: number) => { - return lineIndex === 0 || (options?.title && lineIndex === 1) || lineIndex === rowCount; - }, - } = options || {}; - - if (title) { - rows.unshift([title]); - } - - const tableOutput = table(rows, { - drawHorizontalLine, - border: { - topBody: `─`, - topJoin: `┬`, - topLeft: `┌`, - topRight: `┐`, - - bottomBody: `─`, - bottomJoin: `┴`, - bottomLeft: `└`, - bottomRight: `┘`, - - bodyLeft: `│`, - bodyRight: `│`, - bodyJoin: `│`, - - joinBody: `─`, - joinLeft: `├`, - joinRight: `┤`, - joinJoin: `┼`, - }, - }); - - const indentedOutput = tableOutput.split("\n").join(`\n${indent}`).trim(); - - return `${indent}${indentedOutput}`; -} - -/** - * Print a multi-line string in a formatted box with text wrapping - * @param text - The text to display - * @param options - Optional configuration for the box - */ -export function boxString( - text: string, - options?: { - title?: string; - maxWidth?: number; - indent?: string; - padding?: number; - } -): string { - const { title, maxWidth = 80, indent = "", padding = 1 } = options || {}; - - // Clean up the text and split into lines - const lines = text.trim().split("\n"); - - // Word wrap each line to fit within maxWidth - const wrappedLines: string[] = []; - for (const line of lines) { - if (line.length <= maxWidth - padding * 2) { - wrappedLines.push(line); - } else { - // Word wrap the line - const words = line.split(" "); - let currentLine = ""; - - for (const word of words) { - const testLine = currentLine ? `${currentLine} ${word}` : word; - if (testLine.length <= maxWidth - padding * 2) { - currentLine = testLine; - } else { - if (currentLine) { - wrappedLines.push(currentLine); - currentLine = word; - } else { - // Word is too long, break it - wrappedLines.push(word.substring(0, maxWidth - padding * 2)); - currentLine = word.substring(maxWidth - padding * 2); - } - } - } - - if (currentLine) { - wrappedLines.push(currentLine); - } - } - } - - // Find the maximum line length for box sizing - const maxLineLength = Math.max(...wrappedLines.map((line) => line.length)); - const boxWidth = maxLineLength + padding * 2; - - // Create the box - const topBorder = "┌" + "─".repeat(boxWidth) + "┐"; - const bottomBorder = "└" + "─".repeat(boxWidth) + "┘"; - const sideBorder = "│"; - - let result = ""; - - // Add title if provided - if (title) { - const titleLine = ` ${title} `; - const titlePadding = Math.max(0, boxWidth - titleLine.length); - result += `${indent}┌${titleLine}${"─".repeat(titlePadding)}┐\n`; - } - - // Add top border (or title border) - if (!title) { - result += `${indent}${topBorder}\n`; - } - - // Add content lines - for (const line of wrappedLines) { - const paddedLine = line.padEnd(maxLineLength); - result += `${indent}${sideBorder}${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}${sideBorder}\n`; - } - - // Add bottom border - result += `${indent}${bottomBorder}`; - - return result; -}