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.
Files changed (90) hide show
  1. package/.genie-cli/config.json +7 -0
  2. package/.genie-cli/plan.json +10 -0
  3. package/bin/bytarch-cli.js +3 -0
  4. package/dist/commands/apply.d.ts +9 -0
  5. package/dist/commands/apply.d.ts.map +1 -0
  6. package/dist/commands/apply.js +118 -0
  7. package/dist/commands/apply.js.map +1 -0
  8. package/dist/commands/config.d.ts +14 -0
  9. package/dist/commands/config.d.ts.map +1 -0
  10. package/dist/commands/config.js +189 -0
  11. package/dist/commands/config.js.map +1 -0
  12. package/dist/commands/edit.d.ts +8 -0
  13. package/dist/commands/edit.d.ts.map +1 -0
  14. package/dist/commands/edit.js +152 -0
  15. package/dist/commands/edit.js.map +1 -0
  16. package/dist/commands/init.d.ts +6 -0
  17. package/dist/commands/init.d.ts.map +1 -0
  18. package/dist/commands/init.js +89 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/plan.d.ts +10 -0
  21. package/dist/commands/plan.d.ts.map +1 -0
  22. package/dist/commands/plan.js +173 -0
  23. package/dist/commands/plan.js.map +1 -0
  24. package/dist/commands/review.d.ts +13 -0
  25. package/dist/commands/review.d.ts.map +1 -0
  26. package/dist/commands/review.js +240 -0
  27. package/dist/commands/review.js.map +1 -0
  28. package/dist/commands/status.d.ts +9 -0
  29. package/dist/commands/status.d.ts.map +1 -0
  30. package/dist/commands/status.js +122 -0
  31. package/dist/commands/status.js.map +1 -0
  32. package/dist/fs/patcher.d.ts +19 -0
  33. package/dist/fs/patcher.d.ts.map +1 -0
  34. package/dist/fs/patcher.js +210 -0
  35. package/dist/fs/patcher.js.map +1 -0
  36. package/dist/fs/safe-readwrite.d.ts +20 -0
  37. package/dist/fs/safe-readwrite.d.ts.map +1 -0
  38. package/dist/fs/safe-readwrite.js +146 -0
  39. package/dist/fs/safe-readwrite.js.map +1 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +123 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/provider/bytarch.d.ts +26 -0
  45. package/dist/provider/bytarch.d.ts.map +1 -0
  46. package/dist/provider/bytarch.js +54 -0
  47. package/dist/provider/bytarch.js.map +1 -0
  48. package/dist/provider/evaluation.d.ts +5 -0
  49. package/dist/provider/evaluation.d.ts.map +1 -0
  50. package/dist/provider/evaluation.js +40 -0
  51. package/dist/provider/evaluation.js.map +1 -0
  52. package/dist/provider/prompts.d.ts +4 -0
  53. package/dist/provider/prompts.d.ts.map +1 -0
  54. package/dist/provider/prompts.js +61 -0
  55. package/dist/provider/prompts.js.map +1 -0
  56. package/dist/ui/prompts.d.ts +43 -0
  57. package/dist/ui/prompts.d.ts.map +1 -0
  58. package/dist/ui/prompts.js +122 -0
  59. package/dist/ui/prompts.js.map +1 -0
  60. package/dist/workspace/config.d.ts +19 -0
  61. package/dist/workspace/config.d.ts.map +1 -0
  62. package/dist/workspace/config.js +102 -0
  63. package/dist/workspace/config.js.map +1 -0
  64. package/dist/workspace/edits.d.ts +24 -0
  65. package/dist/workspace/edits.d.ts.map +1 -0
  66. package/dist/workspace/edits.js +119 -0
  67. package/dist/workspace/edits.js.map +1 -0
  68. package/dist/workspace/plan.d.ts +29 -0
  69. package/dist/workspace/plan.d.ts.map +1 -0
  70. package/dist/workspace/plan.js +101 -0
  71. package/dist/workspace/plan.js.map +1 -0
  72. package/package.json +52 -0
  73. package/src/commands/apply.ts +91 -0
  74. package/src/commands/config.ts +194 -0
  75. package/src/commands/edit.ts +133 -0
  76. package/src/commands/init.ts +62 -0
  77. package/src/commands/plan.ts +163 -0
  78. package/src/commands/review.ts +242 -0
  79. package/src/commands/status.ts +101 -0
  80. package/src/fs/patcher.ts +188 -0
  81. package/src/fs/safe-readwrite.ts +141 -0
  82. package/src/index.ts +123 -0
  83. package/src/provider/bytarch.ts +75 -0
  84. package/src/provider/evaluation.ts +39 -0
  85. package/src/provider/prompts.ts +59 -0
  86. package/src/ui/prompts.ts +163 -0
  87. package/src/workspace/config.ts +86 -0
  88. package/src/workspace/edits.ts +106 -0
  89. package/src/workspace/plan.ts +92 -0
  90. package/tsconfig.json +36 -0
