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,194 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { ConfigManager } from '../workspace/config';
|
|
3
|
+
import { UI } from '../ui/prompts';
|
|
4
|
+
|
|
5
|
+
export class ConfigCommand {
|
|
6
|
+
static async execute(options: {
|
|
7
|
+
set?: string;
|
|
8
|
+
get?: string;
|
|
9
|
+
list?: boolean;
|
|
10
|
+
setApiKey?: string;
|
|
11
|
+
}): Promise<void> {
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (!(await require('fs-extra').pathExists(workspacePath))) {
|
|
17
|
+
UI.error('No .genie-cli workspace found. Run "bytarch-cli init" to create one.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configManager = new ConfigManager(workspacePath);
|
|
22
|
+
|
|
23
|
+
if (options.setApiKey) {
|
|
24
|
+
await this.setApiKey(configManager, options.setApiKey);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.set) {
|
|
29
|
+
await this.setConfig(configManager, options.set);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.get) {
|
|
34
|
+
await this.getConfig(configManager, options.get);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.list) {
|
|
39
|
+
await this.listConfig(configManager);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await this.interactiveConfig(configManager);
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Config operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static async setApiKey(configManager: ConfigManager, apiKey: string): Promise<void> {
|
|
51
|
+
await configManager.updateConfig({ apiKey });
|
|
52
|
+
UI.success('API key updated successfully.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static async setConfig(configManager: ConfigManager, setOption: string): Promise<void> {
|
|
56
|
+
const [key, value] = setOption.split('=');
|
|
57
|
+
|
|
58
|
+
if (!key || !value) {
|
|
59
|
+
UI.error('Invalid format. Use: --set key=value');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const config = await configManager.loadConfig();
|
|
64
|
+
|
|
65
|
+
if (!(key in config)) {
|
|
66
|
+
UI.error(`Unknown configuration key: ${key}`);
|
|
67
|
+
UI.info('Available keys: provider, endpoint, model, dryRun');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let parsedValue: any = value;
|
|
72
|
+
|
|
73
|
+
if (key === 'dryRun') {
|
|
74
|
+
parsedValue = value.toLowerCase() === 'true';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const updates: any = {};
|
|
78
|
+
updates[key] = parsedValue;
|
|
79
|
+
|
|
80
|
+
await configManager.updateConfig(updates);
|
|
81
|
+
UI.success(`Updated ${key} to ${value}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static async getConfig(configManager: ConfigManager, key: string): Promise<void> {
|
|
85
|
+
const config = await configManager.loadConfig();
|
|
86
|
+
|
|
87
|
+
if (!(key in config)) {
|
|
88
|
+
UI.error(`Unknown configuration key: ${key}`);
|
|
89
|
+
UI.info('Available keys: provider, endpoint, model, dryRun, apiKey');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let value = (config as any)[key];
|
|
94
|
+
|
|
95
|
+
if (key === 'apiKey') {
|
|
96
|
+
value = value ? '***configured***' : 'not configured';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`${key}: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static async listConfig(configManager: ConfigManager): Promise<void> {
|
|
103
|
+
const config = await configManager.loadConfig();
|
|
104
|
+
|
|
105
|
+
UI.header('Configuration:');
|
|
106
|
+
|
|
107
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
108
|
+
let displayValue = value;
|
|
109
|
+
|
|
110
|
+
if (key === 'apiKey') {
|
|
111
|
+
displayValue = value ? '***configured***' : 'not configured';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(` ${key}: ${displayValue}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
UI.info('Environment variables:');
|
|
119
|
+
console.log(` BYTARCH_API_KEY: ${process.env.BYTARCH_API_KEY ? 'set' : 'not set'}`);
|
|
120
|
+
console.log(` BYTARCH_ENDPOINT: ${process.env.BYTARCH_ENDPOINT || 'not set'}`);
|
|
121
|
+
console.log(` BYTARCH_MODEL: ${process.env.BYTARCH_MODEL || 'not set'}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static async interactiveConfig(configManager: ConfigManager): Promise<void> {
|
|
125
|
+
UI.header('Interactive Configuration');
|
|
126
|
+
|
|
127
|
+
const action = await UI.select({
|
|
128
|
+
message: 'What would you like to configure?',
|
|
129
|
+
choices: [
|
|
130
|
+
{ name: 'Set API Key', value: 'apiKey' },
|
|
131
|
+
{ name: 'Set Model', value: 'model' },
|
|
132
|
+
{ name: 'Set Endpoint', value: 'endpoint' },
|
|
133
|
+
{ name: 'Toggle Dry Run', value: 'dryRun' },
|
|
134
|
+
{ name: 'Show Current Config', value: 'show' },
|
|
135
|
+
{ name: 'Done', value: 'done' }
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (action === 'done') return;
|
|
140
|
+
|
|
141
|
+
const config = await configManager.loadConfig();
|
|
142
|
+
|
|
143
|
+
switch (action) {
|
|
144
|
+
case 'apiKey':
|
|
145
|
+
const apiKey = await UI.input({
|
|
146
|
+
message: 'Enter BYTARCH API key:',
|
|
147
|
+
validate: (input) => input.length > 0 || 'API key cannot be empty'
|
|
148
|
+
});
|
|
149
|
+
await configManager.updateConfig({ apiKey });
|
|
150
|
+
UI.success('API key updated.');
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'model':
|
|
154
|
+
const model = await UI.input({
|
|
155
|
+
message: 'Enter model name:',
|
|
156
|
+
default: config.model
|
|
157
|
+
});
|
|
158
|
+
await configManager.updateConfig({ model });
|
|
159
|
+
UI.success(`Model updated to: ${model}`);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'endpoint':
|
|
163
|
+
const endpoint = await UI.input({
|
|
164
|
+
message: 'Enter API endpoint:',
|
|
165
|
+
default: config.endpoint
|
|
166
|
+
});
|
|
167
|
+
await configManager.updateConfig({ endpoint });
|
|
168
|
+
UI.success(`Endpoint updated to: ${endpoint}`);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'dryRun':
|
|
172
|
+
const dryRun = await UI.confirm({
|
|
173
|
+
message: `Dry run is currently ${config.dryRun ? 'enabled' : 'disabled'}. Toggle?`,
|
|
174
|
+
default: true
|
|
175
|
+
});
|
|
176
|
+
await configManager.updateConfig({ dryRun: !config.dryRun });
|
|
177
|
+
UI.success(`Dry run ${!config.dryRun ? 'enabled' : 'disabled'}.`);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case 'show':
|
|
181
|
+
await this.listConfig(configManager);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const shouldContinue = await UI.confirm({
|
|
186
|
+
message: 'Configure more settings?',
|
|
187
|
+
default: false
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (shouldContinue) {
|
|
191
|
+
await this.interactiveConfig(configManager);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { ConfigManager } from '../workspace/config';
|
|
3
|
+
import { EditManager, FileEdit } from '../workspace/edits';
|
|
4
|
+
import { BytarchProvider } from '../provider/bytarch';
|
|
5
|
+
import { EDIT_PROMPT } from '../provider/prompts';
|
|
6
|
+
import { extractJsonFromResponse, isValidEditResponse } from '../provider/evaluation';
|
|
7
|
+
import { UI } from '../ui/prompts';
|
|
8
|
+
|
|
9
|
+
export class EditCommand {
|
|
10
|
+
static async execute(filePath: string, request: string = '', options: { description?: string }): Promise<void> {
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
13
|
+
|
|
14
|
+
const configManager = new ConfigManager(workspacePath);
|
|
15
|
+
const editManager = new EditManager(workspacePath);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const config = await configManager.loadConfig();
|
|
19
|
+
const apiKey = configManager.getApiKey(config);
|
|
20
|
+
|
|
21
|
+
if (!request) {
|
|
22
|
+
request = await UI.input({
|
|
23
|
+
message: 'Describe the changes you want to make:',
|
|
24
|
+
validate: (input) => input.length > 0 || 'Please describe the changes'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
UI.header(`Generating edit for ${filePath}...`);
|
|
29
|
+
|
|
30
|
+
const provider = new BytarchProvider(config.endpoint, config.model, apiKey);
|
|
31
|
+
|
|
32
|
+
let fileContent = '';
|
|
33
|
+
try {
|
|
34
|
+
const fs = require('fs-extra');
|
|
35
|
+
const resolvedPath = path.resolve(projectRoot, filePath);
|
|
36
|
+
if (await fs.pathExists(resolvedPath)) {
|
|
37
|
+
fileContent = await fs.readFile(resolvedPath, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
UI.warning(`Could not read file ${filePath}, will create new file`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const messages = [
|
|
44
|
+
{
|
|
45
|
+
role: 'system' as const,
|
|
46
|
+
content: EDIT_PROMPT
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
role: 'user' as const,
|
|
50
|
+
content: `File: ${filePath}\\nRequest: ${request}\\n${fileContent ? `Current content:\\n\\n${fileContent}` : 'File does not exist, create new file.'}\\n\\nGenerate a unified diff patch for these changes.`
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const response = await provider.chat(messages);
|
|
55
|
+
const editData = extractJsonFromResponse(response);
|
|
56
|
+
|
|
57
|
+
if (!isValidEditResponse(editData)) {
|
|
58
|
+
throw new Error('Invalid edit response from AI');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const edit: FileEdit = {
|
|
62
|
+
id: editManager.generateEditId(),
|
|
63
|
+
path: filePath,
|
|
64
|
+
description: options.description || editData.description,
|
|
65
|
+
rationale: editData.rationale,
|
|
66
|
+
patch: editData.patch,
|
|
67
|
+
status: 'pending',
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
updatedAt: new Date().toISOString()
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await editManager.saveEdit(edit);
|
|
73
|
+
|
|
74
|
+
UI.success('Edit generated successfully!');
|
|
75
|
+
this.displayEdit(edit);
|
|
76
|
+
|
|
77
|
+
const shouldReview = await UI.confirm({
|
|
78
|
+
message: 'Would you like to review this edit now?',
|
|
79
|
+
default: true
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (shouldReview) {
|
|
83
|
+
await this.showPatchPreview(edit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Failed to generate edit: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static displayEdit(edit: FileEdit): void {
|
|
92
|
+
UI.divider();
|
|
93
|
+
UI.header(`📝 Edit: ${edit.path}`);
|
|
94
|
+
console.log(edit.description);
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
if (edit.rationale) {
|
|
98
|
+
UI.info('Rationale:');
|
|
99
|
+
console.log(edit.rationale);
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`Status: ${edit.status}`);
|
|
104
|
+
console.log(`ID: ${edit.id}`);
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static async showPatchPreview(edit: FileEdit): Promise<void> {
|
|
109
|
+
const { Patcher } = await import('../fs/patcher');
|
|
110
|
+
const projectRoot = process.cwd();
|
|
111
|
+
const patcher = new Patcher(projectRoot);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const preview = await patcher.previewPatch(edit);
|
|
115
|
+
console.log(preview);
|
|
116
|
+
|
|
117
|
+
const shouldAccept = await UI.confirm({
|
|
118
|
+
message: 'Accept this edit?',
|
|
119
|
+
default: false
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (shouldAccept) {
|
|
123
|
+
const editManager = new EditManager(path.join(projectRoot, '.genie-cli'));
|
|
124
|
+
await editManager.updateEditStatus(edit.id, 'accepted');
|
|
125
|
+
UI.success('Edit accepted. Use "bytarch-cli apply" to apply changes.');
|
|
126
|
+
} else {
|
|
127
|
+
UI.info('Edit remains pending for review.');
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
UI.error(`Failed to preview patch: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { UI } from '../ui/prompts';
|
|
5
|
+
|
|
6
|
+
export class InitCommand {
|
|
7
|
+
static async execute(options: { force?: boolean }): Promise<void> {
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
10
|
+
|
|
11
|
+
if (await fs.pathExists(workspacePath)) {
|
|
12
|
+
if (!options.force) {
|
|
13
|
+
const shouldContinue = await UI.confirm({
|
|
14
|
+
message: 'Workspace already exists. Do you want to reinitialize?',
|
|
15
|
+
default: false
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!shouldContinue) {
|
|
19
|
+
UI.info('Initialization cancelled.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await fs.remove(workspacePath);
|
|
25
|
+
UI.warning('Removed existing workspace.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await fs.ensureDir(workspacePath);
|
|
29
|
+
await fs.ensureDir(path.join(workspacePath, 'edits'));
|
|
30
|
+
await fs.ensureDir(path.join(workspacePath, 'history'));
|
|
31
|
+
|
|
32
|
+
const defaultConfig = {
|
|
33
|
+
provider: 'bytarch',
|
|
34
|
+
endpoint: 'https://api.bytarch.dpdns.org/openai/v1/chat/completions',
|
|
35
|
+
model: 'gpt-5-nano',
|
|
36
|
+
apiKey: '',
|
|
37
|
+
dryRun: true
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await fs.writeJson(path.join(workspacePath, 'config.json'), defaultConfig, { spaces: 2 });
|
|
41
|
+
|
|
42
|
+
const defaultPlan = {
|
|
43
|
+
id: 'default',
|
|
44
|
+
title: 'Project Plan',
|
|
45
|
+
description: 'Generated project plan',
|
|
46
|
+
items: [],
|
|
47
|
+
rationale: '',
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
updatedAt: new Date().toISOString()
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
await fs.writeJson(path.join(workspacePath, 'plan.json'), defaultPlan, { spaces: 2 });
|
|
53
|
+
|
|
54
|
+
UI.success(`Initialized .genie-cli workspace in ${projectRoot}`);
|
|
55
|
+
UI.info('Next steps:');
|
|
56
|
+
UI.list([
|
|
57
|
+
'Set your API key: bytarch-cli config --set-api-key YOUR_API_KEY',
|
|
58
|
+
'Generate a plan: bytarch-cli plan "your project description"',
|
|
59
|
+
'Propose edits: bytarch-cli edit path/to/file.js "your request"'
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { ConfigManager } from '../workspace/config';
|
|
3
|
+
import { PlanManager, Plan } from '../workspace/plan';
|
|
4
|
+
import { BytarchProvider } from '../provider/bytarch';
|
|
5
|
+
import { PLAN_PROMPT } from '../provider/prompts';
|
|
6
|
+
import { extractJsonFromResponse, isValidPlanResponse } from '../provider/evaluation';
|
|
7
|
+
import { UI } from '../ui/prompts';
|
|
8
|
+
|
|
9
|
+
export class PlanCommand {
|
|
10
|
+
static async execute(description: string = '', options: { context?: string; analyze?: boolean }): Promise<void> {
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const workspacePath = path.join(projectRoot, '.genie-cli');
|
|
13
|
+
|
|
14
|
+
const configManager = new ConfigManager(workspacePath);
|
|
15
|
+
const planManager = new PlanManager(workspacePath);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const config = await configManager.loadConfig();
|
|
19
|
+
const apiKey = configManager.getApiKey(config);
|
|
20
|
+
|
|
21
|
+
if (!description) {
|
|
22
|
+
description = await UI.input({
|
|
23
|
+
message: 'Describe your project or goal:',
|
|
24
|
+
validate: (input) => input.length > 0 || 'Please provide a description'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
UI.header('Generating project plan...');
|
|
29
|
+
|
|
30
|
+
const provider = new BytarchProvider(config.endpoint, config.model, apiKey);
|
|
31
|
+
|
|
32
|
+
let contextInfo = '';
|
|
33
|
+
if (options.analyze) {
|
|
34
|
+
contextInfo = await this.analyzeProject(projectRoot);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.context) {
|
|
38
|
+
try {
|
|
39
|
+
const contextContent = await require('fs-extra').readFile(options.context, 'utf8');
|
|
40
|
+
contextInfo += `\\n\\nAdditional context from ${options.context}:\\n${contextContent}`;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
UI.warning(`Could not read context file: ${options.context}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const messages = [
|
|
47
|
+
{
|
|
48
|
+
role: 'system' as const,
|
|
49
|
+
content: PLAN_PROMPT
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
role: 'user' as const,
|
|
53
|
+
content: `Project description: ${description}\\n${contextInfo ? `Context:\\n${contextInfo}` : ''}\\n\\nGenerate a comprehensive plan for this project.`
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const response = await provider.chat(messages);
|
|
58
|
+
const planData = extractJsonFromResponse(response);
|
|
59
|
+
|
|
60
|
+
if (!isValidPlanResponse(planData)) {
|
|
61
|
+
throw new Error('Invalid plan response from AI');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const plan: Plan = {
|
|
65
|
+
...planData,
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
updatedAt: new Date().toISOString()
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await planManager.savePlan(plan);
|
|
71
|
+
|
|
72
|
+
UI.success('Project plan generated successfully!');
|
|
73
|
+
this.displayPlan(plan);
|
|
74
|
+
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Failed to generate plan: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static async analyzeProject(projectRoot: string): Promise<string> {
|
|
81
|
+
const fs = require('fs-extra');
|
|
82
|
+
const path = require('path');
|
|
83
|
+
|
|
84
|
+
const analysis: string[] = [];
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
88
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
89
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
90
|
+
analysis.push(`Project: ${packageJson.name || 'Unknown'}`);
|
|
91
|
+
analysis.push(`Scripts: ${Object.keys(packageJson.scripts || {}).join(', ')}`);
|
|
92
|
+
analysis.push(`Dependencies: ${Object.keys(packageJson.dependencies || {}).length}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const srcPath = path.join(projectRoot, 'src');
|
|
96
|
+
if (await fs.pathExists(srcPath)) {
|
|
97
|
+
const files = await this.getFilesRecursive(srcPath);
|
|
98
|
+
const jsFiles = files.filter((f: string) => f.endsWith('.js') || f.endsWith('.ts'));
|
|
99
|
+
analysis.push(`Source files: ${jsFiles.length} (${jsFiles.map((f: string) => path.extname(f)).join(', ')})`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
UI.warning('Could not analyze project structure');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return analysis.join('\\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static async getFilesRecursive(dir: string): Promise<string[]> {
|
|
110
|
+
const fs = require('fs-extra');
|
|
111
|
+
const path = require('path');
|
|
112
|
+
let files: string[] = [];
|
|
113
|
+
|
|
114
|
+
const items = await fs.readdir(dir);
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
const fullPath = path.join(dir, item);
|
|
117
|
+
const stat = await fs.stat(fullPath);
|
|
118
|
+
|
|
119
|
+
if (stat.isDirectory()) {
|
|
120
|
+
files = files.concat(await this.getFilesRecursive(fullPath));
|
|
121
|
+
} else {
|
|
122
|
+
files.push(fullPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static displayPlan(plan: Plan): void {
|
|
130
|
+
UI.divider();
|
|
131
|
+
UI.header(`📋 ${plan.title}`);
|
|
132
|
+
console.log(plan.description);
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
if (plan.rationale) {
|
|
136
|
+
UI.info('Rationale:');
|
|
137
|
+
console.log(plan.rationale);
|
|
138
|
+
console.log('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (plan.items.length === 0) {
|
|
142
|
+
UI.warning('No tasks in the plan.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('Tasks:');
|
|
147
|
+
plan.items.forEach((item, index) => {
|
|
148
|
+
const status = item.status === 'completed' ? '✓' :
|
|
149
|
+
item.status === 'in_progress' ? '⚡' : '○';
|
|
150
|
+
console.log(`${index + 1}. ${status} ${item.title}`);
|
|
151
|
+
|
|
152
|
+
if (item.description) {
|
|
153
|
+
console.log(` ${item.description}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (item.files && item.files.length > 0) {
|
|
157
|
+
console.log(` Files: ${item.files.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('');
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|