Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d633da1be | |||
| ede6cfdfbe | |||
| 7745a0befb | |||
| 1abbd7ff41 | |||
| 6a0d9cc244 | |||
| ab468aa32f | |||
| ba61fc6679 | |||
| a226797098 | |||
| c05a47d5d8 | |||
| ec9f69c670 | |||
| 9239b40372 | |||
| f6efe56478 | |||
| 3d47375b24 | |||
| 609b022547 | |||
| ad2680524d |
@@ -0,0 +1,64 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "package.json"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build for npm with zshy
|
||||
run: |
|
||||
cd action
|
||||
pnpm build:npm
|
||||
|
||||
- name: Get version from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
cd action
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Publishing version: $VERSION"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
release_name: "@pullfrog/action v${{ steps.get_version.outputs.version }}"
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
cd action
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -50,23 +50,16 @@ jobs:
|
||||
echo "Tag ${{ steps.version.outputs.tag }} does not exist"
|
||||
fi
|
||||
|
||||
- name: Install dependencies and build
|
||||
- name: Verify built files are up to date
|
||||
if: steps.check_tag.outputs.exists == 'false'
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
- name: Commit built files if changed
|
||||
if: steps.check_tag.outputs.exists == 'false'
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
# Check if there are any uncommitted changes (built files should already be committed via pre-commit hook)
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
git add dist/
|
||||
git commit -m "chore: rebuild action for v${{ steps.version.outputs.version }}"
|
||||
git push
|
||||
echo "❌ Error: There are uncommitted changes. Built files should be committed via pre-commit hook."
|
||||
git status
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All built files are up to date"
|
||||
|
||||
- name: Create and push tags
|
||||
if: steps.check_tag.outputs.exists == 'false'
|
||||
@@ -86,7 +79,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
release_name: Action Release ${{ steps.version.outputs.tag }}
|
||||
release_name: ${{ steps.version.outputs.tag }}
|
||||
body: |
|
||||
Automated release for action version ${{ steps.version.outputs.version }}
|
||||
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
# macOS settings file
|
||||
# macOS settings file
|
||||
.DS_Store
|
||||
|
||||
# Contains all your dependencies
|
||||
@@ -34,4 +34,14 @@ yarn-error.log*
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
examples
|
||||
examples
|
||||
|
||||
# Act temporary distribution directory
|
||||
.act-dist/
|
||||
|
||||
# Temporary backup of node_modules
|
||||
.node_modules_backup/
|
||||
|
||||
# Temporary directory for cloned repos
|
||||
.temp/
|
||||
dist
|
||||
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
# Build the action before committing
|
||||
echo "🔨 Building action..."
|
||||
npm run build
|
||||
|
||||
# Add the built files to the commit
|
||||
git add index.cjs entry.cjs
|
||||
@@ -1,4 +1,70 @@
|
||||
# Pullfrog
|
||||
# Pullfrog Action
|
||||
|
||||
A simple GitHub Action that prints a customizable message to the console.
|
||||
GitHub Action for running Claude Code and other agents via Pullfrog.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Test with default prompt
|
||||
npm run play # Run locally on your machine
|
||||
npm run play -- --act # Run in Docker (simulates GitHub Actions)
|
||||
```
|
||||
|
||||
## Testing with play.ts
|
||||
|
||||
The `play.ts` script provides two ways to test the action:
|
||||
|
||||
### Local Mode (Default)
|
||||
```bash
|
||||
npm run play # Uses fixtures/play.txt
|
||||
npm run play fixtures/complex.txt # Custom prompt file
|
||||
```
|
||||
- Clones the scratch repository to `.temp`
|
||||
- Runs Claude Code directly on your machine
|
||||
- Fast iteration for development
|
||||
|
||||
### Docker Mode (--act flag)
|
||||
```bash
|
||||
npm run play -- --act # Uses fixtures/play.txt
|
||||
npm run play fixtures/simple.txt -- --act # Custom prompt file
|
||||
```
|
||||
- Builds fresh bundles with esbuild
|
||||
- Creates minimal distribution without node_modules
|
||||
- Runs in Docker container via `act`
|
||||
- Simulates GitHub Actions environment
|
||||
|
||||
### Prompt Files
|
||||
|
||||
Supports `.txt`, `.json`, and `.ts` files:
|
||||
```bash
|
||||
npm run play prompt.txt # Plain text prompt
|
||||
npm run play config.json # JSON configuration
|
||||
npm run play dynamic.ts # TypeScript with default export
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
pnpm build # Production build (bundles & removes node_modules)
|
||||
pnpm build:dev # Development build (keeps node_modules)
|
||||
pnpm dev # Watch mode
|
||||
```
|
||||
|
||||
The action is bundled into `entry.cjs` with all dependencies included, eliminating runtime dependency on node_modules.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **entry.cjs**: Bundled action entry point (self-contained)
|
||||
- **agents/**: Agent implementations (Claude, etc.)
|
||||
- **utils/**: Utilities for subprocess, act, and formatting
|
||||
- **fixtures/**: Test prompt files
|
||||
|
||||
## Why No node_modules?
|
||||
|
||||
pnpm uses symlinks that cause "invalid symlink" errors when `act` copies the action to Docker. Our solution:
|
||||
1. Bundle everything into `entry.cjs`
|
||||
2. Remove node_modules after building
|
||||
3. Create minimal `.act-dist` for Docker testing
|
||||
+13
-10
@@ -1,17 +1,20 @@
|
||||
name: 'Simple Message Action'
|
||||
description: 'A simple GitHub Action that prints a message to the console'
|
||||
author: 'Pullfrog'
|
||||
name: "Pullfrog Claude Code Action"
|
||||
description: "Execute Claude Code with a prompt using Anthropic API"
|
||||
author: "Pullfrog"
|
||||
|
||||
inputs:
|
||||
message:
|
||||
description: 'Message to print to console'
|
||||
prompt:
|
||||
description: "Prompt to send to Claude Code"
|
||||
required: true
|
||||
default: 'Hello from Pullfrog Action!'
|
||||
default: "Hello from Claude Code!"
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key for Claude Code authentication"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'dist/index.js'
|
||||
using: "node20"
|
||||
main: "entry.cjs"
|
||||
|
||||
branding:
|
||||
icon: 'message-circle'
|
||||
color: 'blue'
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
import { access, constants } from "node:fs/promises";
|
||||
import * as core from "@actions/core";
|
||||
import { boxString, tableString } from "../utils";
|
||||
import { spawn } from "../utils/subprocess";
|
||||
import type { Agent, AgentConfig, AgentResult } from "./types";
|
||||
|
||||
/**
|
||||
* Claude Code agent implementation
|
||||
*/
|
||||
export class ClaudeAgent implements Agent {
|
||||
private apiKey: string;
|
||||
public runStats = {
|
||||
toolsUsed: 0,
|
||||
turns: 0,
|
||||
startTime: 0,
|
||||
};
|
||||
|
||||
// $: ExecaMethod;
|
||||
|
||||
constructor(config: AgentConfig) {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("Claude agent requires an API key");
|
||||
}
|
||||
this.apiKey = config.apiKey;
|
||||
// Removed execa dependency - using spawn utility instead
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Claude Code CLI is already installed
|
||||
*/
|
||||
private async isClaudeInstalled(): Promise<boolean> {
|
||||
try {
|
||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||
await access(claudePath, constants.F_OK | constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Claude Code CLI
|
||||
*/
|
||||
async install(): Promise<void> {
|
||||
// Check if Claude Code is already installed
|
||||
if (await this.isClaudeInstalled()) {
|
||||
core.info("Claude Code is already installed, skipping installation");
|
||||
return;
|
||||
}
|
||||
|
||||
core.info("Installing Claude Code...");
|
||||
try {
|
||||
// Use shell execution to properly handle the pipe
|
||||
const result = await spawn({
|
||||
cmd: "bash",
|
||||
args: [
|
||||
"-c",
|
||||
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
|
||||
],
|
||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||
timeout: 120000, // 2 minute timeout
|
||||
onStdout: (chunk) => process.stdout.write(chunk),
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`,
|
||||
);
|
||||
}
|
||||
|
||||
core.info("Claude Code installed successfully");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to install Claude Code: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Claude Code with the given prompt
|
||||
*/
|
||||
async execute(prompt: string): Promise<AgentResult> {
|
||||
core.info("Running Claude Code...");
|
||||
// printTable([[prompt]]);
|
||||
|
||||
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`;
|
||||
// console.log("Using Claude Code from:", claudePath);
|
||||
console.log(boxString(prompt, { title: "Prompt" }));
|
||||
const args = [
|
||||
"--print",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--permission-mode",
|
||||
"acceptEdits",
|
||||
];
|
||||
const env = {
|
||||
ANTHROPIC_API_KEY: this.apiKey,
|
||||
};
|
||||
|
||||
// Start a collapsible log group for streaming output
|
||||
core.startGroup("🔄 Run details");
|
||||
|
||||
// Initialize run statistics
|
||||
this.runStats = {
|
||||
toolsUsed: 0,
|
||||
turns: 0,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
const finalResult = "";
|
||||
const totalCost = 0;
|
||||
|
||||
// run Claude Code with the prompt
|
||||
const result = await spawn({
|
||||
cmd: claudePath,
|
||||
args,
|
||||
env,
|
||||
input: prompt,
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
onStdout: (_chunk) => {
|
||||
// console.log(chunk);
|
||||
processJSONChunk(_chunk, this);
|
||||
},
|
||||
onStderr: (_chunk) => {
|
||||
if (_chunk.trim()) {
|
||||
// core.warning(`[warn] ${chunk}`);
|
||||
processJSONChunk(_chunk, this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// throw on non-zero exit code
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`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;
|
||||
core.info(
|
||||
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`,
|
||||
);
|
||||
|
||||
core.info("✅ Task complete.");
|
||||
core.endGroup(); // End the collapsible log group
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: finalResult,
|
||||
metadata: {
|
||||
promptLength: prompt.length,
|
||||
exitCode: result.exitCode,
|
||||
durationMs: result.durationMs,
|
||||
totalCost,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
// Ensure group is closed even if error occurs before group is started
|
||||
try {
|
||||
core.endGroup();
|
||||
} catch {
|
||||
// Group might not have been started, ignore
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to execute Claude Code: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||
try {
|
||||
// Parse the JSON string first
|
||||
console.log(chunk);
|
||||
const parsedChunk = JSON.parse(chunk.trim());
|
||||
|
||||
switch (parsedChunk.type) {
|
||||
case "system":
|
||||
if (parsedChunk.subtype === "init") {
|
||||
core.info(`🚀 Starting Claude Code session...`);
|
||||
// core.info(`📁 Working directory: ${parsedChunk.cwd}`);
|
||||
// core.info(`🔑 Permission mode: ${parsedChunk.permissionMode}`);
|
||||
core.info(
|
||||
tableString([
|
||||
["model", parsedChunk.model],
|
||||
["cwd", parsedChunk.cwd],
|
||||
["permission_mode", parsedChunk.permissionMode],
|
||||
[
|
||||
"tools",
|
||||
parsedChunk.tools?.length
|
||||
? `${parsedChunk.tools.length} tools`
|
||||
: "none",
|
||||
],
|
||||
[
|
||||
"mcp_servers",
|
||||
parsedChunk.mcp_servers?.length
|
||||
? `${parsedChunk.mcp_servers.length} servers`
|
||||
: "none",
|
||||
],
|
||||
[
|
||||
"slash_commands",
|
||||
parsedChunk.slash_commands?.length
|
||||
? `${parsedChunk.slash_commands.length} commands`
|
||||
: "none",
|
||||
],
|
||||
]),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "assistant":
|
||||
if (parsedChunk.message?.content) {
|
||||
// Track turns
|
||||
if (agent) {
|
||||
agent.runStats.turns++;
|
||||
}
|
||||
|
||||
for (const content of parsedChunk.message.content) {
|
||||
if (content.type === "text") {
|
||||
// Skip empty text content
|
||||
if (content.text.trim()) {
|
||||
core.info(
|
||||
boxString(content.text.trim(), { title: "Claude Code" }),
|
||||
);
|
||||
}
|
||||
} else if (content.type === "tool_use") {
|
||||
// Track tools used
|
||||
if (agent) {
|
||||
agent.runStats.toolsUsed++;
|
||||
}
|
||||
|
||||
// Enhanced tool usage logging
|
||||
const toolName = content.name;
|
||||
// const toolId = content.id;
|
||||
|
||||
core.info(`→ ${toolName}`);
|
||||
|
||||
// Log tool-specific details based on tool type
|
||||
if (content.input) {
|
||||
const input = content.input;
|
||||
|
||||
// Common tool input fields
|
||||
if (input.description) {
|
||||
core.info(` └─ ${input.description}`);
|
||||
}
|
||||
|
||||
// Tool-specific input fields
|
||||
if (input.command) {
|
||||
core.info(` └─ command: ${input.command}`);
|
||||
}
|
||||
|
||||
if (input.file_path) {
|
||||
core.info(` └─ file: ${input.file_path}`);
|
||||
}
|
||||
|
||||
if (input.content) {
|
||||
const contentPreview =
|
||||
input.content.length > 100
|
||||
? `${input.content.substring(0, 100)}...`
|
||||
: input.content;
|
||||
core.info(` └─ content: ${contentPreview}`);
|
||||
}
|
||||
|
||||
if (input.query) {
|
||||
core.info(` └─ query: ${input.query}`);
|
||||
}
|
||||
|
||||
if (input.pattern) {
|
||||
core.info(` └─ pattern: ${input.pattern}`);
|
||||
}
|
||||
|
||||
if (input.url) {
|
||||
core.info(` └─ url: ${input.url}`);
|
||||
}
|
||||
|
||||
// For multi-edit or complex operations
|
||||
if (input.edits && Array.isArray(input.edits)) {
|
||||
core.info(` └─ edits: ${input.edits.length} changes`);
|
||||
input.edits.forEach((edit: any, index: number) => {
|
||||
if (edit.file_path) {
|
||||
core.info(` ${index + 1}. ${edit.file_path}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For task operations
|
||||
if (input.task) {
|
||||
core.info(` └─ task: ${input.task}`);
|
||||
}
|
||||
|
||||
// For bash operations with specific details
|
||||
if (input.bash_command) {
|
||||
core.info(` └─ bash_command: ${input.bash_command}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log tool ID for debugging
|
||||
// core.debug(` 🔗 Tool ID: ${toolId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "user":
|
||||
if (parsedChunk.message?.content) {
|
||||
for (const content of parsedChunk.message.content) {
|
||||
if (content.type === "tool_result") {
|
||||
if (content.is_error) {
|
||||
core.warning(`❌ Tool error: ${content.content}`);
|
||||
} else {
|
||||
// Enhanced tool result logging
|
||||
const _resultContent = content.content.trim();
|
||||
// do nothing for now. usually useless in headless more.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "result":
|
||||
if (parsedChunk.subtype === "success") {
|
||||
if (parsedChunk.result) {
|
||||
core.info(
|
||||
boxString(parsedChunk.result.trim(), {
|
||||
title: "🤖 Claude Code",
|
||||
maxWidth: 70,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
core.info(
|
||||
tableString([
|
||||
[
|
||||
"Cost",
|
||||
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
||||
],
|
||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||
["Turns", parsedChunk.num_turns || 1],
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
core.error(`❌ Failed: ${parsedChunk.error || "Unknown error"}`);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Log unknown chunk types for debugging
|
||||
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
core.debug(`Failed to parse chunk: ${error}`);
|
||||
core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./claude";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Standard interface for all Pullfrog agents
|
||||
*/
|
||||
export interface Agent {
|
||||
/**
|
||||
* Install the agent and any required dependencies
|
||||
*/
|
||||
install(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Execute the agent with the given prompt
|
||||
* @param prompt The prompt to send to the agent
|
||||
* @param options Additional options specific to the agent
|
||||
*/
|
||||
execute(prompt: string, options?: Record<string, any>): Promise<AgentResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned by agent execution
|
||||
*/
|
||||
export interface AgentResult {
|
||||
success: boolean;
|
||||
output?: string;
|
||||
error?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for agent creation
|
||||
*/
|
||||
export interface AgentConfig {
|
||||
apiKey?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
Vendored
-17731
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Entry point for GitHub Action
|
||||
* This file is bundled to entry.cjs and called directly by GitHub Actions
|
||||
*/
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { main } from "./main";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
// Get inputs from GitHub Actions
|
||||
const prompt = core.getInput("prompt", { required: true });
|
||||
const anthropicApiKey = core.getInput("anthropic_api_key", {
|
||||
required: true,
|
||||
});
|
||||
|
||||
if (!anthropicApiKey) {
|
||||
throw new Error("anthropic_api_key is required");
|
||||
}
|
||||
|
||||
if (!prompt) {
|
||||
throw new Error("prompt is required");
|
||||
}
|
||||
|
||||
// Create params object
|
||||
const params = {
|
||||
prompt,
|
||||
anthropicApiKey,
|
||||
};
|
||||
|
||||
// Run the main function
|
||||
const result = await main(params);
|
||||
|
||||
// Set outputs
|
||||
core.setOutput("status", result.success ? "success" : "failed");
|
||||
core.setOutput("prompt", prompt);
|
||||
core.setOutput("output", result.output || "");
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Agent execution failed");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
core.setFailed(`Action failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the action
|
||||
run().catch((error) => {
|
||||
console.error("Action failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { build } from "esbuild";
|
||||
|
||||
// Build the GitHub Action bundle only
|
||||
// For npm package builds, use zshy (pnpm build:npm)
|
||||
await build({
|
||||
entryPoints: ["./entry.ts"],
|
||||
bundle: true,
|
||||
outfile: "./entry.cjs",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
});
|
||||
|
||||
console.log("✅ Build completed successfully!");
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MainParams } from "../main";
|
||||
|
||||
const testParams = {
|
||||
prompt: "List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it."
|
||||
} satisfies MainParams;
|
||||
|
||||
export default testParams;
|
||||
@@ -0,0 +1 @@
|
||||
Print the list of tools available. Then create a new file called test.txt with the content "Hello from Pullfrog!".
|
||||
@@ -1,18 +1,8 @@
|
||||
import * as core from '@actions/core';
|
||||
/**
|
||||
* Library entry point for npm package
|
||||
* This exports the main function for programmatic usage
|
||||
*/
|
||||
|
||||
try {
|
||||
// Get the message input parameter, with a default fallback
|
||||
const message = core.getInput('message') || 'Hello from Pullfrog Action!';
|
||||
|
||||
// Print the message to console and GitHub Actions logs
|
||||
console.log(`🐸 ${message}`);
|
||||
core.info(`Action executed successfully: ${message}`);
|
||||
|
||||
// Set an output for potential use by other actions
|
||||
core.setOutput('message', message);
|
||||
|
||||
} catch (error) {
|
||||
// Handle any errors and fail the action
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
core.setFailed(`Action failed: ${errorMessage}`);
|
||||
}
|
||||
export { main } from "./main";
|
||||
export { ClaudeAgent } from "./agents";
|
||||
export type { Agent, AgentConfig, AgentResult } from "./agents/types";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as core from "@actions/core";
|
||||
import { ClaudeAgent } from "./agents";
|
||||
|
||||
export interface MainParams {
|
||||
prompt: string;
|
||||
anthropicApiKey?: string;
|
||||
}
|
||||
|
||||
export interface MainResult {
|
||||
success: boolean;
|
||||
output?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function main(params: MainParams): Promise<MainResult> {
|
||||
try {
|
||||
// Use provided API key or fall back to environment variable
|
||||
const anthropicApiKey =
|
||||
params.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!anthropicApiKey) {
|
||||
throw new Error("anthropic_api_key is required");
|
||||
}
|
||||
|
||||
core.info(`→ Starting agent run with Claude Code`);
|
||||
|
||||
// Create and install the Claude agent
|
||||
const agent = new ClaudeAgent({ apiKey: anthropicApiKey });
|
||||
await agent.install();
|
||||
|
||||
// Execute the agent with the prompt
|
||||
const result = await agent.execute(params.prompt);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || "Agent execution failed",
|
||||
output: result.output,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result.output || "",
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
Generated
+1306
-237
File diff suppressed because it is too large
Load Diff
+30
-9
@@ -1,22 +1,41 @@
|
||||
{
|
||||
"name": "action",
|
||||
"version": "0.0.2",
|
||||
"main": "index.js",
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.cjs",
|
||||
"index.d.ts",
|
||||
"index.d.cts",
|
||||
"agents",
|
||||
"utils",
|
||||
"main.js",
|
||||
"main.d.ts"
|
||||
],
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rolldown -c rolldown.config.js",
|
||||
"dev": "rolldown -c --watch"
|
||||
"build": "pnpm install && node esbuild.config.js && rm -rf node_modules",
|
||||
"build:npm": "zshy",
|
||||
"build:dev": "node esbuild.config.js",
|
||||
"prepare": "husky",
|
||||
"play": "tsx --env-file=../.env play.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1"
|
||||
"@actions/core": "^1.10.1",
|
||||
"execa": "^9.6.0",
|
||||
"table": "^6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"esbuild": "^0.25.9",
|
||||
"husky": "^9.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"rolldown": "^0.12.0"
|
||||
"zshy": "^0.4.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,9 +44,11 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pullfrog/pullfrog/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pullfrog/pullfrog#readme"
|
||||
"homepage": "https://github.com/pullfrog/pullfrog#readme",
|
||||
"zshy": {
|
||||
"exports": "./index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve, extname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import { main } from "./main";
|
||||
import { runAct } from "./utils/act";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
async function loadPrompt(filePath: string): Promise<string> {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
|
||||
// Try to resolve the file path
|
||||
let resolvedPath: string;
|
||||
|
||||
// First try as fixtures path
|
||||
const fixturesPath = join(__dirname, "fixtures", filePath);
|
||||
if (existsSync(fixturesPath)) {
|
||||
resolvedPath = fixturesPath;
|
||||
} else if (existsSync(filePath)) {
|
||||
resolvedPath = resolve(filePath);
|
||||
} else {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
switch (ext) {
|
||||
case ".txt": {
|
||||
// Plain text - pass directly as prompt
|
||||
return readFileSync(resolvedPath, "utf8").trim();
|
||||
}
|
||||
|
||||
case ".json": {
|
||||
// JSON - stringify and pass as prompt
|
||||
const content = readFileSync(resolvedPath, "utf8");
|
||||
const parsed = JSON.parse(content);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
|
||||
case ".ts": {
|
||||
// TypeScript - dynamic import and stringify default export
|
||||
const fileUrl = pathToFileURL(resolvedPath).href;
|
||||
const module = await import(fileUrl);
|
||||
|
||||
if (!module.default) {
|
||||
throw new Error(
|
||||
`TypeScript file ${filePath} must have a default export`,
|
||||
);
|
||||
}
|
||||
|
||||
// If it's a string, use it directly
|
||||
if (typeof module.default === "string") {
|
||||
return module.default;
|
||||
}
|
||||
|
||||
// If it's a MainParams object with a prompt field, extract the prompt
|
||||
if (typeof module.default === "object" && module.default.prompt) {
|
||||
return module.default.prompt;
|
||||
}
|
||||
|
||||
// Otherwise stringify it
|
||||
return JSON.stringify(module.default, null, 2);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported file type: ${ext}. Supported types: .txt, .json, .ts`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPlay(
|
||||
filePath: string,
|
||||
options: { act?: boolean },
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Load the prompt from the specified file
|
||||
const prompt = await loadPrompt(filePath);
|
||||
|
||||
if (options.act) {
|
||||
// Use Docker/act to run the action
|
||||
console.log("🐳 Running with Docker/act...");
|
||||
runAct(prompt);
|
||||
} else {
|
||||
// Clone the test repository and run directly
|
||||
const tempDir = join(process.cwd(), ".temp");
|
||||
const repoUrl = "git@github.com:pullfrogai/scratch.git";
|
||||
|
||||
// Remove existing temp directory if it exists
|
||||
if (existsSync(tempDir)) {
|
||||
console.log("🗑️ Removing existing .temp directory...");
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
||||
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
||||
|
||||
// List of environment variables to copy to .temp
|
||||
const envVarsToCopy = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GITHUB_TOKEN",
|
||||
// Add more environment variables here as needed
|
||||
];
|
||||
|
||||
// Build .env content from the list
|
||||
const envLines = envVarsToCopy
|
||||
.map((varName) => `${varName}=${process.env[varName] || ""}`)
|
||||
.join("\n");
|
||||
|
||||
const envPath = join(tempDir, ".env");
|
||||
writeFileSync(envPath, envLines + "\n");
|
||||
console.log("📝 Created .env file in .temp directory with:");
|
||||
|
||||
let hasRequiredVars = true;
|
||||
envVarsToCopy.forEach((varName) => {
|
||||
const hasValue = !!process.env[varName];
|
||||
console.log(` - ${varName}: ${hasValue ? "✓" : "✗ (missing)"}`);
|
||||
|
||||
// Check for required variables
|
||||
if (varName === "ANTHROPIC_API_KEY" && !hasValue) {
|
||||
hasRequiredVars = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasRequiredVars) {
|
||||
console.warn("\n⚠️ Warning: ANTHROPIC_API_KEY is not set or empty.");
|
||||
console.warn(
|
||||
" Please ensure you have a valid API key in your .env file.",
|
||||
);
|
||||
console.warn(
|
||||
" Get your API key from: https://console.anthropic.com/api-keys\n",
|
||||
);
|
||||
}
|
||||
|
||||
// Change to the temp directory
|
||||
process.chdir(tempDir);
|
||||
|
||||
console.log("🚀 Running test in .temp directory...");
|
||||
console.log("─".repeat(50));
|
||||
console.log(`Prompt from ${filePath}:`);
|
||||
console.log(prompt);
|
||||
console.log("─".repeat(50));
|
||||
|
||||
// Run main with the params object
|
||||
const result = await main({ prompt });
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ Test completed successfully");
|
||||
if (result.output) {
|
||||
console.log("Output:", result.output);
|
||||
}
|
||||
} else {
|
||||
console.error("❌ Test failed:", result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", (error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up CLI
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("play")
|
||||
.description("Test the Pullfrog action with various prompts")
|
||||
.version("1.0.0")
|
||||
.argument(
|
||||
"[file]",
|
||||
"Prompt file to use (.txt, .json, or .ts)",
|
||||
"fixtures/play.txt",
|
||||
)
|
||||
.option(
|
||||
"--act",
|
||||
"Use Docker/act to run the action instead of running directly",
|
||||
)
|
||||
.action(async (file: string, options: { act?: boolean }) => {
|
||||
await runPlay(file, options);
|
||||
});
|
||||
|
||||
// Parse arguments and run
|
||||
program.parseAsync(process.argv).catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+266
-161
@@ -15,9 +15,12 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20.10.0
|
||||
version: 20.19.11
|
||||
rolldown:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.2
|
||||
esbuild:
|
||||
specifier: ^0.25.9
|
||||
version: 0.25.9
|
||||
husky:
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.7
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.2
|
||||
@@ -36,94 +39,178 @@ packages:
|
||||
'@actions/io@1.1.3':
|
||||
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
||||
|
||||
'@emnapi/core@1.4.5':
|
||||
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@emnapi/runtime@1.4.5':
|
||||
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@emnapi/wasi-threads@1.0.4':
|
||||
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@fastify/busboy@2.1.1':
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@rolldown/binding-darwin-arm64@0.12.2':
|
||||
resolution: {integrity: sha512-Y3Ajye63Z5KymGUwTLaK7Q6YMvycXqNiXtosecgVzjAwMITCmXdzgnWgzzx5UlWHMrDYL4m5zIeXGB5slLwoMA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@0.12.2':
|
||||
resolution: {integrity: sha512-ZcVuVFEFBXhp00TUNn+EDYs7SGGLQCznvCeuW1XkM8EC2/LfewU/o4WuJK7CBC4iCSktuFGpw7+zLe8D6iinzg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@0.12.2':
|
||||
resolution: {integrity: sha512-4fjuQHpm3q/Ly4fcqb8Qn49OQc2EQR2scUbQaOzXr7mIn9Zy8NfdRrsVG4/wpYvihIlTEtVx+ku0IZwcUzzZGg==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@0.12.2':
|
||||
resolution: {integrity: sha512-MGEDaYLzTQ1kpvt13PzOwnd6O668S1mPM/vgi4O9vCfqJNTXZX8SeAg8Z2dQZbMSUyFDBVKGkz23GRTHqkGIsQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@0.12.2':
|
||||
resolution: {integrity: sha512-8uiaMe39twyIAw0do1Gc3O2SpQmyL1A/BucFncEB8eU5jtb3BWIM/X+F+eKDU6c+XZ+S1T025dhR1cGg8y20Xw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@0.12.2':
|
||||
resolution: {integrity: sha512-GOFSKaMJueaSSODcqI+0Hu79buHYtGV7h2tydIDkSDD20mQuvwOF7jIVd27yNdlXWS9wLObwEu3BNgHmIj1M6A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@0.12.2':
|
||||
resolution: {integrity: sha512-+hlRURUSiVP0+PFtjoTxUsiy/2NQpbf3DyUyMyl8Nv5+1BxjB6452VY1iFI+RzG4iLNJslcVcI6d6lJQ5zZYfw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@0.12.2':
|
||||
resolution: {integrity: sha512-9WKIaQSZZ0i9F5msW4I5kEj6ov+TZLteuTqCzI7nYWDBDm7m/hYkOkdIBf41GC8iKFsgVIQO0kRVAT966U8RxQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@0.12.2':
|
||||
resolution: {integrity: sha512-KY7bNrR3Jk6O0ne8LAaAnq9yI6xuKwhr/L2d1lBwrraCCyLHk0UEv4g6PfJkyUx6GfN0gVYuSxFgo9OggycldA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@0.12.2':
|
||||
resolution: {integrity: sha512-XYBXifPk5iNcPUTyV/yB4tlj5nI+fYoe/8CLHjyUG8GS0l8rCD+jKaAv3Jvz2T2erlkjEPqJXmzhUfBscOUo6g==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-ia32-msvc@0.12.2':
|
||||
resolution: {integrity: sha512-SV9LqvEc0d4gCLbkezH+UJ2uj2pTw3mwSKhFJEm6ir1lb05W6y9++m67NRs17fhudkFAz8iKlyPxKtZBKzAKOw==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@0.12.2':
|
||||
resolution: {integrity: sha512-XnRqHx182tyk1M12OMXqLajIj9DZrOUEKSPYrQUSaMCmHHAhULYP6ki240CV16PBWZW4Q1pwQ7YVKYFevRtvKg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tybys/wasm-util@0.10.0':
|
||||
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
|
||||
|
||||
'@types/node@20.19.11':
|
||||
resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==}
|
||||
|
||||
rolldown@0.12.2:
|
||||
resolution: {integrity: sha512-YJYKiYt2O9XytiQ3Na4Kk29avfIXhvK7udB3wAaVaF4kiSsFKE1167tElO/0eD6tjfJXCvwNxwsyYkBJRtsLmQ==}
|
||||
esbuild@0.25.9:
|
||||
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
husky@9.1.7:
|
||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
tunnel@0.0.6:
|
||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||
@@ -141,9 +228,6 @@ packages:
|
||||
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@actions/core@1.11.1':
|
||||
@@ -162,97 +246,120 @@ snapshots:
|
||||
|
||||
'@actions/io@1.1.3': {}
|
||||
|
||||
'@emnapi/core@1.4.5':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.4
|
||||
tslib: 2.8.1
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.4.5':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.0.4':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.4.5
|
||||
'@emnapi/runtime': 1.4.5
|
||||
'@tybys/wasm-util': 0.10.0
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@0.12.2':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 0.2.12
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-ia32-msvc@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@0.12.2':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/node@20.19.11':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
rolldown@0.12.2:
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
esbuild@0.25.9:
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-darwin-arm64': 0.12.2
|
||||
'@rolldown/binding-darwin-x64': 0.12.2
|
||||
'@rolldown/binding-freebsd-x64': 0.12.2
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 0.12.2
|
||||
'@rolldown/binding-linux-arm64-gnu': 0.12.2
|
||||
'@rolldown/binding-linux-arm64-musl': 0.12.2
|
||||
'@rolldown/binding-linux-x64-gnu': 0.12.2
|
||||
'@rolldown/binding-linux-x64-musl': 0.12.2
|
||||
'@rolldown/binding-wasm32-wasi': 0.12.2
|
||||
'@rolldown/binding-win32-arm64-msvc': 0.12.2
|
||||
'@rolldown/binding-win32-ia32-msvc': 0.12.2
|
||||
'@rolldown/binding-win32-x64-msvc': 0.12.2
|
||||
'@esbuild/aix-ppc64': 0.25.9
|
||||
'@esbuild/android-arm': 0.25.9
|
||||
'@esbuild/android-arm64': 0.25.9
|
||||
'@esbuild/android-x64': 0.25.9
|
||||
'@esbuild/darwin-arm64': 0.25.9
|
||||
'@esbuild/darwin-x64': 0.25.9
|
||||
'@esbuild/freebsd-arm64': 0.25.9
|
||||
'@esbuild/freebsd-x64': 0.25.9
|
||||
'@esbuild/linux-arm': 0.25.9
|
||||
'@esbuild/linux-arm64': 0.25.9
|
||||
'@esbuild/linux-ia32': 0.25.9
|
||||
'@esbuild/linux-loong64': 0.25.9
|
||||
'@esbuild/linux-mips64el': 0.25.9
|
||||
'@esbuild/linux-ppc64': 0.25.9
|
||||
'@esbuild/linux-riscv64': 0.25.9
|
||||
'@esbuild/linux-s390x': 0.25.9
|
||||
'@esbuild/linux-x64': 0.25.9
|
||||
'@esbuild/netbsd-arm64': 0.25.9
|
||||
'@esbuild/netbsd-x64': 0.25.9
|
||||
'@esbuild/openbsd-arm64': 0.25.9
|
||||
'@esbuild/openbsd-x64': 0.25.9
|
||||
'@esbuild/openharmony-arm64': 0.25.9
|
||||
'@esbuild/sunos-x64': 0.25.9
|
||||
'@esbuild/win32-arm64': 0.25.9
|
||||
'@esbuild/win32-ia32': 0.25.9
|
||||
'@esbuild/win32-x64': 0.25.9
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
husky@9.1.7: {}
|
||||
|
||||
tunnel@0.0.6: {}
|
||||
|
||||
@@ -263,5 +370,3 @@ snapshots:
|
||||
undici@5.29.0:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineConfig } from 'rolldown';
|
||||
|
||||
export default defineConfig({
|
||||
input: './index.ts',
|
||||
output: {
|
||||
file: './bundle.js',
|
||||
format: 'esm'
|
||||
},
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
external: (id) => {
|
||||
// Mark all node modules as external
|
||||
return id.includes('node_modules') || id.startsWith('node:');
|
||||
}
|
||||
});
|
||||
+40
-12
@@ -1,17 +1,45 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./",
|
||||
"rootDir": "./",
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { config, parse } from "dotenv";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export function runAct(prompt: string): void {
|
||||
// First, ensure the scratch repo is cloned
|
||||
const tempDir = join(__dirname, "..", ".temp");
|
||||
|
||||
// Check if .temp exists and either reset it or clone it
|
||||
if (existsSync(tempDir)) {
|
||||
console.log("📦 Resetting existing .temp repository...");
|
||||
execSync("git reset --hard HEAD && git clean -fd", {
|
||||
cwd: tempDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
} else {
|
||||
console.log("📦 Cloning pullfrogai/scratch into .temp...");
|
||||
const repoUrl = "git@github.com:pullfrogai/scratch.git";
|
||||
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
|
||||
const envPath = join(__dirname, "..", "..", ".env");
|
||||
|
||||
// Load environment variables into process
|
||||
config({ path: envPath });
|
||||
|
||||
// Parse environment variables from .env file to get keys
|
||||
let envVars: string[] = [];
|
||||
try {
|
||||
const content = readFileSync(envPath, "utf8");
|
||||
const parsed = parse(content);
|
||||
envVars = Object.keys(parsed);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Warning: Could not read .env file: ${(error as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build fresh bundles with esbuild
|
||||
const actionPath = join(__dirname, "..");
|
||||
console.log("🔨 Building fresh bundles with esbuild...");
|
||||
execSync("node esbuild.config.js", {
|
||||
cwd: actionPath,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Create minimal dist for act (avoids pnpm symlink issues)
|
||||
const distPath = join(actionPath, ".act-dist");
|
||||
console.log("📦 Creating minimal distribution for act...");
|
||||
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: true });
|
||||
|
||||
// Copy only necessary files (bundled, no node_modules needed)
|
||||
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
|
||||
const src = join(actionPath, file);
|
||||
if (existsSync(src)) {
|
||||
execSync(`cp "${src}" "${distPath}"`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Build the act command with input directly
|
||||
// Properly escape the prompt for shell
|
||||
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||
|
||||
const actCommandParts = [
|
||||
"act",
|
||||
"workflow_dispatch",
|
||||
"-W",
|
||||
workflowPath,
|
||||
"--input",
|
||||
`prompt='${escapedPrompt}'`,
|
||||
"--local-repository",
|
||||
`pullfrog/pullfrog@v0=${distPath}`, // Use minimal dist without symlinks
|
||||
];
|
||||
|
||||
// Add all environment variables as secrets (without values)
|
||||
envVars.forEach((key) => {
|
||||
actCommandParts.push("-s", key);
|
||||
});
|
||||
|
||||
const actCommand = actCommandParts.join(" ");
|
||||
|
||||
console.log("🚀 Running act with prompt:");
|
||||
console.log("─".repeat(50));
|
||||
console.log(prompt);
|
||||
console.log("─".repeat(50));
|
||||
console.log("");
|
||||
|
||||
// Execute act
|
||||
execSync(actCommand, {
|
||||
stdio: "inherit",
|
||||
cwd: join(__dirname, "..", ".."),
|
||||
});
|
||||
// Clean up
|
||||
execSync(`rm -rf "${distPath}"`);
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
execSync(`rm -rf "${distPath}"`);
|
||||
console.error("❌ Act execution failed:", (error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Create a temporary file with the given content
|
||||
*/
|
||||
export function createTempFile(content: string, filename = "temp.txt"): string {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "pullfrog-"));
|
||||
const filePath = join(tempDir, filename);
|
||||
writeFileSync(filePath, content);
|
||||
return filePath;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./files";
|
||||
export * from "./subprocess";
|
||||
export * from "./table";
|
||||
@@ -0,0 +1,117 @@
|
||||
import { spawn as nodeSpawn } from "node:child_process";
|
||||
|
||||
export interface SpawnOptions {
|
||||
cmd: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
input?: string;
|
||||
timeout?: number;
|
||||
onStdout?: (chunk: string) => void;
|
||||
onStderr?: (chunk: string) => void;
|
||||
}
|
||||
|
||||
export interface SpawnResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a subprocess with streaming callbacks and buffered results
|
||||
*/
|
||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||
const { cmd, args, env, input, timeout, onStdout, onStderr } = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
let stdoutBuffer = "";
|
||||
let stderrBuffer = "";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Spawn the child process
|
||||
const child = nodeSpawn(cmd, args, {
|
||||
env: env ? { ...process.env, ...env } : process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// Set up timeout if specified
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
let isTimedOut = false;
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
isTimedOut = true;
|
||||
child.kill("SIGTERM");
|
||||
|
||||
// If SIGTERM doesn't work, use SIGKILL after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
}, 5000);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// Handle stdout streaming
|
||||
if (child.stdout) {
|
||||
child.stdout.on("data", (data: Buffer) => {
|
||||
const chunk = data.toString();
|
||||
stdoutBuffer += chunk;
|
||||
onStdout?.(chunk);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle stderr streaming
|
||||
if (child.stderr) {
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
const chunk = data.toString();
|
||||
stderrBuffer += chunk;
|
||||
onStderr?.(chunk);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle process completion
|
||||
child.on("close", (exitCode) => {
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (isTimedOut) {
|
||||
reject(new Error(`Process timed out after ${timeout}ms`));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
stdout: stdoutBuffer,
|
||||
stderr: stderrBuffer,
|
||||
exitCode: exitCode || 0,
|
||||
durationMs,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle process errors
|
||||
child.on("error", (error) => {
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// Still return buffered output even on error
|
||||
resolve({
|
||||
stdout: stdoutBuffer,
|
||||
stderr: stderrBuffer,
|
||||
exitCode: 1,
|
||||
durationMs,
|
||||
});
|
||||
});
|
||||
|
||||
// Send input if provided
|
||||
if (input && child.stdin) {
|
||||
child.stdin.write(input);
|
||||
child.stdin.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
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 (options?.title) {
|
||||
rows.unshift([options.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;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 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