UNPKG

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
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(); }, };