iterate on prep
This commit is contained in:
+2
-2
@@ -14,11 +14,11 @@ export const claude = agent({
|
||||
executablePath: "cli.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
|
||||
// Ensure API key is NOT in process.env - only pass via SDK's env option
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
const prompt = addInstructions(payload);
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
console.log(prompt);
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ export const codex = agent({
|
||||
executablePath: "bin/codex.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
|
||||
// create config directory for codex before setting HOME
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "codex");
|
||||
@@ -61,7 +61,7 @@ export const codex = agent({
|
||||
);
|
||||
|
||||
try {
|
||||
const streamedTurn = await thread.runStreamed(addInstructions(payload));
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults }));
|
||||
|
||||
let finalOutput = "";
|
||||
for await (const event of streamedTurn.events) {
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ export const cursor = agent({
|
||||
executableName: "cursor-agent",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers }) => {
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => {
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
|
||||
|
||||
@@ -166,7 +166,7 @@ export const cursor = agent({
|
||||
};
|
||||
|
||||
try {
|
||||
const fullPrompt = addInstructions(payload);
|
||||
const fullPrompt = addInstructions({ payload, prepResults });
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
|
||||
|
||||
+2
-2
@@ -154,14 +154,14 @@ export const gemini = agent({
|
||||
...(githubInstallationToken && { githubInstallationToken }),
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath }) => {
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => {
|
||||
configureGeminiMcpServers({ mcpServers, cliPath });
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
|
||||
}
|
||||
|
||||
const sessionPrompt = addInstructions(payload);
|
||||
const sessionPrompt = addInstructions({ payload, prepResults });
|
||||
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
|
||||
+66
-75
@@ -2,92 +2,79 @@ import { encode as toonEncode } from "@toon-format/toon";
|
||||
import type { Payload } from "../external.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { getModes } from "../modes.ts";
|
||||
import type { PrepResult } from "../prep/index.ts";
|
||||
|
||||
/**
|
||||
* Extract only essential fields from event data to reduce token usage.
|
||||
* Removes verbose GitHub API metadata (user objects, repository metadata, etc.)
|
||||
* and keeps only the fields agents actually need.
|
||||
* Format prep results into a human-readable string for the agent prompt
|
||||
*/
|
||||
// function extractEssentialEventData(event: Payload["event"]): Record<string, unknown> {
|
||||
// const trigger = event.trigger;
|
||||
// const essential: Record<string, unknown> = { trigger };
|
||||
function formatPrepResults(results: PrepResult[]): string {
|
||||
if (results.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// // common fields
|
||||
// if ("issue_number" in event) {
|
||||
// essential.issue_number = event.issue_number;
|
||||
// }
|
||||
// if ("branch" in event && event.branch) {
|
||||
// essential.branch = event.branch;
|
||||
// }
|
||||
const lines: string[] = [];
|
||||
|
||||
// // trigger-specific fields
|
||||
// switch (trigger) {
|
||||
// case "issue_comment_created":
|
||||
// if ("comment_id" in event) essential.comment_id = event.comment_id;
|
||||
// if ("comment_body" in event) essential.comment_body = event.comment_body;
|
||||
// // include issue title/body if available in context (but not the entire context object)
|
||||
// if ("context" in event && event.context && typeof event.context === "object") {
|
||||
// const ctx = event.context as Record<string, unknown>;
|
||||
// if (ctx.issue && typeof ctx.issue === "object") {
|
||||
// const issue = ctx.issue as Record<string, unknown>;
|
||||
// if (issue.title) essential.issue_title = issue.title;
|
||||
// if (issue.body) essential.issue_body = issue.body;
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
for (const result of results) {
|
||||
if (result.language === "unknown") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// case "issues_opened":
|
||||
// case "issues_assigned":
|
||||
// case "issues_labeled":
|
||||
// if ("issue_title" in event) essential.issue_title = event.issue_title;
|
||||
// if ("issue_body" in event) essential.issue_body = event.issue_body;
|
||||
// break;
|
||||
const langDisplay = result.language === "node" ? "Node.js" : "Python";
|
||||
|
||||
// case "pull_request_opened":
|
||||
// case "pull_request_review_requested":
|
||||
// if ("pr_title" in event) essential.pr_title = event.pr_title;
|
||||
// if ("pr_body" in event) essential.pr_body = event.pr_body;
|
||||
// if ("branch" in event) essential.branch = event.branch;
|
||||
// break;
|
||||
if (result.language === "node") {
|
||||
if (result.dependenciesInstalled) {
|
||||
lines.push(
|
||||
`✅ ${langDisplay} dependencies installed successfully via \`${result.packageManager}\`.`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`⚠️ ${langDisplay} dependency installation FAILED (using \`${result.packageManager}\`).`
|
||||
);
|
||||
for (const issue of result.issues) {
|
||||
lines.push(` - ${issue}`);
|
||||
}
|
||||
lines.push(
|
||||
` You may need to run \`${result.packageManager} install\` or address this issue before proceeding.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// case "pull_request_review_submitted":
|
||||
// if ("review_id" in event) essential.review_id = event.review_id;
|
||||
// if ("review_body" in event) essential.review_body = event.review_body;
|
||||
// if ("review_state" in event) essential.review_state = event.review_state;
|
||||
// if ("branch" in event) essential.branch = event.branch;
|
||||
// break;
|
||||
if (result.language === "python") {
|
||||
if (result.dependenciesInstalled) {
|
||||
lines.push(
|
||||
`✅ ${langDisplay} dependencies installed successfully via \`${result.packageManager}\` (from ${result.configFile}).`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`⚠️ ${langDisplay} dependency installation FAILED (using \`${result.packageManager}\` from ${result.configFile}).`
|
||||
);
|
||||
for (const issue of result.issues) {
|
||||
lines.push(` - ${issue}`);
|
||||
}
|
||||
lines.push(
|
||||
` You may need to run the appropriate install command or address this issue before proceeding.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// case "pull_request_review_comment_created":
|
||||
// if ("comment_id" in event) essential.comment_id = event.comment_id;
|
||||
// if ("comment_body" in event) essential.comment_body = event.comment_body;
|
||||
// if ("pr_title" in event) essential.pr_title = event.pr_title;
|
||||
// if ("branch" in event) essential.branch = event.branch;
|
||||
// break;
|
||||
if (lines.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// case "check_suite_completed":
|
||||
// if ("pr_title" in event) essential.pr_title = event.pr_title;
|
||||
// if ("pr_body" in event) essential.pr_body = event.pr_body;
|
||||
// if ("branch" in event) essential.branch = event.branch;
|
||||
// if ("check_suite" in event) {
|
||||
// essential.check_suite = {
|
||||
// id: event.check_suite.id,
|
||||
// head_sha: event.check_suite.head_sha,
|
||||
// head_branch: event.check_suite.head_branch,
|
||||
// status: event.check_suite.status,
|
||||
// conclusion: event.check_suite.conclusion,
|
||||
// };
|
||||
// }
|
||||
// break;
|
||||
return `************* ENVIRONMENT SETUP *************
|
||||
|
||||
// case "workflow_dispatch":
|
||||
// if ("inputs" in event) essential.inputs = event.inputs;
|
||||
// break;
|
||||
// }
|
||||
${lines.join("\n")}
|
||||
|
||||
// return essential;
|
||||
// }
|
||||
`;
|
||||
}
|
||||
|
||||
export const addInstructions = (payload: Payload) => {
|
||||
interface AddInstructionsParams {
|
||||
payload: Payload;
|
||||
prepResults: PrepResult[];
|
||||
}
|
||||
|
||||
export const addInstructions = ({ payload, prepResults }: AddInstructionsParams) => {
|
||||
let encodedEvent = "";
|
||||
|
||||
const eventKeys = Object.keys(payload.event);
|
||||
@@ -99,6 +86,9 @@ export const addInstructions = (payload: Payload) => {
|
||||
encodedEvent = toonEncode(payload.event);
|
||||
}
|
||||
|
||||
const envSetup = formatPrepResults(prepResults);
|
||||
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
|
||||
|
||||
return `
|
||||
***********************************************
|
||||
************* SYSTEM INSTRUCTIONS *************
|
||||
@@ -195,7 +185,7 @@ Before starting any work, you must first determine which mode to use by examinin
|
||||
|
||||
Available modes:
|
||||
|
||||
${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
${[...getModes({ disableProgressComment: payload.disableProgressComment, dependenciesPreinstalled }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
|
||||
**Required first step**:
|
||||
1. Examine the user's request/prompt carefully
|
||||
@@ -229,5 +219,6 @@ ${encodedEvent}`
|
||||
************* RUNTIME CONTEXT *************
|
||||
|
||||
working_directory: ${process.cwd()}
|
||||
`;
|
||||
|
||||
${envSetup}`;
|
||||
};
|
||||
|
||||
+2
-2
@@ -304,7 +304,7 @@ export const opencode = agent({
|
||||
installDependencies: true,
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath }) => {
|
||||
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults }) => {
|
||||
// 1. configure home/config directory
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "opencode");
|
||||
@@ -313,7 +313,7 @@ export const opencode = agent({
|
||||
configureOpenCodeMcpServers({ mcpServers });
|
||||
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
|
||||
|
||||
const prompt = addInstructions(payload);
|
||||
const prompt = addInstructions({ payload, prepResults });
|
||||
const args = ["run", "--format", "json", prompt];
|
||||
|
||||
if (payload.sandbox) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pipeline } from "node:stream/promises";
|
||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { show } from "@ark/util";
|
||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
||||
import type { PrepResult } from "../prep/index.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { getGitHubInstallationToken } from "../utils/github.ts";
|
||||
|
||||
@@ -29,6 +30,7 @@ export interface AgentConfig {
|
||||
payload: Payload;
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
cliPath: string;
|
||||
prepResults: PrepResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createMcpConfigs } from "./mcp/config.ts";
|
||||
import { startMcpHttpServer } from "./mcp/server.ts";
|
||||
import { getModes, type Mode, modes } from "./modes.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { type PrepResult, runPrepPhase } from "./prep/index.ts";
|
||||
import { fetchRepoSettings, fetchWorkflowRunInfo, type RepoSettings } from "./utils/api.ts";
|
||||
import { log } from "./utils/cli.ts";
|
||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||
@@ -71,6 +72,21 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
await setupTempDirectory(ctx);
|
||||
timer.checkpoint("setupTempDirectory");
|
||||
|
||||
// run agent CLI installation and prep phase in parallel
|
||||
const [, prepResults] = await Promise.all([installAgentCli(ctx), runPrepPhase()]);
|
||||
ctx.prepResults = prepResults;
|
||||
|
||||
// recompute modes now that we know if dependencies were preinstalled
|
||||
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
|
||||
ctx.modes = [
|
||||
...getModes({
|
||||
disableProgressComment: ctx.payload.disableProgressComment,
|
||||
dependenciesPreinstalled,
|
||||
}),
|
||||
...(ctx.payload.modes || []),
|
||||
];
|
||||
timer.checkpoint("installAgentCli+prepPhase");
|
||||
|
||||
await startMcpServer(ctx);
|
||||
mcpServerClose = ctx.mcpServerClose;
|
||||
timer.checkpoint("startMcpServer");
|
||||
@@ -89,9 +105,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
|
||||
setupMcpServers(ctx);
|
||||
|
||||
await installAgentCli(ctx);
|
||||
timer.checkpoint("installAgentCli");
|
||||
|
||||
await validateApiKey(ctx);
|
||||
|
||||
const result = await runAgent(ctx);
|
||||
@@ -223,6 +236,9 @@ export interface Context {
|
||||
cliPath: string;
|
||||
apiKey: string;
|
||||
apiKeys: Record<string, string>;
|
||||
|
||||
// prep phase results
|
||||
prepResults: PrepResult[];
|
||||
}
|
||||
|
||||
async function initializeContext(
|
||||
@@ -238,6 +254,7 @@ async function initializeContext(
|
||||
| "apiKey"
|
||||
| "apiKeys"
|
||||
| "pushRemote"
|
||||
| "prepResults"
|
||||
>
|
||||
> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
@@ -274,8 +291,12 @@ async function initializeContext(
|
||||
const resolvedPayload = { ...payload, agent: agentName };
|
||||
|
||||
// compute modes from defaults + payload overrides
|
||||
// note: dependenciesPreinstalled is undefined here since prepPhase runs after this
|
||||
const computedModes = [
|
||||
...getModes({ disableProgressComment: resolvedPayload.disableProgressComment }),
|
||||
...getModes({
|
||||
disableProgressComment: resolvedPayload.disableProgressComment,
|
||||
dependenciesPreinstalled: undefined,
|
||||
}),
|
||||
...(resolvedPayload.modes || []),
|
||||
];
|
||||
|
||||
@@ -462,6 +483,7 @@ async function runAgent(ctx: Context): Promise<AgentResult> {
|
||||
apiKey: ctx.apiKey,
|
||||
apiKeys: ctx.apiKeys,
|
||||
cliPath: ctx.cliPath,
|
||||
prepResults: ctx.prepResults,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,26 @@ export interface Mode {
|
||||
|
||||
export interface GetModesParams {
|
||||
disableProgressComment: true | undefined;
|
||||
dependenciesPreinstalled: true | undefined;
|
||||
}
|
||||
|
||||
const reportProgressInstruction = `Use ${ghPullfrogMcpName}/report_progress to share progress and results. Continue calling it as you make progress - it will update the same comment. Never create additional comments manually.`;
|
||||
|
||||
export function getModes({ disableProgressComment }: GetModesParams): Mode[] {
|
||||
export function getModes({
|
||||
disableProgressComment,
|
||||
dependenciesPreinstalled,
|
||||
}: GetModesParams): Mode[] {
|
||||
const depsContext = dependenciesPreinstalled
|
||||
? "Dependencies have already been installed."
|
||||
: "understand how to install dependencies,";
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Build",
|
||||
description:
|
||||
"Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
|
||||
prompt: `Follow these steps:
|
||||
1. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
1. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists, ${depsContext} run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
|
||||
2. Create a branch using ${ghPullfrogMcpName}/create_branch. The branch name should be prefixed with "pullfrog/". The rest of the name should reflect the exact changes you are making. It should be specific to avoid collisions with other branches. Never commit directly to main, master, or production. Do NOT use git commands directly - always use ${ghPullfrogMcpName} MCP tools for git operations.
|
||||
|
||||
@@ -116,7 +124,7 @@ ${
|
||||
description:
|
||||
"Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
||||
prompt: `Follow these steps:
|
||||
1. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
1. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, ${depsContext} run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
|
||||
2. Analyze the request and break it down into clear, actionable tasks
|
||||
|
||||
@@ -145,4 +153,7 @@ ${
|
||||
];
|
||||
}
|
||||
|
||||
export const modes: Mode[] = getModes({ disableProgressComment: undefined });
|
||||
export const modes: Mode[] = getModes({
|
||||
disableProgressComment: undefined,
|
||||
dependenciesPreinstalled: undefined,
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@opencode-ai/sdk": "^1.0.143",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"arktype": "2.1.28",
|
||||
"package-manager-detector": "^1.6.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"execa": "^9.6.0",
|
||||
"fastmcp": "^3.20.0",
|
||||
|
||||
Generated
+8
@@ -50,6 +50,9 @@ importers:
|
||||
fastmcp:
|
||||
specifier: ^3.20.0
|
||||
version: 3.20.0(arktype@2.1.28)
|
||||
package-manager-detector:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
table:
|
||||
specifier: ^6.9.0
|
||||
version: 6.9.0
|
||||
@@ -836,6 +839,9 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
package-manager-detector@1.6.0:
|
||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
||||
|
||||
parse-ms@4.0.0:
|
||||
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1843,6 +1849,8 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
parse-ms@4.0.0: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { installNodeDependencies } from "./installNodeDependencies.ts";
|
||||
import { installPythonDependencies } from "./installPythonDependencies.ts";
|
||||
import type { PrepDefinition, PrepResult } from "./types.ts";
|
||||
|
||||
export type { PrepResult } from "./types.ts";
|
||||
|
||||
// register all prep steps here
|
||||
const prepSteps: PrepDefinition[] = [installNodeDependencies, installPythonDependencies];
|
||||
|
||||
/**
|
||||
* run all prep steps sequentially.
|
||||
* failures are logged as warnings but don't stop the run.
|
||||
*/
|
||||
export async function runPrepPhase(): Promise<PrepResult[]> {
|
||||
log.info("🔧 starting prep phase...");
|
||||
const startTime = Date.now();
|
||||
const results: PrepResult[] = [];
|
||||
|
||||
for (const step of prepSteps) {
|
||||
const shouldRun = await step.shouldRun();
|
||||
if (!shouldRun) {
|
||||
log.info(`⏭️ skipping ${step.name} (not applicable)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info(`▶️ running ${step.name}...`);
|
||||
const result = await step.run();
|
||||
results.push(result);
|
||||
|
||||
if (result.dependenciesInstalled) {
|
||||
log.info(`✅ ${step.name}: dependencies installed`);
|
||||
} else if (result.issues.length > 0) {
|
||||
log.warning(`⚠️ ${step.name}: ${result.issues[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
const totalDurationMs = Date.now() - startTime;
|
||||
log.info(`🔧 prep phase completed (${totalDurationMs}ms)`);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { detect } from "package-manager-detector";
|
||||
import { resolveCommand } from "package-manager-detector/commands";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import type { NodePackageManager, NodePrepResult, PrepDefinition } from "./types.ts";
|
||||
|
||||
// package managers that need installation (npm is always available)
|
||||
type InstallablePackageManager = Exclude<NodePackageManager, "npm">;
|
||||
|
||||
// install commands for each package manager
|
||||
const PM_INSTALL_COMMANDS: Record<InstallablePackageManager, string[]> = {
|
||||
pnpm: ["npm", "install", "-g", "pnpm"],
|
||||
yarn: ["npm", "install", "-g", "yarn"],
|
||||
bun: ["npm", "install", "-g", "bun"],
|
||||
deno: ["sh", "-c", "curl -fsSL https://deno.land/install.sh | sh"],
|
||||
};
|
||||
|
||||
async function isCommandAvailable(command: string): Promise<boolean> {
|
||||
const result = await spawn({
|
||||
cmd: "which",
|
||||
args: [command],
|
||||
env: { PATH: process.env.PATH || "" },
|
||||
});
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
async function installPackageManager(name: InstallablePackageManager): Promise<string | null> {
|
||||
log.info(`📦 installing ${name}...`);
|
||||
const [cmd, ...args] = PM_INSTALL_COMMANDS[name];
|
||||
const result = await spawn({
|
||||
cmd,
|
||||
args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return result.stderr || `failed to install ${name}`;
|
||||
}
|
||||
|
||||
// deno installs to $HOME/.deno/bin - add to PATH for subsequent commands
|
||||
if (name === "deno") {
|
||||
const denoPath = join(process.env.HOME || "", ".deno", "bin");
|
||||
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
log.info(`✅ installed ${name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export const installNodeDependencies: PrepDefinition = {
|
||||
name: "installNodeDependencies",
|
||||
|
||||
shouldRun: () => {
|
||||
const packageJsonPath = join(process.cwd(), "package.json");
|
||||
return existsSync(packageJsonPath);
|
||||
},
|
||||
|
||||
run: async (): Promise<NodePrepResult> => {
|
||||
// detect package manager
|
||||
const detected = await detect({ cwd: process.cwd() });
|
||||
if (!detected) {
|
||||
return {
|
||||
language: "node",
|
||||
packageManager: "npm",
|
||||
dependenciesInstalled: false,
|
||||
issues: ["no package manager detected from lockfile"],
|
||||
};
|
||||
}
|
||||
|
||||
const packageManager = detected.name as NodePackageManager;
|
||||
log.info(`📦 detected package manager: ${packageManager} (${detected.agent})`);
|
||||
|
||||
// check if package manager is available, install if needed (npm is always available)
|
||||
if (packageManager !== "npm" && !(await isCommandAvailable(packageManager))) {
|
||||
log.info(`${packageManager} not found, attempting to install...`);
|
||||
const installError = await installPackageManager(packageManager);
|
||||
if (installError) {
|
||||
return {
|
||||
language: "node",
|
||||
packageManager,
|
||||
dependenciesInstalled: false,
|
||||
issues: [installError],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// get the frozen install command (or fallback to regular install)
|
||||
const resolved =
|
||||
resolveCommand(detected.agent, "frozen", []) || resolveCommand(detected.agent, "install", []);
|
||||
if (!resolved) {
|
||||
return {
|
||||
language: "node",
|
||||
packageManager,
|
||||
dependenciesInstalled: false,
|
||||
issues: [`no install command found for ${detected.agent}`],
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`running: ${resolved.command} ${resolved.args.join(" ")}`);
|
||||
const result = await spawn({
|
||||
cmd: resolved.command,
|
||||
args: resolved.args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
language: "node",
|
||||
packageManager,
|
||||
dependenciesInstalled: false,
|
||||
issues: [result.stderr || `${resolved.command} exited with code ${result.exitCode}`],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
language: "node",
|
||||
packageManager,
|
||||
dependenciesInstalled: true,
|
||||
issues: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import type { PrepDefinition, PythonPackageManager, PythonPrepResult } from "./types.ts";
|
||||
|
||||
interface PythonConfig {
|
||||
file: string;
|
||||
tool: PythonPackageManager;
|
||||
installCmd: string[];
|
||||
}
|
||||
|
||||
// python dependency file patterns in priority order
|
||||
const PYTHON_CONFIGS: PythonConfig[] = [
|
||||
{
|
||||
file: "requirements.txt",
|
||||
tool: "pip",
|
||||
installCmd: ["pip", "install", "-r", "requirements.txt"],
|
||||
},
|
||||
{
|
||||
file: "pyproject.toml",
|
||||
tool: "pip",
|
||||
installCmd: ["pip", "install", "."],
|
||||
},
|
||||
{
|
||||
file: "Pipfile",
|
||||
tool: "pipenv",
|
||||
installCmd: ["pipenv", "install"],
|
||||
},
|
||||
{
|
||||
file: "Pipfile.lock",
|
||||
tool: "pipenv",
|
||||
installCmd: ["pipenv", "sync"],
|
||||
},
|
||||
{
|
||||
file: "poetry.lock",
|
||||
tool: "poetry",
|
||||
installCmd: ["poetry", "install", "--no-interaction"],
|
||||
},
|
||||
{
|
||||
file: "setup.py",
|
||||
tool: "pip",
|
||||
installCmd: ["pip", "install", "-e", "."],
|
||||
},
|
||||
];
|
||||
|
||||
// tool install commands (via pip)
|
||||
const TOOL_INSTALL_COMMANDS: Record<string, string[]> = {
|
||||
pipenv: ["pip", "install", "pipenv"],
|
||||
poetry: ["pip", "install", "poetry"],
|
||||
};
|
||||
|
||||
async function isCommandAvailable(command: string): Promise<boolean> {
|
||||
const result = await spawn({
|
||||
cmd: "which",
|
||||
args: [command],
|
||||
env: { PATH: process.env.PATH || "" },
|
||||
});
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
async function installTool(name: string): Promise<string | null> {
|
||||
const installCmd = TOOL_INSTALL_COMMANDS[name];
|
||||
if (!installCmd) {
|
||||
// tool doesn't need installation (e.g., pip)
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info(`📦 installing ${name}...`);
|
||||
const [cmd, ...args] = installCmd;
|
||||
const result = await spawn({
|
||||
cmd,
|
||||
args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return result.stderr || `failed to install ${name}`;
|
||||
}
|
||||
|
||||
log.info(`✅ installed ${name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export const installPythonDependencies: PrepDefinition = {
|
||||
name: "installPythonDependencies",
|
||||
|
||||
shouldRun: async () => {
|
||||
// check if python is available
|
||||
const hasPython = (await isCommandAvailable("python3")) || (await isCommandAvailable("python"));
|
||||
if (!hasPython) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if any python config file exists
|
||||
const cwd = process.cwd();
|
||||
return PYTHON_CONFIGS.some((config) => existsSync(join(cwd, config.file)));
|
||||
},
|
||||
|
||||
run: async (): Promise<PythonPrepResult> => {
|
||||
const cwd = process.cwd();
|
||||
|
||||
// find the first matching config
|
||||
const config = PYTHON_CONFIGS.find((c) => existsSync(join(cwd, c.file)));
|
||||
if (!config) {
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: "pip",
|
||||
configFile: "unknown",
|
||||
dependenciesInstalled: false,
|
||||
issues: ["no python config file found"],
|
||||
};
|
||||
}
|
||||
|
||||
log.info(`🐍 detected python config: ${config.file} (using ${config.tool})`);
|
||||
|
||||
// check if the tool is available, install if needed
|
||||
const isAvailable = await isCommandAvailable(config.tool);
|
||||
if (!isAvailable) {
|
||||
log.info(`${config.tool} not found, attempting to install...`);
|
||||
const installError = await installTool(config.tool);
|
||||
if (installError) {
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: config.tool,
|
||||
configFile: config.file,
|
||||
dependenciesInstalled: false,
|
||||
issues: [installError],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// run the install command
|
||||
const [cmd, ...args] = config.installCmd;
|
||||
log.info(`running: ${cmd} ${args.join(" ")}`);
|
||||
const result = await spawn({
|
||||
cmd,
|
||||
args,
|
||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
||||
onStderr: (chunk) => process.stderr.write(chunk),
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: config.tool,
|
||||
configFile: config.file,
|
||||
dependenciesInstalled: false,
|
||||
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
language: "python",
|
||||
packageManager: config.tool,
|
||||
configFile: config.file,
|
||||
dependenciesInstalled: true,
|
||||
issues: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
interface PrepResultBase {
|
||||
dependenciesInstalled: boolean;
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
export type NodePackageManager = "npm" | "pnpm" | "yarn" | "bun" | "deno";
|
||||
|
||||
export interface NodePrepResult extends PrepResultBase {
|
||||
language: "node";
|
||||
packageManager: NodePackageManager;
|
||||
}
|
||||
|
||||
export type PythonPackageManager = "pip" | "pipenv" | "poetry";
|
||||
|
||||
export interface PythonPrepResult extends PrepResultBase {
|
||||
language: "python";
|
||||
packageManager: PythonPackageManager;
|
||||
configFile: string;
|
||||
}
|
||||
|
||||
export interface UnknownLanguagePrepResult extends PrepResultBase {
|
||||
language: "unknown";
|
||||
}
|
||||
|
||||
export type PrepResult = NodePrepResult | PythonPrepResult | UnknownLanguagePrepResult;
|
||||
|
||||
export interface PrepDefinition {
|
||||
name: string;
|
||||
shouldRun: () => Promise<boolean> | boolean;
|
||||
run: () => Promise<PrepResult>;
|
||||
}
|
||||
Reference in New Issue
Block a user