abstract tarball installation

This commit is contained in:
David Blass
2025-11-12 20:26:34 -05:00
parent b65a6df9f7
commit 586477f456
6 changed files with 8100 additions and 7276 deletions
+6 -81
View File
@@ -1,93 +1,18 @@
import { execSync } from "node:child_process";
import { createWriteStream, existsSync, rmSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { addInstructions, agent } from "./shared.ts";
import { addInstructions, agent, installFromNpmTarball } from "./shared.ts";
export const claude = agent({
name: "claude",
inputKey: "anthropic_api_key",
install: async () => {
// Get the SDK version from package.json and resolve to actual version
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
let sdkVersion: string;
// If it's a range (starts with ^ or ~), query npm registry for the latest matching version
if (versionRange.startsWith("^") || versionRange.startsWith("~")) {
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
log.info(`Resolving version for range ${versionRange}...`);
try {
const registryResponse = await fetch(`${npmRegistry}/@anthropic-ai/claude-agent-sdk`);
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
// Get the latest version that matches the range (simplified: just use latest)
sdkVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${sdkVersion}`);
} catch (error) {
log.warning(
`Failed to resolve version from registry, using latest: ${error instanceof Error ? error.message : String(error)}`
);
sdkVersion = "latest";
}
} else {
sdkVersion = versionRange;
}
log.info(`📦 Installing Claude Code CLI from @anthropic-ai/claude-agent-sdk@${sdkVersion}...`);
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), "claude-cli-"));
const tarballPath = join(tempDir, "package.tgz");
try {
// Download tarball from npm
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
const tarballUrl = `${npmRegistry}/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-${sdkVersion}.tgz`;
log.info(`Downloading from ${tarballUrl}...`);
const response = await fetch(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
// Write tarball to file
if (!response.body) throw new Error("Response body is null");
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body, fileStream);
log.info(`Downloaded tarball to ${tarballPath}`);
// Extract tarball
log.info(`Extracting tarball...`);
execSync(`tar -xzf "${tarballPath}" -C "${tempDir}"`, { stdio: "pipe" });
// Find cli.js in the extracted package
const extractedDir = join(tempDir, "package");
const cliPath = join(extractedDir, "cli.js");
if (!existsSync(cliPath)) {
throw new Error(`cli.js not found in extracted package at ${cliPath}`);
}
log.info(`✓ Claude Code CLI installed at ${cliPath}`);
return cliPath;
} catch (error) {
// Cleanup on error
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
throw error;
}
return await installFromNpmTarball({
packageName: "@anthropic-ai/claude-agent-sdk",
version: versionRange,
executablePath: "cli.js",
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
+8 -33
View File
@@ -1,43 +1,18 @@
import { spawnSync } from "node:child_process";
import { findCliPath, log } from "../utils/cli.ts";
import { addInstructions, agent } from "./shared.ts";
import { log } from "../utils/cli.ts";
import { addInstructions, agent, installFromNpmTarball } from "./shared.ts";
export const codex = agent({
name: "codex",
inputKey: "openai_api_key",
install: async () => {
const globalCodexPath = findCliPath("codex");
if (globalCodexPath) {
log.info(`Using global Codex CLI at ${globalCodexPath}`);
return globalCodexPath;
}
// Install Codex CLI globally using npm
log.info(`📦 Installing Codex CLI globally with npm...`);
try {
const installResult = spawnSync("npm", ["install", "-g", "codex"], {
stdio: "inherit",
encoding: "utf-8",
});
if (installResult.status !== 0) {
throw new Error(`npm install failed with status ${installResult.status}`);
}
// Verify installation
const installedPath = findCliPath("codex");
if (installedPath) {
log.info(`✓ Codex CLI installed at ${installedPath}`);
return installedPath;
}
throw new Error("Codex CLI installation completed but executable not found");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Failed to install Codex CLI: ${errorMessage}`);
throw new Error(`Codex CLI installation failed: ${errorMessage}`);
}
return await installFromNpmTarball({
packageName: "codex",
version: "latest",
executablePath: "bin/codex",
installDependencies: true,
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
process.env.OPENAI_API_KEY = apiKey;
+106
View File
@@ -1,5 +1,12 @@
import { execSync } from "node:child_process";
import { createWriteStream, existsSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } from "../mcp/config.ts";
import { log } from "../utils/cli.ts";
import { workflows } from "../workflows.ts";
/**
@@ -23,6 +30,105 @@ export interface AgentConfig {
cliPath: string;
}
/**
* Install a CLI tool from an npm package tarball
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
* The temp directory will be cleaned up by the OS automatically
*/
export async function installFromNpmTarball({
packageName,
version,
executablePath,
installDependencies = false,
}: {
packageName: string;
version: string;
executablePath: string;
installDependencies?: boolean;
}): Promise<string> {
// Resolve version if it's a range or "latest"
let resolvedVersion = version;
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
log.info(`Resolving version for ${version}...`);
try {
const registryResponse = await fetch(`${npmRegistry}/${packageName}`);
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
resolvedVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${resolvedVersion}`);
} catch (error) {
log.warning(
`Failed to resolve version from registry: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
log.info(`📦 Installing ${packageName}@${resolvedVersion}...`);
// Derive temp directory prefix from package name (remove @, replace / with -, add trailing -)
const tempDirPrefix = packageName.replace("@", "").replace(/\//g, "-") + "-";
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
const tarballPath = join(tempDir, "package.tgz");
// Download tarball from npm
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
// Handle scoped packages (e.g., @scope/package -> @scope%2Fpackage/-/package-version.tgz)
let tarballUrl: string;
if (packageName.startsWith("@")) {
const [scope, name] = packageName.slice(1).split("/");
const scopedPackageName = `@${scope}%2F${name}`;
tarballUrl = `${npmRegistry}/${scopedPackageName}/-/${name}-${resolvedVersion}.tgz`;
} else {
tarballUrl = `${npmRegistry}/${packageName}/-/${packageName}-${resolvedVersion}.tgz`;
}
log.info(`Downloading from ${tarballUrl}...`);
const response = await fetch(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
// Write tarball to file
if (!response.body) throw new Error("Response body is null");
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body, fileStream);
log.info(`Downloaded tarball to ${tarballPath}`);
// Extract tarball
log.info(`Extracting tarball...`);
execSync(`tar -xzf "${tarballPath}" -C "${tempDir}"`, { stdio: "pipe" });
// Find executable in the extracted package
const extractedDir = join(tempDir, "package");
const cliPath = join(extractedDir, executablePath);
if (!existsSync(cliPath)) {
throw new Error(`Executable not found in extracted package at ${cliPath}`);
}
// Install dependencies if requested and package.json exists
if (installDependencies) {
const packageJsonPath = join(extractedDir, "package.json");
if (existsSync(packageJsonPath)) {
log.info(`Installing dependencies for ${packageName}...`);
execSync(`npm install --production`, { cwd: extractedDir, stdio: "pipe" });
}
}
log.info(`${packageName} installed at ${cliPath}`);
return cliPath;
}
export const agent = <const agent extends Agent>(agent: agent): agent => {
return agent;
};
+436 -475
View File
File diff suppressed because one or more lines are too long
+7543 -6687
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -25,6 +25,7 @@
"dependencies": {
"@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@openai/codex-sdk": "0.57.0",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0",