Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "commander";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { existsSync } from "node:fs";
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import chalk from "chalk";
Expand Down Expand Up @@ -37,8 +37,8 @@ export const initCommand = new Command("init")
const candidates = ["python3", "python"];
for (const name of candidates) {
try {
const { execSync } = await import("node:child_process");
const version = execSync(`${name} --version`, {
const { execFileSync } = await import("node:child_process");
const version = execFileSync(name, ["--version"], {
encoding: "utf-8",
}).trim();
const match = version.match(/Python (\d+\.\d+)/);
Expand Down Expand Up @@ -126,6 +126,12 @@ export const initCommand = new Command("init")

saveConfig(config);

// Create patterns directory for user-defined patterns
const patternsDir = join(homedir(), ".ace", "patterns");
if (!existsSync(patternsDir)) {
mkdirSync(patternsDir, { recursive: true });
}

// Step 5: Summary
console.log(chalk.bold("\nSetup complete:"));
console.log(
Expand Down Expand Up @@ -153,7 +159,7 @@ export const initCommand = new Command("init")
"\n"
);

console.log(chalk.dim("Try: ace workflow list-templates"));
console.log(chalk.dim("Try: ace run --list"));

rl.close();
});
180 changes: 180 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { ensurePython } from "../utils/ensure-python.js";
import {
listPatterns,
loadPattern,
readStdin,
readInputFile,
runPattern,
runBatch,
writeOutput,
getUserPatternsDir,
} from "../utils/patterns.js";
import * as output from "../utils/output.js";

export const runCommand = new Command("run")
.description("Run a pattern on text input (pipe-friendly)")
.argument("[pattern]", "Pattern name to run")
.argument("[text]", "Inline text input")
.option("-l, --list", "List available patterns")
.option("--info <pattern>", "Show pattern details")
.option("-m, --model <model>", "Override the LLM model")
.option("-j, --json", "Output as JSON")
.option("-v, --verbose", "Show debug output")
.option("-f, --file <path>", "Read input from a file")
.option("--input-dir <path>", "Process all files in a directory")
.option("-o, --output <path>", "Write output to a file")
.option("--output-dir <path>", "Write batch outputs to a directory")
.action(
async (
patternName: string | undefined,
inlineText: string | undefined,
options: {
list?: boolean;
info?: string;
model?: string;
json?: boolean;
verbose?: boolean;
file?: string;
inputDir?: string;
output?: string;
outputDir?: string;
}
) => {
// ── List patterns ──────────────────────────────────
if (options.list) {
const patterns = listPatterns();
const grouped = new Map<string, typeof patterns>();
for (const p of patterns) {
const group = grouped.get(p.category) || [];
group.push(p);
grouped.set(p.category, group);
}

for (const [category, categoryPatterns] of grouped) {
console.log(chalk.bold(`\n${category.charAt(0).toUpperCase() + category.slice(1)}`));
for (const p of categoryPatterns) {
console.log(` ${chalk.cyan(p.id.padEnd(22))} ${p.description}`);
}
}

console.log(chalk.dim(`\nUser patterns: ${getUserPatternsDir()}`));
return;
}

// ── Pattern info ───────────────────────────────────
if (options.info) {
const pattern = loadPattern(options.info);
if (!pattern) {
output.error(`Pattern not found: ${options.info}`);
const all = listPatterns();
console.log(chalk.dim(`Available: ${all.map((p) => p.id).join(", ")}`));
process.exit(1);
}

console.log(chalk.bold(pattern.name));
console.log(chalk.dim(pattern.description));
console.log();
console.log(chalk.bold("Category:"), pattern.category);
if (pattern.model) {
console.log(chalk.bold("Model:"), pattern.model);
}
console.log();
console.log(chalk.bold("System Prompt:"));
console.log(chalk.dim("─".repeat(60)));
console.log(pattern.systemPrompt);
console.log(chalk.dim("─".repeat(60)));
return;
}

// ── Validate pattern name ──────────────────────────
if (!patternName) {
output.error("Pattern name required. Use --list to see available patterns.");
console.log(chalk.dim("Usage: ace run <pattern> [text]"));
console.log(chalk.dim(" echo \"text\" | ace run <pattern>"));
process.exit(1);
}

if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(patternName)) {
output.error("Invalid pattern name. Use only letters, numbers, hyphens, and underscores.");
process.exit(1);
}

const pattern = loadPattern(patternName);
if (!pattern) {
output.error(`Pattern not found: ${patternName}`);
const all = listPatterns();
console.log(chalk.dim(`Available: ${all.map((p) => p.id).join(", ")}`));
process.exit(1);
}

// ── Ensure Python + aceteam-nodes ──────────────────
const pythonPath = await ensurePython();

// ── Batch mode (folder → folder) ──────────────────
if (options.inputDir) {
if (!options.outputDir) {
output.error("--output-dir is required with --input-dir");
process.exit(1);
}

await runBatch(pythonPath, pattern, options.inputDir, {
outputDir: options.outputDir,
model: options.model,
json: options.json,
verbose: options.verbose,
});
return;
}

// ── Resolve input text ─────────────────────────────
// Priority: --file > inline text arg > stdin pipe
let inputText: string;

if (options.file) {
try {
inputText = readInputFile(options.file);
} catch (err) {
output.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
} else if (inlineText) {
inputText = inlineText;
} else {
inputText = await readStdin();
}

if (!inputText) {
output.error("No input provided.");
console.log(chalk.dim("Usage: ace run <pattern> \"text to process\""));
console.log(chalk.dim(" ace run <pattern> --file input.txt"));
console.log(chalk.dim(" echo \"text\" | ace run <pattern>"));
process.exit(1);
}

// ── Execute pattern via aceteam-nodes ──────────────
const spinner = ora(`Running ${pattern.name}...`).start();

try {
const result = await runPattern(pythonPath, pattern, inputText, {
model: options.model,
json: options.json,
verbose: options.verbose,
});

spinner.stop();
writeOutput(result, options.output);

if (options.output) {
output.success(`Output written to ${options.output}`);
}
} catch (err) {
spinner.fail(`${pattern.name} failed`);
const message = err instanceof Error ? err.message : String(err);
console.error(chalk.red(message));
process.exit(1);
}
}
);
50 changes: 1 addition & 49 deletions src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,16 @@ import { stdin, stdout } from "node:process";
import chalk from "chalk";
import ora from "ora";
import {
findPython,
isAceteamNodesInstalled,
installAceteamNodes,
runWorkflow,
validateWorkflow,
listNodes,
getVenvPythonPath,
isVenvValid,
} from "../utils/python.js";
import { loadConfig } from "../utils/config.js";
import { FabricClient } from "../utils/fabric.js";
import { classifyPythonError } from "../utils/errors.js";
import { TEMPLATES, getTemplateById } from "../templates/index.js";
import * as output from "../utils/output.js";

async function ensurePython(): Promise<string> {
const config = loadConfig();

// Check config python_path first (managed venv)
if (config.python_path && existsSync(config.python_path)) {
if (isAceteamNodesInstalled(config.python_path)) {
return config.python_path;
}
}

// Check managed venv
if (config.venv_dir && isVenvValid(config.venv_dir)) {
const venvPython = getVenvPythonPath(config.venv_dir);
if (isAceteamNodesInstalled(venvPython)) {
return venvPython;
}
}

// Fallback to PATH detection
const pythonPath = await findPython();
if (!pythonPath) {
output.error(
"Python 3.12+ not found. Please install Python and run: ace init"
);
process.exit(1);
}

if (!isAceteamNodesInstalled(pythonPath)) {
output.warn("aceteam-nodes is not installed.");
console.log("Installing aceteam-nodes...");
try {
installAceteamNodes(pythonPath);
output.success("aceteam-nodes installed");
} catch {
output.error(
"Failed to install aceteam-nodes. Try: pip install aceteam-nodes"
);
process.exit(1);
}
}

return pythonPath;
}
import { ensurePython } from "../utils/ensure-python.js";

function parseInputArgs(inputs: string[]): Record<string, string> {
const result: Record<string, string> = {};
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from "commander";
import { initCommand } from "./commands/init.js";
import { workflowCommand } from "./commands/workflow.js";
import { fabricCommand } from "./commands/fabric.js";
import { runCommand } from "./commands/run.js";

const program = new Command();

Expand All @@ -11,6 +12,7 @@ program
.version("0.2.0");

program.addCommand(initCommand);
program.addCommand(runCommand);
program.addCommand(workflowCommand);
program.addCommand(fabricCommand);

Expand Down
Loading