Skip to content

Commit 79ab15f

Browse files
author
StackMemory Bot (CLI)
committed
chore: fix prettier formatting in rules module
Auto-format pre-existing src/core/rules/ and src/cli/commands/rules.ts to pass pre-push lint gate.
1 parent 47d9dd1 commit 79ab15f

8 files changed

Lines changed: 1576 additions & 0 deletions

File tree

src/cli/commands/rules.ts

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
/**
2+
* CLI commands for rule management.
3+
* Usage: stackmemory rule list|check|enable|disable|seed
4+
*/
5+
6+
import { Command } from 'commander';
7+
import chalk from 'chalk';
8+
import Database from 'better-sqlite3';
9+
import * as fs from 'fs';
10+
import * as path from 'path';
11+
import { RuleEngine } from '../../core/rules/rule-engine.js';
12+
import type {
13+
RuleContext,
14+
RuleTrigger,
15+
RuleSeverity,
16+
RuleRow,
17+
} from '../../core/rules/types.js';
18+
import { filterByScope } from '../../core/rules/built-in-rules.js';
19+
20+
function getDb(): Database.Database {
21+
const smDir = path.join(process.cwd(), '.stackmemory');
22+
if (!fs.existsSync(smDir)) {
23+
fs.mkdirSync(smDir, { recursive: true });
24+
}
25+
return new Database(path.join(smDir, 'context.db'));
26+
}
27+
28+
function severityColor(severity: string): (s: string) => string {
29+
switch (severity) {
30+
case 'error':
31+
return chalk.red;
32+
case 'warn':
33+
return chalk.yellow;
34+
case 'info':
35+
return chalk.blue;
36+
default:
37+
return chalk.gray;
38+
}
39+
}
40+
41+
function severityIcon(severity: string): string {
42+
switch (severity) {
43+
case 'error':
44+
return 'x';
45+
case 'warn':
46+
return '!';
47+
case 'info':
48+
return 'i';
49+
default:
50+
return '-';
51+
}
52+
}
53+
54+
export function createRulesCommand(): Command {
55+
const cmd = new Command('rule').description(
56+
'Manage project rules (lint, commit, migration checks)'
57+
);
58+
59+
// ---- list ----
60+
cmd
61+
.command('list')
62+
.description('List configured rules')
63+
.option('-t, --trigger <type>', 'Filter by trigger type')
64+
.option('-a, --all', 'Include disabled rules')
65+
.option('--json', 'Output as JSON')
66+
.action((options: { trigger?: string; all?: boolean; json?: boolean }) => {
67+
const db = getDb();
68+
try {
69+
const engine = new RuleEngine(db);
70+
const rules = engine.listRules({
71+
trigger: options.trigger as RuleTrigger | undefined,
72+
enabled: options.all ? false : undefined,
73+
});
74+
75+
if (options.json) {
76+
console.log(JSON.stringify(rules, null, 2));
77+
return;
78+
}
79+
80+
if (rules.length === 0) {
81+
console.log(chalk.gray('No rules found.'));
82+
return;
83+
}
84+
85+
console.log(chalk.cyan(`\n Rules (${rules.length})\n`));
86+
for (const rule of rules) {
87+
const enabled = rule.enabled ? chalk.green('on') : chalk.gray('off');
88+
const sev = severityColor(rule.severity)(rule.severity.toUpperCase());
89+
const builtin = rule.builtin ? chalk.gray(' [built-in]') : '';
90+
console.log(
91+
` ${enabled} ${sev} ${chalk.white(rule.id)}${builtin}`
92+
);
93+
console.log(` ${chalk.gray(rule.description)}`);
94+
console.log(
95+
` trigger: ${rule.trigger_type} scope: ${rule.scope}`
96+
);
97+
console.log();
98+
}
99+
} finally {
100+
db.close();
101+
}
102+
});
103+
104+
// ---- check ----
105+
cmd
106+
.command('check')
107+
.description('Run rules against files or commit message')
108+
.option('-t, --trigger <type>', 'Trigger type filter', 'on-demand')
109+
.option('-f, --files <glob>', 'File glob to check')
110+
.option('-m, --commit-message <msg>', 'Commit message to check')
111+
.option('--all', 'Run all rules regardless of trigger')
112+
.option('--json', 'Output as JSON')
113+
.action(
114+
(options: {
115+
trigger?: string;
116+
files?: string;
117+
commitMessage?: string;
118+
all?: boolean;
119+
json?: boolean;
120+
}) => {
121+
const db = getDb();
122+
try {
123+
const engine = new RuleEngine(db);
124+
const projectRoot = process.cwd();
125+
126+
// Collect files
127+
let files: string[] = [];
128+
if (options.files) {
129+
files = collectFiles(projectRoot, options.files);
130+
}
131+
132+
// Read file contents
133+
const content = new Map<string, string>();
134+
for (const file of files) {
135+
const fullPath = path.isAbsolute(file)
136+
? file
137+
: path.join(projectRoot, file);
138+
try {
139+
content.set(file, fs.readFileSync(fullPath, 'utf-8'));
140+
} catch {
141+
// skip unreadable files
142+
}
143+
}
144+
145+
const ctx: RuleContext = {
146+
trigger: (options.trigger ?? 'on-demand') as RuleTrigger,
147+
files,
148+
content,
149+
commitMessage: options.commitMessage ?? '',
150+
projectRoot,
151+
};
152+
153+
const result = options.all
154+
? engine.evaluateAll(ctx)
155+
: engine.evaluate(ctx);
156+
157+
if (options.json) {
158+
console.log(JSON.stringify(result, null, 2));
159+
process.exitCode = result.passed ? 0 : 1;
160+
return;
161+
}
162+
163+
if (result.passed) {
164+
console.log(chalk.green('\n All rules passed.\n'));
165+
return;
166+
}
167+
168+
console.log(
169+
chalk.red(`\n ${result.violations.length} violation(s) found\n`)
170+
);
171+
for (const v of result.violations) {
172+
const icon = severityIcon(v.severity);
173+
const color = severityColor(v.severity);
174+
const loc = v.file ? `${v.file}${v.line ? `:${v.line}` : ''}` : '';
175+
console.log(` ${color(`[${icon}]`)} ${chalk.white(v.ruleName)}`);
176+
console.log(` ${v.message}`);
177+
if (loc) console.log(` ${chalk.gray(loc)}`);
178+
if (v.suggestion) console.log(` ${chalk.cyan(v.suggestion)}`);
179+
console.log();
180+
}
181+
182+
const errors = result.violations.filter(
183+
(v) => v.severity === 'error'
184+
);
185+
if (errors.length > 0) {
186+
process.exitCode = 1;
187+
}
188+
} finally {
189+
db.close();
190+
}
191+
}
192+
);
193+
194+
// ---- enable ----
195+
cmd
196+
.command('enable <id>')
197+
.description('Enable a rule')
198+
.action((id: string) => {
199+
const db = getDb();
200+
try {
201+
const engine = new RuleEngine(db);
202+
if (engine.enableRule(id)) {
203+
console.log(chalk.green(`Rule '${id}' enabled.`));
204+
} else {
205+
console.log(chalk.red(`Rule '${id}' not found.`));
206+
process.exitCode = 1;
207+
}
208+
} finally {
209+
db.close();
210+
}
211+
});
212+
213+
// ---- disable ----
214+
cmd
215+
.command('disable <id>')
216+
.description('Disable a rule')
217+
.action((id: string) => {
218+
const db = getDb();
219+
try {
220+
const engine = new RuleEngine(db);
221+
if (engine.disableRule(id)) {
222+
console.log(chalk.yellow(`Rule '${id}' disabled.`));
223+
} else {
224+
console.log(chalk.red(`Rule '${id}' not found.`));
225+
process.exitCode = 1;
226+
}
227+
} finally {
228+
db.close();
229+
}
230+
});
231+
232+
// ---- seed ----
233+
cmd
234+
.command('seed')
235+
.description('Re-seed built-in rules (useful after upgrades)')
236+
.action(() => {
237+
const db = getDb();
238+
try {
239+
const engine = new RuleEngine(db);
240+
const rules = engine.listRules();
241+
const builtins = rules.filter((r) => r.builtin);
242+
console.log(chalk.green(`Seeded ${builtins.length} built-in rules.`));
243+
} finally {
244+
db.close();
245+
}
246+
});
247+
248+
// ---- add ----
249+
cmd
250+
.command('add <id>')
251+
.description('Add a custom rule (metadata only)')
252+
.requiredOption('-n, --name <name>', 'Rule display name')
253+
.option('-d, --description <desc>', 'Rule description', '')
254+
.option('-t, --trigger <type>', 'Trigger type', 'on-demand')
255+
.option('-s, --severity <level>', 'Severity level', 'warn')
256+
.option('--scope <glob>', 'File scope glob', '**/*')
257+
.action(
258+
(
259+
id: string,
260+
options: {
261+
name: string;
262+
description: string;
263+
trigger: string;
264+
severity: string;
265+
scope: string;
266+
}
267+
) => {
268+
const db = getDb();
269+
try {
270+
const engine = new RuleEngine(db);
271+
engine.getStore().upsert({
272+
id,
273+
name: options.name,
274+
description: options.description,
275+
trigger_type: options.trigger,
276+
severity: options.severity,
277+
scope: options.scope,
278+
enabled: 1,
279+
builtin: 0,
280+
});
281+
console.log(chalk.green(`Rule '${id}' added.`));
282+
} finally {
283+
db.close();
284+
}
285+
}
286+
);
287+
288+
return cmd;
289+
}
290+
291+
/**
292+
* Collect files matching a simple glob pattern relative to root.
293+
*/
294+
function collectFiles(root: string, pattern: string): string[] {
295+
const results: string[] = [];
296+
// For simple patterns, walk directory
297+
if (pattern.includes('*')) {
298+
walkDir(root, root, pattern, results);
299+
} else {
300+
// Treat as a single file or directory
301+
const fullPath = path.join(root, pattern);
302+
if (fs.existsSync(fullPath)) {
303+
const stat = fs.statSync(fullPath);
304+
if (stat.isFile()) {
305+
results.push(pattern);
306+
} else if (stat.isDirectory()) {
307+
walkDir(fullPath, root, '**/*', results);
308+
}
309+
}
310+
}
311+
return results;
312+
}
313+
314+
function walkDir(
315+
dir: string,
316+
root: string,
317+
pattern: string,
318+
results: string[]
319+
): void {
320+
const SKIP = new Set([
321+
'node_modules',
322+
'.git',
323+
'dist',
324+
'coverage',
325+
'.stackmemory',
326+
]);
327+
try {
328+
const entries = fs.readdirSync(dir, { withFileTypes: true });
329+
for (const entry of entries) {
330+
if (SKIP.has(entry.name)) continue;
331+
const fullPath = path.join(dir, entry.name);
332+
const relPath = path.relative(root, fullPath);
333+
if (entry.isDirectory()) {
334+
walkDir(fullPath, root, pattern, results);
335+
} else if (entry.isFile()) {
336+
if (filterByScope([relPath], pattern).length > 0) {
337+
results.push(relPath);
338+
}
339+
}
340+
}
341+
} catch {
342+
// skip unreadable dirs
343+
}
344+
}

src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { createTeamCommands } from './commands/team.js';
6666
import { createDesiresCommands } from './commands/desires.js';
6767
import { createConductorCommands } from './commands/orchestrate.js';
6868
import { createPreflightCommand } from './commands/preflight.js';
69+
import { createRulesCommand } from './commands/rules.js';
6970
import { createSnapshotCommand } from './commands/snapshot.js';
7071
import { createLoopCommand } from './commands/loop.js';
7172
import chalk from 'chalk';
@@ -696,6 +697,7 @@ program.addCommand(createConductorCommands());
696697
program.addCommand(createPreflightCommand());
697698
program.addCommand(createSnapshotCommand());
698699
program.addCommand(createLoopCommand());
700+
program.addCommand(createRulesCommand());
699701

700702
// Register setup and diagnostic commands
701703
registerSetupCommands(program);

0 commit comments

Comments
 (0)