@alex-dbs/coderuler 0.0.0-stage → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CliExit, EXIT_VIOLATION } from '../exit.js';
|
|
2
|
+
import { createProgress } from '../progress.js';
|
|
3
|
+
import { renderRuleTestHeading, renderRuleTestResult, renderRuleTestSummary, } from '../reports.js';
|
|
4
|
+
import { ruleSelection } from './rules.js';
|
|
5
|
+
async function runTest(rulePath, options, context, services) {
|
|
6
|
+
const progress = context.terminal?.interactive === true
|
|
7
|
+
? createProgress(context.writeError, () => context.terminal?.columns ?? 80)
|
|
8
|
+
: undefined;
|
|
9
|
+
const color = context.terminal?.color === true;
|
|
10
|
+
let passed = 0;
|
|
11
|
+
let failed = 0;
|
|
12
|
+
let durationMs = 0;
|
|
13
|
+
let previousRuleName;
|
|
14
|
+
const failures = options.failuresOnly ? [] : undefined;
|
|
15
|
+
let testsStarted = false;
|
|
16
|
+
const flushFailures = () => {
|
|
17
|
+
if (failures === undefined || failures.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
if (previousRuleName !== undefined)
|
|
20
|
+
context.write('\n');
|
|
21
|
+
const ruleName = failures[0].ruleName;
|
|
22
|
+
context.write(renderRuleTestHeading(ruleName, context.redact, color));
|
|
23
|
+
previousRuleName = ruleName;
|
|
24
|
+
for (let index = 0; index < failures.length; index += 1) {
|
|
25
|
+
context.write(renderRuleTestResult(failures[index], context.redact, color, index === failures.length - 1));
|
|
26
|
+
}
|
|
27
|
+
failures.length = 0;
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const choice = rulePath === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: await services.resolveRuleSelector(rulePath, context.cwd, true);
|
|
33
|
+
const rules = await services.loadRules(ruleSelection(context.cwd, choice, context.cwd));
|
|
34
|
+
progress?.message('Running tests');
|
|
35
|
+
testsStarted = true;
|
|
36
|
+
await services.testRules(rules, (result, lastInRule) => {
|
|
37
|
+
progress?.clear();
|
|
38
|
+
if (result.expected === result.actual)
|
|
39
|
+
passed += 1;
|
|
40
|
+
else
|
|
41
|
+
failed += 1;
|
|
42
|
+
durationMs += result.durationMs;
|
|
43
|
+
if (failures !== undefined) {
|
|
44
|
+
if (result.expected !== result.actual)
|
|
45
|
+
failures.push(result);
|
|
46
|
+
if (lastInRule)
|
|
47
|
+
flushFailures();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (result.ruleName !== previousRuleName) {
|
|
51
|
+
if (previousRuleName !== undefined)
|
|
52
|
+
context.write('\n');
|
|
53
|
+
context.write(renderRuleTestHeading(result.ruleName, context.redact, color));
|
|
54
|
+
previousRuleName = result.ruleName;
|
|
55
|
+
}
|
|
56
|
+
context.write(renderRuleTestResult(result, context.redact, color, lastInRule));
|
|
57
|
+
}
|
|
58
|
+
progress?.message(`Running tests (${passed + failed} completed)`);
|
|
59
|
+
}, options.timeout, options.concurrency, options.ai === true);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
progress?.stop();
|
|
63
|
+
if (failures !== undefined && testsStarted) {
|
|
64
|
+
flushFailures();
|
|
65
|
+
if (previousRuleName !== undefined)
|
|
66
|
+
context.write('\n');
|
|
67
|
+
context.write(renderRuleTestSummary(passed, failed, durationMs, context.redact, color));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (failures === undefined) {
|
|
71
|
+
if (previousRuleName !== undefined)
|
|
72
|
+
context.write('\n');
|
|
73
|
+
context.write(renderRuleTestSummary(passed, failed, durationMs, context.redact, color));
|
|
74
|
+
}
|
|
75
|
+
if (failed > 0)
|
|
76
|
+
throw new CliExit(EXIT_VIOLATION);
|
|
77
|
+
}
|
|
78
|
+
export { runTest };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { escapeText } from '../text.js';
|
|
2
|
+
import { ruleSelection } from './rules.js';
|
|
3
|
+
async function runValidate(rulePath, context, services) {
|
|
4
|
+
const choice = rulePath === undefined
|
|
5
|
+
? {}
|
|
6
|
+
: await services.resolveRuleSelector(rulePath, context.cwd, true);
|
|
7
|
+
const rules = await services.loadRules(ruleSelection(context.cwd, choice, context.cwd));
|
|
8
|
+
for (const entry of rules) {
|
|
9
|
+
context.write(context.redact(`Valid rule: ${escapeText(entry.name)}\n`));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export { runValidate };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InspectRenderer } from './reports.js';
|
|
2
|
+
import type { TextRedactor, TextWriter } from './text.js';
|
|
3
|
+
interface InspectJsonOptions {
|
|
4
|
+
readonly write: TextWriter;
|
|
5
|
+
readonly redact: TextRedactor;
|
|
6
|
+
readonly format: 'json' | 'jsonl';
|
|
7
|
+
}
|
|
8
|
+
export declare function createInspectJsonRenderer(options: InspectJsonOptions): InspectRenderer;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createUsageTracker } from './inspect-usage.js';
|
|
2
|
+
function redactRecord(value, redact, sanitize) {
|
|
3
|
+
const output = Object.create(null);
|
|
4
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
5
|
+
const redacted = redact(key);
|
|
6
|
+
let unique = redacted;
|
|
7
|
+
let suffix = 2;
|
|
8
|
+
while (Object.hasOwn(output, unique))
|
|
9
|
+
unique = `${redacted}#${suffix++}`;
|
|
10
|
+
output[unique] = sanitize(entry);
|
|
11
|
+
}
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
function redactJson(value, redact) {
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return redact(value);
|
|
17
|
+
if (value === null || typeof value !== 'object')
|
|
18
|
+
return value;
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return value.map(entry => redactJson(entry, redact));
|
|
21
|
+
return redactRecord(value, redact, entry => redactJson(entry, redact));
|
|
22
|
+
}
|
|
23
|
+
function redactScope(subject, redact) {
|
|
24
|
+
return {
|
|
25
|
+
...subject,
|
|
26
|
+
filepath: redact(subject.filepath),
|
|
27
|
+
id: redact(subject.id),
|
|
28
|
+
source: redact(subject.source),
|
|
29
|
+
...(subject.lines === undefined ? {} : { lines: { ...subject.lines } }),
|
|
30
|
+
...(subject.data === undefined ? {} : { data: redactJson(subject.data, redact) }),
|
|
31
|
+
...(subject.captures === undefined ? {} : {
|
|
32
|
+
captures: redactRecord(subject.captures, redact, value => typeof value === 'string' ? redact(value) : value.map(redact)),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function redactGroup(subject, redact) {
|
|
37
|
+
return {
|
|
38
|
+
...subject,
|
|
39
|
+
group: { ...subject.group, path: redact(subject.group.path) },
|
|
40
|
+
members: subject.members.map(member => redactScope(member, redact)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function redactSubject(subject, redact) {
|
|
44
|
+
return subject.kind === 'scope' ? redactScope(subject, redact) : redactGroup(subject, redact);
|
|
45
|
+
}
|
|
46
|
+
function redactResult(result, redact) {
|
|
47
|
+
return {
|
|
48
|
+
...result,
|
|
49
|
+
...(result.message === undefined ? {} : { message: redact(result.message) }),
|
|
50
|
+
...(result.model === undefined ? {} : { model: redact(result.model) }),
|
|
51
|
+
...(result.usage === undefined ? {} : { usage: { ...result.usage } }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function redactReport(report, redact) {
|
|
55
|
+
return {
|
|
56
|
+
...report,
|
|
57
|
+
name: redact(report.name),
|
|
58
|
+
ruleFile: redact(report.ruleFile),
|
|
59
|
+
...(report.reason === undefined ? {} : { reason: redact(report.reason) }),
|
|
60
|
+
evaluations: report.evaluations.map(evaluation => ({
|
|
61
|
+
subject: redactSubject(evaluation.subject, redact),
|
|
62
|
+
result: redactResult(evaluation.result, redact),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function redactEvent(event, redact) {
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case 'progress':
|
|
69
|
+
return { ...event };
|
|
70
|
+
case 'rule':
|
|
71
|
+
return { ...event, name: redact(event.name), ruleFile: redact(event.ruleFile) };
|
|
72
|
+
case 'gate':
|
|
73
|
+
case 'file':
|
|
74
|
+
return {
|
|
75
|
+
...event,
|
|
76
|
+
name: redact(event.name),
|
|
77
|
+
...('filepath' in event ? { filepath: redact(event.filepath) } : {}),
|
|
78
|
+
...(event.reason === undefined ? {} : { reason: redact(event.reason) }),
|
|
79
|
+
};
|
|
80
|
+
case 'scope':
|
|
81
|
+
return {
|
|
82
|
+
...event,
|
|
83
|
+
name: redact(event.name),
|
|
84
|
+
subject: redactScope(event.subject, redact),
|
|
85
|
+
...(event.reason === undefined ? {} : { reason: redact(event.reason) }),
|
|
86
|
+
};
|
|
87
|
+
case 'group':
|
|
88
|
+
return { ...event, name: redact(event.name), subject: redactGroup(event.subject, redact) };
|
|
89
|
+
case 'evaluation':
|
|
90
|
+
return { ...event, name: redact(event.name), subject: redactSubject(event.subject, redact) };
|
|
91
|
+
case 'usage':
|
|
92
|
+
return { ...event, name: redact(event.name), subject: redactSubject(event.subject, redact), usage: { ...event.usage } };
|
|
93
|
+
case 'request':
|
|
94
|
+
case 'response':
|
|
95
|
+
return { ...event, name: redact(event.name), subject: redactSubject(event.subject, redact), body: redact(event.body) };
|
|
96
|
+
case 'result':
|
|
97
|
+
return {
|
|
98
|
+
...event,
|
|
99
|
+
name: redact(event.name),
|
|
100
|
+
subject: redactSubject(event.subject, redact),
|
|
101
|
+
result: redactResult(event.result, redact),
|
|
102
|
+
};
|
|
103
|
+
case 'trace':
|
|
104
|
+
return {
|
|
105
|
+
...event,
|
|
106
|
+
name: redact(event.name),
|
|
107
|
+
message: redact(event.message),
|
|
108
|
+
...(event.details === undefined ? {} : { details: redactJson(event.details, redact) }),
|
|
109
|
+
};
|
|
110
|
+
case 'error':
|
|
111
|
+
return {
|
|
112
|
+
...event,
|
|
113
|
+
...(event.name === undefined ? {} : { name: redact(event.name) }),
|
|
114
|
+
stage: redact(event.stage),
|
|
115
|
+
message: redact(event.message),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function createInspectJsonRenderer(options) {
|
|
120
|
+
const events = [];
|
|
121
|
+
const usage = createUsageTracker();
|
|
122
|
+
let finished = false;
|
|
123
|
+
const record = (value) => options.write(`${JSON.stringify(value)}\n`);
|
|
124
|
+
return {
|
|
125
|
+
emit: event => {
|
|
126
|
+
if (finished)
|
|
127
|
+
return;
|
|
128
|
+
const snapshot = redactEvent(event, options.redact);
|
|
129
|
+
usage.emit(event);
|
|
130
|
+
if (options.format === 'jsonl')
|
|
131
|
+
record({ type: 'event', event: snapshot });
|
|
132
|
+
else
|
|
133
|
+
events.push(snapshot);
|
|
134
|
+
},
|
|
135
|
+
finish: (reports, error) => {
|
|
136
|
+
if (finished)
|
|
137
|
+
return;
|
|
138
|
+
finished = true;
|
|
139
|
+
const summary = usage.summary();
|
|
140
|
+
if (options.format === 'jsonl') {
|
|
141
|
+
if (error === undefined) {
|
|
142
|
+
for (const report of reports)
|
|
143
|
+
record({ type: 'report', report: redactReport(report, options.redact) });
|
|
144
|
+
}
|
|
145
|
+
if (summary !== undefined)
|
|
146
|
+
record({ type: 'summary', usage: summary });
|
|
147
|
+
if (error !== undefined)
|
|
148
|
+
record({ type: 'error', message: options.redact(error) });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
options.write(`${JSON.stringify({
|
|
152
|
+
events,
|
|
153
|
+
reports: error === undefined ? reports.map(report => redactReport(report, options.redact)) : [],
|
|
154
|
+
...(error === undefined ? {} : { error: options.redact(error) }),
|
|
155
|
+
...(summary === undefined ? {} : { usage: summary }),
|
|
156
|
+
}, null, 2)}\n`);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunEvent } from '../../app/ports.js';
|
|
2
|
+
import type { AiUsage } from '../../app/rule.js';
|
|
3
|
+
export interface InspectionUsage extends AiUsage {
|
|
4
|
+
readonly verifications: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createUsageTracker(): {
|
|
7
|
+
readonly emit: (event: RunEvent) => void;
|
|
8
|
+
readonly summary: () => InspectionUsage | undefined;
|
|
9
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { addUsage } from '../../app/usage.js';
|
|
2
|
+
export function createUsageTracker() {
|
|
3
|
+
const verifications = new Set();
|
|
4
|
+
let total;
|
|
5
|
+
return {
|
|
6
|
+
emit: event => {
|
|
7
|
+
if (event.type !== 'usage' || event.usage.calls === 0)
|
|
8
|
+
return;
|
|
9
|
+
verifications.add(event.evaluationId);
|
|
10
|
+
total = addUsage(total, event.usage);
|
|
11
|
+
},
|
|
12
|
+
summary: () => total === undefined ? undefined : { ...total, verifications: verifications.size },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Command, CommanderError, InvalidArgumentError, Option } from 'commander';
|
|
2
|
+
import { CliExit, EXIT_FAILURE, EXIT_OK } from './exit.js';
|
|
3
|
+
import { escapeDiagnostic } from './text.js';
|
|
4
|
+
const SUCCESSFUL_COMMANDER_CODES = {
|
|
5
|
+
'commander.help': true,
|
|
6
|
+
'commander.helpDisplayed': true,
|
|
7
|
+
'commander.version': true,
|
|
8
|
+
};
|
|
9
|
+
const CONCURRENCY_HINT = 'must be a positive safe integer';
|
|
10
|
+
const THRESHOLD_HINT = 'must be a decimal number between 0 and 1';
|
|
11
|
+
const THRESHOLD_PATTERN = /^(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$/;
|
|
12
|
+
const TIMEOUT_HINT = 'must be a positive duration in ms, s, or m, at most 2147483647ms';
|
|
13
|
+
const TIMEOUT_UNITS = { ms: 1, s: 1_000, m: 60_000 };
|
|
14
|
+
function parseConcurrency(value) {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
if (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
17
|
+
throw new InvalidArgumentError(CONCURRENCY_HINT);
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
function parseThreshold(value) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
const parsed = Number(trimmed);
|
|
24
|
+
if (!THRESHOLD_PATTERN.test(trimmed) || !Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
25
|
+
throw new InvalidArgumentError(THRESHOLD_HINT);
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function parseTokenPrice(value) {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
const parsed = Number(trimmed);
|
|
32
|
+
if (!THRESHOLD_PATTERN.test(trimmed) || !Number.isFinite(parsed) || parsed < 0) {
|
|
33
|
+
throw new InvalidArgumentError('must be a finite nonnegative decimal number (USD per million tokens)');
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
function parseTimeout(value) {
|
|
38
|
+
const match = /^([0-9]+(?:\.[0-9]+)?|\.[0-9]+)(ms|s|m)$/.exec(value.trim());
|
|
39
|
+
const durationMs = match === null
|
|
40
|
+
? NaN
|
|
41
|
+
: Number(match[1]) * TIMEOUT_UNITS[match[2]];
|
|
42
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > 2_147_483_647) {
|
|
43
|
+
throw new InvalidArgumentError(TIMEOUT_HINT);
|
|
44
|
+
}
|
|
45
|
+
return durationMs;
|
|
46
|
+
}
|
|
47
|
+
export function createProgram(services) {
|
|
48
|
+
const program = new Command();
|
|
49
|
+
program
|
|
50
|
+
.name('coderuler')
|
|
51
|
+
.description('Evaluate trusted TypeScript rules against the current working tree')
|
|
52
|
+
.configureOutput({
|
|
53
|
+
writeOut: text => services.write(text),
|
|
54
|
+
writeErr: text => services.writeError(`coderuler: ${services.redact(escapeDiagnostic(text))}`),
|
|
55
|
+
})
|
|
56
|
+
.version(services.version)
|
|
57
|
+
.exitOverride();
|
|
58
|
+
program
|
|
59
|
+
.command('update')
|
|
60
|
+
.description('Upgrade the active global npm installation of CodeRuler to the latest version')
|
|
61
|
+
.action(async () => {
|
|
62
|
+
const { runUpdate } = await import('./commands/update.js');
|
|
63
|
+
const commandServices = await services.load.update(services);
|
|
64
|
+
await runUpdate(commandServices);
|
|
65
|
+
});
|
|
66
|
+
const skill = program
|
|
67
|
+
.command('skill')
|
|
68
|
+
.description('Install or refresh the bundled coderuler-new-rule Agent Skill');
|
|
69
|
+
skill
|
|
70
|
+
.command('install')
|
|
71
|
+
.description('Install the bundled skill; defaults to the shared universal skills directory')
|
|
72
|
+
.option('-g, --global', 'install globally instead of prompting for scope')
|
|
73
|
+
.option('-a, --agent <agents...>', 'target agents (use "*" for all agents)')
|
|
74
|
+
.option('-y, --yes', 'skip prompts (project scope unless --global)')
|
|
75
|
+
.option('--copy', 'copy files instead of symlinking agent directories')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
const { runSkill } = await import('./commands/skill.js');
|
|
78
|
+
const commandServices = await services.load.skill(services);
|
|
79
|
+
await runSkill('install', options, commandServices);
|
|
80
|
+
});
|
|
81
|
+
skill
|
|
82
|
+
.command('update')
|
|
83
|
+
.description('Refresh an existing bundled-skill installation; defaults to the universal target')
|
|
84
|
+
.addOption(new Option('-g, --global', 'refresh a global installation').conflicts('project'))
|
|
85
|
+
.addOption(new Option('-p, --project', 'refresh a project installation (default)').conflicts('global'))
|
|
86
|
+
.option('-a, --agent <agents...>', 'target agents (use "*" for all agents)')
|
|
87
|
+
.option('--copy', 'copy files instead of symlinking agent directories')
|
|
88
|
+
.addHelpText('after', '\nUpgrade CodeRuler first to obtain newer skill content. Local sources are refreshed with skills add, not skills update.')
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
const { runSkill } = await import('./commands/skill.js');
|
|
91
|
+
const commandServices = await services.load.skill(services);
|
|
92
|
+
await runSkill('update', options, commandServices);
|
|
93
|
+
});
|
|
94
|
+
const dependencies = program
|
|
95
|
+
.command('deps')
|
|
96
|
+
.description('Manage the public npm packages declared by trusted rules');
|
|
97
|
+
const dependencyOptions = (command) => command
|
|
98
|
+
.addOption(new Option('--global', 'manage home rules instead of project rules').conflicts('directory'))
|
|
99
|
+
.addOption(new Option('--directory <path>', 'manage a rule directory relative to the working directory').conflicts('global'));
|
|
100
|
+
for (const operation of ['add', 'remove']) {
|
|
101
|
+
dependencyOptions(dependencies.command(operation))
|
|
102
|
+
.description(operation === 'add' ? 'Add or update packages and save exact locked versions' : 'Remove declared direct packages')
|
|
103
|
+
.argument(operation === 'add' ? '<packages...>' : '<names...>', 'public npm packages')
|
|
104
|
+
.action(async (packages, options) => {
|
|
105
|
+
const { runDeps } = await import('./commands/deps.js');
|
|
106
|
+
const commandServices = await services.load.deps(services);
|
|
107
|
+
await runDeps(operation, packages, options, commandServices);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
dependencyOptions(dependencies.command('sync'))
|
|
111
|
+
.description('Acquire locked packages and refresh SDK and dependency editor links')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
const { runDeps } = await import('./commands/deps.js');
|
|
114
|
+
const commandServices = await services.load.deps(services);
|
|
115
|
+
await runDeps('sync', [], options, commandServices);
|
|
116
|
+
});
|
|
117
|
+
program
|
|
118
|
+
.command('validate')
|
|
119
|
+
.description('Load and structurally validate the selected rules without running them')
|
|
120
|
+
.argument('[rule]', 'rule name, module path, or directory')
|
|
121
|
+
.action(async (rulePath) => {
|
|
122
|
+
const { runValidate } = await import('./commands/validate.js');
|
|
123
|
+
const commandServices = await services.load.validate(services);
|
|
124
|
+
await runValidate(rulePath, services, commandServices);
|
|
125
|
+
});
|
|
126
|
+
program
|
|
127
|
+
.command('test')
|
|
128
|
+
.description('Run the declared test cases for the selected rules')
|
|
129
|
+
.argument('[rule]', 'rule name, module path, or directory')
|
|
130
|
+
.option('--ai', 'include aiTests that use the configured decision backend')
|
|
131
|
+
.option('--failures-only', 'show only failed test cases')
|
|
132
|
+
.addOption(new Option('--timeout <duration>', 'per-test timeout (ms, s, or m)')
|
|
133
|
+
.argParser(parseTimeout)
|
|
134
|
+
.default(10_000, '10s'))
|
|
135
|
+
.option('--concurrency <number>', `bounded in-flight test cases (${CONCURRENCY_HINT})`, parseConcurrency, 1)
|
|
136
|
+
.action(async (rulePath, options) => {
|
|
137
|
+
const { runTest } = await import('./commands/test.js');
|
|
138
|
+
const commandServices = await services.load.test(services);
|
|
139
|
+
await runTest(rulePath, options, services, commandServices);
|
|
140
|
+
});
|
|
141
|
+
program
|
|
142
|
+
.command('check')
|
|
143
|
+
.description('Evaluate every selected rule against the working tree')
|
|
144
|
+
.argument('[path]', 'target file or folder that narrows selection', '.')
|
|
145
|
+
.option('--rules <rule>', 'rule name, module path, or directory')
|
|
146
|
+
.option('--tags <tags...>', 'run rules matching any listed tag', [])
|
|
147
|
+
.option('--exclude-tags <tags...>', 'exclude rules matching any listed tag', [])
|
|
148
|
+
.option('--json', 'print violations as {durationMs, reports} JSON on stdout')
|
|
149
|
+
.option('--jsonl', 'print violation rule records as JSON Lines')
|
|
150
|
+
.option('--output-all', 'include passing and skipped rules and every verification in the output')
|
|
151
|
+
.option('--debug', 'stream framework metadata and force concurrency 1')
|
|
152
|
+
.option('--verbose', 'stream the redacted walkthrough at the configured concurrency')
|
|
153
|
+
.option('--diff, --diff-only', 'evaluate changed files and affected groups against a Git base')
|
|
154
|
+
.option('--pr <number-or-url>', 'check a GitHub PR in a temporary checkout (implies --diff-only)')
|
|
155
|
+
.option('--auto-comment', 'post GitHub inline violation comments, with a summary fallback (requires --diff or --pr)')
|
|
156
|
+
.option('--base <ref>', 'Git base for --diff-only (default: HEAD), or base branch for --pr')
|
|
157
|
+
.option('--concurrency <number>', `bounded in-flight selection and evaluation tasks (${CONCURRENCY_HINT})`, parseConcurrency, 8)
|
|
158
|
+
.option('--threshold <number>', `decision threshold override (${THRESHOLD_HINT})`, parseThreshold)
|
|
159
|
+
.action(async (target, options) => {
|
|
160
|
+
if (options.json === true && options.jsonl === true) {
|
|
161
|
+
throw new Error('--json and --jsonl are mutually exclusive');
|
|
162
|
+
}
|
|
163
|
+
const diffOnly = options.diffOnly === true || options.pr !== undefined;
|
|
164
|
+
if (options.base !== undefined && !diffOnly) {
|
|
165
|
+
throw new Error('--base requires --diff-only or --pr');
|
|
166
|
+
}
|
|
167
|
+
if (options.autoComment === true && !diffOnly) {
|
|
168
|
+
throw new Error('--auto-comment requires --diff (or --diff-only) or --pr');
|
|
169
|
+
}
|
|
170
|
+
const { runCheck } = await import('./commands/check.js');
|
|
171
|
+
const commandServices = await services.load.check(services);
|
|
172
|
+
await runCheck(target, options, services, commandServices);
|
|
173
|
+
});
|
|
174
|
+
program
|
|
175
|
+
.command('inspect')
|
|
176
|
+
.description('Walk one rule through selection, filtering, grouping and evaluation')
|
|
177
|
+
.argument('<rule>', 'rule module path or exact rule name')
|
|
178
|
+
.argument('[folder]', 'folder to inspect', '.')
|
|
179
|
+
.option('--json', 'print the complete redacted inspection as one JSON document')
|
|
180
|
+
.option('--jsonl', 'stream redacted inspection events and reports as JSON Lines')
|
|
181
|
+
.option('--raw', 'print the complete redacted inspection as undecorated text')
|
|
182
|
+
.option('--input-cost-per-million <number>', 'input token price override (USD per million tokens)', parseTokenPrice)
|
|
183
|
+
.option('--output-cost-per-million <number>', 'output token price override (USD per million tokens)', parseTokenPrice)
|
|
184
|
+
.action(async (rule, folder, options) => {
|
|
185
|
+
const formats = [
|
|
186
|
+
...(options.json === true ? ['--json'] : []),
|
|
187
|
+
...(options.jsonl === true ? ['--jsonl'] : []),
|
|
188
|
+
...(options.raw === true ? ['--raw'] : []),
|
|
189
|
+
];
|
|
190
|
+
if (formats.length > 1) {
|
|
191
|
+
throw new Error(`${formats.join(', ')} are mutually exclusive`);
|
|
192
|
+
}
|
|
193
|
+
const { runInspect } = await import('./commands/inspect.js');
|
|
194
|
+
const commandServices = await services.load.inspect(services);
|
|
195
|
+
await runInspect(rule, folder, options, services, commandServices);
|
|
196
|
+
});
|
|
197
|
+
return program;
|
|
198
|
+
}
|
|
199
|
+
export async function runProgram(argv, services) {
|
|
200
|
+
try {
|
|
201
|
+
await createProgram(services).parseAsync(argv, { from: 'user' });
|
|
202
|
+
return EXIT_OK;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof CliExit)
|
|
206
|
+
return error.exitCode;
|
|
207
|
+
if (error instanceof CommanderError) {
|
|
208
|
+
return error.exitCode === EXIT_OK && SUCCESSFUL_COMMANDER_CODES[error.code] === true
|
|
209
|
+
? EXIT_OK
|
|
210
|
+
: EXIT_FAILURE;
|
|
211
|
+
}
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
services.writeError(`coderuler: ${services.redact(escapeDiagnostic(message))}\n`);
|
|
214
|
+
return EXIT_FAILURE;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RunEvent } from '../../app/ports.js';
|
|
2
|
+
import type { TextWriter } from './text.js';
|
|
3
|
+
export interface TerminalCapabilities {
|
|
4
|
+
readonly color: boolean;
|
|
5
|
+
readonly interactive: boolean;
|
|
6
|
+
readonly columns?: number;
|
|
7
|
+
readonly stdoutColumns?: number;
|
|
8
|
+
}
|
|
9
|
+
interface TerminalStreams {
|
|
10
|
+
readonly stdoutIsTTY: boolean;
|
|
11
|
+
readonly stderrIsTTY: boolean;
|
|
12
|
+
readonly columns?: number;
|
|
13
|
+
readonly stdoutColumns?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function terminalCapabilities(streams: TerminalStreams, env: Readonly<Record<string, string | undefined>>): TerminalCapabilities;
|
|
16
|
+
export declare function createProgress(write: TextWriter, columns?: () => number): {
|
|
17
|
+
message(text: string): void;
|
|
18
|
+
clear: () => void;
|
|
19
|
+
emit(event: RunEvent): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const FRAMES = ['-', '\\', '|', '/'];
|
|
2
|
+
const FRAME_INTERVAL = 80;
|
|
3
|
+
const CLEAR_LINE = '\r\u001b[2K';
|
|
4
|
+
export function terminalCapabilities(streams, env) {
|
|
5
|
+
const forced = env.FORCE_COLOR?.toLowerCase();
|
|
6
|
+
const color = forced !== undefined
|
|
7
|
+
? forced !== '0' && forced !== 'false'
|
|
8
|
+
: env.NO_COLOR === undefined && env.TERM !== 'dumb' && streams.stdoutIsTTY;
|
|
9
|
+
return {
|
|
10
|
+
color,
|
|
11
|
+
interactive: streams.stdoutIsTTY && streams.stderrIsTTY && env.TERM !== 'dumb',
|
|
12
|
+
get columns() { return streams.columns ?? 80; },
|
|
13
|
+
get stdoutColumns() { return streams.stdoutColumns ?? streams.columns ?? 80; },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function createProgress(write, columns = () => 80) {
|
|
17
|
+
let frame = 0;
|
|
18
|
+
let active = true;
|
|
19
|
+
let drawn = false;
|
|
20
|
+
let progress;
|
|
21
|
+
let message = 'Loading rules';
|
|
22
|
+
const clear = () => {
|
|
23
|
+
if (drawn)
|
|
24
|
+
write(CLEAR_LINE);
|
|
25
|
+
drawn = false;
|
|
26
|
+
};
|
|
27
|
+
const draw = () => {
|
|
28
|
+
const width = Math.max(0, Math.floor(columns()) - 1);
|
|
29
|
+
if (width === 0) {
|
|
30
|
+
clear();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const spinner = FRAMES[frame % FRAMES.length];
|
|
34
|
+
frame += 1;
|
|
35
|
+
let text = `${spinner} ${message}`;
|
|
36
|
+
if (progress !== undefined) {
|
|
37
|
+
const label = progress.phase === 'selection' ? 'Scanning' : 'Evaluating';
|
|
38
|
+
const units = progress.phase === 'selection' ? 'rules' : 'subjects';
|
|
39
|
+
const counts = `${progress.completed}/${progress.total} ${units}`;
|
|
40
|
+
const barWidth = Math.min(24, width - label.length - counts.length - 7);
|
|
41
|
+
if (barWidth >= 3) {
|
|
42
|
+
const filled = progress.total === 0
|
|
43
|
+
? 0
|
|
44
|
+
: Math.min(barWidth, Math.floor(barWidth * progress.completed / progress.total));
|
|
45
|
+
text = `${spinner} ${label} [${'#'.repeat(filled)}${'-'.repeat(barWidth - filled)}] ${counts}`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
text = `${spinner} ${counts}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
write(`${CLEAR_LINE}${text.slice(0, width)}`);
|
|
52
|
+
drawn = true;
|
|
53
|
+
};
|
|
54
|
+
draw();
|
|
55
|
+
const timer = setInterval(draw, FRAME_INTERVAL);
|
|
56
|
+
return {
|
|
57
|
+
message(text) {
|
|
58
|
+
if (!active)
|
|
59
|
+
return;
|
|
60
|
+
message = text;
|
|
61
|
+
progress = undefined;
|
|
62
|
+
draw();
|
|
63
|
+
},
|
|
64
|
+
clear,
|
|
65
|
+
emit(event) {
|
|
66
|
+
if (!active || event.type !== 'progress')
|
|
67
|
+
return;
|
|
68
|
+
progress = event;
|
|
69
|
+
draw();
|
|
70
|
+
},
|
|
71
|
+
stop() {
|
|
72
|
+
if (!active)
|
|
73
|
+
return;
|
|
74
|
+
active = false;
|
|
75
|
+
clearInterval(timer);
|
|
76
|
+
clear();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|