scai
Version:
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** > **100% local • No token cost • Private by design • GDPR-friendly** — made in Denmark/EU with ❤️.
199 lines (198 loc) • 8.11 kB
JavaScript
import { spawn } from 'child_process';
import { createRequire } from 'module';
import fs from 'fs';
import os from 'os';
import path from 'path';
import chalk from 'chalk';
const require = createRequire(import.meta.url);
const shellQuote = require('shell-quote');
import { createProgram as cmdFactory, withContext } from './commands/factory.js';
import { runAskCommand } from './commands/AskCmd.js';
import enquirer from 'enquirer';
import { getLockedRepo, isDaemonRunning, startDaemon, stopDaemon } from './commands/DaemonCmd.js';
import { Config } from './config.js';
const { prompt } = enquirer;
const program = cmdFactory();
const customCommands = {};
// ---------------- Test Queries ----------------
const testQueries = [
'please write me comprehensive comments for spatialmap.js and typescript.ts files',
'refactor spatialmap.js to improve readability and reduce nesting',
'explain the intent and architecture of the semantic analysis module',
'add validation and error handling to the context review step',
'summarize this repo architecture and identify weak coupling points',
'Where are all the database queries defined?',
'Which functions have no tests written for them yet?',
'What could cause memory leaks in this codebase?',
'How does authentication work here?',
'Is there a clear separation between frontend and backend code?',
'please refactor the buildContextualPrompt into smaller functions',
'Is error handling consistent across the codebase?',
'What’s missing from the README?',
'How do I run the test suite?',
'Are there any flaky tests in this repo?',
'Are there any security vulnerabilities in our dependencies?',
'Is there any dead code we can safely remove?'
];
// ---------------- Helpers ----------------
function pickRandom(items) {
return items[Math.floor(Math.random() * items.length)];
}
// ---------------- Run Query with Context ----------------
// The withContext function is a utility that wraps async operations with context handling.
// It ensures that asynchronous operations are executed within a specific execution context,
// which is crucial for managing state, configuration, or environment settings during CLI operations.
// This function is used to properly manage the execution context when running queries or commands.
async function runQuery(query) {
const cfg = Config.getRaw();
const activeRepo = cfg.activeRepo;
const lockedRepo = getLockedRepo();
const shouldPauseDaemon = isDaemonRunning() &&
activeRepo &&
lockedRepo === activeRepo;
if (shouldPauseDaemon) {
await stopDaemon();
}
try {
await withContext(() => runAskCommand(query));
}
finally {
if (shouldPauseDaemon) {
await startDaemon();
}
}
}
// ---------------- Built-in Commands ----------------
customCommands.clear = async () => {
// Clear screen and move cursor to top-left
process.stdout.write('\x1Bc');
};
customCommands.help = async () => {
console.log(chalk.green('\n✔ SCAI Help:\n'));
console.log(program.helpInformation());
};
customCommands.test = async () => {
await runQuery(testQueries[0]);
};
customCommands['test-random'] = async () => {
const query = pickRandom(testQueries);
console.log(`\n🎲 [test-random] Selected query:\n→ ${query}\n`);
await runQuery(query);
};
customCommands.exit = async () => {
console.log('Exiting...');
process.exit(0);
};
customCommands.q = customCommands.exit;
customCommands.quit = customCommands.exit;
// ---------------- Extension API ----------------
export function registerCommand(name, fn) {
customCommands[name] = fn;
}
// ---------------- Editor Helper ----------------
function editInEditorAsync(initialContent = '') {
return new Promise((resolve, reject) => {
const tmpFile = path.join(os.tmpdir(), `scai_input_${Date.now()}.txt`);
fs.writeFileSync(tmpFile, initialContent);
const editor = process.env.EDITOR || 'vi';
const child = spawn(editor, [tmpFile], { stdio: 'inherit' });
child.on('exit', (code) => {
try {
const content = fs.readFileSync(tmpFile, 'utf-8');
fs.unlinkSync(tmpFile);
resolve(content);
}
catch (err) {
reject(err);
}
});
child.on('error', (err) => reject(err));
});
}
// ---------------- REPL / Shell ----------------
// The startShell function initializes an interactive shell for the CLI.
// It allows users to input queries directly, use built-in commands with '/',
// run terminal commands with '!', or edit input in an external editor with '/edit'.
// It uses withContext to ensure proper execution context for each command or query.
async function startShell() {
console.log(chalk.yellow("Welcome to SCAI shell!") + "\n" +
chalk.blueBright(`- Type your query directly for short commands or questions.
- Use !command to run terminal commands.
- Use /command for built-in or custom commands.
- /help
- /edit for pasting long queries & multi-line input
- /git commit\n`));
while (true) {
try {
const { line } = (await prompt({
type: 'input',
name: 'line',
message: 'Write your query:',
validate: (input) => !!input.trim() || 'Please enter something',
}));
const trimmed = line.trim();
// --- Exit immediately
if (['exit', 'quit', 'q'].includes(trimmed)) {
await customCommands.exit();
}
// --- Shell commands (!)
if (trimmed.startsWith('!')) {
const child = spawn(trimmed.slice(1), {
shell: true,
stdio: 'inherit',
});
await new Promise((resolve) => child.on('exit', resolve));
continue;
}
// --- Slash commands (/)
if (trimmed.startsWith('/')) {
const argvParts = shellQuote
.parse(trimmed.slice(1))
.map((tok) => typeof tok === 'object' ? tok.op ?? tok.pattern ?? '' : String(tok))
.filter(Boolean);
const cmdName = argvParts[0];
// Special case: open editor
if (cmdName === 'edit') {
const content = await editInEditorAsync();
const trimmedContent = content.trim();
if (trimmedContent) {
// Print the editor content in orange
console.log(chalk.hex('#FFA500')('\n[Editor input]:\n' + trimmedContent + '\n'));
// Execute the query
await withContext(() => runAskCommand(trimmedContent));
}
continue;
}
if (customCommands[cmdName]) {
await customCommands[cmdName]();
}
else {
await program.parseAsync(argvParts, { from: 'user' });
}
continue;
}
// --- Otherwise → treat as normal query
await runQuery(trimmed);
}
catch (err) {
console.error('REPL error:', err instanceof Error ? err.stack : err);
}
}
}
// ---------------- Main ----------------
async function main() {
process.on('unhandledRejection', (reason) => console.error('Unhandled Rejection:', reason));
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err.stack ?? err);
process.exit(1);
});
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
const args = process.argv.slice(2);
if (isInteractive && (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) {
await startShell();
return;
}
await program.parseAsync(process.argv);
}
main();