UNPKG

codemodctl

Version:

CLI tool and utilities for workflow engine operations, file sharding, and codeowner analysis

392 lines (385 loc) 11.6 kB
#!/usr/bin/env node import "./codemod-cli-DailrcEf.js"; import { analyzeCodeowners } from "./codeowner-analysis-DlwMGduk.js"; import "./consistent-sharding-BfgFDhwr.js"; import { analyzeDirectories } from "./directory-analysis-D4YprDWr.js"; import { defineCommand, runMain } from "citty"; import crypto from "node:crypto"; import { $ } from "execa"; import { writeFile } from "node:fs/promises"; //#region src/commands/git/create-pr.ts const createPrCommand = defineCommand({ meta: { name: "create-pr", description: "Create a pull request" }, args: { title: { type: "string", description: "Title of the pull request", required: true }, body: { type: "string", description: "Body/description of the pull request", required: false }, head: { type: "string", description: "Head branch for the pull request", required: false }, base: { type: "string", description: "Base branch to merge into", required: false }, push: { type: "boolean", required: false }, commitMessage: { alias: "m", type: "string", description: "Message to commit", required: false }, branchName: { alias: "b", type: "string", description: "Branch to create the pull request from", required: false } }, async run({ args }) { const { title, body, head, base, push, commitMessage, branchName } = args; if (push && !commitMessage) { console.error("Error: commitMessage is required if commit is true"); process.exit(1); } const apiEndpoint = process.env.BUTTERFLOW_API_ENDPOINT; const authToken = process.env.BUTTERFLOW_API_AUTH_TOKEN; const taskId = process.env.CODEMOD_TASK_ID; if (!taskId) { console.error("Error: CODEMOD_TASK_ID environment variable is required"); process.exit(1); } if (!apiEndpoint) { console.error("Error: BUTTERFLOW_API_ENDPOINT environment variable is required"); process.exit(1); } if (!authToken) { console.error("Error: BUTTERFLOW_API_AUTH_TOKEN environment variable is required"); process.exit(1); } const prData = { title }; if (push) { try { await $`git diff --quiet`; await $`git diff --cached --quiet`; console.error("No changes detected, skipping pull request creation."); process.exit(0); } catch { console.log("Changes detected, proceeding with pull request creation..."); } const taskIdSignature = crypto.createHash("sha256").update(taskId).digest("hex").slice(0, 8); const codemodBranchName = branchName ? branchName : `codemod-${taskIdSignature}`; let remoteBaseBranch; try { remoteBaseBranch = (await $`git remote show origin`).stdout.match(/HEAD branch: (.+)/)?.[1]?.trim() || "main"; } catch (error) { console.error("Error: Failed to get remote base branch"); console.error(error); process.exit(1); } if (codemodBranchName) prData.head = codemodBranchName; if (remoteBaseBranch) prData.base = remoteBaseBranch; console.debug(`Remote base branch: ${remoteBaseBranch}`); try { await $`git checkout -b ${codemodBranchName}`; } catch (error) { console.error("Error: Failed to checkout branch"); console.error(error); process.exit(1); } try { await $`git add .`; } catch (error) { console.error("Error: Failed to add changes"); console.error(error); process.exit(1); } try { await $`git commit -m ${commitMessage}`; } catch (error) { console.error("Error: Failed to commit changes"); console.error(error); process.exit(1); } try { await $`git push origin ${codemodBranchName} --force`; console.log(`Pushed branch to origin: ${codemodBranchName}`); } catch (error) { console.error("Error: Failed to push changes"); console.error(error); process.exit(1); } } if (body) prData.body = body; if (head && !prData.head) prData.head = head; if (base && !prData.base) prData.base = base; try { console.debug("Creating pull request..."); console.debug(`Title: ${title}`); if (body) console.debug(`Body: ${body}`); if (head) console.debug(`Head: ${head}`); console.debug(`Base: ${base}`); const response = await fetch(`${apiEndpoint}/api/butterflow/v1/tasks/${taskId}/pull-request`, { method: "POST", headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json" }, body: JSON.stringify(prData) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } await response.json(); console.log("✅ Pull request created successfully!"); } catch (error) { console.error("❌ Failed to create pull request:"); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } }); //#endregion //#region src/commands/git/index.ts const gitCommand = defineCommand({ meta: { name: "git", description: "Git operations" }, subCommands: { "create-pr": createPrCommand } }); //#endregion //#region src/commands/shard/codeowner.ts /** * Codeowner-based sharding command * * Creates shards by grouping files by their CODEOWNERS team assignments. * Uses simple file distribution within each team group, maintaining * consistency with existing state when available. * * Example usage: * npx codemodctl shard codeowner -l tsx -c ./codemod.ts -s 30 --stateProp shards --codeowners .github/CODEOWNERS * * This will analyze all applicable files, group them by CODEOWNERS team assignments, and create * shards with approximately 30 files each within each team. */ const codeownerCommand = defineCommand({ meta: { name: "codeowner", description: "Analyze GitHub CODEOWNERS file and create sharding output" }, args: { shardSize: { type: "string", alias: "s", description: "Number of files per shard", required: true }, stateProp: { type: "string", alias: "p", description: "Property name for state output", required: true }, codeowners: { type: "string", description: "Path to CODEOWNERS file (optional)", required: false }, codemodFile: { type: "string", alias: "c", description: "Path to codemod file", required: true }, language: { type: "string", alias: "l", description: "Language of the codemod", required: true } }, async run({ args }) { const { shardSize: shardSizeStr, stateProp, codeowners: codeownersPath, codemodFile: codemodFilePath, language } = args; const shardSize = parseInt(shardSizeStr, 10); if (isNaN(shardSize) || shardSize <= 0) { console.error("Error: shard-size must be a positive number"); process.exit(1); } const stateOutputsPath = process.env.STATE_OUTPUTS; if (!stateOutputsPath) { console.error("Error: STATE_OUTPUTS environment variable is required"); process.exit(1); } try { console.log(`State property: ${stateProp}`); const existingStateJson = process.env.CODEMOD_STATE; let existingState; if (existingStateJson) try { existingState = JSON.parse(existingStateJson)[stateProp]; console.log(`Found existing state with ${existingState.length} shards`); } catch (parseError) { console.warn(`Warning: Failed to parse existing state: ${parseError}`); existingState = void 0; } const analysisOptions = { shardSize, codeownersPath, rulePath: codemodFilePath, projectRoot: process.cwd(), language, existingState }; const result = await analyzeCodeowners(analysisOptions); const stateOutput = `${stateProp}=${JSON.stringify(result.shards)}\n`; console.log(`Writing state output to: ${stateOutputsPath}`); await writeFile(stateOutputsPath, stateOutput, { flag: "a" }); console.log("✅ Sharding completed successfully!"); console.log("Generated shards:", JSON.stringify(result.shards, null, 2)); } catch (error) { console.error("❌ Failed to process codeowner file:"); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } }); //#endregion //#region src/commands/shard/directory.ts /** * Directory-based sharding command * * Creates shards by grouping files within subdirectories of a target directory. * Uses consistent hashing to distribute files within each directory group, maintaining * consistency with existing state when available. * * Example usage: * npx codemodctl shard directory -l tsx -c ./codemod.ts -s 30 --stateProp shards --target packages/ * * This will analyze all applicable files within subdirectories of 'packages/' and create * shards with approximately 30 files each, grouped by directory. */ const directoryCommand = defineCommand({ meta: { name: "directory", description: "Create directory-based sharding output" }, args: { shardSize: { type: "string", alias: "s", description: "Number of files per shard", required: true }, stateProp: { type: "string", alias: "p", description: "Property name for state output", required: true }, target: { type: "string", description: "Target directory to shard by subdirectories", required: true }, codemodFile: { type: "string", alias: "c", description: "Path to codemod file", required: true }, language: { type: "string", alias: "l", description: "Language of the codemod", required: true } }, async run({ args }) { const { shardSize: shardSizeStr, stateProp, target, codemodFile: codemodFilePath, language } = args; const shardSize = parseInt(shardSizeStr, 10); if (isNaN(shardSize) || shardSize <= 0) { console.error("Error: shard-size must be a positive number"); process.exit(1); } const stateOutputsPath = process.env.STATE_OUTPUTS; if (!stateOutputsPath) { console.error("Error: STATE_OUTPUTS environment variable is required"); process.exit(1); } try { console.log(`State property: ${stateProp}`); console.log(`Target directory: ${target}`); const existingStateJson = process.env.CODEMOD_STATE; let existingState; if (existingStateJson) try { existingState = JSON.parse(existingStateJson)[stateProp]; console.log(`Found existing state with ${existingState.length} shards`); } catch (parseError) { console.warn(`Warning: Failed to parse existing state: ${parseError}`); existingState = void 0; } const analysisOptions = { shardSize, target, rulePath: codemodFilePath, projectRoot: process.cwd(), language, existingState }; const result = await analyzeDirectories(analysisOptions); const stateOutput = `${stateProp}=${JSON.stringify(result.shards)}\n`; console.log(`Writing state output to: ${stateOutputsPath}`); await writeFile(stateOutputsPath, stateOutput, { flag: "a" }); console.log("✅ Directory-based sharding completed successfully!"); console.log("Generated shards:", JSON.stringify(result.shards, null, 2)); } catch (error) { console.error("❌ Failed to process directory sharding:"); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } }); //#endregion //#region src/commands/shard/index.ts const shardCommand = defineCommand({ meta: { name: "shard", description: "Sharding operations for distributing work" }, subCommands: { codeowner: codeownerCommand, directory: directoryCommand } }); //#endregion //#region src/cli.ts const main = defineCommand({ meta: { name: "codemodctl", version: "0.1.0", description: "CLI tool for workflow engine operations" }, subCommands: { git: gitCommand, shard: shardCommand } }); runMain(main); //#endregion export { };