package/src/index.ts ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import * as path from 'path';
5
+ import chalk from 'chalk';
6
+
7
+ import { InitCommand } from './commands/init';
8
+ import { PlanCommand } from './commands/plan';
9
+ import { EditCommand } from './commands/edit';
10
+ import { ReviewCommand } from './commands/review';
11
+ import { ApplyCommand } from './commands/apply';
12
+ import { StatusCommand } from './commands/status';
13
+ import { ConfigCommand } from './commands/config';
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('bytarch-cli')
19
+ .description('AI-powered CLI for project planning and code editing using BYTARCH')
20
+ .version('0.1.0');
21
+
22
+ program
23
+ .command('init')
24
+ .description('Initialize a .genie-cli workspace')
25
+ .option('-f, --force', 'Force initialization even if workspace already exists')
26
+ .action(async (options) => {
27
+ try {
28
+ await InitCommand.execute(options);
29
+ } catch (error) {
30
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
31
+ process.exit(1);
32
+ }
33
+ });
34
+
35
+ program
36
+ .command('plan')
37
+ .description('Generate a project plan using AI')
38
+ .argument('[description]', 'Project description or goal')
39
+ .option('-c, --context <file>', 'Provide additional context from a file')
40
+ .option('-a, --analyze', 'Analyze current repository before planning')
41
+ .action(async (description, options) => {
42
+ try {
43
+ await PlanCommand.execute(description, options);
44
+ } catch (error) {
45
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ program
51
+ .command('edit')
52
+ .description('Propose edits to a file using AI')
53
+ .argument('<path>', 'File path to edit')
54
+ .argument('[request]', 'Description of changes to make')
55
+ .option('-d, --description <desc>', 'Description of the changes')
56
+ .action(async (filePath, request, options) => {
57
+ try {
58
+ await EditCommand.execute(filePath, request, options);
59
+ } catch (error) {
60
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ program
66
+ .command('review')
67
+ .description('Review and manage pending edits and plan')
68
+ .option('-p, --plan', 'Show only plan items')
69
+ .option('-e, --edits', 'Show only pending edits')
70
+ .action(async (options) => {
71
+ try {
72
+ await ReviewCommand.execute(options);
73
+ } catch (error) {
74
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
75
+ process.exit(1);
76
+ }
77
+ });
78
+
79
+ program
80
+ .command('apply')
81
+ .description('Apply accepted edits (dry-run by default)')
82
+ .option('-f, --force', 'Apply edits without confirmation (use with caution)')
83
+ .option('-d, --dry-run', 'Show what would be applied without making changes')
84
+ .option('-i, --ids <ids>', 'Apply only specific edit IDs (comma-separated)')
85
+ .action(async (options) => {
86
+ try {
87
+ await ApplyCommand.execute(options);
88
+ } catch (error) {
89
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
90
+ process.exit(1);
91
+ }
92
+ });
93
+
94
+ program
95
+ .command('status')
96
+ .description('Show workspace and plan status')
97
+ .option('-v, --verbose', 'Show detailed information')
98
+ .action(async (options) => {
99
+ try {
100
+ await StatusCommand.execute(options);
101
+ } catch (error) {
102
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ program
108
+ .command('config')
109
+ .description('Manage configuration')
110
+ .option('-s, --set <key=value>', 'Set configuration value')
111
+ .option('-g, --get <key>', 'Get configuration value')
112
+ .option('-l, --list', 'List all configuration')
113
+ .option('--set-api-key <key>', 'Set BYTARCH API key')
114
+ .action(async (options) => {
115
+ try {
116
+ await ConfigCommand.execute(options);
117
+ } catch (error) {
118
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
119
+ process.exit(1);
120
+ }
121
+ });
122
+
123
+ program.parse();
@@ -0,0 +1,75 @@
1
+ export interface ChatMessage {
2
+ role: 'system' | 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+
6
+ export interface BytarchResponse {
7
+ choices: Array<{
8
+ message: {
9
+ role: string;
10
+ content: string;
11
+ };
12
+ }>;
13
+ usage?: {
14
+ prompt_tokens: number;
15
+ completion_tokens: number;
16
+ total_tokens: number;
17
+ };
18
+ }
19
+
20
+ export class BytarchProvider {
21
+ private endpoint: string;
22
+ private model: string;
23
+ private apiKey: string;
24
+
25
+ constructor(endpoint: string, model: string, apiKey: string) {
26
+ this.endpoint = endpoint;
27
+ this.model = model;
28
+ this.apiKey = apiKey;
29
+ }
30
+
31
+ async chat(messages: ChatMessage[]): Promise<string> {
32
+ if (!this.apiKey) {
33
+ throw new Error('BYTARCH API key is required. Set BYTARCH_API_KEY environment variable or configure in .genie-cli/config.json');
34
+ }
35
+
36
+ try {
37
+ const response = await fetch(this.endpoint, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ 'Authorization': `Bearer ${this.apiKey}`
42
+ },
43
+ body: JSON.stringify({
44
+ model: this.model,
45
+ messages,
46
+ stream: false
47
+ })
48
+ });
49
+
50
+ if (!response.ok) {
51
+ const errorText = await response.text();
52
+ throw new Error(`BYTARCH API request failed (${response.status}): ${errorText}`);
53
+ }
54
+
55
+ const data = (await response.json()) as BytarchResponse;
56
+
57
+ if (!data.choices || data.choices.length === 0) {
58
+ throw new Error('No response from BYTARCH API');
59
+ }
60
+
61
+ return data.choices[0].message.content;
62
+ } catch (error) {
63
+ if (error instanceof Error) {
64
+ throw new Error(`BYTARCH provider error: ${error.message}`);
65
+ }
66
+ throw new Error('Unknown BYTARCH provider error');
67
+ }
68
+ }
69
+
70
+ updateConfig(endpoint: string, model: string, apiKey: string): void {
71
+ this.endpoint = endpoint;
72
+ this.model = model;
73
+ this.apiKey = apiKey;
74
+ }
75
+ }
@@ -0,0 +1,39 @@
1
+ export function isValidPlanResponse(data: any): boolean {
2
+ if (!data || typeof data !== 'object') return false;
3
+
4
+ return (
5
+ typeof data.id === 'string' &&
6
+ typeof data.title === 'string' &&
7
+ typeof data.description === 'string' &&
8
+ Array.isArray(data.items) &&
9
+ typeof data.rationale === 'string'
10
+ );
11
+ }
12
+
13
+ export function isValidEditResponse(data: any): boolean {
14
+ if (!data || typeof data !== 'object') return false;
15
+
16
+ return (
17
+ typeof data.id === 'string' &&
18
+ typeof data.path === 'string' &&
19
+ typeof data.description === 'string' &&
20
+ typeof data.rationale === 'string' &&
21
+ typeof data.patch === 'string'
22
+ );
23
+ }
24
+
25
+ export function extractJsonFromResponse(content: string): any {
26
+ try {
27
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
28
+ if (!jsonMatch) {
29
+ throw new Error('No JSON found in response');
30
+ }
31
+ return JSON.parse(jsonMatch[0]);
32
+ } catch (error) {
33
+ throw new Error(`Failed to parse JSON from AI response: ${error instanceof Error ? error.message : 'Unknown error'}`);
34
+ }
35
+ }
36
+
37
+ export function sanitizePath(path: string): string {
38
+ return path.replace(/\./g, '').replace(/^\//, '');
39
+ }
@@ -0,0 +1,59 @@
1
+ export const PLAN_PROMPT = `You are an expert software architect and developer. Given a project description or repository context, generate an actionable plan for implementing the project.
2
+
3
+ Your response should be a JSON object with the following structure:
4
+ {
5
+ "id": "plan-id",
6
+ "title": "Project title",
7
+ "description": "Brief description of the project",
8
+ "items": [
9
+ {
10
+ "id": "task-id",
11
+ "title": "Task title",
12
+ "description": "Detailed description of what needs to be done",
13
+ "status": "pending",
14
+ "rationale": "Why this task is necessary",
15
+ "files": ["file1.js", "file2.ts"] // files that will be created/modified
16
+ }
17
+ ],
18
+ "rationale": "Overall rationale for the plan"
19
+ }
20
+
21
+ Focus on:
22
+ 1. Breaking down the project into manageable tasks
23
+ 2. Identifying files that need to be created or modified
24
+ 3. Providing clear rationale for each task
25
+ 4. Using realistic estimates for task complexity
26
+
27
+ Be specific about file paths and implementations needed.`;
28
+
29
+ export const EDIT_PROMPT = `You are an expert software developer. Given a file path and a request for changes, generate a unified diff patch for the requested edits.
30
+
31
+ Your response should be a JSON object with the following structure:
32
+ {
33
+ "id": "patch-id",
34
+ "path": "path/to/file.js",
35
+ "description": "Brief description of the changes",
36
+ "rationale": "Why these changes are necessary",
37
+ "patch": "--- a/path/to/file.js\\n+++ b/path/to/file.js\\n@@ -1,3 +1,5 @@\\n existing content\\n+new content\\n-more content"
38
+ }
39
+
40
+ Guidelines:
41
+ 1. Generate valid unified diff format
42
+ 2. Be precise about line numbers
43
+ 3. Include context lines in the diff
44
+ 4. Explain the reasoning for changes
45
+ 5. If the file doesn't exist, create a new file patch
46
+
47
+ Always include the full unified diff in the "patch" field.`;
48
+
49
+ export const ANALYSIS_PROMPT = `You are an expert code reviewer. Analyze the current repository structure and provide insights for project planning.
50
+
51
+ Focus on:
52
+ 1. Current project structure and architecture
53
+ 2. Existing technologies and frameworks used
54
+ 3. Areas that need improvement or new features
55
+ 4. Dependencies and package management
56
+ 5. Testing setup and coverage
57
+ 6. Documentation status
58
+
59
+ Provide a concise summary that would help in generating an effective project plan.`;
@@ -0,0 +1,163 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+
4
+ export interface ConfirmOptions {
5
+ message: string;
6
+ default?: boolean;
7
+ }
8
+
9
+ export interface SelectOptions {
10
+ message: string;
11
+ choices: Array<{ name: string; value: any; short?: string }>;
12
+ default?: any;
13
+ }
14
+
15
+ export interface InputOptions {
16
+ message: string;
17
+ default?: string;
18
+ validate?: (input: string) => boolean | string;
19
+ }
20
+
21
+ export interface MultiSelectOptions {
22
+ message: string;
23
+ choices: Array<{ name: string; value: any; checked?: boolean }>;
24
+ }
25
+
26
+ export class UI {
27
+ static success(message: string): void {
28
+ console.log(chalk.green('✓'), message);
29
+ }
30
+
31
+ static error(message: string): void {
32
+ console.error(chalk.red('✗'), message);
33
+ }
34
+
35
+ static warning(message: string): void {
36
+ console.warn(chalk.yellow('⚠'), message);
37
+ }
38
+
39
+ static info(message: string): void {
40
+ console.log(chalk.blue('ℹ'), message);
41
+ }
42
+
43
+ static header(message: string): void {
44
+ console.log(chalk.bold.cyan(message));
45
+ }
46
+
47
+ static async confirm(options: ConfirmOptions): Promise<boolean> {
48
+ const { message, default: defaultValue = false } = options;
49
+
50
+ const answers = await inquirer.prompt([
51
+ {
52
+ type: 'confirm',
53
+ name: 'confirmed',
54
+ message,
55
+ default: defaultValue
56
+ }
57
+ ]);
58
+
59
+ return answers.confirmed;
60
+ }
61
+
62
+ static async select(options: SelectOptions): Promise<any> {
63
+ const { message, choices, default: defaultValue } = options;
64
+
65
+ const answers = await inquirer.prompt([
66
+ {
67
+ type: 'list',
68
+ name: 'selected',
69
+ message,
70
+ choices,
71
+ default: defaultValue
72
+ }
73
+ ]);
74
+
75
+ return answers.selected;
76
+ }
77
+
78
+ static async input(options: InputOptions): Promise<string> {
79
+ const { message, default: defaultValue, validate } = options;
80
+
81
+ const answers = await inquirer.prompt([
82
+ {
83
+ type: 'input',
84
+ name: 'input',
85
+ message,
86
+ default: defaultValue,
87
+ validate: validate ? (input: string) => {
88
+ const result = validate(input);
89
+ if (typeof result === 'boolean') {
90
+ return result;
91
+ }
92
+ return result || true;
93
+ } : undefined
94
+ }
95
+ ]);
96
+
97
+ return answers.input;
98
+ }
99
+
100
+ static async multiSelect(options: MultiSelectOptions): Promise<any[]> {
101
+ const { message, choices } = options;
102
+
103
+ const answers = await inquirer.prompt([
104
+ {
105
+ type: 'checkbox',
106
+ name: 'selected',
107
+ message,
108
+ choices
109
+ }
110
+ ]);
111
+
112
+ return answers.selected;
113
+ }
114
+
115
+ static table(headers: string[], rows: string[][]): void {
116
+ const columnWidths = headers.map((header, index) => {
117
+ const maxRowWidth = Math.max(...rows.map(row => row[index]?.length || 0));
118
+ return Math.max(header.length, maxRowWidth);
119
+ });
120
+
121
+ const formatRow = (row: string[]) => {
122
+ return row.map((cell, index) => cell.padEnd(columnWidths[index])).join(' | ');
123
+ };
124
+
125
+ const separator = columnWidths.map(width => '-'.repeat(width)).join('-+-');
126
+
127
+ console.log(formatRow(headers));
128
+ console.log(separator);
129
+ rows.forEach(row => console.log(formatRow(row)));
130
+ }
131
+
132
+ static list(items: string[], bullet: string = '•'): void {
133
+ items.forEach(item => console.log(`${bullet} ${item}`));
134
+ }
135
+
136
+ static divider(char: string = '-', length: number = 50): void {
137
+ console.log(char.repeat(length));
138
+ }
139
+
140
+ static highlight(text: string, color: 'green' | 'yellow' | 'red' | 'blue' | 'cyan' = 'cyan'): void {
141
+ const colors = {
142
+ green: chalk.green,
143
+ yellow: chalk.yellow,
144
+ red: chalk.red,
145
+ blue: chalk.blue,
146
+ cyan: chalk.cyan
147
+ };
148
+ console.log(colors[color](text));
149
+ }
150
+
151
+ static progress(current: number, total: number, message: string = ''): void {
152
+ const percentage = Math.round((current / total) * 100);
153
+ const barLength = 30;
154
+ const filledLength = Math.round((barLength * current) / total);
155
+ const bar = '█'.repeat(filledLength) + '-'.repeat(barLength - filledLength);
156
+
157
+ process.stdout.write(`\\r[${bar}] ${percentage}% ${message}`);
158
+
159
+ if (current === total) {
160
+ process.stdout.write('\\n');
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,86 @@
1
+ import * as fs from 'fs-extra';
2
+ import * as path from 'path';
3
+
4
+ export interface Config {
5
+ provider: string;
6
+ endpoint: string;
7
+ model: string;
8
+ apiKey?: string;
9
+ dryRun: boolean;
10
+ }
11
+
12
+ export class ConfigManager {
13
+ private workspacePath: string;
14
+ private configPath: string;
15
+
16
+ constructor(workspacePath: string) {
17
+ this.workspacePath = workspacePath;
18
+ this.configPath = path.join(workspacePath, 'config.json');
19
+ }
20
+
21
+ async loadConfig(): Promise<Config> {
22
+ try {
23
+ if (await fs.pathExists(this.configPath)) {
24
+ const config = await fs.readJson(this.configPath);
25
+ return this.mergeWithEnv(config);
26
+ }
27
+ } catch (error) {
28
+ console.warn('Failed to load config, creating default:', error);
29
+ }
30
+
31
+ return this.createDefaultConfig();
32
+ }
33
+
34
+ async saveConfig(config: Config): Promise<void> {
35
+ await fs.writeJson(this.configPath, config, { spaces: 2 });
36
+ }
37
+
38
+ async updateConfig(updates: Partial<Config>): Promise<Config> {
39
+ const config = await this.loadConfig();
40
+ const newConfig = { ...config, ...updates };
41
+ await this.saveConfig(newConfig);
42
+ return newConfig;
43
+ }
44
+
45
+ getApiKey(config: Config): string {
46
+ const envApiKey = process.env.BYTARCH_API_KEY;
47
+ if (envApiKey) {
48
+ return envApiKey;
49
+ }
50
+
51
+ if (config.apiKey) {
52
+ return config.apiKey;
53
+ }
54
+
55
+ throw new Error('BYTARCH API key not found. Set BYTARCH_API_KEY environment variable or configure in .genie-cli/config.json');
56
+ }
57
+
58
+ private mergeWithEnv(config: Partial<Config>): Config {
59
+ const defaultConfig = this.createDefaultConfig();
60
+ const mergedConfig = { ...defaultConfig, ...config };
61
+
62
+ if (process.env.BYTARCH_API_KEY) {
63
+ mergedConfig.apiKey = process.env.BYTARCH_API_KEY;
64
+ }
65
+
66
+ if (process.env.BYTARCH_ENDPOINT) {
67
+ mergedConfig.endpoint = process.env.BYTARCH_ENDPOINT;
68
+ }
69
+
70
+ if (process.env.BYTARCH_MODEL) {
71
+ mergedConfig.model = process.env.BYTARCH_MODEL;
72
+ }
73
+
74
+ return mergedConfig;
75
+ }
76
+
77
+ private createDefaultConfig(): Config {
78
+ return {
79
+ provider: 'bytarch',
80
+ endpoint: 'https://api.bytarch.dpdns.org/openai/v1/chat/completions',
81
+ model: 'gpt-5-nano',
82
+ apiKey: '',
83
+ dryRun: true
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,106 @@
1
+ import * as fs from 'fs-extra';
2
+ import * as path from 'path';
3
+
4
+ export interface FileEdit {
5
+ id: string;
6
+ path: string;
7
+ description: string;
8
+ rationale: string;
9
+ patch: string;
10
+ status: 'pending' | 'accepted' | 'rejected' | 'applied';
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ }
14
+
15
+ export class EditManager {
16
+ private workspacePath: string;
17
+ private editsPath: string;
18
+
19
+ constructor(workspacePath: string) {
20
+ this.workspacePath = workspacePath;
21
+ this.editsPath = path.join(workspacePath, 'edits');
22
+ }
23
+
24
+ async ensureEditsDir(): Promise<void> {
25
+ await fs.ensureDir(this.editsPath);
26
+ }
27
+
28
+ async saveEdit(edit: FileEdit): Promise<void> {
29
+ await this.ensureEditsDir();
30
+ edit.updatedAt = new Date().toISOString();
31
+ const editPath = path.join(this.editsPath, `${edit.id}.json`);
32
+ await fs.writeJson(editPath, edit, { spaces: 2 });
33
+ }
34
+
35
+ async loadEdit(editId: string): Promise<FileEdit | null> {
36
+ await this.ensureEditsDir();
37
+ const editPath = path.join(this.editsPath, `${editId}.json`);
38
+
39
+ try {
40
+ if (await fs.pathExists(editPath)) {
41
+ return await fs.readJson(editPath);
42
+ }
43
+ } catch (error) {
44
+ console.warn(`Failed to load edit ${editId}:`, error);
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ async listEdits(): Promise<FileEdit[]> {
51
+ await this.ensureEditsDir();
52
+
53
+ try {
54
+ const files = await fs.readdir(this.editsPath);
55
+ const jsonFiles = files.filter(file => file.endsWith('.json'));
56
+
57
+ const edits: FileEdit[] = [];
58
+ for (const file of jsonFiles) {
59
+ const editPath = path.join(this.editsPath, file);
60
+ try {
61
+ const edit = await fs.readJson(editPath);
62
+ edits.push(edit);
63
+ } catch (error) {
64
+ console.warn(`Failed to load edit file ${file}:`, error);
65
+ }
66
+ }
67
+
68
+ return edits.sort((a, b) =>
69
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
70
+ );
71
+ } catch (error) {
72
+ console.warn('Failed to list edits:', error);
73
+ return [];
74
+ }
75
+ }
76
+
77
+ async updateEditStatus(editId: string, status: FileEdit['status']): Promise<FileEdit | null> {
78
+ const edit = await this.loadEdit(editId);
79
+ if (!edit) return null;
80
+
81
+ edit.status = status;
82
+ edit.updatedAt = new Date().toISOString();
83
+ await this.saveEdit(edit);
84
+ return edit;
85
+ }
86
+
87
+ async deleteEdit(editId: string): Promise<void> {
88
+ await this.ensureEditsDir();
89
+ const editPath = path.join(this.editsPath, `${editId}.json`);
90
+
91
+ try {
92
+ await fs.remove(editPath);
93
+ } catch (error) {
94
+ console.warn(`Failed to delete edit ${editId}:`, error);
95
+ }
96
+ }
97
+
98
+ async getPendingEdits(): Promise<FileEdit[]> {
99
+ const edits = await this.listEdits();
100
+ return edits.filter(edit => edit.status === 'pending' || edit.status === 'accepted');
101
+ }
102
+
103
+ generateEditId(): string {
104
+ return `edit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
105
+ }
106
+ }