codemodctl
Version:
CLI tool and utilities for workflow engine operations, file sharding, and codeowner analysis
392 lines (385 loc) • 11.6 kB
JavaScript
#!/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 { };