@voria/cli 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -380
- package/bin/voria +625 -486
- package/docs/CHANGELOG.md +19 -0
- package/docs/USER_GUIDE.md +34 -5
- package/package.json +1 -1
- package/python/voria/__init__.py +1 -1
- package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
- package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
- package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
- package/python/voria/core/executor/executor.py +5 -0
- package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__init__.py +16 -0
- package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/deepseek_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/kimi_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/minimax_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/siliconflow_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/base.py +12 -0
- package/python/voria/core/llm/claude_provider.py +46 -0
- package/python/voria/core/llm/deepseek_provider.py +109 -0
- package/python/voria/core/llm/gemini_provider.py +44 -0
- package/python/voria/core/llm/kimi_provider.py +109 -0
- package/python/voria/core/llm/minimax_provider.py +187 -0
- package/python/voria/core/llm/modal_provider.py +33 -0
- package/python/voria/core/llm/model_discovery.py +58 -16
- package/python/voria/core/llm/openai_provider.py +33 -0
- package/python/voria/core/llm/siliconflow_provider.py +109 -0
- package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
- package/python/voria/core/setup.py +4 -1
- package/python/voria/core/testing/__pycache__/definitions.cpython-312.pyc +0 -0
- package/python/voria/core/testing/__pycache__/runner.cpython-312.pyc +0 -0
- package/python/voria/core/testing/definitions.py +87 -0
- package/python/voria/core/testing/runner.py +324 -0
- package/python/voria/engine.py +736 -232
package/bin/voria
CHANGED
|
@@ -53,114 +53,182 @@ const BANNER = `
|
|
|
53
53
|
|
|
54
54
|
const HELP_TEXT = `
|
|
55
55
|
${BANNER}
|
|
56
|
-
${colors.dim("
|
|
57
|
-
${colors.
|
|
58
|
-
${colors.dim("
|
|
56
|
+
${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
|
|
57
|
+
${colors.bold(colors.blue(" AI-Powered Autonomous Fixer v0.0.5"))}
|
|
58
|
+
${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
|
|
59
59
|
|
|
60
60
|
${colors.bold(colors.blue("USAGE:"))}
|
|
61
61
|
voria [command] [options]
|
|
62
62
|
|
|
63
|
-
${colors.bold(colors.blue("
|
|
64
|
-
${colors.blue("🚀")} voria --init ${colors.dim("Initialize project & choose
|
|
65
|
-
${colors.blue("⚙️")}
|
|
63
|
+
${colors.bold(colors.blue("SETUP:"))}
|
|
64
|
+
${colors.blue("🚀")} voria --init ${colors.dim("Initialize project & choose LLM provider")}
|
|
65
|
+
${colors.blue("⚙️")} voria --config ${colors.dim("Configure LLM provider and settings")}
|
|
66
66
|
${colors.blue("🔑")} voria --set-github-token ${colors.dim("Set your GitHub personal access token")}
|
|
67
|
-
${colors.blue("📋")} voria --list-issues <repo>${colors.dim("List issues in a repository")}
|
|
68
|
-
${colors.blue("🔭")} voria --graph ${colors.dim("Show codebase dependency graph")}
|
|
69
|
-
${colors.blue("❓")} voria --help ${colors.dim("Show this help message")}
|
|
70
|
-
${colors.blue("🏷️")} voria --version ${colors.dim("Show current version")}
|
|
71
67
|
|
|
72
|
-
${colors.bold(colors.blue("
|
|
73
|
-
${colors.blue("
|
|
68
|
+
${colors.bold(colors.blue("SECURITY & TESTING:"))}
|
|
69
|
+
${colors.blue("🛡️")} voria scan ${colors.dim("Full security audit — run ALL tests in parallel")}
|
|
70
|
+
${colors.blue("🧪")} voria test <test_id> ${colors.dim("Run a specific security/performance test")}
|
|
71
|
+
${colors.blue("📋")} voria list-tests ${colors.dim("List all 25+ available tests")}
|
|
72
|
+
${colors.blue("👁️")} voria watch ${colors.dim("File watcher — re-runs tests on code changes")}
|
|
73
|
+
${colors.blue("📊")} voria diff [a] [b] ${colors.dim("Compare security posture between git refs")}
|
|
74
|
+
${colors.blue("⚡")} voria benchmark <url> ${colors.dim("HTTP benchmarking — p50/p95/p99 latencies")}
|
|
75
|
+
${colors.blue("🏗️")} voria ci ${colors.dim("CI/CD scan — outputs SARIF for GitHub Security")}
|
|
76
|
+
|
|
77
|
+
${colors.bold(colors.blue("ISSUE FIXING:"))}
|
|
78
|
+
${colors.blue("🐞")} voria issue <repo> <num> ${colors.dim("Fix a specific GitHub issue with AI")}
|
|
79
|
+
${colors.blue("🔧")} voria fix --auto <r> <n> ${colors.dim("Auto-apply patch and run tests")}
|
|
80
|
+
${colors.blue("📝")} voria plan <description> ${colors.dim("Generate fix plan without applying")}
|
|
74
81
|
${colors.blue("📦")} voria apply <patch> ${colors.dim("Apply a patch file")}
|
|
82
|
+
${colors.blue("📋")} voria --list-issues <r> ${colors.dim("List issues in a repository")}
|
|
83
|
+
|
|
84
|
+
${colors.bold(colors.blue("PROJECT:"))}
|
|
75
85
|
${colors.blue("📊")} voria status ${colors.dim("Show current project status")}
|
|
76
86
|
${colors.blue("💰")} voria token info ${colors.dim("Show token usage info")}
|
|
77
87
|
${colors.blue("📝")} voria logs ${colors.dim("Show recent activity logs")}
|
|
78
88
|
${colors.blue("🔄")} voria reset ${colors.dim("Reset project configuration")}
|
|
89
|
+
${colors.blue("🔭")} voria --graph ${colors.dim("Show codebase dependency graph")}
|
|
79
90
|
|
|
80
91
|
${colors.bold(colors.blue("OPTIONS:"))}
|
|
81
|
-
--
|
|
82
|
-
--
|
|
92
|
+
--category <cat> ${colors.dim("Filter scan by category (security/performance/stress/quality/all)")}
|
|
93
|
+
--requests <n> ${colors.dim("Number of requests for benchmark (default: 100)")}
|
|
94
|
+
--concurrency <n> ${colors.dim("Concurrency for benchmark (default: 10)")}
|
|
83
95
|
--apply ${colors.dim("Automatically create PR after fixing")}
|
|
84
96
|
--dry-run ${colors.dim("Don't create PR, just propose")}
|
|
85
97
|
--verbose, -v ${colors.dim("Show detailed output")}
|
|
86
98
|
|
|
87
99
|
${colors.bold(colors.blue("EXAMPLES:"))}
|
|
88
|
-
${colors.cyan("#
|
|
89
|
-
voria
|
|
100
|
+
${colors.cyan("voria --init")} ${colors.dim("# Setup with your LLM provider")}
|
|
101
|
+
${colors.cyan("voria scan")} ${colors.dim("# Full security audit")}
|
|
102
|
+
${colors.cyan("voria scan --category performance")} ${colors.dim("# Performance tests only")}
|
|
103
|
+
${colors.cyan("voria test hardcoded_secrets")} ${colors.dim("# Run specific test")}
|
|
104
|
+
${colors.cyan("voria diff main feature-branch")} ${colors.dim("# Compare branches")}
|
|
105
|
+
${colors.cyan("voria benchmark http://localhost:3000")} ${colors.dim("# HTTP benchmark")}
|
|
106
|
+
${colors.cyan("voria issue owner/repo 42 --apply")} ${colors.dim("# Fix issue and create PR")}
|
|
107
|
+
${colors.cyan("voria ci")} ${colors.dim("# CI/CD SARIF output")}
|
|
108
|
+
|
|
109
|
+
${colors.bold(colors.blue("DOCS:"))} ${colors.blue("https://github.com/Srizdebnath/voria")}
|
|
110
|
+
`;
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
voria issue owner/repo 42 --apply
|
|
112
|
+
const VERSION = '0.0.5';
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
voria plan "Remove deprecated function" --llm claude --model opus-4.6
|
|
114
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
function log(msg) { console.log(`${colors.blue("│")} ${msg}`); }
|
|
117
|
+
function logHeader(msg) { console.log(`\n${colors.bgBlue(colors.bold(` ${msg} `))}`); }
|
|
118
|
+
function logDivider() { console.log(colors.blue("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); }
|
|
119
|
+
function logSuccess(msg) { console.log(`${colors.green("✓")} ${msg}`); }
|
|
120
|
+
function logError(msg) { console.error(`${colors.red("✖")} ${msg}`); }
|
|
121
|
+
function logInfo(msg) { console.log(`${colors.blue("ℹ")} ${colors.dim(msg)}`); }
|
|
122
|
+
function logWarn(msg) { console.log(`${colors.yellow("⚠")} ${msg}`); }
|
|
102
123
|
|
|
103
124
|
// Load configuration
|
|
104
125
|
function loadConfig() {
|
|
105
126
|
const config = {};
|
|
106
|
-
|
|
107
127
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
108
128
|
try {
|
|
109
|
-
|
|
110
|
-
Object.assign(config, data);
|
|
129
|
+
Object.assign(config, JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')));
|
|
111
130
|
} catch (e) {
|
|
112
|
-
|
|
131
|
+
logWarn(`Error reading global config: ${e.message}`);
|
|
113
132
|
}
|
|
114
133
|
}
|
|
115
|
-
|
|
116
134
|
if (fs.existsSync(PROJECT_CONFIG)) {
|
|
117
135
|
try {
|
|
118
|
-
|
|
119
|
-
Object.assign(config, data);
|
|
136
|
+
Object.assign(config, JSON.parse(fs.readFileSync(PROJECT_CONFIG, 'utf-8')));
|
|
120
137
|
} catch (e) {
|
|
121
|
-
|
|
138
|
+
logWarn(`Error reading project config: ${e.message}`);
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
|
-
|
|
125
141
|
return config;
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
// Save configuration
|
|
129
144
|
function saveConfig(config, isGlobal = true) {
|
|
130
145
|
const file = isGlobal ? CONFIG_FILE : PROJECT_CONFIG;
|
|
131
146
|
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
function requireInit() {
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
if (!config.llm_api_key && !config.llm_provider) {
|
|
152
|
+
logError(`voria not initialized. Run ${colors.cyan("voria --init")}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getApiKey(config) {
|
|
159
|
+
let apiKey = config.llm_api_key;
|
|
160
|
+
if (!apiKey) {
|
|
161
|
+
const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
|
|
162
|
+
apiKey = process.env[envKey];
|
|
163
|
+
}
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
logError(`API key not found. Run ${colors.cyan("voria --config")} or set env var.`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
return apiKey;
|
|
169
|
+
}
|
|
170
|
+
|
|
137
171
|
function fetchModels(provider, apiKey) {
|
|
138
172
|
try {
|
|
139
173
|
const scriptPath = path.join(__dirname, '..', 'python', 'voria', 'core', 'llm', 'model_discovery.py');
|
|
140
174
|
const output = execSync(`python3 "${scriptPath}" "${provider}" "${apiKey}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
141
175
|
return JSON.parse(output);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
176
|
+
} catch (e) { return []; }
|
|
145
177
|
}
|
|
146
178
|
|
|
147
|
-
//
|
|
179
|
+
// ─── Python Engine IPC ───────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async function callPythonEngine(command) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const pythonScript = path.join(__dirname, '..', 'python', 'voria', 'engine.py');
|
|
184
|
+
const pythonPath = path.join(__dirname, '..', 'python');
|
|
185
|
+
const venvPath = path.join(__dirname, '..', 'venv', 'bin', 'python');
|
|
186
|
+
const pythonCmd = fs.existsSync(venvPath) ? venvPath : 'python3';
|
|
187
|
+
const env = { ...process.env };
|
|
188
|
+
env.PYTHONPATH = pythonPath + (env.PYTHONPATH ? ':' + env.PYTHONPATH : '');
|
|
189
|
+
|
|
190
|
+
const python = spawn(pythonCmd, [pythonScript], {
|
|
191
|
+
cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], env
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let output = '';
|
|
195
|
+
let errorOutput = '';
|
|
196
|
+
python.stdout.on('data', (d) => { output += d.toString(); });
|
|
197
|
+
python.stderr.on('data', (d) => { errorOutput += d.toString(); });
|
|
198
|
+
|
|
199
|
+
python.on('close', (code) => {
|
|
200
|
+
if (code !== 0) logInfo(`Python engine debug: ${errorOutput.slice(-300)}`);
|
|
201
|
+
try {
|
|
202
|
+
const lines = output.trim().split('\n').filter(l => l.length > 0);
|
|
203
|
+
const lastLine = lines[lines.length - 1];
|
|
204
|
+
resolve(JSON.parse(lastLine));
|
|
205
|
+
} catch (e) {
|
|
206
|
+
reject(new Error(`Failed to parse engine response: ${e.message}`));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
python.stdin.write(JSON.stringify(command) + '\n');
|
|
211
|
+
python.stdin.end();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Setup & Config ──────────────────────────────────────────────
|
|
216
|
+
|
|
148
217
|
async function runSetup() {
|
|
149
218
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
150
219
|
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
151
220
|
|
|
152
221
|
try {
|
|
153
222
|
console.log(BANNER);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
logHeader("PROJECT INITIALIZATION");
|
|
224
|
+
log(colors.blue("Configuring your intelligence backend"));
|
|
225
|
+
logDivider();
|
|
226
|
+
|
|
158
227
|
if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
|
|
159
|
-
|
|
160
|
-
console.log(` voria works best when integrated with Git.\n`);
|
|
228
|
+
logWarn(`Not in a Git repository. voria works best with Git.`);
|
|
161
229
|
}
|
|
162
230
|
|
|
163
|
-
console.log(
|
|
231
|
+
console.log(`\n${colors.bold(colors.blue("Choose LLM Provider:"))}`);
|
|
164
232
|
console.log(` ${colors.blue("1)")} OpenAI (GPT-4o, GPT-4 Mini)`);
|
|
165
233
|
console.log(` ${colors.blue("2)")} Claude (Anthropic)`);
|
|
166
234
|
console.log(` ${colors.blue("3)")} Gemini (Google)`);
|
|
@@ -175,497 +243,558 @@ async function runSetup() {
|
|
|
175
243
|
const provider = providers[parseInt(choice) - 1] || 'openai';
|
|
176
244
|
|
|
177
245
|
const apiKey = await question(`${colors.cyan(provider.toUpperCase() + " API Key")} › `);
|
|
178
|
-
|
|
179
|
-
if (!apiKey) {
|
|
180
|
-
console.log(`\n${colors.red("✖")} API key is required.`);
|
|
181
|
-
rl.close();
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
246
|
+
if (!apiKey) { logError("API key is required."); rl.close(); return; }
|
|
184
247
|
|
|
185
|
-
|
|
248
|
+
logInfo("Fetching latest models...");
|
|
186
249
|
const fetchedModels = fetchModels(provider, apiKey);
|
|
187
250
|
const models = fetchedModels.length > 0 ? fetchedModels.map(m => m.name) : (PROVIDER_MODELS[provider] || ['gpt-4o']);
|
|
188
251
|
|
|
189
|
-
console.log(`\n${colors.bold("Select Model:")}`);
|
|
190
|
-
models.forEach((
|
|
191
|
-
console.log(` ${colors.blue((index + 1) + ")")} ${model}`);
|
|
192
|
-
});
|
|
193
|
-
|
|
252
|
+
console.log(`\n${colors.bold(colors.blue("Select Model:"))}`);
|
|
253
|
+
models.forEach((m, i) => console.log(` ${colors.blue((i + 1) + ")")} ${m}`));
|
|
194
254
|
const modelChoice = await question(`\n${colors.cyan("selection")} ${colors.dim("(1)")} › `) || '1';
|
|
195
255
|
const selectedModel = models[parseInt(modelChoice) - 1] || models[0];
|
|
196
256
|
|
|
197
257
|
const budget = await question(`${colors.cyan("Daily Budget ($)")} ${colors.dim("(10.0)")} › `) || '10.0';
|
|
198
258
|
|
|
199
|
-
console.log(`\n${colors.bold("Test Framework:")}`);
|
|
259
|
+
console.log(`\n${colors.bold(colors.blue("Test Framework:"))}`);
|
|
200
260
|
console.log(` ${colors.blue("1)")} pytest ${colors.dim("(Python)")}`);
|
|
201
261
|
console.log(` ${colors.blue("2)")} Jest ${colors.dim("(JavaScript)")}`);
|
|
202
262
|
console.log(` ${colors.blue("3)")} Go testing`);
|
|
203
|
-
|
|
204
263
|
const frameworkChoice = await question(`${colors.cyan("selection")} › `) || '1';
|
|
205
264
|
const frameworks = ['pytest', 'jest', 'go'];
|
|
206
265
|
const framework = frameworks[parseInt(frameworkChoice) - 1] || 'pytest';
|
|
207
266
|
|
|
208
267
|
const config = {
|
|
209
268
|
llm_provider: provider,
|
|
210
|
-
|
|
269
|
+
llm_model: selectedModel,
|
|
211
270
|
daily_budget: parseFloat(budget),
|
|
212
271
|
test_framework: framework,
|
|
213
272
|
created_at: new Date().toISOString()
|
|
214
273
|
};
|
|
215
274
|
|
|
216
|
-
saveConfig({ ...config,
|
|
275
|
+
saveConfig({ ...config, llm_api_key: apiKey }, true);
|
|
217
276
|
saveConfig({ ...config, api_key_env: `${provider.toUpperCase()}_API_KEY` }, false);
|
|
218
277
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
console.log(
|
|
224
|
-
|
|
278
|
+
logDivider();
|
|
279
|
+
logSuccess(colors.bold("voria initialized successfully!"));
|
|
280
|
+
log(`${colors.dim("Provider:")} ${colors.blue(provider)} | ${colors.dim("Model:")} ${colors.cyan(selectedModel)}`);
|
|
281
|
+
log(`${colors.dim("Config:")} ${colors.blue(CONFIG_FILE)}`);
|
|
282
|
+
console.log(`\n${colors.bold(colors.blue("Next Steps:"))}`);
|
|
283
|
+
log(`Run: ${colors.cyan("voria --set-github-token")}`);
|
|
284
|
+
log(`Run: ${colors.cyan("voria scan")} or ${colors.cyan("voria test hardcoded_secrets")}\n`);
|
|
225
285
|
rl.close();
|
|
226
286
|
} catch (error) {
|
|
227
|
-
|
|
287
|
+
logError(`Setup error: ${error.message}`);
|
|
228
288
|
rl.close();
|
|
229
289
|
process.exit(1);
|
|
230
290
|
}
|
|
231
291
|
}
|
|
232
292
|
|
|
233
|
-
// Configure existing setup
|
|
234
293
|
async function runConfig() {
|
|
235
294
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
236
295
|
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
237
296
|
|
|
238
297
|
try {
|
|
239
|
-
|
|
240
|
-
console.log(` ${colors.dim("┃")} ${colors.blue("Adjusting your local parameters")} ${colors.dim("┃")}`);
|
|
241
|
-
console.log(` ${colors.dim("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}\n`);
|
|
242
|
-
|
|
298
|
+
logHeader("SETTINGS ENGINE");
|
|
243
299
|
const config = loadConfig();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
300
|
+
log(`${colors.dim("Provider:")} ${colors.blue(config.llm_provider || 'None')}`);
|
|
301
|
+
log(`${colors.dim("Model:")} ${colors.cyan(config.llm_model || 'None')}`);
|
|
302
|
+
log(`${colors.dim("Budget:")} ${colors.green("$" + (config.daily_budget || '10.0'))}`);
|
|
303
|
+
log(`${colors.dim("Framework:")} ${colors.cyan(config.test_framework || 'None')}\n`);
|
|
248
304
|
|
|
249
305
|
const change = await question(`${colors.cyan("edit")} ${colors.dim("(provider/budget/framework/none)")} › `);
|
|
250
|
-
|
|
251
306
|
if (change === 'provider') {
|
|
252
307
|
const provider = await question(`${colors.cyan("new provider")} › `);
|
|
253
308
|
const apiKey = await question(`${colors.cyan("api key")} › `);
|
|
254
309
|
config.llm_provider = provider;
|
|
255
|
-
saveConfig({ ...config,
|
|
256
|
-
|
|
310
|
+
saveConfig({ ...config, llm_api_key: apiKey }, true);
|
|
311
|
+
logSuccess("Provider updated");
|
|
257
312
|
} else if (change === 'budget') {
|
|
258
|
-
|
|
259
|
-
config.daily_budget = parseFloat(budget);
|
|
313
|
+
config.daily_budget = parseFloat(await question(`${colors.cyan("new budget")} › `));
|
|
260
314
|
saveConfig(config, true);
|
|
261
|
-
|
|
315
|
+
logSuccess("Budget updated");
|
|
262
316
|
} else if (change === 'framework') {
|
|
263
|
-
|
|
264
|
-
config.test_framework = framework;
|
|
317
|
+
config.test_framework = await question(`${colors.cyan("new framework")} › `);
|
|
265
318
|
saveConfig(config, false);
|
|
266
|
-
|
|
319
|
+
logSuccess("Test framework updated");
|
|
267
320
|
}
|
|
321
|
+
logSuccess("Settings saved");
|
|
322
|
+
rl.close();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logError(`Config error: ${error.message}`);
|
|
325
|
+
rl.close();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
268
328
|
|
|
269
|
-
|
|
329
|
+
async function setGitHubToken() {
|
|
330
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
331
|
+
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
332
|
+
try {
|
|
333
|
+
logHeader("GitHub Integration");
|
|
334
|
+
logInfo(`Generate a token at: ${colors.blue("https://github.com/settings/tokens")}`);
|
|
335
|
+
logInfo(`Required scope: 'repo' (Full control of private repositories)`);
|
|
336
|
+
const token = await question(`\n${colors.cyan("GitHub Token")} › `);
|
|
337
|
+
if (!token) { logError("Token is required"); rl.close(); return; }
|
|
338
|
+
const config = loadConfig();
|
|
339
|
+
config.github_token = token;
|
|
340
|
+
saveConfig(config, true);
|
|
341
|
+
logSuccess(colors.bold("GitHub token saved successfully!"));
|
|
270
342
|
rl.close();
|
|
271
343
|
} catch (error) {
|
|
272
|
-
|
|
344
|
+
logError(`Error: ${error.message}`);
|
|
273
345
|
rl.close();
|
|
274
346
|
}
|
|
275
347
|
}
|
|
276
348
|
|
|
277
|
-
//
|
|
349
|
+
// ─── Status & Utility ────────────────────────────────────────────
|
|
350
|
+
|
|
278
351
|
function showStatus() {
|
|
279
|
-
|
|
352
|
+
logHeader("voria Status");
|
|
280
353
|
const config = loadConfig();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.log(` ${colors.bold("LLM Provider:")} ${colors.blue(config.llm_provider)}`);
|
|
289
|
-
console.log(` ${colors.bold("Daily Budget:")} ${colors.green("$" + config.daily_budget)}`);
|
|
290
|
-
console.log(` ${colors.bold("Test Framework:")} ${colors.cyan(config.test_framework)}`);
|
|
291
|
-
console.log(` ${colors.bold("Global Config:")} ${colors.dim(CONFIG_FILE)}\n`);
|
|
354
|
+
if (!config.llm_provider) { logError(`Not initialized. Run ${colors.cyan("voria --init")}\n`); return; }
|
|
355
|
+
logSuccess("voria is configured");
|
|
356
|
+
log(`${colors.bold("Provider:")} ${colors.blue(config.llm_provider)}`);
|
|
357
|
+
log(`${colors.bold("Model:")} ${colors.cyan(config.llm_model || 'default')}`);
|
|
358
|
+
log(`${colors.bold("Budget:")} ${colors.green("$" + config.daily_budget)}`);
|
|
359
|
+
log(`${colors.bold("Framework:")} ${colors.cyan(config.test_framework)}`);
|
|
360
|
+
log(`${colors.bold("Config:")} ${colors.dim(CONFIG_FILE)}\n`);
|
|
292
361
|
}
|
|
293
362
|
|
|
294
|
-
// Show logs
|
|
295
363
|
function showLogs() {
|
|
296
364
|
const logsFile = path.join(voria_HOME, 'voria.log');
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const logs = fs.readFileSync(logsFile, 'utf-8');
|
|
304
|
-
const lines = logs.split('\n').slice(-50);
|
|
305
|
-
|
|
306
|
-
console.log('\n📝 Recent logs (last 50 lines):\n');
|
|
307
|
-
console.log(lines.join('\n'));
|
|
365
|
+
if (!fs.existsSync(logsFile)) { log(colors.dim("No logs yet")); return; }
|
|
366
|
+
logHeader("Recent Logs");
|
|
367
|
+
const lines = fs.readFileSync(logsFile, 'utf-8').split('\n').slice(-50);
|
|
368
|
+
lines.forEach(l => log(colors.dim(l)));
|
|
308
369
|
}
|
|
309
370
|
|
|
310
|
-
// Reset configuration
|
|
311
371
|
function reset() {
|
|
312
|
-
const config = loadConfig();
|
|
313
|
-
|
|
314
|
-
if (Object.keys(config).length === 0) {
|
|
315
|
-
console.log('\n✅ No configuration to reset\n');
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
372
|
if (fs.existsSync(PROJECT_CONFIG)) {
|
|
320
373
|
fs.unlinkSync(PROJECT_CONFIG);
|
|
321
|
-
|
|
374
|
+
logSuccess(`Removed project config: ${PROJECT_CONFIG}`);
|
|
322
375
|
}
|
|
323
|
-
|
|
324
|
-
console.log('✅ Configuration reset\n');
|
|
376
|
+
logSuccess("Configuration reset");
|
|
325
377
|
}
|
|
326
378
|
|
|
327
|
-
//
|
|
328
|
-
async function setGitHubToken() {
|
|
329
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
330
|
-
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
console.log(`\n${colors.bgBlue(colors.bold(" 🔗 GitHub Integration "))}`);
|
|
334
|
-
console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
|
|
335
|
-
|
|
336
|
-
console.log(`${colors.cyan("ℹ")} ${colors.dim("Generate a token at:")} ${colors.blue("https://github.com/settings/tokens")}`);
|
|
337
|
-
console.log(`${colors.dim(" Required scope: 'repo' (Full control of private repositories)")}\n`);
|
|
338
|
-
|
|
339
|
-
const token = await question(`${colors.cyan("GitHub Token")} › `);
|
|
340
|
-
|
|
341
|
-
if (!token) {
|
|
342
|
-
console.log(`\n${colors.red("✖")} Token is required\n`);
|
|
343
|
-
rl.close();
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const config = loadConfig();
|
|
348
|
-
config.github_token = token;
|
|
349
|
-
saveConfig(config, true);
|
|
350
|
-
|
|
351
|
-
console.log(`\n${colors.green("✓")} ${colors.bold("GitHub token saved successfully!")}\n`);
|
|
352
|
-
rl.close();
|
|
353
|
-
} catch (error) {
|
|
354
|
-
console.error(colors.red('Error saving token:'), error);
|
|
355
|
-
rl.close();
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
379
|
+
// ─── Issue Fixing ────────────────────────────────────────────────
|
|
359
380
|
|
|
360
|
-
// List issues from a repository
|
|
361
381
|
async function listIssues(repoArg, config) {
|
|
362
|
-
// Parse repo from argument
|
|
363
382
|
let owner, repo;
|
|
364
|
-
|
|
365
383
|
if (repoArg.includes('github.com')) {
|
|
366
384
|
const match = repoArg.match(/github\.com[/:]([^/]+)\/([^/\s]+)/);
|
|
367
|
-
if (!match) {
|
|
368
|
-
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
owner = match[1];
|
|
372
|
-
repo = match[2].replace('.git', '');
|
|
385
|
+
if (!match) { logError("Invalid GitHub URL"); return; }
|
|
386
|
+
owner = match[1]; repo = match[2].replace('.git', '');
|
|
373
387
|
} else if (repoArg.includes('/')) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
repo = parts[1];
|
|
377
|
-
} else {
|
|
378
|
-
console.log(`\n${colors.red("✖")} Invalid repository format`);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
388
|
+
[owner, repo] = repoArg.split('/');
|
|
389
|
+
} else { logError("Invalid repository format"); return; }
|
|
381
390
|
|
|
382
391
|
const githubToken = config.github_token || process.env.GITHUB_TOKEN;
|
|
383
|
-
if (!githubToken) {
|
|
384
|
-
console.log(`\n${colors.red("✖")} GitHub token not found. Run ${colors.yellow("voria --set-github-token")}`);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
console.log(`\n${colors.blue("📋")} Fetching issues from ${colors.cyan(owner + "/" + repo)}...\n`);
|
|
392
|
+
if (!githubToken) { logError(`GitHub token not found. Run ${colors.cyan("voria --set-github-token")}`); return; }
|
|
389
393
|
|
|
394
|
+
logInfo(`Fetching issues from ${colors.cyan(owner + "/" + repo)}...`);
|
|
390
395
|
try {
|
|
391
|
-
const response = await callPythonEngine({
|
|
392
|
-
|
|
393
|
-
owner: owner,
|
|
394
|
-
repo: repo,
|
|
395
|
-
github_token: githubToken,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
if (response.status === 'error') {
|
|
399
|
-
console.error(`${colors.red("✖")} Error: ${response.message}\n`);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
396
|
+
const response = await callPythonEngine({ command: 'list_issues', owner, repo, github_token: githubToken });
|
|
397
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
403
398
|
const issues = response.data?.issues || [];
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
console.log(`${colors.yellow("📭")} No open issues found\n`);
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
console.log(`${colors.bgBlue(colors.bold(" 📋 Open Issues "))} ${colors.dim("in " + owner + "/" + repo + " (" + issues.length + " total)")}\n`);
|
|
411
|
-
|
|
399
|
+
if (issues.length === 0) { log(colors.dim("No open issues found")); return; }
|
|
400
|
+
logHeader(`Open Issues in ${owner}/${repo}`);
|
|
412
401
|
for (const issue of issues) {
|
|
413
|
-
const labels = issue.labels?.length > 0
|
|
414
|
-
|
|
415
|
-
: '';
|
|
416
|
-
|
|
417
|
-
console.log(`${colors.brightBlue("#" + issue.number)} ${colors.bold(issue.title)}${labels}`);
|
|
418
|
-
console.log(` ${colors.dim("🔗")} ${colors.blue(issue.url)}`);
|
|
419
|
-
|
|
420
|
-
if (issue.body) {
|
|
421
|
-
const snippet = issue.body.length > 100 ? issue.body.substring(0, 100) + "..." : issue.body;
|
|
422
|
-
console.log(` ${colors.dim("📝")} ${colors.dim(snippet.replace(/\n/g, " "))}`);
|
|
423
|
-
}
|
|
424
|
-
console.log('');
|
|
402
|
+
const labels = issue.labels?.length > 0 ? ` ${colors.yellow("[" + issue.labels.join(", ") + "]")}` : '';
|
|
403
|
+
log(`${colors.brightBlue("#" + issue.number)} ${colors.bold(issue.title)}${labels}`);
|
|
425
404
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
} catch (error) {
|
|
430
|
-
console.error(`${colors.red("✖")} Error: ${error.message}\n`);
|
|
431
|
-
}
|
|
405
|
+
logDivider();
|
|
406
|
+
logInfo(`To fix: ${colors.cyan("voria issue " + owner + "/" + repo + " <number> --apply")}`);
|
|
407
|
+
} catch (error) { logError(error.message); }
|
|
432
408
|
}
|
|
433
409
|
|
|
434
|
-
// Run plan command with LLM - Actually call Python engine
|
|
435
|
-
// Enhanced plan command
|
|
436
410
|
async function runPlanCommand(description, config, args) {
|
|
437
411
|
console.log(BANNER);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
console.log(`${colors.blue("⏳")} Analyzing codebase...`);
|
|
444
|
-
console.log(`${colors.blue("⏳")} Designing solution architecture...\n`);
|
|
412
|
+
logHeader("voria Plan");
|
|
413
|
+
log(colors.cyan(description));
|
|
414
|
+
logDivider();
|
|
415
|
+
logInfo("Analyzing codebase...");
|
|
416
|
+
logInfo("Designing solution architecture...");
|
|
445
417
|
|
|
446
418
|
try {
|
|
447
|
-
|
|
448
|
-
if (!apiKey) {
|
|
449
|
-
const envKey = `${(config.llm_provider || 'openai').toUpperCase()}_API_KEY`;
|
|
450
|
-
apiKey = process.env[envKey];
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (!apiKey) {
|
|
454
|
-
console.error(`${colors.red("✖")} Error: API key not found. Run ${colors.yellow("voria --config")}\n`);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
419
|
+
const apiKey = getApiKey(config);
|
|
458
420
|
const response = await callPythonEngine({
|
|
459
|
-
command: 'plan',
|
|
460
|
-
|
|
461
|
-
repo_path: process.cwd(),
|
|
462
|
-
provider: config.llm_provider || 'openai',
|
|
463
|
-
api_key: apiKey,
|
|
464
|
-
model: config.model,
|
|
421
|
+
command: 'plan', description, repo_path: process.cwd(),
|
|
422
|
+
provider: config.llm_provider || 'openai', api_key: apiKey, model: config.llm_model,
|
|
465
423
|
});
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
424
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
425
|
+
logSuccess(colors.bold("Strategic Plan Generated!"));
|
|
426
|
+
if (response.data?.plan) {
|
|
427
|
+
logHeader("Project Blueprint");
|
|
428
|
+
console.log(typeof response.data.plan === 'string' ? response.data.plan : JSON.stringify(response.data.plan, null, 2));
|
|
470
429
|
}
|
|
430
|
+
if (args.includes('--dry-run')) { logInfo("Dry Run Mode — no changes made"); }
|
|
431
|
+
else { logInfo(`Ready to implement! Run: ${colors.cyan("voria issue <repo> <number> --apply")}`); }
|
|
432
|
+
} catch (error) { logError(error.message); }
|
|
433
|
+
}
|
|
471
434
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
435
|
+
async function runIssueCommand(repo, issueNumber, config, args) {
|
|
436
|
+
console.log(BANNER);
|
|
437
|
+
logHeader("voria Issue Fix");
|
|
438
|
+
log(`${colors.bold("Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
|
|
439
|
+
log(`${colors.bold("AI:")} ${colors.blue(config.llm_provider || 'openai')} / ${colors.cyan(config.llm_model || 'default')}`);
|
|
440
|
+
log(`${colors.bold("Budget:")} ${colors.green("$" + (config.daily_budget || 10.0))}/day`);
|
|
441
|
+
logDivider();
|
|
442
|
+
logInfo("Fetching issue details...");
|
|
443
|
+
logInfo("Analyzing code context...");
|
|
444
|
+
logInfo("Generating optimized fix...");
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const apiKey = getApiKey(config);
|
|
448
|
+
const githubToken = config.github_token || process.env.GITHUB_TOKEN;
|
|
449
|
+
if (!githubToken) { logError(`GitHub token not found. Run ${colors.cyan("voria --set-github-token")}`); return; }
|
|
450
|
+
|
|
451
|
+
const response = await callPythonEngine({
|
|
452
|
+
command: 'issue', issue_number: parseInt(issueNumber), repo, repo_path: process.cwd(),
|
|
453
|
+
provider: config.llm_provider || 'openai', api_key: apiKey, github_token: githubToken, model: config.llm_model,
|
|
454
|
+
});
|
|
455
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
456
|
+
logSuccess(colors.bold("Solution Generated!"));
|
|
457
|
+
if (response.data?.issue_title) log(`${colors.bold("Title:")} ${response.data.issue_title}`);
|
|
479
458
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
459
|
+
if (args.includes('--apply')) {
|
|
460
|
+
logInfo("Creating Pull Request...");
|
|
461
|
+
const prResponse = await callPythonEngine({
|
|
462
|
+
command: 'create_pr', owner: repo.split('/')[0], repo: repo.split('/')[1],
|
|
463
|
+
issue_number: parseInt(issueNumber), patch: response.data.patch, github_token: githubToken
|
|
464
|
+
});
|
|
465
|
+
if (prResponse.status === 'success') {
|
|
466
|
+
logSuccess(colors.bold("Pull Request Created!"));
|
|
467
|
+
log(`${colors.bold("URL:")} ${colors.brightBlue(prResponse.data.pr_url)}`);
|
|
468
|
+
log(`${colors.bold("Branch:")} ${colors.cyan(prResponse.data.branch)}`);
|
|
469
|
+
} else { logError(`Failed to create PR: ${prResponse.message}`); }
|
|
483
470
|
} else {
|
|
484
|
-
|
|
485
|
-
console.log(` Run: ${colors.bold("voria issue <repo> <number> --apply")}\n`);
|
|
471
|
+
logInfo(`Solution ready. Run with ${colors.bold("--apply")} to create PR.`);
|
|
486
472
|
}
|
|
487
|
-
} catch (error) {
|
|
488
|
-
console.error(`${colors.red("✖")} Error: ${error.message}\n`);
|
|
489
|
-
}
|
|
473
|
+
} catch (error) { logError(error.message); }
|
|
490
474
|
}
|
|
491
475
|
|
|
492
|
-
//
|
|
493
|
-
|
|
476
|
+
// ─── Tier 1: scan ────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
async function runScanCommand(config, args) {
|
|
494
479
|
console.log(BANNER);
|
|
495
|
-
|
|
496
|
-
console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`);
|
|
497
|
-
console.log(`${colors.bold("🔗 Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
|
|
498
|
-
console.log(`${colors.bold("🤖 AI:")} ${colors.yellow(config.llm_provider || 'openai')}`);
|
|
499
|
-
console.log(`${colors.bold("💰 Budget:")} ${colors.green("$" + (config.daily_budget || 10.0))}/day`);
|
|
500
|
-
console.log(`${colors.dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
|
|
480
|
+
logHeader("voria Security Scan");
|
|
501
481
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
482
|
+
const categoryIdx = args.indexOf('--category');
|
|
483
|
+
const category = categoryIdx !== -1 && args[categoryIdx + 1] ? args[categoryIdx + 1] : 'all';
|
|
484
|
+
|
|
485
|
+
log(`${colors.bold("Category:")} ${colors.cyan(category)}`);
|
|
486
|
+
log(`${colors.bold("Provider:")} ${colors.blue(config.llm_provider)} / ${colors.cyan(config.llm_model || 'default')}`);
|
|
487
|
+
logDivider();
|
|
488
|
+
logInfo("Running all security tests in parallel...");
|
|
489
|
+
logInfo("This may take a few minutes depending on codebase size...");
|
|
505
490
|
|
|
506
491
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
492
|
+
const apiKey = getApiKey(config);
|
|
493
|
+
const response = await callPythonEngine({
|
|
494
|
+
command: 'scan', provider: config.llm_provider, api_key: apiKey,
|
|
495
|
+
model: config.llm_model, repo_path: process.cwd(), category,
|
|
496
|
+
});
|
|
497
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
498
|
+
|
|
499
|
+
const scan = response.data?.scan || {};
|
|
500
|
+
logDivider();
|
|
501
|
+
logHeader("Scan Results");
|
|
502
|
+
log(`${colors.bold("Total Tests:")} ${colors.blue(scan.total_tests)}`);
|
|
503
|
+
log(`${colors.bold("Passed:")} ${colors.green(scan.passed)}`);
|
|
504
|
+
log(`${colors.bold("Failed:")} ${colors.red(scan.failed)}`);
|
|
505
|
+
log(`${colors.bold("Warnings:")} ${colors.yellow(scan.warnings)}`);
|
|
506
|
+
log(`${colors.bold("Avg Score:")} ${colors.brightBlue(scan.avg_score + "/100")}`);
|
|
507
|
+
|
|
508
|
+
if (scan.findings?.length > 0) {
|
|
509
|
+
logHeader("Top Findings");
|
|
510
|
+
scan.findings.slice(0, 10).forEach(f => {
|
|
511
|
+
const sev = f.severity === 'high' ? colors.red("HIGH") : f.severity === 'medium' ? colors.yellow("MED") : colors.dim("LOW");
|
|
512
|
+
log(` ${sev} ${colors.dim(f.file || '')} ${f.description || ''}`);
|
|
513
|
+
});
|
|
511
514
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return;
|
|
515
|
+
if (scan.recommendations?.length > 0) {
|
|
516
|
+
logHeader("Recommendations");
|
|
517
|
+
scan.recommendations.slice(0, 5).forEach(r => log(` ${colors.blue("→")} ${r}`));
|
|
516
518
|
}
|
|
519
|
+
logDivider();
|
|
520
|
+
logSuccess(`Scan complete: ${colors.blue(response.message)}`);
|
|
521
|
+
} catch (error) { logError(error.message); }
|
|
522
|
+
}
|
|
517
523
|
|
|
518
|
-
|
|
519
|
-
if (!githubToken) {
|
|
520
|
-
console.error(`${colors.red("✖")} Error: GitHub token not found. Run ${colors.yellow("voria --set-github-token")}\n`);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
524
|
+
// ─── Tier 1: test & list-tests ───────────────────────────────────
|
|
523
525
|
|
|
526
|
+
async function runTestCommand(testId, config) {
|
|
527
|
+
logHeader(`Running Test: ${testId}`);
|
|
528
|
+
logInfo(`Provider: ${colors.blue(config.llm_provider)} | Model: ${colors.cyan(config.llm_model || 'default')}`);
|
|
529
|
+
logDivider();
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const apiKey = getApiKey(config);
|
|
524
533
|
const response = await callPythonEngine({
|
|
525
|
-
command: '
|
|
526
|
-
|
|
527
|
-
repo: repo,
|
|
528
|
-
repo_path: process.cwd(),
|
|
529
|
-
provider: config.llm_provider || 'openai',
|
|
530
|
-
api_key: apiKey,
|
|
531
|
-
github_token: githubToken,
|
|
532
|
-
model: config.model,
|
|
534
|
+
command: 'test', test_id: testId, provider: config.llm_provider,
|
|
535
|
+
api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
|
|
533
536
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
537
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
538
|
+
|
|
539
|
+
const result = response.data?.result?.result || response.data?.result || {};
|
|
540
|
+
logDivider();
|
|
541
|
+
const status = result.status === 'passed' ? colors.green("PASSED") : result.status === 'failed' ? colors.red("FAILED") : colors.yellow("WARNING");
|
|
542
|
+
log(`${colors.bold("Status:")} ${status}`);
|
|
543
|
+
log(`${colors.bold("Score:")} ${colors.brightBlue((result.score || 0) + "/100")}`);
|
|
544
|
+
if (result.summary) log(`${colors.bold("Summary:")} ${result.summary}`);
|
|
545
|
+
if (result.findings?.length > 0) {
|
|
546
|
+
result.findings.slice(0, 5).forEach(f => {
|
|
547
|
+
log(` ${colors.yellow("→")} ${f.description || JSON.stringify(f)}`);
|
|
548
|
+
});
|
|
538
549
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (response.data) {
|
|
543
|
-
console.log(`${colors.bold("📝 Title:")} ${response.data.issue_title}`);
|
|
550
|
+
if (result.recommendations?.length > 0) {
|
|
551
|
+
result.recommendations.forEach(r => log(` ${colors.blue("💡")} ${r}`));
|
|
544
552
|
}
|
|
553
|
+
logDivider();
|
|
554
|
+
} catch (error) { logError(error.message); }
|
|
555
|
+
}
|
|
545
556
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
557
|
+
async function runListTests(config) {
|
|
558
|
+
logHeader("Available Tests");
|
|
559
|
+
try {
|
|
560
|
+
const response = await callPythonEngine({ command: 'list_tests' });
|
|
561
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
562
|
+
const tests = response.data?.tests || [];
|
|
563
|
+
const categories = {};
|
|
564
|
+
tests.forEach(t => {
|
|
565
|
+
if (!categories[t.category]) categories[t.category] = [];
|
|
566
|
+
categories[t.category].push(t);
|
|
567
|
+
});
|
|
568
|
+
for (const [cat, items] of Object.entries(categories)) {
|
|
569
|
+
console.log(`\n ${colors.bold(colors.blue(cat.toUpperCase()))}`);
|
|
570
|
+
items.forEach(t => {
|
|
571
|
+
log(` ${colors.cyan(t.id.padEnd(25))} ${t.name} ${colors.dim("— " + (t.description || '').slice(0, 50))}`);
|
|
556
572
|
});
|
|
573
|
+
}
|
|
574
|
+
logDivider();
|
|
575
|
+
logInfo(`Run a test: ${colors.cyan("voria test <test_id>")}`);
|
|
576
|
+
logInfo(`Full scan: ${colors.cyan("voria scan")}`);
|
|
577
|
+
} catch (error) { logError(error.message); }
|
|
578
|
+
}
|
|
557
579
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
// ─── Tier 1: watch ───────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
async function runWatchCommand(config) {
|
|
583
|
+
logHeader("voria Watch Mode");
|
|
584
|
+
logInfo("Monitoring source files for changes...");
|
|
585
|
+
logInfo("Press Ctrl+C to stop\n");
|
|
586
|
+
|
|
587
|
+
const extensions = new Set(['.py', '.js', '.ts', '.go', '.rs', '.java']);
|
|
588
|
+
const ignore = ['node_modules', '.git', 'venv', '__pycache__', 'target'];
|
|
589
|
+
|
|
590
|
+
function getSnapshot(dir) {
|
|
591
|
+
const snap = {};
|
|
592
|
+
try {
|
|
593
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
594
|
+
for (const e of entries) {
|
|
595
|
+
if (ignore.some(i => e.name.includes(i))) continue;
|
|
596
|
+
const fp = path.join(dir, e.name);
|
|
597
|
+
if (e.isDirectory()) Object.assign(snap, getSnapshot(fp));
|
|
598
|
+
else if (extensions.has(path.extname(e.name))) {
|
|
599
|
+
try { snap[fp] = fs.statSync(fp).mtimeMs; } catch {}
|
|
600
|
+
}
|
|
564
601
|
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
console.log(` Run with ${colors.bold("--apply")} to automatically create a PR.\n`);
|
|
568
|
-
}
|
|
569
|
-
} catch (error) {
|
|
570
|
-
console.error(`${colors.red("✖")} Error: ${error.message}\n`);
|
|
602
|
+
} catch {}
|
|
603
|
+
return snap;
|
|
571
604
|
}
|
|
605
|
+
|
|
606
|
+
let prev = getSnapshot(process.cwd());
|
|
607
|
+
logInfo(`Watching ${Object.keys(prev).length} files...`);
|
|
608
|
+
|
|
609
|
+
const testIds = ['hardcoded_secrets', 'xss', 'sql_injection'];
|
|
610
|
+
const apiKey = getApiKey(config);
|
|
611
|
+
|
|
612
|
+
const interval = setInterval(async () => {
|
|
613
|
+
const curr = getSnapshot(process.cwd());
|
|
614
|
+
const changed = [];
|
|
615
|
+
for (const [fp, mtime] of Object.entries(curr)) {
|
|
616
|
+
if (!prev[fp] || prev[fp] !== mtime) changed.push(fp);
|
|
617
|
+
}
|
|
618
|
+
if (changed.length > 0) {
|
|
619
|
+
logInfo(`${colors.yellow(changed.length + " file(s) changed:")} ${changed.map(f => path.basename(f)).join(', ')}`);
|
|
620
|
+
for (const tid of testIds) {
|
|
621
|
+
logInfo(`Re-running test: ${colors.cyan(tid)}...`);
|
|
622
|
+
try {
|
|
623
|
+
const r = await callPythonEngine({
|
|
624
|
+
command: 'test', test_id: tid, provider: config.llm_provider,
|
|
625
|
+
api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
|
|
626
|
+
});
|
|
627
|
+
const st = r.data?.result?.result?.status || r.data?.result?.status || 'unknown';
|
|
628
|
+
const icon = st === 'passed' ? colors.green("✓") : st === 'failed' ? colors.red("✖") : colors.yellow("⚠");
|
|
629
|
+
log(` ${icon} ${tid}: ${st}`);
|
|
630
|
+
} catch (e) { logError(`${tid}: ${e.message}`); }
|
|
631
|
+
}
|
|
632
|
+
prev = curr;
|
|
633
|
+
}
|
|
634
|
+
}, 2000);
|
|
635
|
+
|
|
636
|
+
process.on('SIGINT', () => { clearInterval(interval); logInfo("Watch mode stopped."); process.exit(0); });
|
|
637
|
+
// Keep alive
|
|
638
|
+
await new Promise(() => {});
|
|
572
639
|
}
|
|
573
640
|
|
|
574
|
-
//
|
|
575
|
-
async function callPythonEngine(command) {
|
|
576
|
-
const { spawn } = await import('child_process');
|
|
577
|
-
const pathMod = await import('path');
|
|
578
|
-
const fsMod = await import('fs');
|
|
579
|
-
|
|
580
|
-
return new Promise((resolve, reject) => {
|
|
581
|
-
const pythonScript = pathMod.default.join(__dirname, '..', 'python', 'voria', 'engine.py');
|
|
582
|
-
const pythonPath = pathMod.default.join(__dirname, '..', 'python');
|
|
583
|
-
|
|
584
|
-
// Check if virtual environment exists
|
|
585
|
-
const venvPath = pathMod.default.join(__dirname, '..', 'venv', 'bin', 'python');
|
|
586
|
-
const pythonCmd = fsMod.default.existsSync(venvPath) ? venvPath : 'python3';
|
|
587
|
-
|
|
588
|
-
// Add python directory to PYTHONPATH so voria module can be found
|
|
589
|
-
const env = { ...process.env };
|
|
590
|
-
const currentPath = env.PYTHONPATH || '';
|
|
591
|
-
env.PYTHONPATH = pythonPath + (currentPath ? ':' + currentPath : '');
|
|
592
|
-
|
|
593
|
-
const python = spawn(pythonCmd, [pythonScript], {
|
|
594
|
-
cwd: process.cwd(),
|
|
595
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
596
|
-
env: env
|
|
597
|
-
});
|
|
641
|
+
// ─── Tier 1: diff ────────────────────────────────────────────────
|
|
598
642
|
|
|
599
|
-
|
|
600
|
-
|
|
643
|
+
async function runDiffCommand(args, config) {
|
|
644
|
+
const refA = args[1] || 'HEAD~1';
|
|
645
|
+
const refB = args[2] || 'HEAD';
|
|
646
|
+
logHeader("voria Security Diff");
|
|
647
|
+
log(`${colors.bold("Comparing:")} ${colors.cyan(refA)} → ${colors.cyan(refB)}`);
|
|
648
|
+
logDivider();
|
|
601
649
|
|
|
602
|
-
|
|
603
|
-
|
|
650
|
+
try {
|
|
651
|
+
const response = await callPythonEngine({
|
|
652
|
+
command: 'diff', ref_a: refA, ref_b: refB, repo_path: process.cwd(),
|
|
604
653
|
});
|
|
654
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
655
|
+
const diff = response.data?.diff || {};
|
|
656
|
+
log(`${colors.bold("Files Changed:")} ${colors.blue(diff.total_changed)}`);
|
|
657
|
+
log(`${colors.bold("Risk Level:")} ${diff.risk_level === 'critical' ? colors.red(diff.risk_level) : diff.risk_level === 'elevated' ? colors.yellow(diff.risk_level) : colors.green(diff.risk_level)}`);
|
|
658
|
+
if (diff.high_risk_files?.length > 0) {
|
|
659
|
+
log(`${colors.bold(colors.red("High Risk:"))}`);
|
|
660
|
+
diff.high_risk_files.forEach(f => log(` ${colors.red("●")} ${f}`));
|
|
661
|
+
}
|
|
662
|
+
if (diff.medium_risk_files?.length > 0) {
|
|
663
|
+
log(`${colors.bold(colors.yellow("Medium Risk:"))}`);
|
|
664
|
+
diff.medium_risk_files.forEach(f => log(` ${colors.yellow("●")} ${f}`));
|
|
665
|
+
}
|
|
666
|
+
if (diff.recommendation) log(`\n${colors.blue("💡")} ${diff.recommendation}`);
|
|
667
|
+
logDivider();
|
|
668
|
+
} catch (error) { logError(error.message); }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ─── Tier 1: benchmark ──────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
async function runBenchmarkCommand(args) {
|
|
674
|
+
const url = args[1];
|
|
675
|
+
if (!url) { logError("URL required. Usage: voria benchmark <URL>"); return; }
|
|
676
|
+
|
|
677
|
+
const reqIdx = args.indexOf('--requests');
|
|
678
|
+
const conIdx = args.indexOf('--concurrency');
|
|
679
|
+
const requests = reqIdx !== -1 ? parseInt(args[reqIdx + 1]) : 100;
|
|
680
|
+
const concurrency = conIdx !== -1 ? parseInt(args[conIdx + 1]) : 10;
|
|
605
681
|
|
|
606
|
-
|
|
607
|
-
|
|
682
|
+
logHeader("voria HTTP Benchmark");
|
|
683
|
+
log(`${colors.bold("Target:")} ${colors.cyan(url)}`);
|
|
684
|
+
log(`${colors.bold("Requests:")} ${colors.blue(requests)}`);
|
|
685
|
+
log(`${colors.bold("Concurrency:")} ${colors.blue(concurrency)}`);
|
|
686
|
+
logDivider();
|
|
687
|
+
logInfo("Sending requests...");
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const response = await callPythonEngine({
|
|
691
|
+
command: 'benchmark', url, requests, concurrency,
|
|
608
692
|
});
|
|
693
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
694
|
+
const bm = response.data?.benchmark || {};
|
|
695
|
+
logHeader("Benchmark Results");
|
|
696
|
+
log(`${colors.bold("Requests/sec:")} ${colors.green(bm.rps)}`);
|
|
697
|
+
log(`${colors.bold("Total Time:")} ${colors.blue(bm.total_time_sec + "s")}`);
|
|
698
|
+
log(`${colors.bold("Success:")} ${colors.green(bm.successful)} | ${colors.bold("Failed:")} ${colors.red(bm.failed)}`);
|
|
699
|
+
logDivider();
|
|
700
|
+
log(`${colors.bold("Latency (ms):")}`);
|
|
701
|
+
log(` ${colors.dim("avg:")} ${colors.blue(bm.latency_avg_ms)}`);
|
|
702
|
+
log(` ${colors.dim("p50:")} ${colors.green(bm.latency_p50_ms)}`);
|
|
703
|
+
log(` ${colors.dim("p95:")} ${colors.yellow(bm.latency_p95_ms)}`);
|
|
704
|
+
log(` ${colors.dim("p99:")} ${colors.red(bm.latency_p99_ms)}`);
|
|
705
|
+
log(` ${colors.dim("min:")} ${colors.dim(bm.latency_min_ms)} | ${colors.dim("max:")} ${colors.dim(bm.latency_max_ms)}`);
|
|
706
|
+
logDivider();
|
|
707
|
+
} catch (error) { logError(error.message); }
|
|
708
|
+
}
|
|
609
709
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
resolve(response);
|
|
622
|
-
} catch (e) {
|
|
623
|
-
reject(new Error(`Failed to parse Python response: ${e.message}`));
|
|
624
|
-
}
|
|
710
|
+
// ─── Tier 1: ci ──────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
async function runCiCommand(config) {
|
|
713
|
+
logHeader("voria CI/CD Scan");
|
|
714
|
+
logInfo("Running security tests and generating SARIF...");
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
const apiKey = getApiKey(config);
|
|
718
|
+
const response = await callPythonEngine({
|
|
719
|
+
command: 'ci', provider: config.llm_provider, api_key: apiKey,
|
|
720
|
+
model: config.llm_model, repo_path: process.cwd(),
|
|
625
721
|
});
|
|
722
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
723
|
+
|
|
724
|
+
const sarif = response.data?.sarif;
|
|
725
|
+
if (sarif) {
|
|
726
|
+
const outPath = path.join(process.cwd(), 'voria-results.sarif');
|
|
727
|
+
fs.writeFileSync(outPath, JSON.stringify(sarif, null, 2));
|
|
728
|
+
logSuccess(`SARIF report written to: ${colors.cyan(outPath)}`);
|
|
729
|
+
log(`${colors.bold("Findings:")} ${colors.blue(response.data.findings_count)}`);
|
|
730
|
+
logInfo(`Upload to GitHub: ${colors.cyan("gh api repos/{owner}/{repo}/code-scanning/sarifs -f sarif=@voria-results.sarif")}`);
|
|
731
|
+
}
|
|
732
|
+
logDivider();
|
|
733
|
+
} catch (error) { logError(error.message); }
|
|
734
|
+
}
|
|
626
735
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
736
|
+
// ─── Tier 1: fix --auto ──────────────────────────────────────────
|
|
737
|
+
|
|
738
|
+
async function runFixAutoCommand(args, config) {
|
|
739
|
+
const repo = args[1];
|
|
740
|
+
const issueNumber = args[2];
|
|
741
|
+
if (!repo || !issueNumber) {
|
|
742
|
+
logError("Usage: voria fix --auto <owner/repo> <issue_number>");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
logHeader("voria Auto-Fix");
|
|
746
|
+
log(`${colors.bold("Issue:")} ${colors.brightBlue("#" + issueNumber)} in ${colors.cyan(repo)}`);
|
|
747
|
+
logDivider();
|
|
748
|
+
logInfo("Generating fix, auto-applying, and running tests...");
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
const apiKey = getApiKey(config);
|
|
752
|
+
const githubToken = config.github_token || process.env.GITHUB_TOKEN;
|
|
753
|
+
if (!githubToken) { logError("GitHub token required."); return; }
|
|
754
|
+
|
|
755
|
+
const response = await callPythonEngine({
|
|
756
|
+
command: 'issue', issue_number: parseInt(issueNumber), repo, repo_path: process.cwd(),
|
|
757
|
+
provider: config.llm_provider, api_key: apiKey, github_token: githubToken, model: config.llm_model, auto: true,
|
|
758
|
+
});
|
|
759
|
+
if (response.status === 'error') { logError(response.message); return; }
|
|
760
|
+
logSuccess("Patch generated and auto-applied!");
|
|
761
|
+
if (response.data?.files_modified) {
|
|
762
|
+
log(`${colors.bold("Files modified:")} ${response.data.files_modified.length}`);
|
|
763
|
+
}
|
|
764
|
+
logInfo("Running tests to verify fix...");
|
|
765
|
+
try {
|
|
766
|
+
const testResult = await callPythonEngine({
|
|
767
|
+
command: 'test', test_id: 'hardcoded_secrets', provider: config.llm_provider,
|
|
768
|
+
api_key: apiKey, model: config.llm_model, repo_path: process.cwd(),
|
|
769
|
+
});
|
|
770
|
+
logSuccess(`Post-fix test: ${testResult.data?.result?.result?.status || 'completed'}`);
|
|
771
|
+
} catch (e) { logWarn(`Post-fix test skipped: ${e.message}`); }
|
|
772
|
+
logDivider();
|
|
773
|
+
} catch (error) { logError(error.message); }
|
|
631
774
|
}
|
|
632
775
|
|
|
633
|
-
//
|
|
776
|
+
// ─── Rust Core ───────────────────────────────────────────────────
|
|
777
|
+
|
|
634
778
|
async function runRustCore(args) {
|
|
635
|
-
const { spawn } = await import('child_process');
|
|
636
|
-
const pathMod = await import('path');
|
|
637
|
-
const fsMod = await import('fs');
|
|
638
|
-
|
|
639
779
|
return new Promise((resolve, reject) => {
|
|
640
|
-
// Try to find the rust binary
|
|
641
780
|
const possiblePaths = [
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
781
|
+
path.join(__dirname, '..', 'target', 'release', 'voria'),
|
|
782
|
+
path.join(__dirname, '..', 'target', 'debug', 'voria'),
|
|
783
|
+
path.join(__dirname, '..', 'rust', 'target', 'release', 'voria'),
|
|
784
|
+
path.join(__dirname, '..', 'rust', 'target', 'debug', 'voria'),
|
|
646
785
|
];
|
|
647
|
-
|
|
648
|
-
let rustBin = possiblePaths.find(p => fsMod.default.existsSync(p));
|
|
649
|
-
|
|
786
|
+
let rustBin = possiblePaths.find(p => fs.existsSync(p));
|
|
650
787
|
if (!rustBin) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
rustBin = 'voria';
|
|
655
|
-
} catch (e) {
|
|
656
|
-
return reject(new Error('Rust core binary (voria) not found. Please run "cargo build --release" in the rust directory.'));
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
788
|
+
try { execSync('voria --version', { stdio: 'ignore' }); rustBin = 'voria'; }
|
|
789
|
+
catch (e) { return reject(new Error('Rust core binary not found.')); }
|
|
790
|
+
}
|
|
660
791
|
const child = spawn(rustBin, args, { stdio: 'inherit' });
|
|
661
|
-
child.on('exit', (code) => {
|
|
662
|
-
if (code === 0) resolve();
|
|
663
|
-
else reject(new Error(`Rust core exited with code ${code}`));
|
|
664
|
-
});
|
|
792
|
+
child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)));
|
|
665
793
|
});
|
|
666
794
|
}
|
|
667
795
|
|
|
668
|
-
// Main CLI
|
|
796
|
+
// ─── Main CLI Router ─────────────────────────────────────────────
|
|
797
|
+
|
|
669
798
|
async function main() {
|
|
670
799
|
const args = process.argv.slice(2);
|
|
671
800
|
|
|
@@ -678,8 +807,7 @@ async function main() {
|
|
|
678
807
|
|
|
679
808
|
switch (command) {
|
|
680
809
|
case '--version':
|
|
681
|
-
|
|
682
|
-
console.log(`voria v${VERSION}`);
|
|
810
|
+
console.log(`${colors.blue("voria")} v${VERSION}`);
|
|
683
811
|
break;
|
|
684
812
|
|
|
685
813
|
case '--init':
|
|
@@ -696,14 +824,8 @@ async function main() {
|
|
|
696
824
|
|
|
697
825
|
case '--list-issues': {
|
|
698
826
|
const config = loadConfig();
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
console.log('\n❌ Please provide a repository');
|
|
702
|
-
console.log(' Example: voria --list-issues owner/repo');
|
|
703
|
-
console.log(' Or: voria --list-issues https://github.com/owner/repo\n');
|
|
704
|
-
process.exit(1);
|
|
705
|
-
}
|
|
706
|
-
await listIssues(repoArg, config);
|
|
827
|
+
if (!args[1]) { logError("Provide a repo. Example: voria --list-issues owner/repo"); process.exit(1); }
|
|
828
|
+
await listIssues(args[1], config);
|
|
707
829
|
break;
|
|
708
830
|
}
|
|
709
831
|
|
|
@@ -720,102 +842,119 @@ async function main() {
|
|
|
720
842
|
break;
|
|
721
843
|
|
|
722
844
|
case 'plan': {
|
|
723
|
-
const config =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
process.exit(1);
|
|
728
|
-
}
|
|
729
|
-
const description = args.slice(1).join(' ');
|
|
730
|
-
if (!description) {
|
|
731
|
-
console.log('\n❌ Please provide a description');
|
|
732
|
-
console.log(' Example: voria plan "Remove deprecated login() method"\n');
|
|
733
|
-
process.exit(1);
|
|
734
|
-
}
|
|
735
|
-
await runPlanCommand(description, config, args);
|
|
845
|
+
const config = requireInit();
|
|
846
|
+
const desc = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
|
|
847
|
+
if (!desc) { logError('Provide a description. Example: voria plan "Remove deprecated method"'); process.exit(1); }
|
|
848
|
+
await runPlanCommand(desc, config, args);
|
|
736
849
|
break;
|
|
737
850
|
}
|
|
738
851
|
|
|
739
852
|
case 'issue': {
|
|
740
|
-
const config =
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
console.log(' Run: voria --init\n');
|
|
744
|
-
process.exit(1);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Parse: voria issue owner/repo issue_number
|
|
748
|
-
const issueArgs = args.slice(1);
|
|
749
|
-
if (issueArgs.length < 2) {
|
|
750
|
-
console.log('\n❌ Please provide repository and issue number');
|
|
751
|
-
console.log(' Example: voria issue owner/repo 42\n');
|
|
752
|
-
process.exit(1);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const repo = issueArgs[0];
|
|
756
|
-
const issueNumber = issueArgs[1];
|
|
757
|
-
|
|
758
|
-
await runIssueCommand(repo, issueNumber, config, args);
|
|
853
|
+
const config = requireInit();
|
|
854
|
+
if (args.length < 3) { logError("Usage: voria issue owner/repo 42"); process.exit(1); }
|
|
855
|
+
await runIssueCommand(args[1], args[2], config, args);
|
|
759
856
|
break;
|
|
760
857
|
}
|
|
761
858
|
|
|
762
859
|
case 'apply': {
|
|
763
|
-
|
|
764
|
-
if (Object.keys(config).length === 0) {
|
|
765
|
-
console.log('\n❌ voria not initialized');
|
|
766
|
-
console.log(' Run: voria --init\n');
|
|
767
|
-
process.exit(1);
|
|
768
|
-
}
|
|
860
|
+
requireInit();
|
|
769
861
|
const patchFile = args[1];
|
|
770
|
-
if (!patchFile) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
862
|
+
if (!patchFile) { logError("Provide a patch file. Example: voria apply fix.patch"); process.exit(1); }
|
|
863
|
+
if (!fs.existsSync(patchFile)) { logError(`Patch file not found: ${patchFile}`); process.exit(1); }
|
|
864
|
+
logInfo(`Applying patch: ${patchFile}`);
|
|
865
|
+
try {
|
|
866
|
+
const response = await callPythonEngine({ command: 'apply', patch: fs.readFileSync(patchFile, 'utf-8'), repo_path: process.cwd() });
|
|
867
|
+
response.status === 'error' ? logError(response.message) : logSuccess(response.message);
|
|
868
|
+
} catch (error) { logError(error.message); }
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ─── Tier 1 Commands ─────────────────────────────
|
|
873
|
+
|
|
874
|
+
case 'scan': {
|
|
875
|
+
const config = requireInit();
|
|
876
|
+
await runScanCommand(config, args);
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
case 'test': {
|
|
881
|
+
const config = requireInit();
|
|
882
|
+
const testId = args[1];
|
|
883
|
+
if (!testId) { logError("Provide a test ID. Run 'voria list-tests' to see available tests."); process.exit(1); }
|
|
884
|
+
await runTestCommand(testId, config);
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
case 'list-tests': {
|
|
889
|
+
const config = loadConfig();
|
|
890
|
+
await runListTests(config);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
case 'watch': {
|
|
895
|
+
const config = requireInit();
|
|
896
|
+
await runWatchCommand(config);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
case 'diff': {
|
|
901
|
+
const config = loadConfig();
|
|
902
|
+
await runDiffCommand(args, config);
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
case 'benchmark': {
|
|
907
|
+
await runBenchmarkCommand(args);
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
case 'ci': {
|
|
912
|
+
const config = requireInit();
|
|
913
|
+
await runCiCommand(config);
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
case 'fix': {
|
|
918
|
+
const config = requireInit();
|
|
919
|
+
if (args.includes('--auto')) {
|
|
920
|
+
const fixArgs = args.filter(a => a !== 'fix' && a !== '--auto');
|
|
921
|
+
await runFixAutoCommand(fixArgs.length >= 2 ? fixArgs : args.slice(1), config);
|
|
922
|
+
} else {
|
|
923
|
+
logError("Usage: voria fix --auto <owner/repo> <issue_number>");
|
|
779
924
|
}
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
780
927
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
928
|
+
case 'token': {
|
|
929
|
+
requireInit();
|
|
930
|
+
logHeader("Token Usage");
|
|
784
931
|
try {
|
|
785
|
-
const response = await callPythonEngine({
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
console.error(`❌ Error: ${response.message}\n`);
|
|
793
|
-
} else {
|
|
794
|
-
console.log(`✅ ${response.message}\n`);
|
|
795
|
-
}
|
|
796
|
-
} catch (error) {
|
|
797
|
-
console.error(`❌ Error: ${error.message}\n`);
|
|
798
|
-
}
|
|
932
|
+
const response = await callPythonEngine({ command: 'token', subcommand: args[1] || 'info' });
|
|
933
|
+
if (response.data?.tokens) {
|
|
934
|
+
const t = response.data.tokens;
|
|
935
|
+
log(`${colors.bold("Used:")} ${colors.blue(t.used || 0)} tokens`);
|
|
936
|
+
log(`${colors.bold("Cost:")} ${colors.green("$" + (t.cost || 0))}`);
|
|
937
|
+
} else { logInfo(response.message); }
|
|
938
|
+
} catch (error) { logError(error.message); }
|
|
799
939
|
break;
|
|
800
940
|
}
|
|
801
941
|
|
|
802
942
|
case '--graph':
|
|
803
|
-
try {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
console.log(`${colors.yellow("💡")} Make sure to build the rust core: ${colors.bold("cargo build --release")}`);
|
|
943
|
+
try { await runRustCore(['--graph']); }
|
|
944
|
+
catch (error) {
|
|
945
|
+
logError(`Error running analysis: ${error.message}`);
|
|
946
|
+
logInfo(`Build rust core: ${colors.cyan("cargo build --release")}`);
|
|
808
947
|
}
|
|
809
948
|
break;
|
|
810
949
|
|
|
811
950
|
default:
|
|
812
|
-
|
|
813
|
-
|
|
951
|
+
logError(`Unknown command: ${colors.bold(command)}`);
|
|
952
|
+
logInfo(`Run ${colors.cyan("voria --help")} for usage.`);
|
|
814
953
|
process.exit(1);
|
|
815
954
|
}
|
|
816
955
|
}
|
|
817
956
|
|
|
818
957
|
main().catch(error => {
|
|
819
|
-
|
|
958
|
+
logError(error.message);
|
|
820
959
|
process.exit(1);
|
|
821
960
|
});
|