@agile-vibe-coding/avc 0.1.0 → 0.1.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 +2 -0
- package/cli/agents/documentation.md +302 -0
- package/cli/build-docs.js +277 -0
- package/cli/command-logger.js +208 -0
- package/cli/index.js +3 -25
- package/cli/init.js +705 -77
- package/cli/llm-claude.js +27 -0
- package/cli/llm-gemini.js +30 -0
- package/cli/llm-provider.js +63 -0
- package/cli/logger.js +32 -5
- package/cli/process-manager.js +261 -0
- package/cli/repl-ink.js +1784 -219
- package/cli/template-processor.js +274 -73
- package/cli/templates/vitepress-config.mts.template +33 -0
- package/package.json +17 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { LLMProvider } from './llm-provider.js';
|
|
3
|
+
|
|
4
|
+
export class ClaudeProvider extends LLMProvider {
|
|
5
|
+
constructor(model) { super('claude', model); }
|
|
6
|
+
|
|
7
|
+
_createClient() {
|
|
8
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
9
|
+
if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set. Add it to your .env file.');
|
|
10
|
+
return new Anthropic({ apiKey });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async _callProvider(prompt, maxTokens, systemInstructions) {
|
|
14
|
+
const params = {
|
|
15
|
+
model: this.model,
|
|
16
|
+
max_tokens: maxTokens,
|
|
17
|
+
messages: [{ role: 'user', content: prompt }]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (systemInstructions) {
|
|
21
|
+
params.system = systemInstructions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await this._client.messages.create(params);
|
|
25
|
+
return response.content[0].text;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import { LLMProvider } from './llm-provider.js';
|
|
3
|
+
|
|
4
|
+
export class GeminiProvider extends LLMProvider {
|
|
5
|
+
constructor(model = 'gemini-2.5-flash') { super('gemini', model); }
|
|
6
|
+
|
|
7
|
+
_createClient() {
|
|
8
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
9
|
+
if (!apiKey) throw new Error('GEMINI_API_KEY not set. Add it to your .env file.');
|
|
10
|
+
return new GoogleGenAI({ apiKey });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async _callProvider(prompt, maxTokens, systemInstructions) {
|
|
14
|
+
const params = {
|
|
15
|
+
model: this.model,
|
|
16
|
+
contents: prompt,
|
|
17
|
+
generationConfig: { maxOutputTokens: maxTokens }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (systemInstructions) {
|
|
21
|
+
params.systemInstruction = systemInstructions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await this._client.models.generateContent(params);
|
|
25
|
+
if (!response.text) {
|
|
26
|
+
throw new Error('Gemini returned no text (possible safety filter block).');
|
|
27
|
+
}
|
|
28
|
+
return response.text;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class LLMProvider {
|
|
2
|
+
constructor(providerName, model) {
|
|
3
|
+
this.providerName = providerName;
|
|
4
|
+
this.model = model;
|
|
5
|
+
this._client = null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Factory — async because of dynamic import (only loads the SDK you need)
|
|
9
|
+
static async create(providerName, model) {
|
|
10
|
+
switch (providerName) {
|
|
11
|
+
case 'claude': {
|
|
12
|
+
const { ClaudeProvider } = await import('./llm-claude.js');
|
|
13
|
+
return new ClaudeProvider(model);
|
|
14
|
+
}
|
|
15
|
+
case 'gemini': {
|
|
16
|
+
const { GeminiProvider } = await import('./llm-gemini.js');
|
|
17
|
+
return new GeminiProvider(model);
|
|
18
|
+
}
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unknown LLM provider: "${providerName}". Supported: claude, gemini`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Public API — single method, callers never touch SDK objects
|
|
25
|
+
async generate(prompt, maxTokens = 256, systemInstructions = null) {
|
|
26
|
+
if (!this._client) {
|
|
27
|
+
this._client = this._createClient();
|
|
28
|
+
}
|
|
29
|
+
return this._callProvider(prompt, maxTokens, systemInstructions);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate API key and provider connectivity with a minimal test call
|
|
33
|
+
async validateApiKey() {
|
|
34
|
+
try {
|
|
35
|
+
// Make a minimal API call (just asks for a single word)
|
|
36
|
+
await this.generate('Reply with only the word "ok"', 10);
|
|
37
|
+
return { valid: true };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
error: error.message,
|
|
42
|
+
code: error.status || error.code
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Static helper to validate a provider config
|
|
48
|
+
static async validate(providerName, model) {
|
|
49
|
+
try {
|
|
50
|
+
const provider = await LLMProvider.create(providerName, model);
|
|
51
|
+
return await provider.validateApiKey();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
error: error.message
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Subclass hooks — throw if not overridden
|
|
61
|
+
_createClient() { throw new Error(`${this.constructor.name} must implement _createClient()`); }
|
|
62
|
+
async _callProvider(prompt, maxTokens, systemInstructions) { throw new Error(`${this.constructor.name} must implement _callProvider()`); }
|
|
63
|
+
}
|
package/cli/logger.js
CHANGED
|
@@ -5,31 +5,48 @@ import os from 'os';
|
|
|
5
5
|
/**
|
|
6
6
|
* Logger - Writes debug and error logs to file
|
|
7
7
|
*
|
|
8
|
-
* Log location: ~/.avc/logs/avc.log
|
|
8
|
+
* Log location: <project>/.avc/logs/avc.log (or ~/.avc/logs/avc.log if no project)
|
|
9
9
|
* Log rotation: Keeps last 10MB, rotates to avc.log.old
|
|
10
10
|
*/
|
|
11
11
|
export class Logger {
|
|
12
|
-
constructor(componentName = 'AVC') {
|
|
12
|
+
constructor(componentName = 'AVC', projectRoot = null) {
|
|
13
13
|
this.componentName = componentName;
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
// Only log if .avc folder exists - don't create it, don't use fallback
|
|
16
|
+
const baseDir = projectRoot || process.cwd();
|
|
17
|
+
const projectAvcDir = path.join(baseDir, '.avc');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(projectAvcDir)) {
|
|
20
|
+
// No .avc folder - disable logging
|
|
21
|
+
this.loggingDisabled = true;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// .avc exists, set up logging
|
|
26
|
+
this.logDir = path.join(projectAvcDir, 'logs');
|
|
15
27
|
this.logFile = path.join(this.logDir, 'avc.log');
|
|
16
28
|
this.maxLogSize = 10 * 1024 * 1024; // 10MB
|
|
29
|
+
this.loggingDisabled = false;
|
|
17
30
|
|
|
18
31
|
this.ensureLogDir();
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
ensureLogDir() {
|
|
35
|
+
if (this.loggingDisabled) return;
|
|
36
|
+
|
|
22
37
|
try {
|
|
23
38
|
if (!fs.existsSync(this.logDir)) {
|
|
24
39
|
fs.mkdirSync(this.logDir, { recursive: true });
|
|
25
40
|
}
|
|
26
41
|
} catch (error) {
|
|
27
42
|
// Silently fail if we can't create log directory
|
|
28
|
-
|
|
43
|
+
this.loggingDisabled = true;
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
rotateLogIfNeeded() {
|
|
48
|
+
if (this.loggingDisabled) return;
|
|
49
|
+
|
|
33
50
|
try {
|
|
34
51
|
if (fs.existsSync(this.logFile)) {
|
|
35
52
|
const stats = fs.statSync(this.logFile);
|
|
@@ -71,13 +88,17 @@ export class Logger {
|
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
writeLog(level, message, data = null) {
|
|
91
|
+
// Skip logging if directory creation failed
|
|
92
|
+
if (this.loggingDisabled) return;
|
|
93
|
+
|
|
74
94
|
try {
|
|
75
95
|
this.rotateLogIfNeeded();
|
|
76
96
|
const logMessage = this.formatMessage(level, message, data);
|
|
77
97
|
fs.appendFileSync(this.logFile, logMessage, 'utf8');
|
|
78
98
|
} catch (error) {
|
|
79
99
|
// Silently fail if we can't write to log
|
|
80
|
-
|
|
100
|
+
// Disable further logging attempts to avoid repeated errors
|
|
101
|
+
this.loggingDisabled = true;
|
|
81
102
|
}
|
|
82
103
|
}
|
|
83
104
|
|
|
@@ -99,6 +120,10 @@ export class Logger {
|
|
|
99
120
|
|
|
100
121
|
// Read recent logs (last N lines)
|
|
101
122
|
readRecentLogs(lines = 50) {
|
|
123
|
+
if (this.loggingDisabled) {
|
|
124
|
+
return 'No logs available (project not initialized).';
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
try {
|
|
103
128
|
if (!fs.existsSync(this.logFile)) {
|
|
104
129
|
return 'No logs available yet.';
|
|
@@ -116,6 +141,8 @@ export class Logger {
|
|
|
116
141
|
|
|
117
142
|
// Clear all logs
|
|
118
143
|
clearLogs() {
|
|
144
|
+
if (this.loggingDisabled) return;
|
|
145
|
+
|
|
119
146
|
try {
|
|
120
147
|
if (fs.existsSync(this.logFile)) {
|
|
121
148
|
fs.unlinkSync(this.logFile);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Background Process Manager
|
|
6
|
+
* Manages lifecycle of long-running background processes
|
|
7
|
+
*/
|
|
8
|
+
export class BackgroundProcessManager extends EventEmitter {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.processes = new Map(); // id -> processMetadata
|
|
12
|
+
this.maxOutputLines = 500; // Keep last 500 lines per process
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Start a background process
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} options.name - Human-readable name
|
|
19
|
+
* @param {string} options.command - Command to execute
|
|
20
|
+
* @param {string[]} options.args - Command arguments
|
|
21
|
+
* @param {string} options.cwd - Working directory
|
|
22
|
+
* @param {Object} options.env - Environment variables
|
|
23
|
+
* @returns {string} Process ID
|
|
24
|
+
*/
|
|
25
|
+
startProcess({ name, command, args = [], cwd, env = process.env }) {
|
|
26
|
+
const id = `${this.sanitizeName(name)}-${Date.now()}`;
|
|
27
|
+
|
|
28
|
+
const childProcess = spawn(command, args, {
|
|
29
|
+
cwd,
|
|
30
|
+
env,
|
|
31
|
+
stdio: 'pipe',
|
|
32
|
+
detached: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const metadata = {
|
|
36
|
+
id,
|
|
37
|
+
name,
|
|
38
|
+
command: `${command} ${args.join(' ')}`,
|
|
39
|
+
cwd,
|
|
40
|
+
pid: childProcess.pid,
|
|
41
|
+
status: 'running',
|
|
42
|
+
startTime: new Date().toISOString(),
|
|
43
|
+
exitCode: null,
|
|
44
|
+
exitSignal: null,
|
|
45
|
+
output: [],
|
|
46
|
+
process: childProcess
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.processes.set(id, metadata);
|
|
50
|
+
|
|
51
|
+
// Capture stdout
|
|
52
|
+
childProcess.stdout.on('data', (data) => {
|
|
53
|
+
this.appendOutput(id, 'stdout', data.toString());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Capture stderr
|
|
57
|
+
childProcess.stderr.on('data', (data) => {
|
|
58
|
+
this.appendOutput(id, 'stderr', data.toString());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Handle process exit
|
|
62
|
+
childProcess.on('exit', (code, signal) => {
|
|
63
|
+
this.handleProcessExit(id, code, signal);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Handle process error
|
|
67
|
+
childProcess.on('error', (error) => {
|
|
68
|
+
this.handleProcessError(id, error);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.emit('process-started', { id, name });
|
|
72
|
+
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stop a running process
|
|
78
|
+
*/
|
|
79
|
+
stopProcess(id) {
|
|
80
|
+
const metadata = this.processes.get(id);
|
|
81
|
+
if (!metadata) return false;
|
|
82
|
+
|
|
83
|
+
if (metadata.status === 'running' && metadata.process) {
|
|
84
|
+
metadata.process.kill('SIGTERM');
|
|
85
|
+
metadata.status = 'stopped';
|
|
86
|
+
this.emit('process-stopped', { id, name: metadata.name });
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find process by PID
|
|
95
|
+
* @param {number} pid - Process ID to search for
|
|
96
|
+
* @returns {Object|null} Process metadata or null if not found
|
|
97
|
+
*/
|
|
98
|
+
findProcessByPid(pid) {
|
|
99
|
+
for (const metadata of this.processes.values()) {
|
|
100
|
+
if (metadata.pid === pid) {
|
|
101
|
+
return metadata;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove process from manager by PID
|
|
109
|
+
* Used when external kill is performed on a managed process
|
|
110
|
+
* @param {number} pid - Process ID to remove
|
|
111
|
+
* @returns {boolean} True if process was found and removed
|
|
112
|
+
*/
|
|
113
|
+
removeProcessByPid(pid) {
|
|
114
|
+
const process = this.findProcessByPid(pid);
|
|
115
|
+
if (process) {
|
|
116
|
+
this.processes.delete(process.id);
|
|
117
|
+
this.emit('process-removed', { id: process.id, name: process.name, pid });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get process metadata
|
|
125
|
+
*/
|
|
126
|
+
getProcess(id) {
|
|
127
|
+
return this.processes.get(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all processes
|
|
132
|
+
*/
|
|
133
|
+
getAllProcesses() {
|
|
134
|
+
return Array.from(this.processes.values());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get running processes only
|
|
139
|
+
*/
|
|
140
|
+
getRunningProcesses() {
|
|
141
|
+
return this.getAllProcesses().filter(p => p.status === 'running');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Append output to process buffer
|
|
146
|
+
*/
|
|
147
|
+
appendOutput(id, type, text) {
|
|
148
|
+
const metadata = this.processes.get(id);
|
|
149
|
+
if (!metadata) return;
|
|
150
|
+
|
|
151
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
152
|
+
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
metadata.output.push({
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
type,
|
|
157
|
+
text: line
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Trim to max lines
|
|
162
|
+
if (metadata.output.length > this.maxOutputLines) {
|
|
163
|
+
metadata.output = metadata.output.slice(-this.maxOutputLines);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.emit('output', { id, type, text });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle process exit
|
|
171
|
+
*/
|
|
172
|
+
handleProcessExit(id, code, signal) {
|
|
173
|
+
const metadata = this.processes.get(id);
|
|
174
|
+
if (!metadata) return;
|
|
175
|
+
|
|
176
|
+
metadata.exitCode = code;
|
|
177
|
+
metadata.exitSignal = signal;
|
|
178
|
+
metadata.status = code === 0 ? 'exited' : 'crashed';
|
|
179
|
+
|
|
180
|
+
this.emit('process-exited', {
|
|
181
|
+
id,
|
|
182
|
+
name: metadata.name,
|
|
183
|
+
code,
|
|
184
|
+
signal,
|
|
185
|
+
status: metadata.status
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle process error
|
|
191
|
+
*/
|
|
192
|
+
handleProcessError(id, error) {
|
|
193
|
+
const metadata = this.processes.get(id);
|
|
194
|
+
if (!metadata) return;
|
|
195
|
+
|
|
196
|
+
metadata.status = 'crashed';
|
|
197
|
+
this.appendOutput(id, 'stderr', `Process error: ${error.message}`);
|
|
198
|
+
|
|
199
|
+
this.emit('process-error', { id, name: metadata.name, error });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clean up finished processes
|
|
204
|
+
* Removes processes with status: exited, crashed, or stopped
|
|
205
|
+
*/
|
|
206
|
+
cleanupFinished() {
|
|
207
|
+
const finished = this.getAllProcesses().filter(
|
|
208
|
+
p => p.status === 'exited' || p.status === 'crashed' || p.status === 'stopped'
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
for (const process of finished) {
|
|
212
|
+
this.processes.delete(process.id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return finished.length;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Stop all running processes
|
|
220
|
+
*/
|
|
221
|
+
stopAll() {
|
|
222
|
+
const running = this.getRunningProcesses();
|
|
223
|
+
|
|
224
|
+
for (const process of running) {
|
|
225
|
+
this.stopProcess(process.id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return running.length;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get process uptime in seconds
|
|
233
|
+
*/
|
|
234
|
+
getUptime(id) {
|
|
235
|
+
const metadata = this.processes.get(id);
|
|
236
|
+
if (!metadata) return 0;
|
|
237
|
+
|
|
238
|
+
const start = new Date(metadata.startTime);
|
|
239
|
+
const now = new Date();
|
|
240
|
+
return Math.floor((now - start) / 1000);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format uptime as human-readable string
|
|
245
|
+
*/
|
|
246
|
+
formatUptime(seconds) {
|
|
247
|
+
if (seconds < 60) return `${seconds}s`;
|
|
248
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
249
|
+
|
|
250
|
+
const hours = Math.floor(seconds / 3600);
|
|
251
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
252
|
+
return `${hours}h ${minutes}m`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Sanitize name for use as ID prefix
|
|
257
|
+
*/
|
|
258
|
+
sanitizeName(name) {
|
|
259
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
260
|
+
}
|
|
261
|
+
}
|