@compilr-dev/agents 0.0.1
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/README.md +1277 -0
- package/dist/agent.d.ts +1272 -0
- package/dist/agent.js +1912 -0
- package/dist/anchors/builtin.d.ts +24 -0
- package/dist/anchors/builtin.js +61 -0
- package/dist/anchors/index.d.ts +6 -0
- package/dist/anchors/index.js +5 -0
- package/dist/anchors/manager.d.ts +115 -0
- package/dist/anchors/manager.js +412 -0
- package/dist/anchors/types.d.ts +168 -0
- package/dist/anchors/types.js +10 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +10 -0
- package/dist/context/manager.d.ts +224 -0
- package/dist/context/manager.js +770 -0
- package/dist/context/types.d.ts +377 -0
- package/dist/context/types.js +7 -0
- package/dist/costs/index.d.ts +8 -0
- package/dist/costs/index.js +7 -0
- package/dist/costs/tracker.d.ts +121 -0
- package/dist/costs/tracker.js +295 -0
- package/dist/costs/types.d.ts +157 -0
- package/dist/costs/types.js +8 -0
- package/dist/errors.d.ts +178 -0
- package/dist/errors.js +249 -0
- package/dist/guardrails/builtin.d.ts +27 -0
- package/dist/guardrails/builtin.js +223 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.js +5 -0
- package/dist/guardrails/manager.d.ts +117 -0
- package/dist/guardrails/manager.js +288 -0
- package/dist/guardrails/types.d.ts +159 -0
- package/dist/guardrails/types.js +7 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +29 -0
- package/dist/hooks/manager.d.ts +147 -0
- package/dist/hooks/manager.js +600 -0
- package/dist/hooks/types.d.ts +368 -0
- package/dist/hooks/types.js +12 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +73 -0
- package/dist/mcp/client.d.ts +93 -0
- package/dist/mcp/client.js +287 -0
- package/dist/mcp/errors.d.ts +60 -0
- package/dist/mcp/errors.js +78 -0
- package/dist/mcp/index.d.ts +43 -0
- package/dist/mcp/index.js +45 -0
- package/dist/mcp/manager.d.ts +120 -0
- package/dist/mcp/manager.js +276 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +99 -0
- package/dist/mcp/types.d.ts +150 -0
- package/dist/mcp/types.js +40 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/loader.d.ts +114 -0
- package/dist/memory/loader.js +463 -0
- package/dist/memory/types.d.ts +182 -0
- package/dist/memory/types.js +8 -0
- package/dist/messages/index.d.ts +82 -0
- package/dist/messages/index.js +155 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +4 -0
- package/dist/permissions/manager.d.ts +125 -0
- package/dist/permissions/manager.js +379 -0
- package/dist/permissions/types.d.ts +162 -0
- package/dist/permissions/types.js +7 -0
- package/dist/providers/claude.d.ts +90 -0
- package/dist/providers/claude.js +348 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +11 -0
- package/dist/providers/mock.d.ts +133 -0
- package/dist/providers/mock.js +204 -0
- package/dist/providers/types.d.ts +168 -0
- package/dist/providers/types.js +4 -0
- package/dist/rate-limit/index.d.ts +45 -0
- package/dist/rate-limit/index.js +47 -0
- package/dist/rate-limit/limiter.d.ts +104 -0
- package/dist/rate-limit/limiter.js +326 -0
- package/dist/rate-limit/provider-wrapper.d.ts +112 -0
- package/dist/rate-limit/provider-wrapper.js +201 -0
- package/dist/rate-limit/retry.d.ts +108 -0
- package/dist/rate-limit/retry.js +287 -0
- package/dist/rate-limit/types.d.ts +181 -0
- package/dist/rate-limit/types.js +22 -0
- package/dist/rehearsal/file-analyzer.d.ts +22 -0
- package/dist/rehearsal/file-analyzer.js +351 -0
- package/dist/rehearsal/git-analyzer.d.ts +22 -0
- package/dist/rehearsal/git-analyzer.js +472 -0
- package/dist/rehearsal/index.d.ts +35 -0
- package/dist/rehearsal/index.js +36 -0
- package/dist/rehearsal/manager.d.ts +100 -0
- package/dist/rehearsal/manager.js +290 -0
- package/dist/rehearsal/types.d.ts +235 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/skills/index.d.ts +160 -0
- package/dist/skills/index.js +282 -0
- package/dist/state/agent-state.d.ts +41 -0
- package/dist/state/agent-state.js +88 -0
- package/dist/state/checkpointer.d.ts +110 -0
- package/dist/state/checkpointer.js +362 -0
- package/dist/state/errors.d.ts +66 -0
- package/dist/state/errors.js +88 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.js +37 -0
- package/dist/state/serializer.d.ts +55 -0
- package/dist/state/serializer.js +172 -0
- package/dist/state/types.d.ts +312 -0
- package/dist/state/types.js +14 -0
- package/dist/tools/builtin/bash-output.d.ts +61 -0
- package/dist/tools/builtin/bash-output.js +90 -0
- package/dist/tools/builtin/bash.d.ts +150 -0
- package/dist/tools/builtin/bash.js +354 -0
- package/dist/tools/builtin/edit.d.ts +50 -0
- package/dist/tools/builtin/edit.js +215 -0
- package/dist/tools/builtin/glob.d.ts +62 -0
- package/dist/tools/builtin/glob.js +244 -0
- package/dist/tools/builtin/grep.d.ts +74 -0
- package/dist/tools/builtin/grep.js +363 -0
- package/dist/tools/builtin/index.d.ts +44 -0
- package/dist/tools/builtin/index.js +69 -0
- package/dist/tools/builtin/kill-shell.d.ts +44 -0
- package/dist/tools/builtin/kill-shell.js +80 -0
- package/dist/tools/builtin/read-file.d.ts +57 -0
- package/dist/tools/builtin/read-file.js +184 -0
- package/dist/tools/builtin/shell-manager.d.ts +176 -0
- package/dist/tools/builtin/shell-manager.js +337 -0
- package/dist/tools/builtin/task.d.ts +202 -0
- package/dist/tools/builtin/task.js +350 -0
- package/dist/tools/builtin/todo.d.ts +207 -0
- package/dist/tools/builtin/todo.js +453 -0
- package/dist/tools/builtin/utils.d.ts +27 -0
- package/dist/tools/builtin/utils.js +70 -0
- package/dist/tools/builtin/web-fetch.d.ts +96 -0
- package/dist/tools/builtin/web-fetch.js +290 -0
- package/dist/tools/builtin/write-file.d.ts +54 -0
- package/dist/tools/builtin/write-file.js +147 -0
- package/dist/tools/define.d.ts +60 -0
- package/dist/tools/define.js +65 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/registry.d.ts +79 -0
- package/dist/tools/registry.js +151 -0
- package/dist/tools/types.d.ts +59 -0
- package/dist/tools/types.js +4 -0
- package/dist/tracing/hooks.d.ts +58 -0
- package/dist/tracing/hooks.js +377 -0
- package/dist/tracing/index.d.ts +51 -0
- package/dist/tracing/index.js +55 -0
- package/dist/tracing/logging.d.ts +78 -0
- package/dist/tracing/logging.js +310 -0
- package/dist/tracing/manager.d.ts +160 -0
- package/dist/tracing/manager.js +468 -0
- package/dist/tracing/otel.d.ts +102 -0
- package/dist/tracing/otel.js +246 -0
- package/dist/tracing/types.d.ts +346 -0
- package/dist/tracing/types.js +38 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.js +44 -0
- package/package.json +79 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Rehearsal Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes destructive git operations and provides impact assessment.
|
|
5
|
+
* Handles: git reset, git checkout --, git restore, git clean, git stash drop
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Patterns for destructive git operations
|
|
10
|
+
*/
|
|
11
|
+
const GIT_DESTRUCTIVE_PATTERNS = [
|
|
12
|
+
// git reset (all forms)
|
|
13
|
+
/^git\s+reset\s+(--hard|--mixed|--soft)?\s*/i,
|
|
14
|
+
// git checkout -- (discard changes)
|
|
15
|
+
/^git\s+checkout\s+--\s*/i,
|
|
16
|
+
/^git\s+checkout\s+\.\s*$/i,
|
|
17
|
+
/^git\s+checkout\s+HEAD\s+--\s*/i,
|
|
18
|
+
// git restore (discard changes)
|
|
19
|
+
/^git\s+restore\s+/i,
|
|
20
|
+
// git clean (remove untracked)
|
|
21
|
+
/^git\s+clean\s+/i,
|
|
22
|
+
// git stash drop
|
|
23
|
+
/^git\s+stash\s+drop/i,
|
|
24
|
+
// git branch -D (force delete)
|
|
25
|
+
/^git\s+branch\s+-D\s+/i,
|
|
26
|
+
// git push --force
|
|
27
|
+
/^git\s+push\s+.*--force/i,
|
|
28
|
+
/^git\s+push\s+-f\s+/i,
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Execute a git command and return output
|
|
32
|
+
*/
|
|
33
|
+
function execGit(command, cwd) {
|
|
34
|
+
try {
|
|
35
|
+
return execSync(command, {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
}).trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if directory is a git repository
|
|
47
|
+
*/
|
|
48
|
+
function isGitRepo(cwd) {
|
|
49
|
+
try {
|
|
50
|
+
execSync('git rev-parse --git-dir', {
|
|
51
|
+
cwd,
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get list of modified files with their status
|
|
63
|
+
*/
|
|
64
|
+
function getModifiedFiles(cwd) {
|
|
65
|
+
const output = execGit('git status --porcelain', cwd);
|
|
66
|
+
if (!output)
|
|
67
|
+
return [];
|
|
68
|
+
const files = [];
|
|
69
|
+
for (const line of output.split('\n')) {
|
|
70
|
+
if (!line.trim())
|
|
71
|
+
continue;
|
|
72
|
+
const _status = line.substring(0, 2); // Status prefix (e.g., "M ", "D ", "??")
|
|
73
|
+
const filePath = line.substring(3).trim();
|
|
74
|
+
files.push({
|
|
75
|
+
path: filePath,
|
|
76
|
+
changeType: 'reset', // For git operations, changes would be reset
|
|
77
|
+
hasUncommittedChanges: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get diff stats for modified files
|
|
84
|
+
*/
|
|
85
|
+
function getDiffStats(cwd) {
|
|
86
|
+
const output = execGit('git diff --stat --numstat', cwd);
|
|
87
|
+
if (!output)
|
|
88
|
+
return { files: 0, insertions: 0, deletions: 0 };
|
|
89
|
+
let insertions = 0;
|
|
90
|
+
let deletions = 0;
|
|
91
|
+
let files = 0;
|
|
92
|
+
for (const line of output.split('\n')) {
|
|
93
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+/);
|
|
94
|
+
if (match) {
|
|
95
|
+
insertions += parseInt(match[1], 10);
|
|
96
|
+
deletions += parseInt(match[2], 10);
|
|
97
|
+
files++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { files, insertions, deletions };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get staged diff stats
|
|
104
|
+
*/
|
|
105
|
+
function getStagedDiffStats(cwd) {
|
|
106
|
+
const output = execGit('git diff --cached --stat --numstat', cwd);
|
|
107
|
+
if (!output)
|
|
108
|
+
return { files: 0, insertions: 0, deletions: 0 };
|
|
109
|
+
let insertions = 0;
|
|
110
|
+
let deletions = 0;
|
|
111
|
+
let files = 0;
|
|
112
|
+
for (const line of output.split('\n')) {
|
|
113
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+/);
|
|
114
|
+
if (match) {
|
|
115
|
+
insertions += parseInt(match[1], 10);
|
|
116
|
+
deletions += parseInt(match[2], 10);
|
|
117
|
+
files++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { files, insertions, deletions };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get untracked files that would be affected by git clean
|
|
124
|
+
*/
|
|
125
|
+
function getUntrackedFiles(cwd) {
|
|
126
|
+
const output = execGit('git clean -n -d', cwd);
|
|
127
|
+
if (!output)
|
|
128
|
+
return [];
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const line of output.split('\n')) {
|
|
131
|
+
const match = line.match(/^Would remove (.+)$/);
|
|
132
|
+
if (match) {
|
|
133
|
+
files.push({
|
|
134
|
+
path: match[1],
|
|
135
|
+
changeType: 'delete',
|
|
136
|
+
hasUncommittedChanges: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return files;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Calculate time since last commit
|
|
144
|
+
*/
|
|
145
|
+
function getTimeSinceLastCommit(cwd) {
|
|
146
|
+
const output = execGit('git log -1 --format=%cr', cwd);
|
|
147
|
+
return output || undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Determine severity based on impact
|
|
151
|
+
*/
|
|
152
|
+
function calculateSeverity(filesAffected, linesAffected, hasUncommittedChanges) {
|
|
153
|
+
if (!hasUncommittedChanges && filesAffected === 0) {
|
|
154
|
+
return 'low';
|
|
155
|
+
}
|
|
156
|
+
if (linesAffected > 500 || filesAffected > 20) {
|
|
157
|
+
return 'critical';
|
|
158
|
+
}
|
|
159
|
+
if (linesAffected > 100 || filesAffected > 10) {
|
|
160
|
+
return 'high';
|
|
161
|
+
}
|
|
162
|
+
if (linesAffected > 20 || filesAffected > 3) {
|
|
163
|
+
return 'medium';
|
|
164
|
+
}
|
|
165
|
+
return 'low';
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Determine recommendation based on analysis
|
|
169
|
+
*/
|
|
170
|
+
function calculateRecommendation(severity, isReversible) {
|
|
171
|
+
if (severity === 'critical') {
|
|
172
|
+
return isReversible ? 'confirm' : 'abort';
|
|
173
|
+
}
|
|
174
|
+
if (severity === 'high') {
|
|
175
|
+
return 'confirm';
|
|
176
|
+
}
|
|
177
|
+
if (severity === 'medium') {
|
|
178
|
+
return 'caution';
|
|
179
|
+
}
|
|
180
|
+
return 'proceed';
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Analyze git reset command
|
|
184
|
+
*/
|
|
185
|
+
async function analyzeGitReset(operation, context) {
|
|
186
|
+
await Promise.resolve(); // Ensure async function has await
|
|
187
|
+
const cwd = context.workingDirectory;
|
|
188
|
+
const isHard = operation.includes('--hard');
|
|
189
|
+
const isMixed = operation.includes('--mixed') || (!isHard && !operation.includes('--soft'));
|
|
190
|
+
const modifiedFiles = getModifiedFiles(cwd);
|
|
191
|
+
const diffStats = getDiffStats(cwd);
|
|
192
|
+
const stagedStats = getStagedDiffStats(cwd);
|
|
193
|
+
const totalLines = diffStats.insertions + diffStats.deletions + stagedStats.insertions + stagedStats.deletions;
|
|
194
|
+
const warnings = [];
|
|
195
|
+
if (isHard) {
|
|
196
|
+
warnings.push('--hard will permanently discard all uncommitted changes');
|
|
197
|
+
if (totalLines > 100) {
|
|
198
|
+
warnings.push(`You will lose ${String(totalLines)} lines of changes`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (isMixed && stagedStats.files > 0) {
|
|
202
|
+
warnings.push(`${String(stagedStats.files)} staged files will be unstaged`);
|
|
203
|
+
}
|
|
204
|
+
const timeSinceCommit = getTimeSinceLastCommit(cwd);
|
|
205
|
+
const timeInvestment = timeSinceCommit
|
|
206
|
+
? `Changes made since last commit (${timeSinceCommit})`
|
|
207
|
+
: undefined;
|
|
208
|
+
return {
|
|
209
|
+
isDestructive: isHard,
|
|
210
|
+
isReversible: !isHard, // soft and mixed are reversible
|
|
211
|
+
impact: {
|
|
212
|
+
filesAffected: modifiedFiles.length,
|
|
213
|
+
linesAffected: totalLines,
|
|
214
|
+
affectedFiles: modifiedFiles,
|
|
215
|
+
timeInvestment,
|
|
216
|
+
summary: isHard
|
|
217
|
+
? `Will discard ${String(modifiedFiles.length)} modified files (${String(totalLines)} lines of changes)`
|
|
218
|
+
: `Will unstage ${String(stagedStats.files)} files`,
|
|
219
|
+
},
|
|
220
|
+
warnings,
|
|
221
|
+
alternatives: isHard
|
|
222
|
+
? ['git stash (save changes for later)', 'git commit (save changes permanently)']
|
|
223
|
+
: undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Analyze git checkout -- or git restore
|
|
228
|
+
*/
|
|
229
|
+
async function analyzeGitDiscard(operation, context) {
|
|
230
|
+
await Promise.resolve(); // Ensure async function has await
|
|
231
|
+
const cwd = context.workingDirectory;
|
|
232
|
+
// Check if specific files are targeted
|
|
233
|
+
const filePattern = operation.match(/(?:checkout\s+--|restore\s+(?:--staged\s+)?)\s*(.+)$/i);
|
|
234
|
+
const targetPath = filePattern?.[1]?.trim() || '.';
|
|
235
|
+
let affectedFiles;
|
|
236
|
+
let diffStats;
|
|
237
|
+
if (targetPath === '.' || targetPath === '*') {
|
|
238
|
+
// All files
|
|
239
|
+
affectedFiles = getModifiedFiles(cwd);
|
|
240
|
+
diffStats = getDiffStats(cwd);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Specific file(s)
|
|
244
|
+
const output = execGit(`git diff --numstat -- "${targetPath}"`, cwd);
|
|
245
|
+
affectedFiles = [];
|
|
246
|
+
diffStats = { files: 0, insertions: 0, deletions: 0 };
|
|
247
|
+
if (output) {
|
|
248
|
+
for (const line of output.split('\n')) {
|
|
249
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
250
|
+
if (match) {
|
|
251
|
+
diffStats.insertions += parseInt(match[1], 10);
|
|
252
|
+
diffStats.deletions += parseInt(match[2], 10);
|
|
253
|
+
diffStats.files++;
|
|
254
|
+
affectedFiles.push({
|
|
255
|
+
path: match[3],
|
|
256
|
+
changeType: 'reset',
|
|
257
|
+
hasUncommittedChanges: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const totalLines = diffStats.insertions + diffStats.deletions;
|
|
264
|
+
const warnings = [];
|
|
265
|
+
if (affectedFiles.length > 0) {
|
|
266
|
+
warnings.push('This will permanently discard uncommitted changes');
|
|
267
|
+
}
|
|
268
|
+
if (totalLines > 50) {
|
|
269
|
+
warnings.push(`${String(totalLines)} lines of changes will be lost`);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
isDestructive: affectedFiles.length > 0,
|
|
273
|
+
isReversible: false,
|
|
274
|
+
impact: {
|
|
275
|
+
filesAffected: affectedFiles.length,
|
|
276
|
+
linesAffected: totalLines,
|
|
277
|
+
affectedFiles,
|
|
278
|
+
summary: affectedFiles.length > 0
|
|
279
|
+
? `Will discard changes in ${String(affectedFiles.length)} file(s) (${String(totalLines)} lines)`
|
|
280
|
+
: 'No changes to discard',
|
|
281
|
+
},
|
|
282
|
+
warnings,
|
|
283
|
+
alternatives: ['git stash (save changes for later)', 'git diff (review changes first)'],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Analyze git clean command
|
|
288
|
+
*/
|
|
289
|
+
async function analyzeGitClean(operation, context) {
|
|
290
|
+
await Promise.resolve(); // Ensure async function has await
|
|
291
|
+
const cwd = context.workingDirectory;
|
|
292
|
+
const untrackedFiles = getUntrackedFiles(cwd);
|
|
293
|
+
const warnings = [];
|
|
294
|
+
if (untrackedFiles.length > 0) {
|
|
295
|
+
warnings.push('This will permanently delete untracked files');
|
|
296
|
+
}
|
|
297
|
+
if (operation.includes('-x')) {
|
|
298
|
+
warnings.push('Including ignored files (from .gitignore)');
|
|
299
|
+
}
|
|
300
|
+
if (operation.includes('-d')) {
|
|
301
|
+
warnings.push('Including untracked directories');
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
isDestructive: untrackedFiles.length > 0,
|
|
305
|
+
isReversible: false,
|
|
306
|
+
impact: {
|
|
307
|
+
filesAffected: untrackedFiles.length,
|
|
308
|
+
linesAffected: 0,
|
|
309
|
+
affectedFiles: untrackedFiles,
|
|
310
|
+
summary: untrackedFiles.length > 0
|
|
311
|
+
? `Will delete ${String(untrackedFiles.length)} untracked file(s)`
|
|
312
|
+
: 'No untracked files to remove',
|
|
313
|
+
},
|
|
314
|
+
warnings,
|
|
315
|
+
alternatives: ['git clean -n (preview what would be deleted)'],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Analyze git stash drop
|
|
320
|
+
*/
|
|
321
|
+
async function analyzeGitStashDrop(operation, context) {
|
|
322
|
+
await Promise.resolve(); // Ensure async function has await
|
|
323
|
+
const cwd = context.workingDirectory;
|
|
324
|
+
// Get stash info
|
|
325
|
+
const stashMatch = operation.match(/stash@\{(\d+)\}/);
|
|
326
|
+
const stashIndex = stashMatch ? stashMatch[1] : '0';
|
|
327
|
+
const stashInfo = execGit(`git stash show stash@{${stashIndex}} --stat`, cwd);
|
|
328
|
+
const stashMessage = execGit(`git stash list | head -${String(parseInt(stashIndex, 10) + 1)} | tail -1`, cwd);
|
|
329
|
+
let filesAffected = 0;
|
|
330
|
+
if (stashInfo) {
|
|
331
|
+
const match = stashInfo.match(/(\d+) files? changed/);
|
|
332
|
+
filesAffected = match ? parseInt(match[1], 10) : 0;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
isDestructive: true,
|
|
336
|
+
isReversible: false,
|
|
337
|
+
impact: {
|
|
338
|
+
filesAffected,
|
|
339
|
+
linesAffected: 0,
|
|
340
|
+
affectedFiles: [],
|
|
341
|
+
summary: stashMessage
|
|
342
|
+
? `Will drop stash: ${stashMessage}`
|
|
343
|
+
: `Will drop stash@{${stashIndex}}`,
|
|
344
|
+
},
|
|
345
|
+
warnings: ['Dropped stashes cannot be recovered'],
|
|
346
|
+
alternatives: [
|
|
347
|
+
'git stash apply (apply without dropping)',
|
|
348
|
+
'git stash branch (create branch from stash)',
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Analyze git push --force
|
|
354
|
+
*/
|
|
355
|
+
async function analyzeGitForcePush(_operation, context) {
|
|
356
|
+
await Promise.resolve(); // Ensure async function has await
|
|
357
|
+
const cwd = context.workingDirectory;
|
|
358
|
+
const currentBranch = execGit('git rev-parse --abbrev-ref HEAD', cwd);
|
|
359
|
+
const warnings = [
|
|
360
|
+
'Force push will overwrite remote history',
|
|
361
|
+
'Other collaborators may lose their work',
|
|
362
|
+
];
|
|
363
|
+
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
364
|
+
warnings.unshift('⚠️ DANGER: Force pushing to main/master branch!');
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
isDestructive: true,
|
|
368
|
+
isReversible: false, // Technically recoverable with reflog, but practically destructive
|
|
369
|
+
impact: {
|
|
370
|
+
filesAffected: 0,
|
|
371
|
+
linesAffected: 0,
|
|
372
|
+
affectedFiles: [],
|
|
373
|
+
summary: `Will force push to remote, overwriting history on ${currentBranch}`,
|
|
374
|
+
},
|
|
375
|
+
warnings,
|
|
376
|
+
alternatives: [
|
|
377
|
+
'git push --force-with-lease (safer, fails if remote has new commits)',
|
|
378
|
+
'git pull --rebase && git push (sync with remote first)',
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Git Rehearsal Analyzer
|
|
384
|
+
*/
|
|
385
|
+
export class GitRehearsalAnalyzer {
|
|
386
|
+
id = 'git-analyzer';
|
|
387
|
+
name = 'Git Operations Analyzer';
|
|
388
|
+
category = 'git';
|
|
389
|
+
patterns = GIT_DESTRUCTIVE_PATTERNS;
|
|
390
|
+
canAnalyze(operation) {
|
|
391
|
+
return this.patterns.some((pattern) => pattern.test(operation));
|
|
392
|
+
}
|
|
393
|
+
async analyze(operation, context) {
|
|
394
|
+
const startTime = Date.now();
|
|
395
|
+
const cwd = context.workingDirectory;
|
|
396
|
+
// Check if we're in a git repo
|
|
397
|
+
if (!isGitRepo(cwd)) {
|
|
398
|
+
return {
|
|
399
|
+
operation,
|
|
400
|
+
category: 'git',
|
|
401
|
+
isDestructive: false,
|
|
402
|
+
isReversible: true,
|
|
403
|
+
impact: {
|
|
404
|
+
filesAffected: 0,
|
|
405
|
+
linesAffected: 0,
|
|
406
|
+
affectedFiles: [],
|
|
407
|
+
summary: 'Not a git repository',
|
|
408
|
+
},
|
|
409
|
+
severity: 'low',
|
|
410
|
+
warnings: ['Not in a git repository - command will fail'],
|
|
411
|
+
recommendation: 'abort',
|
|
412
|
+
analysisTimeMs: Date.now() - startTime,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// Determine which analysis to perform
|
|
416
|
+
let partialResult;
|
|
417
|
+
if (/git\s+reset/i.test(operation)) {
|
|
418
|
+
partialResult = await analyzeGitReset(operation, context);
|
|
419
|
+
}
|
|
420
|
+
else if (/git\s+(checkout\s+--|restore)/i.test(operation)) {
|
|
421
|
+
partialResult = await analyzeGitDiscard(operation, context);
|
|
422
|
+
}
|
|
423
|
+
else if (/git\s+clean/i.test(operation)) {
|
|
424
|
+
partialResult = await analyzeGitClean(operation, context);
|
|
425
|
+
}
|
|
426
|
+
else if (/git\s+stash\s+drop/i.test(operation)) {
|
|
427
|
+
partialResult = await analyzeGitStashDrop(operation, context);
|
|
428
|
+
}
|
|
429
|
+
else if (/git\s+push\s+.*--force|-f/i.test(operation)) {
|
|
430
|
+
partialResult = await analyzeGitForcePush(operation, context);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Generic destructive git operation
|
|
434
|
+
partialResult = {
|
|
435
|
+
isDestructive: true,
|
|
436
|
+
isReversible: false,
|
|
437
|
+
impact: {
|
|
438
|
+
filesAffected: 0,
|
|
439
|
+
linesAffected: 0,
|
|
440
|
+
affectedFiles: [],
|
|
441
|
+
summary: 'Potentially destructive git operation',
|
|
442
|
+
},
|
|
443
|
+
warnings: ['This git operation may cause data loss'],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const severity = calculateSeverity(partialResult.impact?.filesAffected ?? 0, partialResult.impact?.linesAffected ?? 0, partialResult.isDestructive ?? false);
|
|
447
|
+
const recommendation = calculateRecommendation(severity, partialResult.isReversible ?? false);
|
|
448
|
+
return {
|
|
449
|
+
operation,
|
|
450
|
+
category: 'git',
|
|
451
|
+
isDestructive: partialResult.isDestructive ?? false,
|
|
452
|
+
isReversible: partialResult.isReversible ?? false,
|
|
453
|
+
impact: partialResult.impact ?? {
|
|
454
|
+
filesAffected: 0,
|
|
455
|
+
linesAffected: 0,
|
|
456
|
+
affectedFiles: [],
|
|
457
|
+
summary: 'Unknown impact',
|
|
458
|
+
},
|
|
459
|
+
severity,
|
|
460
|
+
warnings: partialResult.warnings ?? [],
|
|
461
|
+
recommendation,
|
|
462
|
+
alternatives: partialResult.alternatives,
|
|
463
|
+
analysisTimeMs: Date.now() - startTime,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Create a Git rehearsal analyzer
|
|
469
|
+
*/
|
|
470
|
+
export function createGitAnalyzer() {
|
|
471
|
+
return new GitRehearsalAnalyzer();
|
|
472
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehearsal System
|
|
3
|
+
*
|
|
4
|
+
* Provides impact analysis for destructive operations before execution.
|
|
5
|
+
* This is a novel safety feature that goes beyond simple dry-run by
|
|
6
|
+
* actually analyzing what would be affected.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RehearsalManager } from '@compilr-dev/agents';
|
|
11
|
+
*
|
|
12
|
+
* const rehearsal = new RehearsalManager({
|
|
13
|
+
* workingDirectory: '/path/to/project',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Before executing a destructive command
|
|
17
|
+
* const result = await rehearsal.rehearse('git reset --hard HEAD~1');
|
|
18
|
+
*
|
|
19
|
+
* console.log(result.impact.summary);
|
|
20
|
+
* // "Will discard 5 modified files (127 lines of changes)"
|
|
21
|
+
*
|
|
22
|
+
* console.log(result.warnings);
|
|
23
|
+
* // ["--hard will permanently discard all uncommitted changes"]
|
|
24
|
+
*
|
|
25
|
+
* if (result.recommendation === 'abort') {
|
|
26
|
+
* console.log('Operation too dangerous!');
|
|
27
|
+
* } else if (result.recommendation === 'confirm') {
|
|
28
|
+
* // Ask user for confirmation
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type { ImpactSeverity, RehearsalRecommendation, OperationCategory, AffectedFile, RehearsalImpact, RehearsalResult, RehearsalContext, RehearsalAnalyzer, RehearsalManagerOptions, RehearsalEventType, RehearsalEvent, RehearsalEventHandler, } from './types.js';
|
|
33
|
+
export { RehearsalManager, createRehearsalManager } from './manager.js';
|
|
34
|
+
export { GitRehearsalAnalyzer, createGitAnalyzer } from './git-analyzer.js';
|
|
35
|
+
export { FileRehearsalAnalyzer, createFileAnalyzer } from './file-analyzer.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehearsal System
|
|
3
|
+
*
|
|
4
|
+
* Provides impact analysis for destructive operations before execution.
|
|
5
|
+
* This is a novel safety feature that goes beyond simple dry-run by
|
|
6
|
+
* actually analyzing what would be affected.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RehearsalManager } from '@compilr-dev/agents';
|
|
11
|
+
*
|
|
12
|
+
* const rehearsal = new RehearsalManager({
|
|
13
|
+
* workingDirectory: '/path/to/project',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Before executing a destructive command
|
|
17
|
+
* const result = await rehearsal.rehearse('git reset --hard HEAD~1');
|
|
18
|
+
*
|
|
19
|
+
* console.log(result.impact.summary);
|
|
20
|
+
* // "Will discard 5 modified files (127 lines of changes)"
|
|
21
|
+
*
|
|
22
|
+
* console.log(result.warnings);
|
|
23
|
+
* // ["--hard will permanently discard all uncommitted changes"]
|
|
24
|
+
*
|
|
25
|
+
* if (result.recommendation === 'abort') {
|
|
26
|
+
* console.log('Operation too dangerous!');
|
|
27
|
+
* } else if (result.recommendation === 'confirm') {
|
|
28
|
+
* // Ask user for confirmation
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
// Manager
|
|
33
|
+
export { RehearsalManager, createRehearsalManager } from './manager.js';
|
|
34
|
+
// Built-in analyzers
|
|
35
|
+
export { GitRehearsalAnalyzer, createGitAnalyzer } from './git-analyzer.js';
|
|
36
|
+
export { FileRehearsalAnalyzer, createFileAnalyzer } from './file-analyzer.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehearsal Manager
|
|
3
|
+
*
|
|
4
|
+
* Coordinates rehearsal analyzers to provide impact analysis
|
|
5
|
+
* for destructive operations before they are executed.
|
|
6
|
+
*/
|
|
7
|
+
import type { RehearsalAnalyzer, RehearsalResult, RehearsalManagerOptions, RehearsalEventHandler } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* RehearsalManager - Coordinates impact analysis for destructive operations
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const manager = new RehearsalManager({
|
|
14
|
+
* workingDirectory: '/path/to/project',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Analyze before executing
|
|
18
|
+
* const result = await manager.rehearse('rm -rf node_modules');
|
|
19
|
+
*
|
|
20
|
+
* if (result.recommendation === 'abort') {
|
|
21
|
+
* console.log('Operation too dangerous:', result.warnings);
|
|
22
|
+
* } else if (result.recommendation === 'confirm') {
|
|
23
|
+
* const confirmed = await askUser(result.impact.summary);
|
|
24
|
+
* if (confirmed) {
|
|
25
|
+
* // Execute the operation
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class RehearsalManager {
|
|
31
|
+
private readonly analyzers;
|
|
32
|
+
private readonly workingDirectory;
|
|
33
|
+
private readonly warningThreshold;
|
|
34
|
+
private readonly sessionStartTime;
|
|
35
|
+
private readonly sessionModifiedFiles;
|
|
36
|
+
private readonly trackSessionFiles;
|
|
37
|
+
private eventHandler?;
|
|
38
|
+
constructor(options?: RehearsalManagerOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Register an analyzer
|
|
41
|
+
*/
|
|
42
|
+
registerAnalyzer(analyzer: RehearsalAnalyzer): this;
|
|
43
|
+
/**
|
|
44
|
+
* Unregister an analyzer
|
|
45
|
+
*/
|
|
46
|
+
unregisterAnalyzer(id: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Get all registered analyzers
|
|
49
|
+
*/
|
|
50
|
+
getAnalyzers(): RehearsalAnalyzer[];
|
|
51
|
+
/**
|
|
52
|
+
* Set event handler
|
|
53
|
+
*/
|
|
54
|
+
onEvent(handler: RehearsalEventHandler): this;
|
|
55
|
+
/**
|
|
56
|
+
* Emit an event
|
|
57
|
+
*/
|
|
58
|
+
private emit;
|
|
59
|
+
/**
|
|
60
|
+
* Track a file as modified in this session
|
|
61
|
+
*/
|
|
62
|
+
trackFileModification(filePath: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Get files modified in this session
|
|
65
|
+
*/
|
|
66
|
+
getSessionModifiedFiles(): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Check if an operation is potentially destructive
|
|
69
|
+
*/
|
|
70
|
+
isDestructive(operation: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Find the appropriate analyzer for an operation
|
|
73
|
+
*/
|
|
74
|
+
findAnalyzer(operation: string): RehearsalAnalyzer | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Rehearse an operation - analyze its impact before execution
|
|
77
|
+
*
|
|
78
|
+
* @param operation - The command or operation to analyze
|
|
79
|
+
* @returns Impact analysis result
|
|
80
|
+
*/
|
|
81
|
+
rehearse(operation: string): Promise<RehearsalResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Rehearse multiple operations
|
|
84
|
+
*/
|
|
85
|
+
rehearseAll(operations: string[]): Promise<RehearsalResult[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Check if an operation should proceed based on rehearsal
|
|
88
|
+
*
|
|
89
|
+
* @returns true if safe to proceed, false if should abort
|
|
90
|
+
*/
|
|
91
|
+
shouldProceed(operation: string): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Get a formatted summary of a rehearsal result
|
|
94
|
+
*/
|
|
95
|
+
formatResult(result: RehearsalResult): string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a RehearsalManager with default configuration
|
|
99
|
+
*/
|
|
100
|
+
export declare function createRehearsalManager(options?: RehearsalManagerOptions): RehearsalManager;
|