iterate on prep
This commit is contained in:
@@ -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