bytarch-cli 0.1.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/.genie-cli/config.json +7 -0
- package/.genie-cli/plan.json +10 -0
- package/bin/bytarch-cli.js +3 -0
- package/dist/commands/apply.d.ts +9 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js +118 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +189 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/edit.d.ts +8 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +152 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +89 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/plan.d.ts +10 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +173 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/review.d.ts +13 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +240 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +122 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/fs/patcher.d.ts +19 -0
- package/dist/fs/patcher.d.ts.map +1 -0
- package/dist/fs/patcher.js +210 -0
- package/dist/fs/patcher.js.map +1 -0
- package/dist/fs/safe-readwrite.d.ts +20 -0
- package/dist/fs/safe-readwrite.d.ts.map +1 -0
- package/dist/fs/safe-readwrite.js +146 -0
- package/dist/fs/safe-readwrite.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/provider/bytarch.d.ts +26 -0
- package/dist/provider/bytarch.d.ts.map +1 -0
- package/dist/provider/bytarch.js +54 -0
- package/dist/provider/bytarch.js.map +1 -0
- package/dist/provider/evaluation.d.ts +5 -0
- package/dist/provider/evaluation.d.ts.map +1 -0
- package/dist/provider/evaluation.js +40 -0
- package/dist/provider/evaluation.js.map +1 -0
- package/dist/provider/prompts.d.ts +4 -0
- package/dist/provider/prompts.d.ts.map +1 -0
- package/dist/provider/prompts.js +61 -0
- package/dist/provider/prompts.js.map +1 -0
- package/dist/ui/prompts.d.ts +43 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +122 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/workspace/config.d.ts +19 -0
- package/dist/workspace/config.d.ts.map +1 -0
- package/dist/workspace/config.js +102 -0
- package/dist/workspace/config.js.map +1 -0
- package/dist/workspace/edits.d.ts +24 -0
- package/dist/workspace/edits.d.ts.map +1 -0
- package/dist/workspace/edits.js +119 -0
- package/dist/workspace/edits.js.map +1 -0
- package/dist/workspace/plan.d.ts +29 -0
- package/dist/workspace/plan.d.ts.map +1 -0
- package/dist/workspace/plan.js +101 -0
- package/dist/workspace/plan.js.map +1 -0
- package/package.json +52 -0
- package/src/commands/apply.ts +91 -0
- package/src/commands/config.ts +194 -0
- package/src/commands/edit.ts +133 -0
- package/src/commands/init.ts +62 -0
- package/src/commands/plan.ts +163 -0
- package/src/commands/review.ts +242 -0
- package/src/commands/status.ts +101 -0
- package/src/fs/patcher.ts +188 -0
- package/src/fs/safe-readwrite.ts +141 -0
- package/src/index.ts +123 -0
- package/src/provider/bytarch.ts +75 -0
- package/src/provider/evaluation.ts +39 -0
- package/src/provider/prompts.ts +59 -0
- package/src/ui/prompts.ts +163 -0
- package/src/workspace/config.ts +86 -0
- package/src/workspace/edits.ts +106 -0
- package/src/workspace/plan.ts +92 -0
- package/tsconfig.json +36 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { PlanManager } from '../workspace/plan';
|
|
3
|
+
import { EditManager } from '../workspace/edits';
|
|
4
|
+
import { UI } from '../ui/prompts';
|
|
5
|
+
|
|
6
|
+
export class ReviewCommand {
|
|
7
|
+
static async execute(options: { plan?: boolean; edits?: boolean }): Promise<void> {
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
10
|
+
|
|
11
|
+
const planManager = new PlanManager(workspacePath);
|
|
12
|
+
const editManager = new EditManager(workspacePath);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const showPlan = !options.edits;
|
|
16
|
+
const showEdits = !options.plan;
|
|
17
|
+
|
|
18
|
+
if (showPlan) {
|
|
19
|
+
await this.reviewPlan(planManager);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (showEdits) {
|
|
23
|
+
await this.reviewEdits(editManager);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!showPlan && !showEdits) {
|
|
27
|
+
UI.info('Nothing to review.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Failed to review: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static async reviewPlan(planManager: PlanManager): Promise<void> {
|
|
36
|
+
UI.header('📋 Plan Review');
|
|
37
|
+
|
|
38
|
+
const plan = await planManager.loadPlan();
|
|
39
|
+
|
|
40
|
+
if (plan.items.length === 0) {
|
|
41
|
+
UI.warning('No tasks in the plan.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`Plan: ${plan.title}`);
|
|
46
|
+
console.log(`Tasks: ${plan.items.length}`);
|
|
47
|
+
console.log('');
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < plan.items.length; i++) {
|
|
50
|
+
const item = plan.items[i];
|
|
51
|
+
const status = this.getStatusIcon(item.status);
|
|
52
|
+
|
|
53
|
+
console.log(`${i + 1}. ${status} ${item.title}`);
|
|
54
|
+
|
|
55
|
+
if (item.description) {
|
|
56
|
+
console.log(` ${item.description}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (item.files && item.files.length > 0) {
|
|
60
|
+
console.log(` Files: ${item.files.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (item.rationale) {
|
|
64
|
+
console.log(` Rationale: ${item.rationale}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const shouldManage = await UI.confirm({
|
|
71
|
+
message: 'Manage plan items?',
|
|
72
|
+
default: false
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (shouldManage) {
|
|
76
|
+
await this.managePlanItems(planManager, plan);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static async reviewEdits(editManager: EditManager): Promise<void> {
|
|
81
|
+
UI.header('📝 Edits Review');
|
|
82
|
+
|
|
83
|
+
const edits = await editManager.listEdits();
|
|
84
|
+
|
|
85
|
+
if (edits.length === 0) {
|
|
86
|
+
UI.warning('No edits found.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pendingEdits = edits.filter(e => e.status === 'pending' || e.status === 'accepted');
|
|
91
|
+
|
|
92
|
+
if (pendingEdits.length === 0) {
|
|
93
|
+
UI.info('No pending edits to review.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Pending edits: ${pendingEdits.length}`);
|
|
98
|
+
console.log('');
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < pendingEdits.length; i++) {
|
|
101
|
+
const edit = pendingEdits[i];
|
|
102
|
+
const status = edit.status === 'accepted' ? '✓' : '○';
|
|
103
|
+
|
|
104
|
+
console.log(`${i + 1}. ${status} ${edit.path} (${edit.id})`);
|
|
105
|
+
console.log(` ${edit.description}`);
|
|
106
|
+
|
|
107
|
+
if (edit.rationale) {
|
|
108
|
+
console.log(` Rationale: ${edit.rationale}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const shouldManage = await UI.confirm({
|
|
115
|
+
message: 'Manage edits?',
|
|
116
|
+
default: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (shouldManage) {
|
|
120
|
+
await this.manageEdits(editManager, pendingEdits);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static async managePlanItems(planManager: PlanManager, plan: any): Promise<void> {
|
|
125
|
+
const action = await UI.select({
|
|
126
|
+
message: 'What would you like to do?',
|
|
127
|
+
choices: [
|
|
128
|
+
{ name: 'Update item status', value: 'status' },
|
|
129
|
+
{ name: 'Delete item', value: 'delete' },
|
|
130
|
+
{ name: 'Done', value: 'done' }
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (action === 'done') return;
|
|
135
|
+
|
|
136
|
+
const itemIndex = await UI.select({
|
|
137
|
+
message: 'Select item:',
|
|
138
|
+
choices: plan.items.map((item: any, index: number) => ({
|
|
139
|
+
name: `${item.title}`,
|
|
140
|
+
value: index
|
|
141
|
+
}))
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const item = plan.items[itemIndex];
|
|
145
|
+
|
|
146
|
+
if (action === 'status') {
|
|
147
|
+
const newStatus = await UI.select({
|
|
148
|
+
message: 'Select new status:',
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: 'Pending', value: 'pending' },
|
|
151
|
+
{ name: 'In Progress', value: 'in_progress' },
|
|
152
|
+
{ name: 'Completed', value: 'completed' }
|
|
153
|
+
],
|
|
154
|
+
default: item.status
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await planManager.updatePlanItem(item.id, { status: newStatus });
|
|
158
|
+
UI.success(`Updated ${item.title} status to ${newStatus}`);
|
|
159
|
+
} else if (action === 'delete') {
|
|
160
|
+
const shouldDelete = await UI.confirm({
|
|
161
|
+
message: `Delete "${item.title}"?`,
|
|
162
|
+
default: false
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (shouldDelete) {
|
|
166
|
+
await planManager.removePlanItem(item.id);
|
|
167
|
+
UI.success(`Deleted ${item.title}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private static async manageEdits(editManager: EditManager, edits: any[]): Promise<void> {
|
|
173
|
+
const action = await UI.select({
|
|
174
|
+
message: 'What would you like to do?',
|
|
175
|
+
choices: [
|
|
176
|
+
{ name: 'Accept/Reject edit', value: 'status' },
|
|
177
|
+
{ name: 'Preview patch', value: 'preview' },
|
|
178
|
+
{ name: 'Delete edit', value: 'delete' },
|
|
179
|
+
{ name: 'Done', value: 'done' }
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (action === 'done') return;
|
|
184
|
+
|
|
185
|
+
const edit = await UI.select({
|
|
186
|
+
message: 'Select edit:',
|
|
187
|
+
choices: edits.map((e: any) => ({
|
|
188
|
+
name: `${e.path} - ${e.description}`,
|
|
189
|
+
value: e
|
|
190
|
+
}))
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (action === 'status') {
|
|
194
|
+
const newStatus = await UI.select({
|
|
195
|
+
message: 'Select status:',
|
|
196
|
+
choices: [
|
|
197
|
+
{ name: 'Pending', value: 'pending' },
|
|
198
|
+
{ name: 'Accepted', value: 'accepted' },
|
|
199
|
+
{ name: 'Rejected', value: 'rejected' }
|
|
200
|
+
],
|
|
201
|
+
default: edit.status
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await editManager.updateEditStatus(edit.id, newStatus);
|
|
205
|
+
UI.success(`Updated ${edit.path} status to ${newStatus}`);
|
|
206
|
+
} else if (action === 'preview') {
|
|
207
|
+
await this.previewEdit(edit);
|
|
208
|
+
} else if (action === 'delete') {
|
|
209
|
+
const shouldDelete = await UI.confirm({
|
|
210
|
+
message: `Delete edit for ${edit.path}?`,
|
|
211
|
+
default: false
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (shouldDelete) {
|
|
215
|
+
await editManager.deleteEdit(edit.id);
|
|
216
|
+
UI.success(`Deleted edit for ${edit.path}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static async previewEdit(edit: any): Promise<void> {
|
|
222
|
+
const { Patcher } = await import('../fs/patcher');
|
|
223
|
+
const projectRoot = process.cwd();
|
|
224
|
+
const patcher = new Patcher(projectRoot);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const preview = await patcher.previewPatch(edit);
|
|
228
|
+
console.log(preview);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
UI.error(`Failed to preview patch: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private static getStatusIcon(status: string): string {
|
|
235
|
+
switch (status) {
|
|
236
|
+
case 'completed': return '✓';
|
|
237
|
+
case 'in_progress': return '⚡';
|
|
238
|
+
case 'pending': return '○';
|
|
239
|
+
default: return '?';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { ConfigManager } from '../workspace/config';
|
|
3
|
+
import { PlanManager } from '../workspace/plan';
|
|
4
|
+
import { EditManager } from '../workspace/edits';
|
|
5
|
+
import { UI } from '../ui/prompts';
|
|
6
|
+
|
|
7
|
+
export class StatusCommand {
|
|
8
|
+
static async execute(options: { verbose?: boolean }): Promise<void> {
|
|
9
|
+
const projectRoot = process.cwd();
|
|
10
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const configManager = new ConfigManager(workspacePath);
|
|
14
|
+
const planManager = new PlanManager(workspacePath);
|
|
15
|
+
const editManager = new EditManager(workspacePath);
|
|
16
|
+
|
|
17
|
+
UI.header('📊 Workspace Status');
|
|
18
|
+
|
|
19
|
+
if (!(await require('fs-extra').pathExists(workspacePath))) {
|
|
20
|
+
UI.error('No .genie-cli workspace found. Run "bytarch-cli init" to create one.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const config = await configManager.loadConfig();
|
|
25
|
+
const plan = await planManager.loadPlan();
|
|
26
|
+
const edits = await editManager.listEdits();
|
|
27
|
+
|
|
28
|
+
this.displayConfig(config);
|
|
29
|
+
this.displayPlanStatus(plan, !!options.verbose);
|
|
30
|
+
this.displayEditStatus(edits, !!options.verbose);
|
|
31
|
+
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`Failed to get status: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static displayConfig(config: any): void {
|
|
38
|
+
UI.info('Configuration:');
|
|
39
|
+
console.log(` Provider: ${config.provider}`);
|
|
40
|
+
console.log(` Model: ${config.model}`);
|
|
41
|
+
console.log(` Endpoint: ${config.endpoint}`);
|
|
42
|
+
console.log(` Dry Run: ${config.dryRun ? 'Enabled' : 'Disabled'}`);
|
|
43
|
+
console.log(` API Key: ${config.apiKey ? 'Configured' : 'Not configured'}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private static displayPlanStatus(plan: any, verbose: boolean): void {
|
|
48
|
+
UI.info('Plan Status:');
|
|
49
|
+
console.log(` Title: ${plan.title}`);
|
|
50
|
+
console.log(` Tasks: ${plan.items.length}`);
|
|
51
|
+
|
|
52
|
+
const completed = plan.items.filter((item: any) => item.status === 'completed').length;
|
|
53
|
+
const inProgress = plan.items.filter((item: any) => item.status === 'in_progress').length;
|
|
54
|
+
const pending = plan.items.filter((item: any) => item.status === 'pending').length;
|
|
55
|
+
|
|
56
|
+
console.log(` ✓ Completed: ${completed}`);
|
|
57
|
+
console.log(` ⚡ In Progress: ${inProgress}`);
|
|
58
|
+
console.log(` ○ Pending: ${pending}`);
|
|
59
|
+
|
|
60
|
+
if (verbose && plan.items.length > 0) {
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(' Tasks:');
|
|
63
|
+
plan.items.forEach((item: any, index: number) => {
|
|
64
|
+
const status = item.status === 'completed' ? '✓' :
|
|
65
|
+
item.status === 'in_progress' ? '⚡' : '○';
|
|
66
|
+
console.log(` ${index + 1}. ${status} ${item.title}`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static displayEditStatus(edits: any[], verbose: boolean): void {
|
|
74
|
+
UI.info('Edit Status:');
|
|
75
|
+
console.log(` Total Edits: ${edits.length}`);
|
|
76
|
+
|
|
77
|
+
const applied = edits.filter(edit => edit.status === 'applied').length;
|
|
78
|
+
const accepted = edits.filter(edit => edit.status === 'accepted').length;
|
|
79
|
+
const pending = edits.filter(edit => edit.status === 'pending').length;
|
|
80
|
+
const rejected = edits.filter(edit => edit.status === 'rejected').length;
|
|
81
|
+
|
|
82
|
+
console.log(` ✓ Applied: ${applied}`);
|
|
83
|
+
console.log(` ✓ Accepted: ${accepted}`);
|
|
84
|
+
console.log(` ○ Pending: ${pending}`);
|
|
85
|
+
console.log(` ✗ Rejected: ${rejected}`);
|
|
86
|
+
|
|
87
|
+
if (verbose && edits.length > 0) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(' Recent Edits:');
|
|
90
|
+
const recentEdits = edits.slice(0, 5);
|
|
91
|
+
recentEdits.forEach((edit, index) => {
|
|
92
|
+
const status = edit.status === 'applied' ? '✓' :
|
|
93
|
+
edit.status === 'accepted' ? '✓' :
|
|
94
|
+
edit.status === 'pending' ? '○' : '✗';
|
|
95
|
+
console.log(` ${index + 1}. ${status} ${edit.path} - ${edit.description}`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as diff from 'diff';
|
|
4
|
+
import { FileEdit } from '../workspace/edits';
|
|
5
|
+
|
|
6
|
+
export interface PatchResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
appliedChanges?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Patcher {
|
|
13
|
+
private projectRoot: string;
|
|
14
|
+
|
|
15
|
+
constructor(projectRoot: string) {
|
|
16
|
+
this.projectRoot = projectRoot;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async previewPatch(edit: FileEdit): Promise<string> {
|
|
20
|
+
const filePath = this.resolvePath(edit.path);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
let originalContent = '';
|
|
24
|
+
if (await fs.pathExists(filePath)) {
|
|
25
|
+
originalContent = await fs.readFile(filePath, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return this.formatPatchPreview(edit.patch, filePath, originalContent);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to preview patch for ${edit.path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async applyPatch(edit: FileEdit): Promise<PatchResult> {
|
|
35
|
+
const filePath = this.resolvePath(edit.path);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await this.ensureDirectoryExists(filePath);
|
|
39
|
+
|
|
40
|
+
let originalContent = '';
|
|
41
|
+
if (await fs.pathExists(filePath)) {
|
|
42
|
+
originalContent = await fs.readFile(filePath, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = this.applyUnifiedDiff(originalContent, edit.patch);
|
|
46
|
+
|
|
47
|
+
if (result.success) {
|
|
48
|
+
await fs.writeFile(filePath, result.content!, 'utf8');
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
appliedChanges: result.changes
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: result.error
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private formatPatchPreview(patchString: string, filePath: string, originalContent: string): string {
|
|
68
|
+
const lines = patchString.split('\n');
|
|
69
|
+
let preview = `\n📁 File: ${filePath}\n`;
|
|
70
|
+
preview += `${'='.repeat(60)}\n\n`;
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
74
|
+
preview += `\x1b[36m${line}\x1b[0m\n`;
|
|
75
|
+
} else if (line.startsWith('@@')) {
|
|
76
|
+
preview += `\x1b[33m${line}\x1b[0m\n`;
|
|
77
|
+
} else if (line.startsWith('+')) {
|
|
78
|
+
preview += `\x1b[32m${line}\x1b[0m\n`;
|
|
79
|
+
} else if (line.startsWith('-')) {
|
|
80
|
+
preview += `\x1b[31m${line}\x1b[0m\n`;
|
|
81
|
+
} else if (line.startsWith(' ')) {
|
|
82
|
+
preview += `${line}\n`;
|
|
83
|
+
} else {
|
|
84
|
+
preview += `${line}\n`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return preview;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private applyUnifiedDiff(originalContent: string, patchString: string): { success: boolean; content?: string; changes?: number; error?: string } {
|
|
92
|
+
try {
|
|
93
|
+
const lines = patchString.split('\n');
|
|
94
|
+
let isHeader = true;
|
|
95
|
+
let hunkStart = -1;
|
|
96
|
+
let hunkLines = 0;
|
|
97
|
+
let contextLines = 0;
|
|
98
|
+
let deletions = 0;
|
|
99
|
+
let additions = 0;
|
|
100
|
+
|
|
101
|
+
let result = originalContent.split('\n');
|
|
102
|
+
let resultIndex = 0;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
const line = lines[i];
|
|
106
|
+
|
|
107
|
+
if (isHeader) {
|
|
108
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
109
|
+
continue;
|
|
110
|
+
} else if (line.startsWith('@@')) {
|
|
111
|
+
isHeader = false;
|
|
112
|
+
const match = line.match(/@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/);
|
|
113
|
+
if (match) {
|
|
114
|
+
hunkStart = parseInt(match[1]) - 1;
|
|
115
|
+
hunkLines = parseInt(match[4] || match[2] || '1');
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
} else {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (line.startsWith(' ')) {
|
|
124
|
+
contextLines++;
|
|
125
|
+
resultIndex++;
|
|
126
|
+
} else if (line.startsWith('-')) {
|
|
127
|
+
deletions++;
|
|
128
|
+
if (resultIndex < result.length) {
|
|
129
|
+
result.splice(resultIndex, 1);
|
|
130
|
+
}
|
|
131
|
+
} else if (line.startsWith('+')) {
|
|
132
|
+
additions++;
|
|
133
|
+
result.splice(resultIndex, 0, line.substring(1));
|
|
134
|
+
resultIndex++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
content: result.join('\n'),
|
|
141
|
+
changes: additions + deletions
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: error instanceof Error ? error.message : 'Unknown patch application error'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private resolvePath(relativePath: string): string {
|
|
152
|
+
const sanitized = relativePath.replace(/\.\./g, '').replace(/^\//, '');
|
|
153
|
+
return path.resolve(this.projectRoot, sanitized);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async ensureDirectoryExists(filePath: string): Promise<void> {
|
|
157
|
+
const dir = path.dirname(filePath);
|
|
158
|
+
await fs.ensureDir(dir);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async createBackup(filePath: string): Promise<string | null> {
|
|
162
|
+
try {
|
|
163
|
+
if (await fs.pathExists(filePath)) {
|
|
164
|
+
const backupPath = `${filePath}.backup.${Date.now()}`;
|
|
165
|
+
await fs.copy(filePath, backupPath);
|
|
166
|
+
return backupPath;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn(`Failed to create backup for ${filePath}:`, error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async restoreFromBackup(filePath: string, backupPath: string): Promise<boolean> {
|
|
176
|
+
try {
|
|
177
|
+
if (await fs.pathExists(backupPath)) {
|
|
178
|
+
await fs.copy(backupPath, filePath);
|
|
179
|
+
await fs.remove(backupPath);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.warn(`Failed to restore from backup ${backupPath}:`, error);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export class SafeFileSystem {
|
|
5
|
+
private projectRoot: string;
|
|
6
|
+
private allowedPaths: Set<string>;
|
|
7
|
+
|
|
8
|
+
constructor(projectRoot: string) {
|
|
9
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
10
|
+
this.allowedPaths = new Set();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
setAllowedPaths(paths: string[]): void {
|
|
14
|
+
this.allowedPaths = new Set(paths.map(p => path.resolve(this.projectRoot, p)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async safeReadFile(filePath: string): Promise<string> {
|
|
18
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
19
|
+
this.validatePath(resolvedPath);
|
|
20
|
+
|
|
21
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
22
|
+
throw new Error(`File does not exist: ${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stats = await fs.stat(resolvedPath);
|
|
26
|
+
if (!stats.isFile()) {
|
|
27
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return await fs.readFile(resolvedPath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async safeWriteFile(filePath: string, content: string): Promise<void> {
|
|
34
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
35
|
+
this.validatePath(resolvedPath);
|
|
36
|
+
|
|
37
|
+
await fs.ensureDir(path.dirname(resolvedPath));
|
|
38
|
+
await fs.writeFile(resolvedPath, content, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async safeReadDir(dirPath: string): Promise<string[]> {
|
|
42
|
+
const resolvedPath = this.resolvePath(dirPath);
|
|
43
|
+
this.validatePath(resolvedPath);
|
|
44
|
+
|
|
45
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
46
|
+
throw new Error(`Directory does not exist: ${dirPath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stats = await fs.stat(resolvedPath);
|
|
50
|
+
if (!stats.isDirectory()) {
|
|
51
|
+
throw new Error(`Path is not a directory: ${dirPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return await fs.readdir(resolvedPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async safeExists(filePath: string): Promise<boolean> {
|
|
58
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
59
|
+
this.validatePath(resolvedPath);
|
|
60
|
+
return await fs.pathExists(resolvedPath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async safeStat(filePath: string): Promise<fs.Stats | null> {
|
|
64
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
65
|
+
this.validatePath(resolvedPath);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
return await fs.stat(resolvedPath);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async safeCopy(src: string, dest: string): Promise<void> {
|
|
75
|
+
const srcPath = this.resolvePath(src);
|
|
76
|
+
const destPath = this.resolvePath(dest);
|
|
77
|
+
|
|
78
|
+
this.validatePath(srcPath);
|
|
79
|
+
this.validatePath(destPath);
|
|
80
|
+
|
|
81
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
82
|
+
await fs.copy(srcPath, destPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async safeMove(src: string, dest: string): Promise<void> {
|
|
86
|
+
const srcPath = this.resolvePath(src);
|
|
87
|
+
const destPath = this.resolvePath(dest);
|
|
88
|
+
|
|
89
|
+
this.validatePath(srcPath);
|
|
90
|
+
this.validatePath(destPath);
|
|
91
|
+
|
|
92
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
93
|
+
await fs.move(srcPath, destPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async safeRemove(target: string): Promise<void> {
|
|
97
|
+
const resolvedPath = this.resolvePath(target);
|
|
98
|
+
this.validatePath(resolvedPath);
|
|
99
|
+
await fs.remove(resolvedPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getFileExtension(filePath: string): string {
|
|
103
|
+
return path.extname(filePath).toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getRelativePath(filePath: string): string {
|
|
107
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
108
|
+
return path.relative(this.projectRoot, resolvedPath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private resolvePath(filePath: string): string {
|
|
112
|
+
const normalized = path.normalize(filePath);
|
|
113
|
+
|
|
114
|
+
if (path.isAbsolute(normalized)) {
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return path.resolve(this.projectRoot, normalized);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private validatePath(resolvedPath: string): void {
|
|
122
|
+
if (!resolvedPath.startsWith(this.projectRoot)) {
|
|
123
|
+
throw new Error(`Access denied: Path ${resolvedPath} is outside project root ${this.projectRoot}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (this.allowedPaths.size > 0) {
|
|
127
|
+
const isAllowed = Array.from(this.allowedPaths).some(allowed =>
|
|
128
|
+
resolvedPath.startsWith(allowed) || resolvedPath === allowed
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!isAllowed) {
|
|
132
|
+
throw new Error(`Access denied: Path ${resolvedPath} is not in allowed paths`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const relativePath = path.relative(this.projectRoot, resolvedPath);
|
|
137
|
+
if (relativePath.includes('..')) {
|
|
138
|
+
throw new Error(`Access denied: Path traversal detected in ${relativePath}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|