gitvan
Version:
Autonomic Git-native development automation platform with AI-powered workflows
408 lines (354 loc) • 11.5 kB
JavaScript
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, extname } from "pathe";
import { GitVanDaemon, startDaemon } from "./runtime/daemon.mjs";
import { discoverEvents, loadEventDefinition } from "./runtime/events.mjs";
import { readReceiptsRange } from "./runtime/receipt.mjs";
import {
discoverJobs,
findJobFile,
findAllJobs,
loadJobDefinition,
} from "./runtime/jobs.mjs";
import { useGit } from "./composables/git/index.mjs";
import { runJobWithContext } from "./runtime/boot.mjs";
import { loadConfig } from "./runtime/config.mjs";
// Import new CLI commands
import { cronCommand } from "./cli/cron.mjs";
import { daemonCommand } from "./cli/daemon.mjs";
import { eventCommand } from "./cli/event.mjs";
import { auditCommand } from "./cli/audit.mjs";
import { chatCommand } from "./cli/chat.mjs";
const commands = {
daemon: handleDaemon,
run: handleRun,
list: handleList,
event: handleEvent,
schedule: handleSchedule,
worktree: handleWorktree,
job: handleJob,
help: handleHelp,
// New v2 commands
cron: cronCommand,
audit: auditCommand,
chat: handleChat,
llm: handleLLM,
};
async function main() {
const [, , command, ...args] = process.argv;
if (!command || command === "help") {
handleHelp();
return;
}
const handler = commands[command];
if (!handler) {
console.error(`Unknown command: ${command}`);
handleHelp();
process.exit(1);
}
try {
await handler(...args);
} catch (err) {
console.error("Error:", err.message);
process.exit(1);
}
}
async function handleDaemon(action = "start", ...options) {
const worktreePath = process.cwd();
// Parse options
const opts = {};
for (let i = 0; i < options.length; i += 2) {
const key = options[i]?.replace(/^--/, "");
const value = options[i + 1];
if (key) opts[key] = value;
}
switch (action) {
case "start":
if (opts.worktrees === "all") {
console.log("Starting daemon for all worktrees...");
await startDaemon({ rootDir: worktreePath }, null, "all");
} else {
const daemon = new GitVanDaemon(worktreePath);
await daemon.start();
}
break;
case "stop":
const daemon = new GitVanDaemon(worktreePath);
daemon.stop();
break;
case "status":
const statusDaemon = new GitVanDaemon(worktreePath);
console.log(
`Daemon ${
statusDaemon.isRunning() ? "running" : "not running"
} for: ${worktreePath}`
);
break;
default:
console.error(`Unknown daemon action: ${action}`);
process.exit(1);
}
}
async function handleEvent(action = "list", ...args) {
// Use new event command handler
return await eventCommand(action, parseArgs(args));
}
async function handleChat(action = "draft", ...args) {
// Use new chat command handler
return await chatCommand(action, parseArgs(args));
}
function parseArgs(args) {
const parsed = {};
let positionalIndex = 0;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith("--")) {
// Named argument
const key = arg.replace(/^--/, "");
const value = args[i + 1];
if (value && !value.startsWith("--")) {
parsed[key] = value;
i++; // Skip the value
} else {
parsed[key] = true;
}
} else {
// Positional argument
parsed[`arg${positionalIndex}`] = arg;
positionalIndex++;
}
}
return parsed;
}
async function handleSchedule(action = "apply") {
switch (action) {
case "apply":
console.log("Schedule management not yet implemented");
// TODO: Implement cron-like scheduling
break;
default:
console.error(`Unknown schedule action: ${action}`);
process.exit(1);
}
}
async function handleWorktree(action = "list") {
switch (action) {
case "list":
try {
// We need to create a minimal context for Git operations
const ctx = {
root: process.cwd(),
env: process.env,
now: () => new Date().toISOString(),
};
const { withGitVan } = await import("./composables/ctx.mjs");
await withGitVan(ctx, async () => {
const git = useGit();
const worktrees = git.listWorktrees();
if (worktrees.length === 0) {
console.log("No worktrees found");
return;
}
console.log("\nWorktrees:");
console.log("==========");
for (const wt of worktrees) {
console.log(`${wt.path} ${wt.isMain ? "(main)" : ""}`);
console.log(` Branch: ${wt.branch || "detached"}`);
if (wt.head) console.log(` HEAD: ${wt.head.slice(0, 8)}`);
console.log();
}
});
} catch (err) {
console.error("Error listing worktrees:", err.message);
}
break;
default:
console.error(`Unknown worktree action: ${action}`);
process.exit(1);
}
}
async function handleJob(action = "list", ...args) {
const worktreePath = process.cwd();
const jobsDir = join(worktreePath, "jobs");
switch (action) {
case "list":
if (!statSync(jobsDir).isDirectory()) {
console.log("No jobs directory found");
return;
}
const jobs = discoverJobs(jobsDir);
if (jobs.length === 0) {
console.log("No jobs found");
return;
}
console.log("Available jobs:");
console.log("==============");
jobs.forEach((job) => {
console.log(`${job.id}`);
console.log(` File: ${job.relativePath}`);
console.log(` Directory: ${job.directory}`);
console.log();
});
break;
case "run":
const nameIndex = args.indexOf("--name");
if (nameIndex === -1 || !args[nameIndex + 1]) {
console.error("Job name required: gitvan job run --name <job-name>");
process.exit(1);
}
const jobName = args[nameIndex + 1];
const jobPath = findJobFile(jobsDir, jobName);
if (!jobPath) {
console.error(`Job not found: ${jobName}`);
process.exit(1);
}
try {
const jobDef = await loadJobDefinition(jobPath);
if (!jobDef) {
console.error(`Failed to load job: ${jobName}`);
process.exit(1);
}
const ctx = {
root: worktreePath,
env: process.env,
now: () => new Date().toISOString(),
nowISO: new Date().toISOString(),
id: jobName,
logger: {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
},
};
console.log(`Running job: ${jobName}`);
const result = await runJobWithContext(ctx, jobDef);
console.log("Result:", JSON.stringify(result, null, 2));
} catch (error) {
console.error(`Error running job ${jobName}:`, error.message);
process.exit(1);
}
break;
default:
console.error(`Unknown job action: ${action}`);
process.exit(1);
}
}
async function handleRun(jobName) {
if (!jobName) {
console.error("Job name required");
process.exit(1);
}
const worktreePath = process.cwd();
const jobsDir = join(worktreePath, "jobs");
const jobPath = findJobFile(jobsDir, jobName);
if (!jobPath) {
console.error(`Job not found: ${jobName}`);
process.exit(1);
}
try {
const jobDef = await loadJobDefinition(jobPath);
if (!jobDef) {
console.error(`Failed to load job: ${jobName}`);
process.exit(1);
}
const ctx = {
root: worktreePath,
env: process.env,
now: () => new Date().toISOString(),
nowISO: new Date().toISOString(),
id: jobName,
logger: {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
},
};
console.log(`Running job: ${jobName}`);
const result = await runJobWithContext(ctx, jobDef);
console.log("Result:", JSON.stringify(result, null, 2));
} catch (error) {
console.error(`Error running job ${jobName}:`, error.message);
process.exit(1);
}
}
function handleList() {
const worktreePath = process.cwd();
const jobsDir = join(worktreePath, "jobs");
if (!statSync(jobsDir).isDirectory()) {
console.log("No jobs directory found");
return;
}
const jobs = findAllJobs(jobsDir);
console.log("Available jobs:");
jobs.forEach((job) => console.log(` ${job}`));
}
// LLM command handler
async function handleLLM(subcommand = "call", ...args) {
const { generateText, checkAIAvailability } = await import(
"./ai/provider.mjs"
);
const { loadOptions } = await import("./config/loader.mjs");
const config = await loadOptions();
switch (subcommand) {
case "call":
if (!args[0]) {
console.error('Prompt required: gitvan llm call "<prompt>"');
process.exit(1);
}
const prompt = args[0];
const model = args.includes("--model")
? args[args.indexOf("--model") + 1]
: undefined;
try {
const result = await generateText({ prompt, model, config });
console.log(result.output);
} catch (error) {
console.error("LLM call failed:", error.message);
process.exit(1);
}
break;
case "models":
const availability = await checkAIAvailability(config);
console.log(`Provider: ${availability.provider}`);
console.log(`Model: ${availability.model}`);
console.log(`Available: ${availability.available ? "Yes" : "No"}`);
if (!availability.available) {
console.log(`Message: ${availability.message}`);
}
break;
default:
console.error(`Unknown llm subcommand: ${subcommand}`);
process.exit(1);
}
}
function handleHelp() {
console.log(`
GitVan v2 - AI-powered Git workflow automation
Usage:
gitvan daemon [start|stop|status] [--worktrees all] Manage daemon
gitvan job [list|run] [--name <job-name>] Job management
gitvan event [list|simulate|test] Event management
gitvan cron [list|start|dry-run] Cron job management
gitvan audit [build|verify|list] Receipt audit
gitvan chat [draft|generate|explain] AI job generation
gitvan llm [call|models] AI operations
gitvan schedule apply Apply scheduled tasks
gitvan worktree list List all worktrees
gitvan run <job-name> Run a specific job (legacy)
gitvan list List available jobs (legacy)
gitvan help Show this help
Examples:
gitvan daemon start Start daemon for current worktree
gitvan cron list List all cron jobs
gitvan event simulate --files "src/**" Simulate file change event
gitvan chat generate "Create a changelog job" Generate job via AI
gitvan llm call "Summarize recent commits" Call AI directly
gitvan audit build --out audit.json Build audit pack
`);
}
export { main };
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}