scai
Version:
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.
255 lines (254 loc) • 11 kB
JavaScript
// commands/factory.ts
import { Command } from 'commander';
import { bootstrap } from '../modelSetup.js';
import { runAskCommand } from './AskCmd.js';
import { runIndexCommand } from './IndexCmd.js';
import { resetDatabase } from './ResetDbCmd.js';
import { suggestCommitMessage } from './CommitSuggesterCmd.js';
import { reviewPullRequestCmd } from './ReviewCmd.js';
import { checkGit } from './GitCmd.js';
import { promptForToken } from '../github/token.js';
import { validateGitHubTokenAgainstRepo } from '../github/githubAuthCheck.js';
import { runWorkflowCommand } from './WorkflowCmd.js';
import { handleStandaloneChangelogUpdate } from './ChangeLogUpdateCmd.js';
import { runInteractiveSwitch } from './SwitchCmd.js';
import { runInteractiveDelete } from './DeleteIndex.js';
import { startDaemon, stopDaemon, restartDaemon, statusDaemon, unlockConfig, showLogs } from './DaemonCmd.js';
import { Config } from '../config.js';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { execSync } from 'child_process';
import { runInspectCommand } from './InspectCmd.js';
import { runFindCommand } from './FindCmd.js';
import { runBackupCommand } from './BackupCmd.js';
import { updateContext } from '../context.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { version } = require('../../package.json');
export async function withContext(action) {
const ok = await updateContext();
if (!ok)
return; // or process.exit(1)
await action();
}
export function createProgram() {
const cmd = new Command('scai');
cmd.version(version, '-v, --version', 'output the current version');
// ---------------- Init commands ----------------
cmd
.command('init')
.description('Initialize the model and download required models')
.action(async () => {
await bootstrap();
console.log('✅ Model initialization completed!');
});
// ---------------- Git commands ----------------
const git = cmd.command('git').description('Git utilities');
git
.command('review')
.description('Review an open pull request using AI')
.option('-a, --all', 'Show all PRs requiring a review', false)
.action(async (opts) => {
await withContext(async () => {
await reviewPullRequestCmd('main', opts.all);
});
});
git
.command('commit')
.description('Suggest a commit message from staged changes')
.option('-l, --changelog', 'Generate and optionally stage a changelog entry')
.action(async (opts) => {
await withContext(async () => {
await suggestCommitMessage(opts);
});
});
git
.command('check')
.description('Check Git working directory and branch status')
.action(async () => {
await withContext(async () => { checkGit(); });
});
// ---------------- Auth commands ----------------
const auth = cmd.command('auth').description('GitHub authentication commands');
auth
.command('check')
.description('Check if GitHub authentication is valid')
.action(async () => {
await withContext(async () => {
try {
const token = Config.getGitHubToken();
if (!token)
return console.log('❌ GitHub token not found.');
const result = await validateGitHubTokenAgainstRepo();
console.log(result);
}
catch (err) {
console.error(err instanceof Error ? err.message : err);
}
});
});
auth
.command('reset')
.description('Reset GitHub authentication credentials')
.action(async () => {
await withContext(async () => {
Config.setGitHubToken('');
console.log('✅ GitHub token reset.');
});
});
auth
.command('set')
.description('Set GitHub Personal Access Token')
.action(async () => {
await withContext(async () => {
const token = (await promptForToken()).trim();
Config.setGitHubToken(token);
console.log('🔑 GitHub token set.');
});
});
// ---------------- Workflow ----------------
const workflow = cmd.command('workflow').description('Run or manage module pipelines');
workflow
.command('run <goals...>')
.description('Run one or more modules as a pipeline')
.option('-f, --file <filepath>', 'File to process (omit to read from stdin)')
.action(async (goals, options) => {
await withContext(async () => {
await runWorkflowCommand(goals, options);
});
})
.on('--help', () => {
console.log('\nExamples:');
console.log(' $ scai workflow run comments tests -f path/to/myfile.ts');
console.log(' $ cat file.ts | scai workflow run comments tests > file.test.ts\n');
console.log('Available modules: summary, comments, tests');
});
// ---------------- Gen / Changelog ----------------
const gen = cmd.command('gen').description('Generate code-related output');
gen
.command('changelog')
.description('Update or create CHANGELOG.md')
.action(async () => {
await withContext(async () => { await handleStandaloneChangelogUpdate(); });
});
// ---------------- Config ----------------
const config = cmd.command('config').description('Manage SCAI configuration');
config
.command('set-model <model>')
.description('Set the model to use')
.option('-g, --global', 'Set globally instead of repo')
.action(async (model, opts) => {
await withContext(async () => {
Config.setModel(model, opts.global ? 'global' : 'repo');
Config.show();
});
});
config
.command('set-lang <lang>')
.description('Set programming language')
.action(async (lang) => {
await withContext(async () => { Config.setLanguage(lang); Config.show(); });
});
config
.command('show')
.option('--raw', 'Show raw config')
.description('Display current configuration')
.action(async (opts) => {
await withContext(async () => {
if (opts.raw)
console.log(JSON.stringify(Config.getRaw(), null, 2));
else
Config.show();
});
});
// 💤 Set daemon sleep intervals
config
.command('set-daemon')
.description('Set daemon sleep intervals (ms between runs)')
.option('--sleep <ms>', 'Sleep time in milliseconds between batches')
.option('--idle <ms>', 'Sleep time in milliseconds when idle')
.action(async (options) => {
await withContext(async () => {
const updates = {};
if (options.sleep)
updates.sleepMs = parseInt(options.sleep, 10);
if (options.idle)
updates.idleSleepMs = parseInt(options.idle, 10);
if (!Object.keys(updates).length) {
console.log('⚠️ No options provided. Use --sleep or --idle.');
return;
}
Config.setDaemonConfig(updates);
const current = Config.getDaemonConfig();
console.log(`🕒 Updated daemon settings:\n sleepMs=${current.sleepMs}ms\n idleSleepMs=${current.idleSleepMs}ms`);
});
});
// ---------------- Index ----------------
const index = cmd.command('index').description('Index operations');
index
.command('start')
.description('Index supported files')
.action(async () => await withContext(runIndexCommand));
index
.command('set [dir]')
.description('Set index directory')
.action(async (dir = process.cwd()) => {
await Config.setIndexDir(dir);
Config.show();
});
index
.command('list')
.description('List all indexed repositories')
.action(async () => await withContext(async () => { await Config.printAllRepos(); }));
index
.command('switch')
.description('Switch active repository interactively')
.action(async () => await withContext(runInteractiveSwitch));
index
.command('delete')
.description('Delete repository interactively')
.action(async () => await withContext(runInteractiveDelete));
// ---------------- DB ----------------
const db = cmd.command('db').description('Database operations');
db
.command('check')
.description('Run dbcheck script')
.action(async () => {
await withContext(async () => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const scriptPath = resolve(__dirname, '../..', 'dist/scripts', 'dbcheck.js');
try {
execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
}
catch (err) {
console.error('❌ DB check failed:', err instanceof Error ? err.message : err);
}
});
});
db
.command('reset')
.description('Reset SQLite database')
.action(async () => await withContext(resetDatabase));
db
.command('inspect <filepath>')
.description('Inspect a file in the index')
.action(async (filepath) => await withContext(() => runInspectCommand(filepath)));
// ---------------- Daemon ----------------
const daemon = cmd.command('daemon').description('Daemon management');
daemon.command('start').description('Start daemon').action(async () => withContext(startDaemon));
daemon.command('stop').description('Stop daemon').action(async () => withContext(stopDaemon));
daemon.command('restart').description('Restart daemon').action(async () => withContext(restartDaemon));
daemon.command('status').description('Daemon status').action(async () => withContext(statusDaemon));
daemon.command('unlock').description('Force unlock').action(async () => withContext(unlockConfig));
daemon.command('logs').option('-n, --lines <n>', 'Last N log lines', '20')
.description('Show daemon logs')
.action(async (opts) => withContext(() => showLogs(parseInt(opts.lines, 10))));
// ---------------- Backup / Find / Ask ----------------
cmd.command('backup').description('Backup .scai folder').action(async () => await withContext(runBackupCommand));
cmd.command('find <query>').description('Search indexed files').action(async (q) => await withContext(() => runFindCommand(q)));
cmd.command('ask [question...]').description('Ask a question').action(async (parts) => {
await withContext(async () => { await runAskCommand(parts?.join(' ')); });
});
return cmd;
}