UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

251 lines (250 loc) 8.19 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { Command } from "commander"; import chalk from "chalk"; import Database from "better-sqlite3"; import * as fs from "fs"; import * as path from "path"; import { RuleEngine } from "../../core/rules/rule-engine.js"; import { filterByScope } from "../../core/rules/built-in-rules.js"; function getDb() { const smDir = path.join(process.cwd(), ".stackmemory"); if (!fs.existsSync(smDir)) { fs.mkdirSync(smDir, { recursive: true }); } return new Database(path.join(smDir, "context.db")); } function severityColor(severity) { switch (severity) { case "error": return chalk.red; case "warn": return chalk.yellow; case "info": return chalk.blue; default: return chalk.gray; } } function severityIcon(severity) { switch (severity) { case "error": return "x"; case "warn": return "!"; case "info": return "i"; default: return "-"; } } function createRulesCommand() { const cmd = new Command("rule").description( "Manage project rules (lint, commit, migration checks)" ); cmd.command("list").description("List configured rules").option("-t, --trigger <type>", "Filter by trigger type").option("-a, --all", "Include disabled rules").option("--json", "Output as JSON").action((options) => { const db = getDb(); try { const engine = new RuleEngine(db); const rules = engine.listRules({ trigger: options.trigger, enabled: options.all ? false : void 0 }); if (options.json) { console.log(JSON.stringify(rules, null, 2)); return; } if (rules.length === 0) { console.log(chalk.gray("No rules found.")); return; } console.log(chalk.cyan(` Rules (${rules.length}) `)); for (const rule of rules) { const enabled = rule.enabled ? chalk.green("on") : chalk.gray("off"); const sev = severityColor(rule.severity)(rule.severity.toUpperCase()); const builtin = rule.builtin ? chalk.gray(" [built-in]") : ""; console.log( ` ${enabled} ${sev} ${chalk.white(rule.id)}${builtin}` ); console.log(` ${chalk.gray(rule.description)}`); console.log( ` trigger: ${rule.trigger_type} scope: ${rule.scope}` ); console.log(); } } finally { db.close(); } }); cmd.command("check").description("Run rules against files or commit message").option("-t, --trigger <type>", "Trigger type filter", "on-demand").option("-f, --files <glob>", "File glob to check").option("-m, --commit-message <msg>", "Commit message to check").option("--all", "Run all rules regardless of trigger").option("--json", "Output as JSON").action( (options) => { const db = getDb(); try { const engine = new RuleEngine(db); const projectRoot = process.cwd(); let files = []; if (options.files) { files = collectFiles(projectRoot, options.files); } const content = /* @__PURE__ */ new Map(); for (const file of files) { const fullPath = path.isAbsolute(file) ? file : path.join(projectRoot, file); try { content.set(file, fs.readFileSync(fullPath, "utf-8")); } catch { } } const ctx = { trigger: options.trigger ?? "on-demand", files, content, commitMessage: options.commitMessage ?? "", projectRoot }; const result = options.all ? engine.evaluateAll(ctx) : engine.evaluate(ctx); if (options.json) { console.log(JSON.stringify(result, null, 2)); process.exitCode = result.passed ? 0 : 1; return; } if (result.passed) { console.log(chalk.green("\n All rules passed.\n")); return; } console.log( chalk.red(` ${result.violations.length} violation(s) found `) ); for (const v of result.violations) { const icon = severityIcon(v.severity); const color = severityColor(v.severity); const loc = v.file ? `${v.file}${v.line ? `:${v.line}` : ""}` : ""; console.log(` ${color(`[${icon}]`)} ${chalk.white(v.ruleName)}`); console.log(` ${v.message}`); if (loc) console.log(` ${chalk.gray(loc)}`); if (v.suggestion) console.log(` ${chalk.cyan(v.suggestion)}`); console.log(); } const errors = result.violations.filter( (v) => v.severity === "error" ); if (errors.length > 0) { process.exitCode = 1; } } finally { db.close(); } } ); cmd.command("enable <id>").description("Enable a rule").action((id) => { const db = getDb(); try { const engine = new RuleEngine(db); if (engine.enableRule(id)) { console.log(chalk.green(`Rule '${id}' enabled.`)); } else { console.log(chalk.red(`Rule '${id}' not found.`)); process.exitCode = 1; } } finally { db.close(); } }); cmd.command("disable <id>").description("Disable a rule").action((id) => { const db = getDb(); try { const engine = new RuleEngine(db); if (engine.disableRule(id)) { console.log(chalk.yellow(`Rule '${id}' disabled.`)); } else { console.log(chalk.red(`Rule '${id}' not found.`)); process.exitCode = 1; } } finally { db.close(); } }); cmd.command("seed").description("Re-seed built-in rules (useful after upgrades)").action(() => { const db = getDb(); try { const engine = new RuleEngine(db); const rules = engine.listRules(); const builtins = rules.filter((r) => r.builtin); console.log(chalk.green(`Seeded ${builtins.length} built-in rules.`)); } finally { db.close(); } }); cmd.command("add <id>").description("Add a custom rule (metadata only)").requiredOption("-n, --name <name>", "Rule display name").option("-d, --description <desc>", "Rule description", "").option("-t, --trigger <type>", "Trigger type", "on-demand").option("-s, --severity <level>", "Severity level", "warn").option("--scope <glob>", "File scope glob", "**/*").action( (id, options) => { const db = getDb(); try { const engine = new RuleEngine(db); engine.getStore().upsert({ id, name: options.name, description: options.description, trigger_type: options.trigger, severity: options.severity, scope: options.scope, enabled: 1, builtin: 0 }); console.log(chalk.green(`Rule '${id}' added.`)); } finally { db.close(); } } ); return cmd; } function collectFiles(root, pattern) { const results = []; if (pattern.includes("*")) { walkDir(root, root, pattern, results); } else { const fullPath = path.join(root, pattern); if (fs.existsSync(fullPath)) { const stat = fs.statSync(fullPath); if (stat.isFile()) { results.push(pattern); } else if (stat.isDirectory()) { walkDir(fullPath, root, "**/*", results); } } } return results; } function walkDir(dir, root, pattern, results) { const SKIP = /* @__PURE__ */ new Set([ "node_modules", ".git", "dist", "coverage", ".stackmemory" ]); try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (SKIP.has(entry.name)) continue; const fullPath = path.join(dir, entry.name); const relPath = path.relative(root, fullPath); if (entry.isDirectory()) { walkDir(fullPath, root, pattern, results); } else if (entry.isFile()) { if (filterByScope([relPath], pattern).length > 0) { results.push(relPath); } } } } catch { } } export { createRulesCommand };