cleanup comments
This commit is contained in:
+1
-85
@@ -16,14 +16,12 @@ export class ClaudeAgent implements Agent {
|
|||||||
startTime: 0,
|
startTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// $: ExecaMethod;
|
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
throw new Error("Claude agent requires an API key");
|
throw new Error("Claude agent requires an API key");
|
||||||
}
|
}
|
||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
// Removed execa dependency - using spawn utility instead
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +41,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
* Install Claude Code CLI
|
* Install Claude Code CLI
|
||||||
*/
|
*/
|
||||||
async install(): Promise<void> {
|
async install(): Promise<void> {
|
||||||
// Check if Claude Code is already installed
|
|
||||||
if (await this.isClaudeInstalled()) {
|
if (await this.isClaudeInstalled()) {
|
||||||
core.info("Claude Code is already installed, skipping installation");
|
core.info("Claude Code is already installed, skipping installation");
|
||||||
return;
|
return;
|
||||||
@@ -51,16 +48,12 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
core.info("Installing Claude Code...");
|
core.info("Installing Claude Code...");
|
||||||
try {
|
try {
|
||||||
// Use shell execution to properly handle the pipe
|
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 120000, // 2 minute timeout
|
timeout: 120000, // 2 minute timeout
|
||||||
onStdout: () => {
|
onStdout: () => {},
|
||||||
// no logs
|
|
||||||
// process.stdout.write(chunk)
|
|
||||||
},
|
|
||||||
onStderr: (chunk) => process.stderr.write(chunk),
|
onStderr: (chunk) => process.stderr.write(chunk),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,14 +72,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
*/
|
*/
|
||||||
async execute(prompt: string): Promise<AgentResult> {
|
async execute(prompt: string): Promise<AgentResult> {
|
||||||
core.info("Running Claude Code...");
|
core.info("Running Claude Code...");
|
||||||
// printTable([[prompt]]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute Claude Code with the prompt directly using proper headless mode
|
|
||||||
// core.info(`Executing Claude Code with prompt: ${prompt.substring(0, 100)}...`);
|
|
||||||
|
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
// console.log("Using Claude Code from:", claudePath);
|
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
const args = [
|
const args = [
|
||||||
"--print",
|
"--print",
|
||||||
@@ -97,7 +86,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
"bypassPermissions",
|
"bypassPermissions",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add MCP configuration if GitHub credentials are available
|
|
||||||
if (
|
if (
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN &&
|
process.env.GITHUB_INSTALLATION_TOKEN &&
|
||||||
process.env.REPO_OWNER &&
|
process.env.REPO_OWNER &&
|
||||||
@@ -116,10 +104,8 @@ export class ClaudeAgent implements Agent {
|
|||||||
ANTHROPIC_API_KEY: this.apiKey,
|
ANTHROPIC_API_KEY: this.apiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start a collapsible log group for streaming output
|
|
||||||
core.startGroup("🔄 Run details");
|
core.startGroup("🔄 Run details");
|
||||||
|
|
||||||
// Initialize run statistics
|
|
||||||
this.runStats = {
|
this.runStats = {
|
||||||
toolsUsed: 0,
|
toolsUsed: 0,
|
||||||
turns: 0,
|
turns: 0,
|
||||||
@@ -129,7 +115,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
const finalResult = "";
|
const finalResult = "";
|
||||||
const totalCost = 0;
|
const totalCost = 0;
|
||||||
|
|
||||||
// run Claude Code with the prompt
|
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: claudePath,
|
cmd: claudePath,
|
||||||
args,
|
args,
|
||||||
@@ -137,37 +122,21 @@ export class ClaudeAgent implements Agent {
|
|||||||
input: prompt,
|
input: prompt,
|
||||||
timeout: 10 * 60 * 1000, // 10 minutes
|
timeout: 10 * 60 * 1000, // 10 minutes
|
||||||
onStdout: (_chunk) => {
|
onStdout: (_chunk) => {
|
||||||
// console.log(chunk);
|
|
||||||
processJSONChunk(_chunk, this);
|
processJSONChunk(_chunk, this);
|
||||||
},
|
},
|
||||||
onStderr: (_chunk) => {
|
onStderr: (_chunk) => {
|
||||||
if (_chunk.trim()) {
|
if (_chunk.trim()) {
|
||||||
// core.warning(`[warn] ${chunk}`);
|
|
||||||
processJSONChunk(_chunk, this);
|
processJSONChunk(_chunk, this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// throw on non-zero exit code
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`
|
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the complete buffered stdout to extract final results
|
|
||||||
// if (result.stdout.trim()) {
|
|
||||||
// const lines = result.stdout.trim().split("\n");
|
|
||||||
// for (const line of lines) {
|
|
||||||
// if (line.trim()) {
|
|
||||||
// const chunkResult = processJsonChunk(line);
|
|
||||||
// if (chunkResult.finalResult) finalResult = chunkResult.finalResult;
|
|
||||||
// if (chunkResult.totalCost) totalCost = chunkResult.totalCost;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Log run summary
|
|
||||||
const duration = Date.now() - this.runStats.startTime;
|
const duration = Date.now() - this.runStats.startTime;
|
||||||
core.info(
|
core.info(
|
||||||
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
|
||||||
@@ -187,11 +156,9 @@ export class ClaudeAgent implements Agent {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Ensure group is closed even if error occurs before group is started
|
|
||||||
try {
|
try {
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
} catch {
|
} catch {
|
||||||
// Group might not have been started, ignore
|
|
||||||
}
|
}
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
return {
|
return {
|
||||||
@@ -202,34 +169,11 @@ export class ClaudeAgent implements Agent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a JSON chunk line and extract result data
|
|
||||||
*/
|
|
||||||
// function processJsonChunk(line: string): { finalResult?: string; totalCost?: number } {
|
|
||||||
// try {
|
|
||||||
// const chunk = JSON.parse(line.trim());
|
|
||||||
// processJSONChunk(chunk);
|
|
||||||
|
|
||||||
// // Collect final result and cost data
|
|
||||||
// if (chunk.type === "result" && chunk.result) {
|
|
||||||
// return {
|
|
||||||
// finalResult: chunk.result,
|
|
||||||
// totalCost: chunk.total_cost_usd || 0,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// return {};
|
|
||||||
// } catch {
|
|
||||||
// core.debug(`Failed to parse JSON line: ${line}`);
|
|
||||||
// return {};
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pretty print a JSON chunk based on its type
|
* Pretty print a JSON chunk based on its type
|
||||||
*/
|
*/
|
||||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||||
try {
|
try {
|
||||||
// Parse the JSON string first
|
|
||||||
console.log(chunk);
|
console.log(chunk);
|
||||||
const parsedChunk = JSON.parse(chunk.trim());
|
const parsedChunk = JSON.parse(chunk.trim());
|
||||||
|
|
||||||
@@ -237,8 +181,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
case "system":
|
case "system":
|
||||||
if (parsedChunk.subtype === "init") {
|
if (parsedChunk.subtype === "init") {
|
||||||
core.info(`🚀 Starting Claude Code session...`);
|
core.info(`🚀 Starting Claude Code session...`);
|
||||||
// core.info(`📁 Working directory: ${parsedChunk.cwd}`);
|
|
||||||
// core.info(`🔑 Permission mode: ${parsedChunk.permissionMode}`);
|
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
@@ -264,39 +206,31 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
|
|
||||||
case "assistant":
|
case "assistant":
|
||||||
if (parsedChunk.message?.content) {
|
if (parsedChunk.message?.content) {
|
||||||
// Track turns
|
|
||||||
if (agent) {
|
if (agent) {
|
||||||
agent.runStats.turns++;
|
agent.runStats.turns++;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
// Skip empty text content
|
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
// Track tools used
|
|
||||||
if (agent) {
|
if (agent) {
|
||||||
agent.runStats.toolsUsed++;
|
agent.runStats.toolsUsed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced tool usage logging
|
|
||||||
const toolName = content.name;
|
const toolName = content.name;
|
||||||
// const toolId = content.id;
|
|
||||||
|
|
||||||
core.info(`→ ${toolName}`);
|
core.info(`→ ${toolName}`);
|
||||||
|
|
||||||
// Log tool-specific details based on tool type
|
|
||||||
if (content.input) {
|
if (content.input) {
|
||||||
const input = content.input;
|
const input = content.input;
|
||||||
|
|
||||||
// Common tool input fields
|
|
||||||
if (input.description) {
|
if (input.description) {
|
||||||
core.info(` └─ ${input.description}`);
|
core.info(` └─ ${input.description}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool-specific input fields
|
|
||||||
if (input.command) {
|
if (input.command) {
|
||||||
core.info(` └─ command: ${input.command}`);
|
core.info(` └─ command: ${input.command}`);
|
||||||
}
|
}
|
||||||
@@ -325,7 +259,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
core.info(` └─ url: ${input.url}`);
|
core.info(` └─ url: ${input.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For multi-edit or complex operations
|
|
||||||
if (input.edits && Array.isArray(input.edits)) {
|
if (input.edits && Array.isArray(input.edits)) {
|
||||||
core.info(` └─ edits: ${input.edits.length} changes`);
|
core.info(` └─ edits: ${input.edits.length} changes`);
|
||||||
input.edits.forEach((edit: any, index: number) => {
|
input.edits.forEach((edit: any, index: number) => {
|
||||||
@@ -335,19 +268,15 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// For task operations
|
|
||||||
if (input.task) {
|
if (input.task) {
|
||||||
core.info(` └─ task: ${input.task}`);
|
core.info(` └─ task: ${input.task}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For bash operations with specific details
|
|
||||||
if (input.bash_command) {
|
if (input.bash_command) {
|
||||||
core.info(` └─ bash_command: ${input.bash_command}`);
|
core.info(` └─ bash_command: ${input.bash_command}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tool ID for debugging
|
|
||||||
// core.debug(` 🔗 Tool ID: ${toolId}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,9 +289,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
if (content.is_error) {
|
if (content.is_error) {
|
||||||
core.warning(`❌ Tool error: ${content.content}`);
|
core.warning(`❌ Tool error: ${content.content}`);
|
||||||
} else {
|
} else {
|
||||||
// Enhanced tool result logging
|
|
||||||
const _resultContent = content.content.trim();
|
|
||||||
// do nothing for now. usually useless in headless more.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,15 +297,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
|
|
||||||
case "result":
|
case "result":
|
||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
// Claude already prints something almost identical to this, so skip for now
|
|
||||||
// if (parsedChunk.result) {
|
|
||||||
// core.info(
|
|
||||||
// boxString(parsedChunk.result.trim(), {
|
|
||||||
// title: "🤖 Claude Code",
|
|
||||||
// maxWidth: 70,
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
@@ -396,7 +313,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Log unknown chunk types for debugging
|
|
||||||
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25580,7 +25580,7 @@ async function spawn(options) {
|
|||||||
durationMs
|
durationMs
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
child.on("error", (error2) => {
|
child.on("error", (_error) => {
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -25609,8 +25609,8 @@ function tableString(rows, options) {
|
|||||||
return lineIndex === 0 || options?.title && lineIndex === 1 || lineIndex === rowCount;
|
return lineIndex === 0 || options?.title && lineIndex === 1 || lineIndex === rowCount;
|
||||||
}
|
}
|
||||||
} = options || {};
|
} = options || {};
|
||||||
if (options?.title) {
|
if (title) {
|
||||||
rows.unshift([options.title]);
|
rows.unshift([title]);
|
||||||
}
|
}
|
||||||
const tableOutput = (0, import_table.table)(rows, {
|
const tableOutput = (0, import_table.table)(rows, {
|
||||||
drawHorizontalLine,
|
drawHorizontalLine,
|
||||||
@@ -25698,7 +25698,6 @@ var ClaudeAgent = class {
|
|||||||
turns: 0,
|
turns: 0,
|
||||||
startTime: 0
|
startTime: 0
|
||||||
};
|
};
|
||||||
// $: ExecaMethod;
|
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
throw new Error("Claude agent requires an API key");
|
throw new Error("Claude agent requires an API key");
|
||||||
@@ -25927,7 +25926,6 @@ function processJSONChunk(chunk, agent) {
|
|||||||
if (content.is_error) {
|
if (content.is_error) {
|
||||||
core.warning(`\u274C Tool error: ${content.content}`);
|
core.warning(`\u274C Tool error: ${content.content}`);
|
||||||
} else {
|
} else {
|
||||||
const _resultContent = content.content.trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { setupGitHubInstallationToken } from "./utils/github.ts";
|
|||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Get inputs from GitHub Actions
|
|
||||||
const prompt = core.getInput("prompt", { required: true });
|
const prompt = core.getInput("prompt", { required: true });
|
||||||
const anthropic_api_key = core.getInput("anthropic_api_key");
|
const anthropic_api_key = core.getInput("anthropic_api_key");
|
||||||
|
|
||||||
@@ -19,13 +18,11 @@ async function run(): Promise<void> {
|
|||||||
throw new Error("prompt is required");
|
throw new Error("prompt is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create params object with new structure
|
|
||||||
const inputs: ExecutionInputs = {
|
const inputs: ExecutionInputs = {
|
||||||
prompt,
|
prompt,
|
||||||
anthropic_api_key,
|
anthropic_api_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add optional properties only if they exist
|
|
||||||
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
|
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
inputs.github_token = githubToken;
|
inputs.github_token = githubToken;
|
||||||
@@ -47,7 +44,6 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
const result = await main(params);
|
const result = await main(params);
|
||||||
|
|
||||||
// TODO: Set outputs
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Agent execution failed");
|
throw new Error(result.error || "Agent execution failed");
|
||||||
@@ -58,7 +54,6 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the action
|
|
||||||
run().catch((error) => {
|
run().catch((error) => {
|
||||||
console.error("Action failed:", error);
|
console.error("Action failed:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { ClaudeAgent } from "./agents/claude.ts";
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
|
|
||||||
// Expected environment variables that should be passed as inputs
|
|
||||||
export const EXPECTED_INPUTS: string[] = [
|
export const EXPECTED_INPUTS: string[] = [
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
"GITHUB_TOKEN",
|
"GITHUB_TOKEN",
|
||||||
@@ -29,24 +28,19 @@ export interface MainResult {
|
|||||||
|
|
||||||
export async function main(params: MainParams): Promise<MainResult> {
|
export async function main(params: MainParams): Promise<MainResult> {
|
||||||
try {
|
try {
|
||||||
// Extract inputs from params
|
|
||||||
const { inputs, env, cwd } = params;
|
const { inputs, env, cwd } = params;
|
||||||
|
|
||||||
// Set working directory if different from current
|
|
||||||
if (cwd !== process.cwd()) {
|
if (cwd !== process.cwd()) {
|
||||||
process.chdir(cwd);
|
process.chdir(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment variables
|
|
||||||
Object.assign(process.env, env);
|
Object.assign(process.env, env);
|
||||||
|
|
||||||
core.info(`→ Starting agent run with Claude Code`);
|
core.info(`→ Starting agent run with Claude Code`);
|
||||||
|
|
||||||
// Create and install the Claude agent
|
|
||||||
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
|
||||||
await agent.install();
|
await agent.install();
|
||||||
|
|
||||||
// Execute the agent with the prompt
|
|
||||||
const result = await agent.execute(inputs.prompt);
|
const result = await agent.execute(inputs.prompt);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { runAct } from "./utils/act.ts";
|
|||||||
import { generateInstallationToken } from "./utils/generate-installation-token.ts";
|
import { generateInstallationToken } from "./utils/generate-installation-token.ts";
|
||||||
import { setupTestRepo } from "./utils/setup.ts";
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -20,17 +19,14 @@ export async function run(
|
|||||||
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||||
try {
|
try {
|
||||||
if (options.act) {
|
if (options.act) {
|
||||||
// Use Docker/act to run the action
|
|
||||||
console.log("🐳 Running with Docker/act...");
|
console.log("🐳 Running with Docker/act...");
|
||||||
runAct(prompt);
|
runAct(prompt);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup test repository and run directly
|
|
||||||
const tempDir = join(process.cwd(), ".temp");
|
const tempDir = join(process.cwd(), ".temp");
|
||||||
setupTestRepo({ tempDir, forceClean: true });
|
setupTestRepo({ tempDir, forceClean: true });
|
||||||
|
|
||||||
// Change to the temp directory
|
|
||||||
const originalCwd = process.cwd();
|
const originalCwd = process.cwd();
|
||||||
process.chdir(tempDir);
|
process.chdir(tempDir);
|
||||||
|
|
||||||
@@ -40,7 +36,6 @@ export async function run(
|
|||||||
console.log(prompt);
|
console.log(prompt);
|
||||||
console.log("─".repeat(50));
|
console.log("─".repeat(50));
|
||||||
|
|
||||||
// Set environment variables from our .env for the action to use
|
|
||||||
const { EXPECTED_INPUTS } = await import("./main.ts");
|
const { EXPECTED_INPUTS } = await import("./main.ts");
|
||||||
EXPECTED_INPUTS.forEach((inputName) => {
|
EXPECTED_INPUTS.forEach((inputName) => {
|
||||||
const value = process.env[inputName];
|
const value = process.env[inputName];
|
||||||
@@ -49,13 +44,11 @@ export async function run(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run main with the new params structure
|
|
||||||
const inputs: any = {
|
const inputs: any = {
|
||||||
prompt,
|
prompt,
|
||||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add optional properties only if they exist
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
if (process.env.GITHUB_TOKEN) {
|
||||||
inputs.github_token = process.env.GITHUB_TOKEN;
|
inputs.github_token = process.env.GITHUB_TOKEN;
|
||||||
}
|
}
|
||||||
@@ -65,7 +58,6 @@ export async function run(
|
|||||||
inputs.github_installation_token = installationToken;
|
inputs.github_installation_token = installationToken;
|
||||||
console.log("✅ GitHub installation token generated successfully");
|
console.log("✅ GitHub installation token generated successfully");
|
||||||
|
|
||||||
// Set installation token in environment for Claude agent MCP configuration
|
|
||||||
const envWithToken = {
|
const envWithToken = {
|
||||||
...process.env,
|
...process.env,
|
||||||
GITHUB_INSTALLATION_TOKEN: installationToken,
|
GITHUB_INSTALLATION_TOKEN: installationToken,
|
||||||
@@ -77,7 +69,6 @@ export async function run(
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change back to original directory
|
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -97,7 +88,6 @@ export async function run(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI execution when run directly
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
const args = arg({
|
const args = arg({
|
||||||
"--help": Boolean,
|
"--help": Boolean,
|
||||||
@@ -133,15 +123,12 @@ Examples:
|
|||||||
let prompt: string;
|
let prompt: string;
|
||||||
|
|
||||||
if (args["--raw"]) {
|
if (args["--raw"]) {
|
||||||
// Use raw prompt string
|
|
||||||
prompt = args["--raw"];
|
prompt = args["--raw"];
|
||||||
} else {
|
} else {
|
||||||
// Load prompt from file
|
|
||||||
const filePath = args._[0] || "fixtures/basic.txt";
|
const filePath = args._[0] || "fixtures/basic.txt";
|
||||||
const ext = extname(filePath).toLowerCase();
|
const ext = extname(filePath).toLowerCase();
|
||||||
let resolvedPath: string;
|
let resolvedPath: string;
|
||||||
|
|
||||||
// First try as fixtures path
|
|
||||||
const fixturesPath = join(__dirname, "fixtures", filePath);
|
const fixturesPath = join(__dirname, "fixtures", filePath);
|
||||||
if (existsSync(fixturesPath)) {
|
if (existsSync(fixturesPath)) {
|
||||||
resolvedPath = fixturesPath;
|
resolvedPath = fixturesPath;
|
||||||
@@ -153,13 +140,11 @@ Examples:
|
|||||||
|
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case ".txt": {
|
case ".txt": {
|
||||||
// Plain text - pass directly as prompt
|
|
||||||
prompt = readFileSync(resolvedPath, "utf8").trim();
|
prompt = readFileSync(resolvedPath, "utf8").trim();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case ".json": {
|
case ".json": {
|
||||||
// JSON - stringify and pass as prompt
|
|
||||||
const content = readFileSync(resolvedPath, "utf8");
|
const content = readFileSync(resolvedPath, "utf8");
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
prompt = JSON.stringify(parsed, null, 2);
|
prompt = JSON.stringify(parsed, null, 2);
|
||||||
@@ -167,7 +152,6 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ".ts": {
|
case ".ts": {
|
||||||
// TypeScript - dynamic import and stringify default export
|
|
||||||
const fileUrl = pathToFileURL(resolvedPath).href;
|
const fileUrl = pathToFileURL(resolvedPath).href;
|
||||||
const module = await import(fileUrl);
|
const module = await import(fileUrl);
|
||||||
|
|
||||||
@@ -175,14 +159,11 @@ Examples:
|
|||||||
throw new Error(`TypeScript file ${filePath} must have a default export`);
|
throw new Error(`TypeScript file ${filePath} must have a default export`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a string, use it directly
|
|
||||||
if (typeof module.default === "string") {
|
if (typeof module.default === "string") {
|
||||||
prompt = module.default;
|
prompt = module.default;
|
||||||
} else if (typeof module.default === "object" && module.default.prompt) {
|
} else if (typeof module.default === "object" && module.default.prompt) {
|
||||||
// If it's a MainParams object with a prompt field, extract the prompt
|
|
||||||
prompt = module.default.prompt;
|
prompt = module.default.prompt;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise stringify it
|
|
||||||
prompt = JSON.stringify(module.default, null, 2);
|
prompt = JSON.stringify(module.default, null, 2);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -12,27 +12,21 @@ const tempDir = join(__dirname, "..", ".temp");
|
|||||||
const actionPath = join(__dirname, "..");
|
const actionPath = join(__dirname, "..");
|
||||||
const envPath = join(__dirname, "..", "..", ".env");
|
const envPath = join(__dirname, "..", "..", ".env");
|
||||||
|
|
||||||
// Environment variables that should be passed as secrets to the workflow
|
|
||||||
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
|
||||||
|
|
||||||
export function runAct(prompt: string): void {
|
export function runAct(prompt: string): void {
|
||||||
// Setup test repository
|
|
||||||
setupTestRepo({ tempDir });
|
setupTestRepo({ tempDir });
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
config({ path: envPath });
|
config({ path: envPath });
|
||||||
|
|
||||||
// Build action bundles
|
|
||||||
buildAction(actionPath);
|
buildAction(actionPath);
|
||||||
|
|
||||||
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||||
|
|
||||||
// Create minimal dist for act (avoids pnpm symlink issues)
|
|
||||||
const distPath = join(actionPath, ".act-dist");
|
const distPath = join(actionPath, ".act-dist");
|
||||||
console.log("📦 Creating minimal distribution for act...");
|
console.log("📦 Creating minimal distribution for act...");
|
||||||
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
|
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
|
||||||
|
|
||||||
// Copy only necessary files (bundled, no node_modules needed)
|
|
||||||
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
|
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
|
||||||
const src = join(actionPath, file);
|
const src = join(actionPath, file);
|
||||||
if (existsSync(src)) {
|
if (existsSync(src)) {
|
||||||
@@ -41,8 +35,6 @@ export function runAct(prompt: string): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build the act command with input directly
|
|
||||||
// Properly escape the prompt for shell
|
|
||||||
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||||
|
|
||||||
const actCommandParts = [
|
const actCommandParts = [
|
||||||
@@ -56,14 +48,12 @@ export function runAct(prompt: string): void {
|
|||||||
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
|
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add environment variables as secrets that will be available to the workflow
|
|
||||||
ENV_VARS.forEach((key) => {
|
ENV_VARS.forEach((key) => {
|
||||||
if (process.env[key]) {
|
if (process.env[key]) {
|
||||||
actCommandParts.push("-s", key);
|
actCommandParts.push("-s", key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// We only need the specific ENV_VARS, no need to add other variables
|
|
||||||
|
|
||||||
const actCommand = actCommandParts.join(" ");
|
const actCommand = actCommandParts.join(" ");
|
||||||
|
|
||||||
@@ -73,15 +63,12 @@ export function runAct(prompt: string): void {
|
|||||||
console.log("─".repeat(50));
|
console.log("─".repeat(50));
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
||||||
// Execute act
|
|
||||||
execSync(actCommand, {
|
execSync(actCommand, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
cwd: join(__dirname, "..", ".."),
|
cwd: join(__dirname, "..", ".."),
|
||||||
});
|
});
|
||||||
// Clean up
|
|
||||||
execSync(`rm -rf "${distPath}"`);
|
execSync(`rm -rf "${distPath}"`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up on error
|
|
||||||
execSync(`rm -rf "${distPath}"`);
|
execSync(`rm -rf "${distPath}"`);
|
||||||
console.error("❌ Act execution failed:", (error as Error).message);
|
console.error("❌ Act execution failed:", (error as Error).message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ const findInstallationId = async (jwt: string, repoOwner: string, repoName: stri
|
|||||||
return installation.id;
|
return installation.id;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Installation doesn't have access to repository
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ export interface InstallationToken {
|
|||||||
* Setup GitHub installation token for the action
|
* Setup GitHub installation token for the action
|
||||||
*/
|
*/
|
||||||
export async function setupGitHubInstallationToken(): Promise<string> {
|
export async function setupGitHubInstallationToken(): Promise<string> {
|
||||||
// Check if we have an installation token from inputs or environment
|
|
||||||
const inputToken = core.getInput("github_installation_token");
|
const inputToken = core.getInput("github_installation_token");
|
||||||
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
|
||||||
const existingToken = inputToken || envToken;
|
const existingToken = inputToken || envToken;
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
// Mask the existing token in logs for security
|
|
||||||
core.setSecret(existingToken);
|
core.setSecret(existingToken);
|
||||||
core.info("Using provided GitHub installation token");
|
core.info("Using provided GitHub installation token");
|
||||||
return existingToken;
|
return existingToken;
|
||||||
@@ -29,11 +27,9 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
core.info("Generating OIDC token...");
|
core.info("Generating OIDC token...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate OIDC token for our API
|
|
||||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||||
core.info("OIDC token generated successfully");
|
core.info("OIDC token generated successfully");
|
||||||
|
|
||||||
// Exchange OIDC token for installation token
|
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||||
|
|
||||||
core.info("Exchanging OIDC token for installation token...");
|
core.info("Exchanging OIDC token for installation token...");
|
||||||
@@ -52,14 +48,11 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This type is enforced by us when the response is created
|
|
||||||
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
||||||
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
|
|
||||||
// Mask the token in logs for security
|
|
||||||
core.setSecret(tokenData.token);
|
core.setSecret(tokenData.token);
|
||||||
|
|
||||||
// Set the token as an environment variable for this run
|
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
||||||
|
|
||||||
return tokenData.token;
|
return tokenData.token;
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ export function setupTestRepo(options: SetupOptions): void {
|
|||||||
forceClean = false,
|
forceClean = false,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Handle existing temp directory
|
|
||||||
if (existsSync(tempDir)) {
|
if (existsSync(tempDir)) {
|
||||||
if (forceClean) {
|
if (forceClean) {
|
||||||
console.log("🗑️ Removing existing .temp directory...");
|
console.log("🗑️ Removing existing .temp directory...");
|
||||||
rmSync(tempDir, { recursive: true, force: true });
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
|
||||||
// Clone the repository
|
|
||||||
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
||||||
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-10
@@ -28,13 +28,11 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
let stderrBuffer = "";
|
let stderrBuffer = "";
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Spawn the child process
|
|
||||||
const child = nodeSpawn(cmd, args, {
|
const child = nodeSpawn(cmd, args, {
|
||||||
env: env ? { ...process.env, ...env } : process.env,
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up timeout if specified
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
let isTimedOut = false;
|
let isTimedOut = false;
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
isTimedOut = true;
|
isTimedOut = true;
|
||||||
child.kill("SIGTERM");
|
child.kill("SIGTERM");
|
||||||
|
|
||||||
// If SIGTERM doesn't work, use SIGKILL after 5 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!child.killed) {
|
if (!child.killed) {
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
@@ -52,7 +49,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stdout streaming
|
|
||||||
if (child.stdout) {
|
if (child.stdout) {
|
||||||
child.stdout.on("data", (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
@@ -61,7 +57,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stderr streaming
|
|
||||||
if (child.stderr) {
|
if (child.stderr) {
|
||||||
child.stderr.on("data", (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
@@ -70,7 +65,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle process completion
|
|
||||||
child.on("close", (exitCode) => {
|
child.on("close", (exitCode) => {
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
@@ -91,15 +85,13 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle process errors
|
child.on("error", (_error) => {
|
||||||
child.on("error", (error) => {
|
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still return buffered output even on error
|
|
||||||
resolve({
|
resolve({
|
||||||
stdout: stdoutBuffer,
|
stdout: stdoutBuffer,
|
||||||
stderr: stderrBuffer,
|
stderr: stderrBuffer,
|
||||||
@@ -108,7 +100,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send input if provided
|
|
||||||
if (input && child.stdin) {
|
if (input && child.stdin) {
|
||||||
child.stdin.write(input);
|
child.stdin.write(input);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
|
|||||||
+2
-32
@@ -22,8 +22,8 @@ export function tableString(
|
|||||||
},
|
},
|
||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
if (options?.title) {
|
if (title) {
|
||||||
rows.unshift([options.title]);
|
rows.unshift([title]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableOutput = table(rows, {
|
const tableOutput = table(rows, {
|
||||||
@@ -140,33 +140,3 @@ export function boxString(
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Create a simple two-column table for displaying key-value pairs
|
|
||||||
// * @param data - Array of [key, value] pairs
|
|
||||||
// * @param title - Optional table title
|
|
||||||
// * @param indent - Optional indentation string
|
|
||||||
// */
|
|
||||||
// export function printKeyValueTable(
|
|
||||||
// data: [string, string][],
|
|
||||||
// title?: string,
|
|
||||||
// indent?: string
|
|
||||||
// ): void {
|
|
||||||
// const rows: string[][] = [["Key", "Value"], ...data];
|
|
||||||
// const options: Parameters<typeof printTable>[1] = {};
|
|
||||||
// if (title !== undefined) options.title = title;
|
|
||||||
// if (indent !== undefined) options.indent = indent;
|
|
||||||
// printTable(rows, options);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Create a path resolution table (specific use case)
|
|
||||||
// * @param pathData - Array of [location, resolvedPath] pairs
|
|
||||||
// * @param indent - Optional indentation string
|
|
||||||
// */
|
|
||||||
// export function printPathTable(pathData: [string, string][], indent?: string): void {
|
|
||||||
// const rows: string[][] = [["Location", "Resolved path"], ...pathData];
|
|
||||||
// const options: Parameters<typeof printTable>[1] = {};
|
|
||||||
// if (indent !== undefined) options.indent = indent;
|
|
||||||
// printTable(rows, options);
|
|
||||||
// }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user