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 â¤ī¸.
244 lines (243 loc) âĸ 8.69 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { CONFIG_LOCK_PATH, CONFIG_PATH, DEFAULT_DAEMON_IDLE_SLEEP_MS, DEFAULT_DAEMON_SLEEP_MS, PID_PATH, SCAI_HOME, SCAI_REPOS } from './constants.js';
import { getDbForRepo } from './db/client.js';
import { normalizePath } from './utils/contentUtils.js';
import chalk from 'chalk';
import { getHashedRepoKey } from './utils/repoKey.js';
const defaultConfig = {
model: 'qwen3-coder:30b',
contextLength: 32768,
language: 'ts',
indexDir: '',
githubToken: '',
repos: {},
activeRepo: null,
daemon: {
sleepMs: DEFAULT_DAEMON_SLEEP_MS,
idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS,
},
};
function ensureConfigDir() {
if (!fs.existsSync(SCAI_HOME)) {
fs.mkdirSync(SCAI_HOME, { recursive: true });
}
}
export function readConfig() {
try {
const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
return { ...defaultConfig, ...JSON.parse(content) };
}
catch {
return defaultConfig;
}
}
export function writeConfig(newCfg) {
ensureConfigDir();
const current = readConfig();
const merged = {
...current,
...newCfg,
repos: {
...current.repos,
...(newCfg.repos || {}),
},
};
// Remove repos explicitly set to null
if (newCfg.repos) {
for (const [key, value] of Object.entries(newCfg.repos)) {
if (value === null) {
delete merged.repos[key];
}
}
}
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
}
export function daemonIsRunning() {
if (!fs.existsSync(PID_PATH))
return false;
const pid = parseInt(fs.readFileSync(PID_PATH, 'utf8'), 10);
try {
process.kill(pid, 0);
return true;
}
catch {
return false;
}
}
export function getDaemonLockedRepo() {
if (!fs.existsSync(CONFIG_LOCK_PATH))
return null;
return fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
}
export const Config = {
getModel() {
const cfg = readConfig();
const repoCfg = cfg.repos?.[cfg.activeRepo ?? ''];
return repoCfg?.model || cfg.model;
},
setModel(model, scope = 'repo') {
const cfg = readConfig();
if (scope === 'repo') {
const active = cfg.activeRepo;
if (!active) {
console.error("â No active repo to set model for.");
return;
}
cfg.repos[active] = { ...cfg.repos[active], model };
console.log(`đĻ Model set for repo '${active}': ${model}`);
}
else {
cfg.model = model;
console.log(`đĻ Global default model set to: ${model}`);
}
writeConfig(cfg);
},
getLanguage() {
const cfg = readConfig();
const repoCfg = cfg.repos?.[cfg.activeRepo ?? ''];
return repoCfg?.language || cfg.language;
},
setLanguage(language) {
const cfg = readConfig();
const active = cfg.activeRepo;
if (active) {
cfg.repos[active] = { ...cfg.repos[active], language };
writeConfig(cfg);
console.log(`đŖī¸ Language set to: ${language}`);
}
else {
writeConfig({ language });
console.log(`đŖī¸ Default language set to: ${language}`);
}
},
getIndexDir() {
const cfg = readConfig();
const activeRepo = cfg.activeRepo;
if (!activeRepo)
return '';
return cfg.repos[activeRepo]?.indexDir ?? '';
},
async setIndexDir(indexDir) {
const normalizedIndexDir = normalizePath(indexDir);
const repoKey = getHashedRepoKey(normalizedIndexDir);
const scaiRepoRoot = path.join(SCAI_REPOS, repoKey);
fs.mkdirSync(scaiRepoRoot, { recursive: true });
this.setActiveRepo(repoKey);
await this.setRepoIndexDir(repoKey, normalizedIndexDir);
const dbPath = path.join(scaiRepoRoot, 'db.sqlite');
if (!fs.existsSync(dbPath)) {
console.log(`đĻ Database not found. ${chalk.green('Initializing DB')} at ${normalizePath(dbPath)}`);
getDbForRepo();
}
},
async setRepoIndexDir(repoKey, indexDir) {
const cfg = readConfig();
if (!cfg.repos[repoKey])
cfg.repos[repoKey] = {};
cfg.repos[repoKey] = { ...cfg.repos[repoKey], indexDir };
writeConfig(cfg);
console.log(`â
Repo index directory set for ${repoKey} : ${indexDir}`);
},
setActiveRepo(repoKey) {
const cfg = readConfig();
// đ Check for running daemon lock
const lockedRepo = getDaemonLockedRepo();
if (daemonIsRunning() && lockedRepo && lockedRepo !== repoKey) {
const sps = lockedRepo.split('-')[0];
console.log(`â Cannot switch active repo while daemon is running.\n` +
` Daemon is currently locked to repo: ${sps}\n` +
` Go to repository: ${sps}, and stop the daemon first: scai daemon stop`);
return false;
}
// â
If allowed, switch
cfg.activeRepo = repoKey;
if (!cfg.repos[repoKey])
cfg.repos[repoKey] = {};
writeConfig(cfg);
console.log(`â
Active repo switched to: ${repoKey}`);
return true;
},
printAllRepos() {
const cfg = readConfig();
const keys = Object.keys(cfg.repos || {});
if (!keys.length) {
console.log('âšī¸ No repositories configured yet.');
return;
}
console.log('đ Configured repositories:\n');
for (const key of keys) {
const r = cfg.repos[key];
const isActive = cfg.activeRepo === key;
const label = isActive
? chalk.green(`â
${key} (active)`)
: chalk.white(` ${key}`);
console.log(`- ${label}`);
console.log(` âŗ indexDir: ${isActive ? chalk.yellow(r.indexDir) : r.indexDir}`);
}
},
getGitHubToken() {
const cfg = readConfig();
const active = cfg.activeRepo;
if (active)
return cfg.repos[active]?.githubToken || null;
return cfg.githubToken || null;
},
setGitHubToken(token) {
const cfg = readConfig();
const active = cfg.activeRepo;
if (active) {
if (!cfg.repos[active])
cfg.repos[active] = {};
cfg.repos[active] = { ...cfg.repos[active], githubToken: token };
}
else {
cfg.githubToken = token;
}
writeConfig(cfg);
console.log('â
GitHub token updated');
},
/* ------------------- đ¤ Daemon Config ------------------- */
getDaemonConfig() {
const cfg = readConfig();
const daemonCfg = cfg.daemon ?? { sleepMs: DEFAULT_DAEMON_SLEEP_MS, idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS };
const sleepMs = Number(process.env.SCAI_SLEEP_MS) || daemonCfg.sleepMs;
const idleSleepMs = Number(process.env.SCAI_IDLE_SLEEP_MS) || daemonCfg.idleSleepMs;
return { sleepMs, idleSleepMs };
},
setDaemonConfig(newCfg) {
const cfg = readConfig();
cfg.daemon = {
sleepMs: DEFAULT_DAEMON_SLEEP_MS,
idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS,
...cfg.daemon,
...newCfg,
};
writeConfig(cfg);
console.log(`đ Daemon configuration updated: ${JSON.stringify(cfg.daemon, null, 2)}`);
},
show() {
const cfg = readConfig();
const active = cfg.activeRepo;
console.log(`đ§ Current configuration:`);
console.log(` Active index dir: ${active || 'Not Set'}`);
const repoCfg = active ? cfg.repos[active] : {};
console.log(` Model : ${repoCfg?.model || cfg.model}`);
console.log(` Language : ${repoCfg?.language || cfg.language}`);
console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
const daemon = this.getDaemonConfig();
console.log(` Daemon sleepMs : ${daemon.sleepMs}ms`);
console.log(` Daemon idleMs : ${daemon.idleSleepMs}ms`);
// â
Show lock status
let lockStatus = 'Unlocked';
let lockedRepo = null;
if (fs.existsSync(CONFIG_LOCK_PATH)) {
lockedRepo = fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
lockStatus = `Locked to repo '${lockedRepo}'`;
}
console.log(` Daemon Lock : ${lockStatus}`);
},
getRaw() {
return readConfig();
},
};