devwing 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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2066 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2066 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk3 from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import axios2 from 'axios';
|
|
6
|
+
import Conf from 'conf';
|
|
7
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
8
|
+
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import boxen from 'boxen';
|
|
11
|
+
import gradient from 'gradient-string';
|
|
12
|
+
import terminalLink from 'terminal-link';
|
|
13
|
+
import { readFileSync, promises } from 'fs';
|
|
14
|
+
import path, { dirname, join } from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import { exec, execSync } from 'child_process';
|
|
17
|
+
import { simpleGit } from 'simple-git';
|
|
18
|
+
import { marked } from 'marked';
|
|
19
|
+
import TerminalRenderer from 'marked-terminal';
|
|
20
|
+
import fs2 from 'fs/promises';
|
|
21
|
+
import util from 'util';
|
|
22
|
+
import Table from 'cli-table3';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import semver from 'semver';
|
|
25
|
+
|
|
26
|
+
var SERVICE_NAME = "DevWing CLI";
|
|
27
|
+
var API_KEY_ACCOUNT = "api-key";
|
|
28
|
+
var ConfigManager = class {
|
|
29
|
+
conf;
|
|
30
|
+
constructor() {
|
|
31
|
+
this.conf = new Conf({
|
|
32
|
+
projectName: "devwing",
|
|
33
|
+
defaults: {
|
|
34
|
+
apiUrl: process.env.DEVWING_API_URL || "https://api.devwing.ai"
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get API key from OS keychain
|
|
40
|
+
*/
|
|
41
|
+
async getApiKey() {
|
|
42
|
+
try {
|
|
43
|
+
const entry = new AsyncEntry(SERVICE_NAME, API_KEY_ACCOUNT);
|
|
44
|
+
const password = await entry.getPassword();
|
|
45
|
+
return password || null;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Failed to retrieve API key from keychain:", error);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Store API key in OS keychain
|
|
53
|
+
*/
|
|
54
|
+
async setApiKey(apiKey) {
|
|
55
|
+
try {
|
|
56
|
+
const entry = new AsyncEntry(SERVICE_NAME, API_KEY_ACCOUNT);
|
|
57
|
+
await entry.setPassword(apiKey);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`Failed to store API key in keychain: ${error}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Delete API key from OS keychain
|
|
64
|
+
*/
|
|
65
|
+
async deleteApiKey() {
|
|
66
|
+
try {
|
|
67
|
+
const entry = new AsyncEntry(SERVICE_NAME, API_KEY_ACCOUNT);
|
|
68
|
+
await entry.deletePassword();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Failed to delete API key from keychain:", error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get API URL
|
|
75
|
+
*/
|
|
76
|
+
getApiUrl() {
|
|
77
|
+
return this.conf.get("apiUrl");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Set API URL (for development/testing)
|
|
81
|
+
*/
|
|
82
|
+
setApiUrl(url) {
|
|
83
|
+
this.conf.set("apiUrl", url);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get current workspace ID
|
|
87
|
+
*/
|
|
88
|
+
getWorkspaceId() {
|
|
89
|
+
return this.conf.get("workspaceId");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Set current workspace ID
|
|
93
|
+
*/
|
|
94
|
+
setWorkspaceId(workspaceId) {
|
|
95
|
+
this.conf.set("workspaceId", workspaceId);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get current project ID
|
|
99
|
+
*/
|
|
100
|
+
getProjectId() {
|
|
101
|
+
return this.conf.get("projectId");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set current project ID
|
|
105
|
+
*/
|
|
106
|
+
setProjectId(projectId) {
|
|
107
|
+
this.conf.set("projectId", projectId);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get preferred model
|
|
111
|
+
*/
|
|
112
|
+
getModel() {
|
|
113
|
+
return this.conf.get("model");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Set preferred model
|
|
117
|
+
*/
|
|
118
|
+
setModel(model) {
|
|
119
|
+
this.conf.set("model", model);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get preferred AI mode
|
|
123
|
+
*/
|
|
124
|
+
getMode() {
|
|
125
|
+
return this.conf.get("mode");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Set preferred AI mode
|
|
129
|
+
*/
|
|
130
|
+
setMode(mode) {
|
|
131
|
+
this.conf.set("mode", mode);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get all config
|
|
135
|
+
*/
|
|
136
|
+
getAll() {
|
|
137
|
+
return {
|
|
138
|
+
apiUrl: this.getApiUrl(),
|
|
139
|
+
workspaceId: this.getWorkspaceId(),
|
|
140
|
+
projectId: this.getProjectId(),
|
|
141
|
+
model: this.getModel(),
|
|
142
|
+
mode: this.getMode()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Clear all config (except API key)
|
|
147
|
+
*/
|
|
148
|
+
clear() {
|
|
149
|
+
this.conf.clear();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get config file path (for debugging)
|
|
153
|
+
*/
|
|
154
|
+
getPath() {
|
|
155
|
+
return this.conf.path;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var configManager = new ConfigManager();
|
|
159
|
+
var APIClient = class {
|
|
160
|
+
client;
|
|
161
|
+
jwtToken = null;
|
|
162
|
+
constructor() {
|
|
163
|
+
this.client = axios2.create({
|
|
164
|
+
timeout: 12e4
|
|
165
|
+
// 2 minutes
|
|
166
|
+
});
|
|
167
|
+
this.client.interceptors.request.use(async (config) => {
|
|
168
|
+
const apiKey = await configManager.getApiKey();
|
|
169
|
+
const apiUrl = configManager.getApiUrl();
|
|
170
|
+
config.baseURL = apiUrl;
|
|
171
|
+
if (this.jwtToken) {
|
|
172
|
+
config.headers.Authorization = `Bearer ${this.jwtToken}`;
|
|
173
|
+
} else if (apiKey) {
|
|
174
|
+
config.headers.Authorization = `Bearer ${apiKey}`;
|
|
175
|
+
}
|
|
176
|
+
return config;
|
|
177
|
+
});
|
|
178
|
+
this.client.interceptors.response.use(
|
|
179
|
+
(response) => response,
|
|
180
|
+
(error) => {
|
|
181
|
+
if (error.response?.data) {
|
|
182
|
+
throw new Error(error.response.data.message || error.response.data.error);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Set JWT token for temporary authentication (login flow only)
|
|
190
|
+
*/
|
|
191
|
+
setJwtToken(token) {
|
|
192
|
+
this.jwtToken = token;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Login with email and password
|
|
196
|
+
*/
|
|
197
|
+
async login(email, password) {
|
|
198
|
+
const response = await this.client.post("/auth/login", { email, password });
|
|
199
|
+
return response.data;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get current user profile
|
|
203
|
+
*/
|
|
204
|
+
async getProfile() {
|
|
205
|
+
const response = await this.client.get("/users/me");
|
|
206
|
+
return response.data;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Create a new API key
|
|
210
|
+
*/
|
|
211
|
+
async createApiKey(name) {
|
|
212
|
+
const response = await this.client.post("/api-keys", { name });
|
|
213
|
+
return response.data;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Initiate CLI browser authentication (device flow)
|
|
217
|
+
*/
|
|
218
|
+
async initiateCliAuth(deviceInfo) {
|
|
219
|
+
const response = await this.client.post("/auth/cli/initiate", {
|
|
220
|
+
device_info: deviceInfo
|
|
221
|
+
});
|
|
222
|
+
return response.data;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Poll for CLI authentication status
|
|
226
|
+
*/
|
|
227
|
+
async pollCliAuth(deviceCode) {
|
|
228
|
+
const response = await this.client.post("/auth/cli/poll", {
|
|
229
|
+
device_code: deviceCode
|
|
230
|
+
});
|
|
231
|
+
return response.data;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Stream AI completion
|
|
235
|
+
*/
|
|
236
|
+
async *streamCompletion(request) {
|
|
237
|
+
const apiKey = await configManager.getApiKey();
|
|
238
|
+
const apiUrl = configManager.getApiUrl();
|
|
239
|
+
if (!apiKey) {
|
|
240
|
+
throw new Error('Not authenticated. Run "devwing login" first.');
|
|
241
|
+
}
|
|
242
|
+
const response = await fetch(`${apiUrl}/completions`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
Authorization: `Bearer ${apiKey}`
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify(request)
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
const error = await response.json();
|
|
252
|
+
throw new Error(error.message || "Failed to get completion");
|
|
253
|
+
}
|
|
254
|
+
if (!response.body) {
|
|
255
|
+
throw new Error("No response body");
|
|
256
|
+
}
|
|
257
|
+
const stream = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream());
|
|
258
|
+
for await (const event of stream) {
|
|
259
|
+
if (event.data) {
|
|
260
|
+
try {
|
|
261
|
+
const message = JSON.parse(event.data);
|
|
262
|
+
yield message;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Failed to parse SSE message:", error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Continue completion with tool results
|
|
271
|
+
*/
|
|
272
|
+
async *continueCompletion(sessionId, toolResults) {
|
|
273
|
+
const apiKey = await configManager.getApiKey();
|
|
274
|
+
const apiUrl = configManager.getApiUrl();
|
|
275
|
+
if (!apiKey) {
|
|
276
|
+
throw new Error('Not authenticated. Run "devwing login" first.');
|
|
277
|
+
}
|
|
278
|
+
const response = await fetch(`${apiUrl}/completions/continue`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
Authorization: `Bearer ${apiKey}`
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
session_id: sessionId,
|
|
286
|
+
tool_results: toolResults
|
|
287
|
+
})
|
|
288
|
+
});
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
const error = await response.json();
|
|
291
|
+
throw new Error(error.message || "Failed to continue completion");
|
|
292
|
+
}
|
|
293
|
+
if (!response.body) {
|
|
294
|
+
throw new Error("No response body");
|
|
295
|
+
}
|
|
296
|
+
const stream = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream());
|
|
297
|
+
for await (const event of stream) {
|
|
298
|
+
if (event.data) {
|
|
299
|
+
try {
|
|
300
|
+
const message = JSON.parse(event.data);
|
|
301
|
+
yield message;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error("Failed to parse SSE message:", error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get project memory
|
|
310
|
+
*/
|
|
311
|
+
async getProjectMemory(projectId) {
|
|
312
|
+
const response = await this.client.get(`/memory/${projectId}`);
|
|
313
|
+
return response.data;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Add project memory
|
|
317
|
+
*/
|
|
318
|
+
async addProjectMemory(projectId, content, type) {
|
|
319
|
+
const response = await this.client.post(`/memory/${projectId}`, { content, type });
|
|
320
|
+
return response.data;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Delete project memory
|
|
324
|
+
*/
|
|
325
|
+
async deleteProjectMemory(projectId, memoryId) {
|
|
326
|
+
await this.client.delete(`/memory/${projectId}/${memoryId}`);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get available models
|
|
330
|
+
*/
|
|
331
|
+
async getModels() {
|
|
332
|
+
const response = await this.client.get("/models");
|
|
333
|
+
return response.data;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get usage stats
|
|
337
|
+
*/
|
|
338
|
+
async getUsage(params) {
|
|
339
|
+
const response = await this.client.get("/analytics/overview", { params });
|
|
340
|
+
return response.data;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
var apiClient = new APIClient();
|
|
344
|
+
var Logger = class {
|
|
345
|
+
spinner = null;
|
|
346
|
+
/**
|
|
347
|
+
* Success message
|
|
348
|
+
*/
|
|
349
|
+
success(message) {
|
|
350
|
+
console.log(chalk3.green("\u2713"), message);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Error message
|
|
354
|
+
*/
|
|
355
|
+
error(message, error) {
|
|
356
|
+
console.log(chalk3.red("\u2717"), message);
|
|
357
|
+
if (error && process.env.DEBUG) {
|
|
358
|
+
console.error(chalk3.gray(error.stack || error));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Warning message
|
|
363
|
+
*/
|
|
364
|
+
warn(message) {
|
|
365
|
+
console.log(chalk3.yellow("\u26A0"), message);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Info message
|
|
369
|
+
*/
|
|
370
|
+
info(message) {
|
|
371
|
+
console.log(chalk3.blue("\u2139"), message);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Debug message (only shown if DEBUG env var is set)
|
|
375
|
+
*/
|
|
376
|
+
debug(message, data) {
|
|
377
|
+
if (process.env.DEBUG) {
|
|
378
|
+
console.log(chalk3.gray("\u2699"), chalk3.gray(message));
|
|
379
|
+
if (data) {
|
|
380
|
+
console.log(chalk3.gray(JSON.stringify(data, null, 2)));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Start a spinner
|
|
386
|
+
*/
|
|
387
|
+
startSpinner(text) {
|
|
388
|
+
this.spinner = ora({
|
|
389
|
+
text,
|
|
390
|
+
color: "cyan"
|
|
391
|
+
}).start();
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Update spinner text
|
|
395
|
+
*/
|
|
396
|
+
updateSpinner(text) {
|
|
397
|
+
if (this.spinner) {
|
|
398
|
+
this.spinner.text = text;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Stop spinner with success
|
|
403
|
+
*/
|
|
404
|
+
succeedSpinner(text) {
|
|
405
|
+
if (this.spinner) {
|
|
406
|
+
this.spinner.succeed(text);
|
|
407
|
+
this.spinner = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Stop spinner with failure
|
|
412
|
+
*/
|
|
413
|
+
failSpinner(text) {
|
|
414
|
+
if (this.spinner) {
|
|
415
|
+
this.spinner.fail(text);
|
|
416
|
+
this.spinner = null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Stop spinner
|
|
421
|
+
*/
|
|
422
|
+
stopSpinner() {
|
|
423
|
+
if (this.spinner) {
|
|
424
|
+
this.spinner.stop();
|
|
425
|
+
this.spinner = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Print DevWing banner
|
|
430
|
+
*/
|
|
431
|
+
printBanner() {
|
|
432
|
+
const banner = gradient.pastel.multiline([
|
|
433
|
+
"",
|
|
434
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
435
|
+
" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D ",
|
|
436
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557",
|
|
437
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
438
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
439
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D ",
|
|
440
|
+
"",
|
|
441
|
+
" Your AI Wingman in the Terminal",
|
|
442
|
+
""
|
|
443
|
+
].join("\n"));
|
|
444
|
+
console.log(banner);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Print a box message
|
|
448
|
+
*/
|
|
449
|
+
box(message, options) {
|
|
450
|
+
console.log(
|
|
451
|
+
boxen(message, {
|
|
452
|
+
padding: 1,
|
|
453
|
+
margin: 1,
|
|
454
|
+
borderStyle: "round",
|
|
455
|
+
borderColor: options?.color || "cyan",
|
|
456
|
+
title: options?.title,
|
|
457
|
+
titleAlignment: "center"
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Print a newline
|
|
463
|
+
*/
|
|
464
|
+
newline() {
|
|
465
|
+
console.log();
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var logger = new Logger();
|
|
469
|
+
async function loginCommand() {
|
|
470
|
+
try {
|
|
471
|
+
const existingKey = await configManager.getApiKey();
|
|
472
|
+
if (existingKey) {
|
|
473
|
+
const { overwrite } = await inquirer.prompt([
|
|
474
|
+
{
|
|
475
|
+
type: "confirm",
|
|
476
|
+
name: "overwrite",
|
|
477
|
+
message: "You are already logged in. Do you want to log in with a different account?",
|
|
478
|
+
default: false
|
|
479
|
+
}
|
|
480
|
+
]);
|
|
481
|
+
if (!overwrite) {
|
|
482
|
+
logger.info("Login cancelled");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
logger.printBanner();
|
|
487
|
+
logger.newline();
|
|
488
|
+
const { method } = await inquirer.prompt([
|
|
489
|
+
{
|
|
490
|
+
type: "list",
|
|
491
|
+
name: "method",
|
|
492
|
+
message: "How would you like to authenticate?",
|
|
493
|
+
choices: [
|
|
494
|
+
{ name: "Email & Password", value: "email" },
|
|
495
|
+
{ name: "Via Browser (easiest)", value: "browser" },
|
|
496
|
+
{ name: "API Key (from dashboard)", value: "apikey" }
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
]);
|
|
500
|
+
if (method === "email") {
|
|
501
|
+
await loginWithEmail();
|
|
502
|
+
} else if (method === "browser") {
|
|
503
|
+
await loginWithBrowser();
|
|
504
|
+
} else {
|
|
505
|
+
await loginWithApiKey();
|
|
506
|
+
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
logger.error("Login failed", error);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async function loginWithEmail() {
|
|
513
|
+
const { email, password } = await inquirer.prompt([
|
|
514
|
+
{
|
|
515
|
+
type: "input",
|
|
516
|
+
name: "email",
|
|
517
|
+
message: "Email:",
|
|
518
|
+
validate: (input) => {
|
|
519
|
+
if (!input.includes("@")) {
|
|
520
|
+
return "Please enter a valid email address";
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
type: "password",
|
|
527
|
+
name: "password",
|
|
528
|
+
message: "Password:",
|
|
529
|
+
mask: "*"
|
|
530
|
+
}
|
|
531
|
+
]);
|
|
532
|
+
logger.startSpinner("Authenticating...");
|
|
533
|
+
try {
|
|
534
|
+
const { access_token, user } = await apiClient.login(email, password);
|
|
535
|
+
apiClient.setJwtToken(access_token);
|
|
536
|
+
logger.updateSpinner("Creating API key...");
|
|
537
|
+
const { key } = await apiClient.createApiKey("CLI - Auto-generated");
|
|
538
|
+
apiClient.setJwtToken(null);
|
|
539
|
+
await configManager.setApiKey(key);
|
|
540
|
+
logger.succeedSpinner(`Welcome back, ${user.full_name || user.email}!`);
|
|
541
|
+
logger.success(`Plan: ${user.plan.toUpperCase()}`);
|
|
542
|
+
logger.newline();
|
|
543
|
+
showQuickStart();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
logger.failSpinner("Authentication failed");
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function loginWithBrowser() {
|
|
550
|
+
const os2 = await import('os');
|
|
551
|
+
const open = await import('open');
|
|
552
|
+
logger.startSpinner("Initiating browser authentication...");
|
|
553
|
+
try {
|
|
554
|
+
const deviceInfo = `DevWing CLI on ${os2.platform()}`;
|
|
555
|
+
const initResponse = await apiClient.initiateCliAuth(deviceInfo);
|
|
556
|
+
logger.succeedSpinner("Ready to authenticate!");
|
|
557
|
+
logger.newline();
|
|
558
|
+
logger.box(
|
|
559
|
+
[
|
|
560
|
+
"Browser Authentication",
|
|
561
|
+
"",
|
|
562
|
+
`Code: ${chalk3.bold.yellow(initResponse.user_code)}`,
|
|
563
|
+
"",
|
|
564
|
+
`Opening browser to ${chalk3.cyan(initResponse.verification_url)}`,
|
|
565
|
+
"",
|
|
566
|
+
`Enter the code above when prompted.`,
|
|
567
|
+
"",
|
|
568
|
+
`Expires in ${Math.floor(initResponse.expires_in / 60)} minutes.`
|
|
569
|
+
].join("\n"),
|
|
570
|
+
{ title: "Verify Device", color: "blue" }
|
|
571
|
+
);
|
|
572
|
+
logger.newline();
|
|
573
|
+
logger.info("Opening browser...");
|
|
574
|
+
try {
|
|
575
|
+
await open.default(initResponse.verification_url_complete);
|
|
576
|
+
logger.success("Browser opened! Please complete authentication.");
|
|
577
|
+
} catch (err) {
|
|
578
|
+
logger.warn("Could not open browser automatically");
|
|
579
|
+
logger.info(
|
|
580
|
+
`Please visit: ${chalk3.cyan(initResponse.verification_url_complete)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
logger.newline();
|
|
584
|
+
logger.startSpinner("Waiting for authorization...");
|
|
585
|
+
const pollInterval = initResponse.interval * 1e3;
|
|
586
|
+
const maxAttempts = Math.floor(initResponse.expires_in / initResponse.interval);
|
|
587
|
+
let attempts = 0;
|
|
588
|
+
const pollForAuth = async () => {
|
|
589
|
+
return new Promise((resolve, reject) => {
|
|
590
|
+
const interval = setInterval(async () => {
|
|
591
|
+
attempts++;
|
|
592
|
+
if (attempts >= maxAttempts) {
|
|
593
|
+
clearInterval(interval);
|
|
594
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const pollResponse = await apiClient.pollCliAuth(
|
|
599
|
+
initResponse.device_code
|
|
600
|
+
);
|
|
601
|
+
if (pollResponse.status === "authorized" && pollResponse.api_key) {
|
|
602
|
+
clearInterval(interval);
|
|
603
|
+
resolve(pollResponse.api_key);
|
|
604
|
+
} else if (pollResponse.status === "denied") {
|
|
605
|
+
clearInterval(interval);
|
|
606
|
+
reject(new Error("Authorization denied by user."));
|
|
607
|
+
} else if (pollResponse.status === "expired") {
|
|
608
|
+
clearInterval(interval);
|
|
609
|
+
reject(
|
|
610
|
+
new Error('Device code expired. Please run "devwing login" again.')
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
clearInterval(interval);
|
|
615
|
+
reject(error);
|
|
616
|
+
}
|
|
617
|
+
}, pollInterval);
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
const apiKey = await pollForAuth();
|
|
621
|
+
await configManager.setApiKey(apiKey);
|
|
622
|
+
logger.updateSpinner("Fetching profile...");
|
|
623
|
+
const user = await apiClient.getProfile();
|
|
624
|
+
logger.succeedSpinner(`Welcome, ${user.full_name || user.email}!`);
|
|
625
|
+
logger.success(`Plan: ${user.plan.toUpperCase()}`);
|
|
626
|
+
logger.newline();
|
|
627
|
+
showQuickStart();
|
|
628
|
+
} catch (error) {
|
|
629
|
+
logger.failSpinner("Browser authentication failed");
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function loginWithApiKey() {
|
|
634
|
+
const dashboardUrl = terminalLink(
|
|
635
|
+
"dashboard",
|
|
636
|
+
"https://devwing.ai/dashboard/api-keys",
|
|
637
|
+
{ fallback: () => "https://devwing.ai/dashboard/api-keys" }
|
|
638
|
+
);
|
|
639
|
+
logger.info(`Get your API key from the ${dashboardUrl}`);
|
|
640
|
+
logger.newline();
|
|
641
|
+
const { apiKey } = await inquirer.prompt([
|
|
642
|
+
{
|
|
643
|
+
type: "password",
|
|
644
|
+
name: "apiKey",
|
|
645
|
+
message: "API Key:",
|
|
646
|
+
mask: "*",
|
|
647
|
+
validate: (input) => {
|
|
648
|
+
if (!input.startsWith("dw_sk_")) {
|
|
649
|
+
return 'Invalid API key format. Should start with "dw_sk_"';
|
|
650
|
+
}
|
|
651
|
+
if (input.length < 20) {
|
|
652
|
+
return "API key seems too short";
|
|
653
|
+
}
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
]);
|
|
658
|
+
logger.startSpinner("Verifying API key...");
|
|
659
|
+
try {
|
|
660
|
+
await configManager.setApiKey(apiKey);
|
|
661
|
+
const user = await apiClient.getProfile();
|
|
662
|
+
logger.succeedSpinner(`Successfully authenticated as ${user.full_name || user.email}!`);
|
|
663
|
+
logger.success(`Plan: ${user.plan.toUpperCase()}`);
|
|
664
|
+
logger.newline();
|
|
665
|
+
showQuickStart();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
await configManager.deleteApiKey();
|
|
668
|
+
logger.failSpinner("Invalid API key");
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function logoutCommand() {
|
|
673
|
+
try {
|
|
674
|
+
const apiKey = await configManager.getApiKey();
|
|
675
|
+
if (!apiKey) {
|
|
676
|
+
logger.info("You are not logged in");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const { confirm } = await inquirer.prompt([
|
|
680
|
+
{
|
|
681
|
+
type: "confirm",
|
|
682
|
+
name: "confirm",
|
|
683
|
+
message: "Are you sure you want to log out?",
|
|
684
|
+
default: false
|
|
685
|
+
}
|
|
686
|
+
]);
|
|
687
|
+
if (!confirm) {
|
|
688
|
+
logger.info("Logout cancelled");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
await configManager.deleteApiKey();
|
|
692
|
+
const apiUrl = configManager.getApiUrl();
|
|
693
|
+
configManager.clear();
|
|
694
|
+
configManager.setApiUrl(apiUrl);
|
|
695
|
+
logger.success("Successfully logged out");
|
|
696
|
+
} catch (error) {
|
|
697
|
+
logger.error("Logout failed", error);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function statusCommand() {
|
|
702
|
+
try {
|
|
703
|
+
const apiKey = await configManager.getApiKey();
|
|
704
|
+
if (!apiKey) {
|
|
705
|
+
logger.warn("Not authenticated");
|
|
706
|
+
logger.info('Run "devwing login" to get started');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
logger.startSpinner("Fetching profile...");
|
|
710
|
+
try {
|
|
711
|
+
const user = await apiClient.getProfile();
|
|
712
|
+
logger.succeedSpinner("Authenticated");
|
|
713
|
+
logger.newline();
|
|
714
|
+
console.log(chalk3.bold("User Profile:"));
|
|
715
|
+
console.log(` Email: ${user.email}`);
|
|
716
|
+
console.log(` Name: ${user.full_name || "Not set"}`);
|
|
717
|
+
console.log(` Plan: ${user.plan.toUpperCase()}`);
|
|
718
|
+
console.log(
|
|
719
|
+
` Tokens used today: ${user.tokens_used_today.toLocaleString()}`
|
|
720
|
+
);
|
|
721
|
+
const config = configManager.getAll();
|
|
722
|
+
if (config.workspaceId) {
|
|
723
|
+
console.log(` Workspace: ${config.workspaceId}`);
|
|
724
|
+
}
|
|
725
|
+
if (config.projectId) {
|
|
726
|
+
console.log(` Project: ${config.projectId}`);
|
|
727
|
+
}
|
|
728
|
+
logger.newline();
|
|
729
|
+
} catch (error) {
|
|
730
|
+
logger.failSpinner("Failed to fetch profile");
|
|
731
|
+
logger.error("API key may be invalid or expired");
|
|
732
|
+
logger.info('Run "devwing login" to re-authenticate');
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
logger.error("Failed to check status", error);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function showQuickStart() {
|
|
740
|
+
logger.box(
|
|
741
|
+
[
|
|
742
|
+
"Quick Start:",
|
|
743
|
+
"",
|
|
744
|
+
' devwing "fix the auth bug" Ask anything',
|
|
745
|
+
" devwing scan Security scan",
|
|
746
|
+
" devwing review Code review",
|
|
747
|
+
" devwing explain file.js Explain code",
|
|
748
|
+
"",
|
|
749
|
+
"For more commands, run: devwing --help"
|
|
750
|
+
].join("\n"),
|
|
751
|
+
{ title: "Ready to go!", color: "green" }
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
var DEFAULT_OPTIONS = {
|
|
755
|
+
maxFiles: 50,
|
|
756
|
+
maxFileSize: 100 * 1024,
|
|
757
|
+
// 100KB
|
|
758
|
+
maxTotalTokens: 1e5,
|
|
759
|
+
excludePatterns: [
|
|
760
|
+
"**/node_modules/**",
|
|
761
|
+
"**/dist/**",
|
|
762
|
+
"**/build/**",
|
|
763
|
+
"**/.git/**",
|
|
764
|
+
"**/coverage/**",
|
|
765
|
+
"**/.next/**",
|
|
766
|
+
"**/.vscode/**",
|
|
767
|
+
"**/.idea/**",
|
|
768
|
+
"**/*.log",
|
|
769
|
+
"**/*.lock",
|
|
770
|
+
"**/package-lock.json",
|
|
771
|
+
"**/yarn.lock",
|
|
772
|
+
"**/pnpm-lock.yaml"
|
|
773
|
+
]
|
|
774
|
+
};
|
|
775
|
+
var ContextCollector = class {
|
|
776
|
+
git;
|
|
777
|
+
cwd;
|
|
778
|
+
options;
|
|
779
|
+
constructor(cwd = process.cwd(), options = {}) {
|
|
780
|
+
this.cwd = cwd;
|
|
781
|
+
this.git = simpleGit(cwd);
|
|
782
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Collect all context for AI completion
|
|
786
|
+
*/
|
|
787
|
+
async collect(prompt) {
|
|
788
|
+
logger.debug("Collecting context from:", this.cwd);
|
|
789
|
+
const context = {
|
|
790
|
+
files: [],
|
|
791
|
+
cwd: this.cwd,
|
|
792
|
+
shell: this.detectShell(),
|
|
793
|
+
os: os.platform()
|
|
794
|
+
};
|
|
795
|
+
try {
|
|
796
|
+
const isRepo = await this.git.checkIsRepo();
|
|
797
|
+
if (isRepo) {
|
|
798
|
+
context.git_branch = await this.getCurrentBranch();
|
|
799
|
+
context.git_diff = await this.getGitDiff();
|
|
800
|
+
context.git_log = await this.getRecentCommits();
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
logger.debug("Not a git repository or git not available");
|
|
804
|
+
}
|
|
805
|
+
context.node_version = this.getNodeVersion();
|
|
806
|
+
context.python_version = this.getPythonVersion();
|
|
807
|
+
context.framework = await this.detectFramework();
|
|
808
|
+
context.files = await this.collectFiles(prompt);
|
|
809
|
+
logger.debug(`Collected ${context.files.length} files`);
|
|
810
|
+
return context;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Collect relevant files based on prompt and project state
|
|
814
|
+
*/
|
|
815
|
+
async collectFiles(prompt) {
|
|
816
|
+
const files = [];
|
|
817
|
+
const modifiedFiles = await this.getModifiedFiles();
|
|
818
|
+
const mentionedFiles = prompt ? this.extractFilePaths(prompt) : [];
|
|
819
|
+
const configFiles = await this.getConfigFiles();
|
|
820
|
+
const filePaths = Array.from(
|
|
821
|
+
/* @__PURE__ */ new Set([...modifiedFiles, ...mentionedFiles, ...configFiles])
|
|
822
|
+
);
|
|
823
|
+
for (const filePath of filePaths.slice(0, this.options.maxFiles)) {
|
|
824
|
+
try {
|
|
825
|
+
const fullPath = path.resolve(this.cwd, filePath);
|
|
826
|
+
const stat = await promises.stat(fullPath);
|
|
827
|
+
if (stat.size > this.options.maxFileSize) {
|
|
828
|
+
logger.debug(`Skipping large file: ${filePath} (${stat.size} bytes)`);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const content = await promises.readFile(fullPath, "utf-8");
|
|
832
|
+
const language = this.detectLanguage(filePath);
|
|
833
|
+
files.push({
|
|
834
|
+
path: filePath,
|
|
835
|
+
content,
|
|
836
|
+
language,
|
|
837
|
+
size: stat.size
|
|
838
|
+
});
|
|
839
|
+
} catch (error) {
|
|
840
|
+
logger.debug(`Failed to read file ${filePath}:`, error);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return files;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get modified files from git
|
|
847
|
+
*/
|
|
848
|
+
async getModifiedFiles() {
|
|
849
|
+
try {
|
|
850
|
+
const status = await this.git.status();
|
|
851
|
+
return [
|
|
852
|
+
...status.modified,
|
|
853
|
+
...status.created,
|
|
854
|
+
...status.staged
|
|
855
|
+
];
|
|
856
|
+
} catch (error) {
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Extract file paths mentioned in prompt
|
|
862
|
+
*/
|
|
863
|
+
extractFilePaths(prompt) {
|
|
864
|
+
const paths = [];
|
|
865
|
+
const patterns = [
|
|
866
|
+
/[\w-]+\/[\w-/.]+\.\w+/g,
|
|
867
|
+
// path/to/file.ext
|
|
868
|
+
/[\w-]+\.\w+/g
|
|
869
|
+
// file.ext
|
|
870
|
+
];
|
|
871
|
+
for (const pattern of patterns) {
|
|
872
|
+
const matches = prompt.match(pattern);
|
|
873
|
+
if (matches) {
|
|
874
|
+
paths.push(...matches);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return paths;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Get important config files
|
|
881
|
+
*/
|
|
882
|
+
async getConfigFiles() {
|
|
883
|
+
const configPatterns = [
|
|
884
|
+
"package.json",
|
|
885
|
+
"tsconfig.json",
|
|
886
|
+
"requirements.txt",
|
|
887
|
+
"Pipfile",
|
|
888
|
+
"Dockerfile",
|
|
889
|
+
"docker-compose.yml",
|
|
890
|
+
".env.example",
|
|
891
|
+
"README.md"
|
|
892
|
+
];
|
|
893
|
+
const files = [];
|
|
894
|
+
for (const pattern of configPatterns) {
|
|
895
|
+
try {
|
|
896
|
+
const fullPath = path.join(this.cwd, pattern);
|
|
897
|
+
await promises.access(fullPath);
|
|
898
|
+
files.push(pattern);
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return files;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Get current git branch
|
|
906
|
+
*/
|
|
907
|
+
async getCurrentBranch() {
|
|
908
|
+
try {
|
|
909
|
+
const branch = await this.git.revparse(["--abbrev-ref", "HEAD"]);
|
|
910
|
+
return branch.trim();
|
|
911
|
+
} catch {
|
|
912
|
+
return "unknown";
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Get git diff
|
|
917
|
+
*/
|
|
918
|
+
async getGitDiff() {
|
|
919
|
+
try {
|
|
920
|
+
const diff = await this.git.diff();
|
|
921
|
+
return diff.substring(0, 1e4);
|
|
922
|
+
} catch {
|
|
923
|
+
return "";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Get recent commits
|
|
928
|
+
*/
|
|
929
|
+
async getRecentCommits() {
|
|
930
|
+
try {
|
|
931
|
+
const log = await this.git.log({ maxCount: 5 });
|
|
932
|
+
return log.all.map((commit) => `${commit.hash.substring(0, 7)} - ${commit.message}`).join("\n");
|
|
933
|
+
} catch {
|
|
934
|
+
return "";
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Detect shell type
|
|
939
|
+
*/
|
|
940
|
+
detectShell() {
|
|
941
|
+
return path.basename(process.env.SHELL || "sh");
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Get Node.js version
|
|
945
|
+
*/
|
|
946
|
+
getNodeVersion() {
|
|
947
|
+
try {
|
|
948
|
+
return process.version;
|
|
949
|
+
} catch {
|
|
950
|
+
return void 0;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Get Python version
|
|
955
|
+
*/
|
|
956
|
+
getPythonVersion() {
|
|
957
|
+
try {
|
|
958
|
+
const version = execSync("python --version 2>&1", { encoding: "utf-8" });
|
|
959
|
+
return version.trim();
|
|
960
|
+
} catch {
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Detect framework from package.json or requirements.txt
|
|
966
|
+
*/
|
|
967
|
+
async detectFramework() {
|
|
968
|
+
try {
|
|
969
|
+
const packageJsonPath = path.join(this.cwd, "package.json");
|
|
970
|
+
try {
|
|
971
|
+
const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf-8"));
|
|
972
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
973
|
+
if (deps.next) return "Next.js";
|
|
974
|
+
if (deps.react) return "React";
|
|
975
|
+
if (deps.vue) return "Vue";
|
|
976
|
+
if (deps["@angular/core"]) return "Angular";
|
|
977
|
+
if (deps.express) return "Express";
|
|
978
|
+
if (deps.fastify) return "Fastify";
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
const requirementsPath = path.join(this.cwd, "requirements.txt");
|
|
982
|
+
try {
|
|
983
|
+
const requirements = await promises.readFile(requirementsPath, "utf-8");
|
|
984
|
+
if (requirements.includes("django")) return "Django";
|
|
985
|
+
if (requirements.includes("flask")) return "Flask";
|
|
986
|
+
if (requirements.includes("fastapi")) return "FastAPI";
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
} catch (error) {
|
|
990
|
+
logger.debug("Failed to detect framework:", error);
|
|
991
|
+
}
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Detect programming language from file extension
|
|
996
|
+
*/
|
|
997
|
+
detectLanguage(filePath) {
|
|
998
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
999
|
+
const languageMap = {
|
|
1000
|
+
".js": "javascript",
|
|
1001
|
+
".jsx": "javascript",
|
|
1002
|
+
".ts": "typescript",
|
|
1003
|
+
".tsx": "typescript",
|
|
1004
|
+
".py": "python",
|
|
1005
|
+
".rb": "ruby",
|
|
1006
|
+
".go": "go",
|
|
1007
|
+
".rs": "rust",
|
|
1008
|
+
".java": "java",
|
|
1009
|
+
".cpp": "cpp",
|
|
1010
|
+
".c": "c",
|
|
1011
|
+
".h": "c",
|
|
1012
|
+
".hpp": "cpp",
|
|
1013
|
+
".cs": "csharp",
|
|
1014
|
+
".php": "php",
|
|
1015
|
+
".swift": "swift",
|
|
1016
|
+
".kt": "kotlin",
|
|
1017
|
+
".dart": "dart",
|
|
1018
|
+
".sh": "bash",
|
|
1019
|
+
".yml": "yaml",
|
|
1020
|
+
".yaml": "yaml",
|
|
1021
|
+
".json": "json",
|
|
1022
|
+
".xml": "xml",
|
|
1023
|
+
".html": "html",
|
|
1024
|
+
".css": "css",
|
|
1025
|
+
".scss": "scss",
|
|
1026
|
+
".md": "markdown",
|
|
1027
|
+
".sql": "sql"
|
|
1028
|
+
};
|
|
1029
|
+
return languageMap[ext] || "text";
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var execAsync = util.promisify(exec);
|
|
1033
|
+
var ToolEngine = class {
|
|
1034
|
+
allowedProjectPath;
|
|
1035
|
+
commandTimeout = 3e4;
|
|
1036
|
+
// 30 seconds
|
|
1037
|
+
maxFileSize = 10 * 1024 * 1024;
|
|
1038
|
+
// 10MB
|
|
1039
|
+
constructor(allowedProjectPath = process.cwd()) {
|
|
1040
|
+
this.allowedProjectPath = path.resolve(allowedProjectPath);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Validate and resolve file path to prevent directory traversal
|
|
1044
|
+
*/
|
|
1045
|
+
validatePath(filePath) {
|
|
1046
|
+
const resolvedPath = path.resolve(this.allowedProjectPath, filePath);
|
|
1047
|
+
if (!resolvedPath.startsWith(this.allowedProjectPath)) {
|
|
1048
|
+
throw new Error(`Security Error: Path '${filePath}' is outside allowed project directory`);
|
|
1049
|
+
}
|
|
1050
|
+
return resolvedPath;
|
|
1051
|
+
}
|
|
1052
|
+
// =================================================================
|
|
1053
|
+
// FILE OPERATIONS
|
|
1054
|
+
// =================================================================
|
|
1055
|
+
async readFile(filePath) {
|
|
1056
|
+
try {
|
|
1057
|
+
const validatedPath = this.validatePath(filePath);
|
|
1058
|
+
try {
|
|
1059
|
+
const stat = await fs2.stat(validatedPath);
|
|
1060
|
+
if (!stat.isFile()) {
|
|
1061
|
+
return { success: false, output: "", error: `Path is not a file: ${filePath}` };
|
|
1062
|
+
}
|
|
1063
|
+
if (stat.size > this.maxFileSize) {
|
|
1064
|
+
return { success: false, output: "", error: `File too large: ${stat.size} bytes (max ${this.maxFileSize})` };
|
|
1065
|
+
}
|
|
1066
|
+
const content = await fs2.readFile(validatedPath, "utf-8");
|
|
1067
|
+
return {
|
|
1068
|
+
success: true,
|
|
1069
|
+
output: content,
|
|
1070
|
+
metadata: { size: stat.size, lines: content.split("\n").length }
|
|
1071
|
+
};
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
if (e.code === "ENOENT") {
|
|
1074
|
+
return { success: false, output: "", error: `File not found: ${filePath}` };
|
|
1075
|
+
}
|
|
1076
|
+
throw e;
|
|
1077
|
+
}
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
return { success: false, output: "", error: e.message };
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async writeFile(filePath, content, userConfirmed = false) {
|
|
1083
|
+
if (!userConfirmed) {
|
|
1084
|
+
return { success: false, output: "", error: "User confirmation required for write_file operation" };
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const validatedPath = this.validatePath(filePath);
|
|
1088
|
+
await fs2.mkdir(path.dirname(validatedPath), { recursive: true });
|
|
1089
|
+
await fs2.writeFile(validatedPath, content, "utf-8");
|
|
1090
|
+
return {
|
|
1091
|
+
success: true,
|
|
1092
|
+
output: `File written: ${filePath}`,
|
|
1093
|
+
metadata: { path: validatedPath, size: Buffer.byteLength(content, "utf8") }
|
|
1094
|
+
};
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
return { success: false, output: "", error: e.message };
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async listDirectory(dirPath = ".") {
|
|
1100
|
+
try {
|
|
1101
|
+
const validatedPath = this.validatePath(dirPath);
|
|
1102
|
+
try {
|
|
1103
|
+
const stat = await fs2.stat(validatedPath);
|
|
1104
|
+
if (!stat.isDirectory()) {
|
|
1105
|
+
return { success: false, output: "", error: `Path is not a directory: ${dirPath}` };
|
|
1106
|
+
}
|
|
1107
|
+
const items = await fs2.readdir(validatedPath, { withFileTypes: true });
|
|
1108
|
+
const formattedItems = await Promise.all(items.map(async (item) => {
|
|
1109
|
+
const itemType = item.isDirectory() ? "dir" : "file";
|
|
1110
|
+
let size = 0;
|
|
1111
|
+
if (item.isFile()) {
|
|
1112
|
+
try {
|
|
1113
|
+
const itemStat = await fs2.stat(path.join(validatedPath, item.name));
|
|
1114
|
+
size = itemStat.size;
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return { name: item.name, type: itemType, size };
|
|
1119
|
+
}));
|
|
1120
|
+
formattedItems.sort((a, b) => {
|
|
1121
|
+
if (a.type === b.type) return a.name.localeCompare(b.name);
|
|
1122
|
+
return a.type === "dir" ? -1 : 1;
|
|
1123
|
+
});
|
|
1124
|
+
const outputLines = formattedItems.map((item) => {
|
|
1125
|
+
return item.type === "dir" ? `\u{1F4C1} ${item.name}/` : `\u{1F4C4} ${item.name} (${item.size} bytes)`;
|
|
1126
|
+
});
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
output: outputLines.join("\n"),
|
|
1130
|
+
metadata: { item_count: formattedItems.length }
|
|
1131
|
+
};
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
if (e.code === "ENOENT") {
|
|
1134
|
+
return { success: false, output: "", error: `Directory not found: ${dirPath}` };
|
|
1135
|
+
}
|
|
1136
|
+
throw e;
|
|
1137
|
+
}
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
return { success: false, output: "", error: e.message };
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// =================================================================
|
|
1143
|
+
// SHELL EXECUTION
|
|
1144
|
+
// =================================================================
|
|
1145
|
+
async runCommand(command, cwd, userConfirmed = false) {
|
|
1146
|
+
if (!userConfirmed) {
|
|
1147
|
+
return { success: false, output: "", error: "User confirmation required for run_command operation" };
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
let execCwd = this.allowedProjectPath;
|
|
1151
|
+
if (cwd) {
|
|
1152
|
+
execCwd = this.validatePath(cwd);
|
|
1153
|
+
const stat = await fs2.stat(execCwd);
|
|
1154
|
+
if (!stat.isDirectory()) {
|
|
1155
|
+
return { success: false, output: "", error: `Working directory not found: ${cwd}` };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
1160
|
+
cwd: execCwd,
|
|
1161
|
+
timeout: this.commandTimeout,
|
|
1162
|
+
env: { ...process.env, TERM: "dumb" }
|
|
1163
|
+
});
|
|
1164
|
+
return {
|
|
1165
|
+
success: true,
|
|
1166
|
+
output: stdout || stderr,
|
|
1167
|
+
// Some commands write to stderr even on success
|
|
1168
|
+
metadata: { command }
|
|
1169
|
+
};
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
return {
|
|
1172
|
+
success: false,
|
|
1173
|
+
output: e.stdout || "",
|
|
1174
|
+
error: e.stderr || e.message,
|
|
1175
|
+
metadata: { command, exit_code: e.code }
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
return { success: false, output: "", error: e.message };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// =================================================================
|
|
1183
|
+
// GIT OPERATIONS
|
|
1184
|
+
// =================================================================
|
|
1185
|
+
async gitDiff(cwd) {
|
|
1186
|
+
try {
|
|
1187
|
+
let gitCwd = this.allowedProjectPath;
|
|
1188
|
+
if (cwd) gitCwd = this.validatePath(cwd);
|
|
1189
|
+
const git = simpleGit(gitCwd);
|
|
1190
|
+
const diff = await git.diff();
|
|
1191
|
+
return { success: true, output: diff };
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
return { success: false, output: "", error: e.message };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async gitLog(limit = 5, cwd) {
|
|
1197
|
+
try {
|
|
1198
|
+
let gitCwd = this.allowedProjectPath;
|
|
1199
|
+
if (cwd) gitCwd = this.validatePath(cwd);
|
|
1200
|
+
const git = simpleGit(gitCwd);
|
|
1201
|
+
const log = await git.log({ maxCount: limit });
|
|
1202
|
+
const output = log.all.map((commit) => `${commit.hash.substring(0, 7)} - ${commit.message}`).join("\n");
|
|
1203
|
+
return { success: true, output };
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
return { success: false, output: "", error: e.message };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async gitCommit(message, cwd, userConfirmed = false) {
|
|
1209
|
+
if (!userConfirmed) {
|
|
1210
|
+
return { success: false, output: "", error: "User confirmation required for git_commit operation" };
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
let gitCwd = this.allowedProjectPath;
|
|
1214
|
+
if (cwd) gitCwd = this.validatePath(cwd);
|
|
1215
|
+
const git = simpleGit(gitCwd);
|
|
1216
|
+
const result = await git.commit(message);
|
|
1217
|
+
return {
|
|
1218
|
+
success: true,
|
|
1219
|
+
output: `Committed: ${result.commit}
|
|
1220
|
+
Branch: ${result.branch}
|
|
1221
|
+
Summary: ${result.summary.changes} changes, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions`
|
|
1222
|
+
};
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
return { success: false, output: "", error: e.message };
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// =================================================================
|
|
1228
|
+
// EXTERNAL API OPERATIONS
|
|
1229
|
+
// =================================================================
|
|
1230
|
+
async searchDocs(query, source = "mdn") {
|
|
1231
|
+
try {
|
|
1232
|
+
if (source === "mdn") {
|
|
1233
|
+
const url = `https://developer.mozilla.org/api/v1/search?q=${encodeURIComponent(query)}`;
|
|
1234
|
+
const response = await axios2.get(url, { timeout: 1e4 });
|
|
1235
|
+
return { success: true, output: JSON.stringify(response.data, null, 2), metadata: { source } };
|
|
1236
|
+
} else if (source === "stackoverflow") {
|
|
1237
|
+
const url = `https://api.stackexchange.com/2.3/search?order=desc&sort=activity&intitle=${encodeURIComponent(query)}&site=stackoverflow`;
|
|
1238
|
+
const response = await axios2.get(url, { timeout: 1e4 });
|
|
1239
|
+
return { success: true, output: JSON.stringify(response.data, null, 2), metadata: { source } };
|
|
1240
|
+
} else {
|
|
1241
|
+
return { success: false, output: "", error: `Unsupported documentation source: ${source}` };
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
return { success: false, output: "", error: e.message };
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async httpRequest(url, method = "GET", headers, body) {
|
|
1248
|
+
try {
|
|
1249
|
+
const response = await axios2({
|
|
1250
|
+
url,
|
|
1251
|
+
method,
|
|
1252
|
+
headers,
|
|
1253
|
+
data: body,
|
|
1254
|
+
timeout: 1e4
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
success: true,
|
|
1258
|
+
output: typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2),
|
|
1259
|
+
metadata: { status_code: response.status }
|
|
1260
|
+
};
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
return {
|
|
1263
|
+
success: false,
|
|
1264
|
+
output: "",
|
|
1265
|
+
error: e.response ? `HTTP ${e.response.status}: ${e.message}` : e.message
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
async cveLookup(cveId) {
|
|
1270
|
+
try {
|
|
1271
|
+
const url = `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(cveId)}`;
|
|
1272
|
+
const response = await axios2.get(url, { timeout: 1e4 });
|
|
1273
|
+
return { success: true, output: JSON.stringify(response.data, null, 2), metadata: { cve_id: cveId } };
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
return { success: false, output: "", error: e.message };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// =================================================================
|
|
1279
|
+
// TOOL EXECUTION DISPATCHER
|
|
1280
|
+
// =================================================================
|
|
1281
|
+
async executeTool(toolName, args, userConfirmed = false) {
|
|
1282
|
+
switch (toolName) {
|
|
1283
|
+
case "read_file":
|
|
1284
|
+
return this.readFile(args.file_path || "");
|
|
1285
|
+
case "write_file":
|
|
1286
|
+
return this.writeFile(args.file_path || "", args.content || "", userConfirmed);
|
|
1287
|
+
case "list_directory":
|
|
1288
|
+
return this.listDirectory(args.dir_path || ".");
|
|
1289
|
+
case "run_command":
|
|
1290
|
+
return this.runCommand(args.command || "", args.cwd, userConfirmed);
|
|
1291
|
+
case "git_diff":
|
|
1292
|
+
return this.gitDiff(args.cwd);
|
|
1293
|
+
case "git_log":
|
|
1294
|
+
return this.gitLog(args.limit || 5, args.cwd);
|
|
1295
|
+
case "git_commit":
|
|
1296
|
+
return this.gitCommit(args.message || "", args.cwd, userConfirmed);
|
|
1297
|
+
case "search_docs":
|
|
1298
|
+
return this.searchDocs(args.query || "", args.source || "mdn");
|
|
1299
|
+
case "http_request":
|
|
1300
|
+
return this.httpRequest(args.url || "", args.method || "GET", args.headers, args.body);
|
|
1301
|
+
case "cve_lookup":
|
|
1302
|
+
return this.cveLookup(args.cve_id || "");
|
|
1303
|
+
default:
|
|
1304
|
+
return { success: false, output: "", error: `Tool not implemented: ${toolName}` };
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// src/streaming/renderer.ts
|
|
1310
|
+
marked.setOptions({
|
|
1311
|
+
renderer: new TerminalRenderer({
|
|
1312
|
+
code: chalk3.cyan,
|
|
1313
|
+
blockquote: chalk3.gray.italic,
|
|
1314
|
+
html: chalk3.gray,
|
|
1315
|
+
heading: chalk3.bold.underline,
|
|
1316
|
+
firstHeading: chalk3.bold.magenta,
|
|
1317
|
+
hr: chalk3.reset,
|
|
1318
|
+
listitem: chalk3.reset,
|
|
1319
|
+
list: chalk3.reset,
|
|
1320
|
+
table: chalk3.reset,
|
|
1321
|
+
paragraph: chalk3.reset,
|
|
1322
|
+
strong: chalk3.bold,
|
|
1323
|
+
em: chalk3.italic,
|
|
1324
|
+
codespan: chalk3.cyan,
|
|
1325
|
+
del: chalk3.dim.strikethrough,
|
|
1326
|
+
link: chalk3.blue.underline,
|
|
1327
|
+
href: chalk3.blue.underline
|
|
1328
|
+
})
|
|
1329
|
+
});
|
|
1330
|
+
var StreamingRenderer = class {
|
|
1331
|
+
buffer = "";
|
|
1332
|
+
inCodeBlock = false;
|
|
1333
|
+
toolEngine;
|
|
1334
|
+
pendingToolCalls = [];
|
|
1335
|
+
sessionId = null;
|
|
1336
|
+
constructor() {
|
|
1337
|
+
this.toolEngine = new ToolEngine(process.cwd());
|
|
1338
|
+
}
|
|
1339
|
+
getPendingToolCalls() {
|
|
1340
|
+
return this.pendingToolCalls;
|
|
1341
|
+
}
|
|
1342
|
+
getSessionId() {
|
|
1343
|
+
return this.sessionId;
|
|
1344
|
+
}
|
|
1345
|
+
clearPendingTools() {
|
|
1346
|
+
this.pendingToolCalls = [];
|
|
1347
|
+
this.sessionId = null;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Process stream message
|
|
1351
|
+
*/
|
|
1352
|
+
async processMessage(message) {
|
|
1353
|
+
switch (message.type) {
|
|
1354
|
+
case "token":
|
|
1355
|
+
await this.handleToken(message.content || "");
|
|
1356
|
+
break;
|
|
1357
|
+
case "tool_call":
|
|
1358
|
+
if (message.tool) {
|
|
1359
|
+
this.pendingToolCalls.push({
|
|
1360
|
+
tool: message.tool,
|
|
1361
|
+
args: message.args || {}
|
|
1362
|
+
});
|
|
1363
|
+
await this.handleToolCall(message);
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
case "tools_pending":
|
|
1367
|
+
if (message.session_id) {
|
|
1368
|
+
this.sessionId = message.session_id;
|
|
1369
|
+
}
|
|
1370
|
+
await this.flushBuffer();
|
|
1371
|
+
break;
|
|
1372
|
+
case "tool_result":
|
|
1373
|
+
await this.handleToolResult(message);
|
|
1374
|
+
break;
|
|
1375
|
+
case "done":
|
|
1376
|
+
await this.handleDone(message);
|
|
1377
|
+
break;
|
|
1378
|
+
case "error":
|
|
1379
|
+
await this.handleError(message);
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Handle token (word/character from AI)
|
|
1385
|
+
*/
|
|
1386
|
+
async handleToken(token) {
|
|
1387
|
+
this.buffer += token;
|
|
1388
|
+
if (token.includes("```")) {
|
|
1389
|
+
this.inCodeBlock = !this.inCodeBlock;
|
|
1390
|
+
}
|
|
1391
|
+
if (token.includes("\n") || this.buffer.length > 1e3) {
|
|
1392
|
+
await this.flushBuffer();
|
|
1393
|
+
} else {
|
|
1394
|
+
process.stdout.write(token);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Flush accumulated buffer
|
|
1399
|
+
*/
|
|
1400
|
+
async flushBuffer() {
|
|
1401
|
+
if (this.buffer.length === 0) return;
|
|
1402
|
+
if (this.inCodeBlock) {
|
|
1403
|
+
process.stdout.write(this.buffer);
|
|
1404
|
+
} else {
|
|
1405
|
+
try {
|
|
1406
|
+
const rendered = await marked(this.buffer);
|
|
1407
|
+
process.stdout.write(rendered);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
process.stdout.write(this.buffer);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
this.buffer = "";
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Handle tool call request from AI - just display it, execution happens later
|
|
1416
|
+
*/
|
|
1417
|
+
async handleToolCall(message) {
|
|
1418
|
+
await this.flushBuffer();
|
|
1419
|
+
logger.newline();
|
|
1420
|
+
const { tool, args } = message;
|
|
1421
|
+
if (!tool) return;
|
|
1422
|
+
logger.info(chalk3.yellow(`AI requested tool: ${chalk3.bold(tool)}`));
|
|
1423
|
+
if (args && Object.keys(args).length > 0) {
|
|
1424
|
+
console.log(chalk3.gray("Arguments:"));
|
|
1425
|
+
console.log(chalk3.gray(JSON.stringify(args, null, 2)));
|
|
1426
|
+
}
|
|
1427
|
+
logger.newline();
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Execute all pending tool calls
|
|
1431
|
+
* This is called from prompt.ts after stream ends
|
|
1432
|
+
*/
|
|
1433
|
+
async executePendingTools() {
|
|
1434
|
+
const results = [];
|
|
1435
|
+
for (const { tool, args } of this.pendingToolCalls) {
|
|
1436
|
+
logger.newline();
|
|
1437
|
+
logger.info(chalk3.yellow(`Executing ${chalk3.bold(tool)}...`));
|
|
1438
|
+
const requiresConfirmation = this.requiresConfirmation(tool);
|
|
1439
|
+
let confirmed = true;
|
|
1440
|
+
if (requiresConfirmation) {
|
|
1441
|
+
const answers = await inquirer.prompt([
|
|
1442
|
+
{
|
|
1443
|
+
type: "confirm",
|
|
1444
|
+
name: "confirmed",
|
|
1445
|
+
message: `Allow ${tool} to execute?`,
|
|
1446
|
+
default: false
|
|
1447
|
+
}
|
|
1448
|
+
]);
|
|
1449
|
+
confirmed = answers.confirmed;
|
|
1450
|
+
if (!confirmed) {
|
|
1451
|
+
logger.warn("Tool execution cancelled by user");
|
|
1452
|
+
results.push({
|
|
1453
|
+
tool,
|
|
1454
|
+
args,
|
|
1455
|
+
result: "User denied permission",
|
|
1456
|
+
success: false
|
|
1457
|
+
});
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
logger.startSpinner(`Executing ${tool}...`);
|
|
1462
|
+
try {
|
|
1463
|
+
const result = await this.toolEngine.executeTool(tool, args || {}, confirmed);
|
|
1464
|
+
logger.stopSpinner();
|
|
1465
|
+
if (result.success) {
|
|
1466
|
+
logger.success(`${tool} completed successfully`);
|
|
1467
|
+
const content = result.output || "";
|
|
1468
|
+
const preview = content.substring(0, 500);
|
|
1469
|
+
if (preview.length < content.length) {
|
|
1470
|
+
console.log(chalk3.gray(preview + "... (truncated)"));
|
|
1471
|
+
} else {
|
|
1472
|
+
console.log(chalk3.gray(preview));
|
|
1473
|
+
}
|
|
1474
|
+
results.push({
|
|
1475
|
+
tool,
|
|
1476
|
+
args,
|
|
1477
|
+
result: result.output || "",
|
|
1478
|
+
success: true
|
|
1479
|
+
});
|
|
1480
|
+
} else {
|
|
1481
|
+
logger.error(`${tool} failed: ${result.error}`);
|
|
1482
|
+
results.push({
|
|
1483
|
+
tool,
|
|
1484
|
+
args,
|
|
1485
|
+
result: result.error || "Unknown error",
|
|
1486
|
+
success: false
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
logger.stopSpinner();
|
|
1491
|
+
logger.error(`${tool} threw error: ${error.message}`);
|
|
1492
|
+
results.push({
|
|
1493
|
+
tool,
|
|
1494
|
+
args,
|
|
1495
|
+
result: error.message,
|
|
1496
|
+
success: false
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return results;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Handle tool execution result (from backend)
|
|
1504
|
+
*/
|
|
1505
|
+
async handleToolResult(message) {
|
|
1506
|
+
logger.stopSpinner();
|
|
1507
|
+
if (message.content) {
|
|
1508
|
+
const preview = message.content.substring(0, 500);
|
|
1509
|
+
if (preview.length < message.content.length) {
|
|
1510
|
+
console.log(chalk3.gray(preview + "... (truncated)"));
|
|
1511
|
+
} else {
|
|
1512
|
+
console.log(chalk3.gray(preview));
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
logger.newline();
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Handle completion done
|
|
1519
|
+
*/
|
|
1520
|
+
async handleDone(message) {
|
|
1521
|
+
await this.flushBuffer();
|
|
1522
|
+
logger.newline();
|
|
1523
|
+
if (message.usage) {
|
|
1524
|
+
const { tokens_in, tokens_out, cost_usd } = message.usage;
|
|
1525
|
+
console.log(
|
|
1526
|
+
chalk3.gray(
|
|
1527
|
+
`\u{1F4CA} Tokens: ${tokens_in.toLocaleString()} in, ${tokens_out.toLocaleString()} out | Cost: $${cost_usd.toFixed(4)}`
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
logger.newline();
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Handle error
|
|
1535
|
+
*/
|
|
1536
|
+
async handleError(message) {
|
|
1537
|
+
await this.flushBuffer();
|
|
1538
|
+
logger.newline();
|
|
1539
|
+
logger.error(message.error || "An error occurred");
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Check if tool requires user confirmation
|
|
1543
|
+
*/
|
|
1544
|
+
requiresConfirmation(tool) {
|
|
1545
|
+
const destructiveTools = [
|
|
1546
|
+
"write_file",
|
|
1547
|
+
"run_command",
|
|
1548
|
+
"git_commit",
|
|
1549
|
+
"delete_file",
|
|
1550
|
+
"modify_file"
|
|
1551
|
+
];
|
|
1552
|
+
return destructiveTools.includes(tool);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Reset renderer state
|
|
1556
|
+
*/
|
|
1557
|
+
reset() {
|
|
1558
|
+
this.buffer = "";
|
|
1559
|
+
this.inCodeBlock = false;
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
var streamingRenderer = new StreamingRenderer();
|
|
1563
|
+
|
|
1564
|
+
// src/commands/prompt.ts
|
|
1565
|
+
async function promptCommand(prompt, options) {
|
|
1566
|
+
try {
|
|
1567
|
+
const apiKey = await configManager.getApiKey();
|
|
1568
|
+
if (!apiKey) {
|
|
1569
|
+
logger.error('Not authenticated. Run "devwing login" first.');
|
|
1570
|
+
process.exit(1);
|
|
1571
|
+
}
|
|
1572
|
+
logger.startSpinner("Analyzing codebase...");
|
|
1573
|
+
const collector = new ContextCollector(process.cwd());
|
|
1574
|
+
const context = await collector.collect(prompt);
|
|
1575
|
+
logger.succeedSpinner("Context collected");
|
|
1576
|
+
logger.newline();
|
|
1577
|
+
logger.info("DevWing is thinking...");
|
|
1578
|
+
logger.newline();
|
|
1579
|
+
const request = {
|
|
1580
|
+
prompt,
|
|
1581
|
+
mode: options.mode,
|
|
1582
|
+
model: options.model,
|
|
1583
|
+
project_id: options.project || configManager.getProjectId(),
|
|
1584
|
+
context,
|
|
1585
|
+
stream: true,
|
|
1586
|
+
max_tokens: 4096
|
|
1587
|
+
};
|
|
1588
|
+
let continueLoop = true;
|
|
1589
|
+
while (continueLoop) {
|
|
1590
|
+
streamingRenderer.reset();
|
|
1591
|
+
streamingRenderer.clearPendingTools();
|
|
1592
|
+
try {
|
|
1593
|
+
const sessionId = streamingRenderer.getSessionId();
|
|
1594
|
+
if (sessionId) {
|
|
1595
|
+
const toolResults = await streamingRenderer.executePendingTools();
|
|
1596
|
+
logger.newline();
|
|
1597
|
+
logger.info("Sending tool results back to AI...");
|
|
1598
|
+
logger.newline();
|
|
1599
|
+
for await (const message of apiClient.continueCompletion(sessionId, toolResults)) {
|
|
1600
|
+
await streamingRenderer.processMessage(message);
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
for await (const message of apiClient.streamCompletion(request)) {
|
|
1604
|
+
await streamingRenderer.processMessage(message);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const pendingTools = streamingRenderer.getPendingToolCalls();
|
|
1608
|
+
const newSessionId = streamingRenderer.getSessionId();
|
|
1609
|
+
if (pendingTools.length > 0 && newSessionId) {
|
|
1610
|
+
continueLoop = true;
|
|
1611
|
+
} else {
|
|
1612
|
+
continueLoop = false;
|
|
1613
|
+
}
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
logger.newline();
|
|
1616
|
+
logger.error("Request failed:", error);
|
|
1617
|
+
if (error.message.includes("429")) {
|
|
1618
|
+
logger.warn("Rate limit exceeded. Please wait a moment and try again.");
|
|
1619
|
+
} else if (error.message.includes("quota")) {
|
|
1620
|
+
logger.warn("Token quota exceeded. Consider upgrading your plan.");
|
|
1621
|
+
} else if (error.message.includes("unauthorized")) {
|
|
1622
|
+
logger.warn('Authentication failed. Try running "devwing login" again.');
|
|
1623
|
+
}
|
|
1624
|
+
process.exit(1);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
logger.success("Done!");
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
logger.error("Command failed", error);
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async function scanCommand(options) {
|
|
1634
|
+
logger.info("Running security vulnerability scan...");
|
|
1635
|
+
logger.newline();
|
|
1636
|
+
const prompt = `Perform a comprehensive security scan of this codebase. Check for:
|
|
1637
|
+
- Common vulnerabilities (OWASP Top 10)
|
|
1638
|
+
- Dependency vulnerabilities
|
|
1639
|
+
- Hardcoded secrets or credentials
|
|
1640
|
+
- SQL injection risks
|
|
1641
|
+
- XSS vulnerabilities
|
|
1642
|
+
- Insecure authentication/authorization
|
|
1643
|
+
- API security issues
|
|
1644
|
+
|
|
1645
|
+
Provide a detailed report with severity levels and remediation steps.`;
|
|
1646
|
+
await promptCommand(prompt, { ...options, mode: "security" });
|
|
1647
|
+
}
|
|
1648
|
+
async function reviewCommand(options) {
|
|
1649
|
+
logger.info("Performing code review...");
|
|
1650
|
+
logger.newline();
|
|
1651
|
+
const prompt = `Conduct a thorough code review of the recent changes. Focus on:
|
|
1652
|
+
- Code quality and best practices
|
|
1653
|
+
- Potential bugs or edge cases
|
|
1654
|
+
- Performance issues
|
|
1655
|
+
- Readability and maintainability
|
|
1656
|
+
- Documentation completeness
|
|
1657
|
+
- Test coverage
|
|
1658
|
+
- Security concerns
|
|
1659
|
+
|
|
1660
|
+
Provide specific suggestions for improvement.`;
|
|
1661
|
+
await promptCommand(prompt, { ...options, mode: options.mode || "general" });
|
|
1662
|
+
}
|
|
1663
|
+
async function explainCommand(target, options) {
|
|
1664
|
+
logger.info(`Explaining: ${target}`);
|
|
1665
|
+
logger.newline();
|
|
1666
|
+
const prompt = `Please explain ${target} in detail. Include:
|
|
1667
|
+
- What it does and how it works
|
|
1668
|
+
- Key components and their relationships
|
|
1669
|
+
- Any important patterns or techniques used
|
|
1670
|
+
- Potential gotchas or edge cases
|
|
1671
|
+
- How it fits into the larger system
|
|
1672
|
+
|
|
1673
|
+
Make the explanation clear and accessible.`;
|
|
1674
|
+
await promptCommand(prompt, { ...options });
|
|
1675
|
+
}
|
|
1676
|
+
async function memoryCommand(action, content, options = {}) {
|
|
1677
|
+
const projectId = options.project || configManager.getProjectId();
|
|
1678
|
+
if (!projectId) {
|
|
1679
|
+
logger.error('No project configured. Use "devwing config set project <project-id>"');
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
switch (action) {
|
|
1684
|
+
case "save":
|
|
1685
|
+
await memorySave(projectId, content);
|
|
1686
|
+
break;
|
|
1687
|
+
case "show":
|
|
1688
|
+
await memoryShow(projectId);
|
|
1689
|
+
break;
|
|
1690
|
+
case "clear":
|
|
1691
|
+
await memoryClear(projectId);
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
logger.error("Memory command failed", error);
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async function memorySave(projectId, content) {
|
|
1700
|
+
if (!content) {
|
|
1701
|
+
logger.error("Please provide content to save");
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const { type } = await inquirer.prompt([
|
|
1705
|
+
{
|
|
1706
|
+
type: "list",
|
|
1707
|
+
name: "type",
|
|
1708
|
+
message: "What type of memory is this?",
|
|
1709
|
+
choices: [
|
|
1710
|
+
{ name: "Context (general information)", value: "context" },
|
|
1711
|
+
{ name: "Rule (coding standard or guideline)", value: "rule" },
|
|
1712
|
+
{ name: "Decision (architectural decision)", value: "decision" },
|
|
1713
|
+
{ name: "Summary (project summary)", value: "summary" }
|
|
1714
|
+
]
|
|
1715
|
+
}
|
|
1716
|
+
]);
|
|
1717
|
+
logger.startSpinner("Saving to project memory...");
|
|
1718
|
+
try {
|
|
1719
|
+
await apiClient.addProjectMemory(projectId, content, type);
|
|
1720
|
+
logger.succeedSpinner("Memory saved successfully");
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
logger.failSpinner("Failed to save memory");
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
async function memoryShow(projectId) {
|
|
1727
|
+
logger.startSpinner("Loading project memory...");
|
|
1728
|
+
try {
|
|
1729
|
+
const memories = await apiClient.getProjectMemory(projectId);
|
|
1730
|
+
logger.stopSpinner();
|
|
1731
|
+
if (memories.length === 0) {
|
|
1732
|
+
logger.info("No memories stored for this project yet");
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
logger.success(`Found ${memories.length} memory items`);
|
|
1736
|
+
logger.newline();
|
|
1737
|
+
const table = new Table({
|
|
1738
|
+
head: ["Type", "Content", "Created"],
|
|
1739
|
+
colWidths: [15, 60, 20],
|
|
1740
|
+
wordWrap: true
|
|
1741
|
+
});
|
|
1742
|
+
for (const memory of memories) {
|
|
1743
|
+
const preview = memory.content.length > 100 ? memory.content.substring(0, 100) + "..." : memory.content;
|
|
1744
|
+
table.push([
|
|
1745
|
+
chalk3.cyan(memory.type),
|
|
1746
|
+
preview,
|
|
1747
|
+
new Date(memory.created_at).toLocaleDateString()
|
|
1748
|
+
]);
|
|
1749
|
+
}
|
|
1750
|
+
console.log(table.toString());
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
logger.failSpinner("Failed to load memory");
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async function memoryClear(projectId) {
|
|
1757
|
+
const { confirm } = await inquirer.prompt([
|
|
1758
|
+
{
|
|
1759
|
+
type: "confirm",
|
|
1760
|
+
name: "confirm",
|
|
1761
|
+
message: "Are you sure you want to clear all project memory?",
|
|
1762
|
+
default: false
|
|
1763
|
+
}
|
|
1764
|
+
]);
|
|
1765
|
+
if (!confirm) {
|
|
1766
|
+
logger.info("Operation cancelled");
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
logger.startSpinner("Clearing project memory...");
|
|
1770
|
+
try {
|
|
1771
|
+
const memories = await apiClient.getProjectMemory(projectId);
|
|
1772
|
+
for (const memory of memories) {
|
|
1773
|
+
await apiClient.deleteProjectMemory(projectId, memory.id);
|
|
1774
|
+
}
|
|
1775
|
+
logger.succeedSpinner("Project memory cleared");
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
logger.failSpinner("Failed to clear memory");
|
|
1778
|
+
throw error;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
async function configCommand(action, key, value) {
|
|
1782
|
+
try {
|
|
1783
|
+
if (!action || action === "list") {
|
|
1784
|
+
const config = configManager.getAll();
|
|
1785
|
+
const apiKey = await configManager.getApiKey();
|
|
1786
|
+
console.log(chalk3.bold("Current Configuration:"));
|
|
1787
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
1788
|
+
console.log(` API Key: ${apiKey ? apiKey.substring(0, 12) + "..." : "Not set"}`);
|
|
1789
|
+
console.log(` Workspace: ${config.workspaceId || "Not set"}`);
|
|
1790
|
+
console.log(` Project: ${config.projectId || "Not set"}`);
|
|
1791
|
+
console.log(` Model: ${config.model || "Auto"}`);
|
|
1792
|
+
console.log(` Mode: ${config.mode || "Auto"}`);
|
|
1793
|
+
console.log();
|
|
1794
|
+
console.log(chalk3.gray(`Config file: ${configManager.getPath()}`));
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (action === "get") {
|
|
1798
|
+
if (!key) {
|
|
1799
|
+
logger.error("Please specify a key to get");
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const config = configManager.getAll();
|
|
1803
|
+
const val = config[key];
|
|
1804
|
+
console.log(val || "Not set");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (action === "set") {
|
|
1808
|
+
if (!key || !value) {
|
|
1809
|
+
logger.error("Please specify both key and value");
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
switch (key) {
|
|
1813
|
+
case "project":
|
|
1814
|
+
configManager.setProjectId(value);
|
|
1815
|
+
logger.success(`project set to: ${value}`);
|
|
1816
|
+
break;
|
|
1817
|
+
case "workspace":
|
|
1818
|
+
configManager.setWorkspaceId(value);
|
|
1819
|
+
logger.success(`workspace set to: ${value}`);
|
|
1820
|
+
break;
|
|
1821
|
+
case "model":
|
|
1822
|
+
configManager.setModel(value);
|
|
1823
|
+
logger.success(`model set to: ${value}`);
|
|
1824
|
+
break;
|
|
1825
|
+
case "mode":
|
|
1826
|
+
const validModes = ["general", "frontend", "backend", "security", "devops"];
|
|
1827
|
+
if (validModes.includes(value)) {
|
|
1828
|
+
configManager.setMode(value);
|
|
1829
|
+
logger.success(`mode set to: ${value}`);
|
|
1830
|
+
} else {
|
|
1831
|
+
logger.error(`Invalid mode. Must be one of: ${validModes.join(", ")}`);
|
|
1832
|
+
}
|
|
1833
|
+
break;
|
|
1834
|
+
case "apiUrl":
|
|
1835
|
+
configManager.setApiUrl(value);
|
|
1836
|
+
logger.success(`apiUrl set to: ${value}`);
|
|
1837
|
+
break;
|
|
1838
|
+
default:
|
|
1839
|
+
logger.error(`Unknown config key: ${key}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
logger.error("Config command failed", error);
|
|
1844
|
+
process.exit(1);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
1848
|
+
var __dirname2 = dirname(__filename2);
|
|
1849
|
+
function getCurrentVersion() {
|
|
1850
|
+
try {
|
|
1851
|
+
const packageJsonPath = join(__dirname2, "../../package.json");
|
|
1852
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1853
|
+
return packageJson.version;
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
logger.error("Failed to read package version", error);
|
|
1856
|
+
return "0.1.0";
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function getPackageName() {
|
|
1860
|
+
try {
|
|
1861
|
+
const packageJsonPath = join(__dirname2, "../../package.json");
|
|
1862
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1863
|
+
return packageJson.name;
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
logger.error("Failed to read package name", error);
|
|
1866
|
+
return "@devwing/cli";
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function getLatestVersion(packageName) {
|
|
1870
|
+
try {
|
|
1871
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
1872
|
+
if (!response.ok) {
|
|
1873
|
+
throw new Error(`Registry responded with ${response.status}`);
|
|
1874
|
+
}
|
|
1875
|
+
const data = await response.json();
|
|
1876
|
+
return data["dist-tags"].latest;
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
logger.error("Failed to fetch latest version from npm registry", error);
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function detectPackageManager() {
|
|
1883
|
+
try {
|
|
1884
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
1885
|
+
return "pnpm";
|
|
1886
|
+
} catch {
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
execSync("yarn --version", { stdio: "ignore" });
|
|
1890
|
+
return "yarn";
|
|
1891
|
+
} catch {
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
execSync("npm --version", { stdio: "ignore" });
|
|
1895
|
+
return "npm";
|
|
1896
|
+
} catch {
|
|
1897
|
+
}
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
function installUpdate(packageName, packageManager, version) {
|
|
1901
|
+
try {
|
|
1902
|
+
logger.startSpinner(`Installing ${packageName}@${version}...`);
|
|
1903
|
+
const command = packageManager === "yarn" ? `yarn global add ${packageName}@${version}` : `${packageManager} install -g ${packageName}@${version}`;
|
|
1904
|
+
execSync(command, { stdio: "inherit" });
|
|
1905
|
+
logger.stopSpinner();
|
|
1906
|
+
return true;
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
logger.stopSpinner();
|
|
1909
|
+
logger.error("Failed to install update", error);
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function displayUpdateInfo(currentVersion, latestVersion) {
|
|
1914
|
+
console.log();
|
|
1915
|
+
console.log(chalk3.cyan("\u2501".repeat(60)));
|
|
1916
|
+
console.log(chalk3.bold.cyan(" DevWing CLI Update Available"));
|
|
1917
|
+
console.log(chalk3.cyan("\u2501".repeat(60)));
|
|
1918
|
+
console.log();
|
|
1919
|
+
console.log(` ${chalk3.gray("Current version:")} ${chalk3.yellow(currentVersion)}`);
|
|
1920
|
+
console.log(` ${chalk3.gray("Latest version:")} ${chalk3.green(latestVersion)}`);
|
|
1921
|
+
console.log();
|
|
1922
|
+
console.log(chalk3.gray(" Changelog: https://github.com/devwing/devwing/releases"));
|
|
1923
|
+
console.log();
|
|
1924
|
+
}
|
|
1925
|
+
async function updateCommand(options) {
|
|
1926
|
+
try {
|
|
1927
|
+
const currentVersion = getCurrentVersion();
|
|
1928
|
+
const packageName = getPackageName();
|
|
1929
|
+
console.log();
|
|
1930
|
+
console.log(chalk3.cyan(`\u{1F4E6} DevWing CLI v${currentVersion}`));
|
|
1931
|
+
console.log();
|
|
1932
|
+
logger.startSpinner("Checking for updates...");
|
|
1933
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
1934
|
+
logger.stopSpinner();
|
|
1935
|
+
if (!latestVersion) {
|
|
1936
|
+
logger.warn("Unable to check for updates. Please check your internet connection.");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
const isNewer = semver.gt(latestVersion, currentVersion);
|
|
1940
|
+
if (!isNewer) {
|
|
1941
|
+
logger.success(`You're running the latest version (v${currentVersion})`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
displayUpdateInfo(currentVersion, latestVersion);
|
|
1945
|
+
if (options?.check) {
|
|
1946
|
+
logger.info('Run "devwing update" to install the latest version');
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const packageManager = detectPackageManager();
|
|
1950
|
+
if (!packageManager) {
|
|
1951
|
+
logger.error("No package manager found. Please install npm, pnpm, or yarn.");
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
logger.info(`Detected package manager: ${chalk3.cyan(packageManager)}`);
|
|
1955
|
+
if (!options?.force) {
|
|
1956
|
+
const { confirm } = await inquirer.prompt([
|
|
1957
|
+
{
|
|
1958
|
+
type: "confirm",
|
|
1959
|
+
name: "confirm",
|
|
1960
|
+
message: `Update to v${latestVersion}?`,
|
|
1961
|
+
default: true
|
|
1962
|
+
}
|
|
1963
|
+
]);
|
|
1964
|
+
if (!confirm) {
|
|
1965
|
+
logger.info("Update cancelled");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
console.log();
|
|
1970
|
+
const success = installUpdate(packageName, packageManager, latestVersion);
|
|
1971
|
+
if (success) {
|
|
1972
|
+
console.log();
|
|
1973
|
+
logger.success(`Successfully updated to v${latestVersion}`);
|
|
1974
|
+
console.log();
|
|
1975
|
+
console.log(chalk3.gray(' Run "devwing --version" to verify'));
|
|
1976
|
+
console.log();
|
|
1977
|
+
} else {
|
|
1978
|
+
console.log();
|
|
1979
|
+
logger.error("Update failed. Please try manually:");
|
|
1980
|
+
console.log();
|
|
1981
|
+
console.log(chalk3.gray(` ${packageManager} install -g ${packageName}@${latestVersion}`));
|
|
1982
|
+
console.log();
|
|
1983
|
+
process.exit(1);
|
|
1984
|
+
}
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
logger.error("Update check failed", error);
|
|
1987
|
+
process.exit(1);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/index.ts
|
|
1992
|
+
var program = new Command();
|
|
1993
|
+
var VERSION = "0.1.0";
|
|
1994
|
+
program.name("devwing").description("DevWing.ai - Your AI Wingman in the Terminal").version(VERSION, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information");
|
|
1995
|
+
program.option("--mode <mode>", "AI mode: general|frontend|backend|security|devops").option("--model <model>", "Specific model to use").option("--project <id>", "Project ID").option("--workspace <id>", "Workspace ID").option("--verbose", "Verbose output for debugging").option("-y, --yes", "Auto-confirm destructive actions");
|
|
1996
|
+
program.command("login").description("Authenticate with DevWing").action(async () => {
|
|
1997
|
+
await loginCommand();
|
|
1998
|
+
});
|
|
1999
|
+
program.command("logout").description("Clear stored credentials").action(async () => {
|
|
2000
|
+
await logoutCommand();
|
|
2001
|
+
});
|
|
2002
|
+
program.command("status").description("Show authentication status and profile").action(async () => {
|
|
2003
|
+
await statusCommand();
|
|
2004
|
+
});
|
|
2005
|
+
program.command("scan").description("Run security vulnerability scan").action(async () => {
|
|
2006
|
+
const opts = program.opts();
|
|
2007
|
+
await scanCommand(opts);
|
|
2008
|
+
});
|
|
2009
|
+
program.command("review").description("Perform code review on recent changes").action(async () => {
|
|
2010
|
+
const opts = program.opts();
|
|
2011
|
+
await reviewCommand(opts);
|
|
2012
|
+
});
|
|
2013
|
+
program.command("explain <target>").description("Explain code, file, or concept").action(async (target) => {
|
|
2014
|
+
const opts = program.opts();
|
|
2015
|
+
await explainCommand(target, opts);
|
|
2016
|
+
});
|
|
2017
|
+
var memoryCmd = program.command("memory").description("Manage project memory");
|
|
2018
|
+
memoryCmd.command("save <content>").description("Save information to project memory").action(async (content) => {
|
|
2019
|
+
const opts = program.opts();
|
|
2020
|
+
await memoryCommand("save", content, opts);
|
|
2021
|
+
});
|
|
2022
|
+
memoryCmd.command("show").description("Show all project memories").action(async () => {
|
|
2023
|
+
const opts = program.opts();
|
|
2024
|
+
await memoryCommand("show", void 0, opts);
|
|
2025
|
+
});
|
|
2026
|
+
memoryCmd.command("clear").description("Clear all project memories").action(async () => {
|
|
2027
|
+
const opts = program.opts();
|
|
2028
|
+
await memoryCommand("clear", void 0, opts);
|
|
2029
|
+
});
|
|
2030
|
+
var configCmd = program.command("config").description("Manage CLI configuration");
|
|
2031
|
+
configCmd.command("list").description("Show all configuration").action(async () => {
|
|
2032
|
+
await configCommand("list");
|
|
2033
|
+
});
|
|
2034
|
+
configCmd.command("get <key>").description("Get configuration value").action(async (key) => {
|
|
2035
|
+
await configCommand("get", key);
|
|
2036
|
+
});
|
|
2037
|
+
configCmd.command("set <key> <value>").description("Set configuration value").action(async (key, value) => {
|
|
2038
|
+
await configCommand("set", key, value);
|
|
2039
|
+
});
|
|
2040
|
+
program.command("update").description("Check for and install CLI updates").option("--check", "Only check for updates, do not install").option("--force", "Skip confirmation prompt").action(async (options) => {
|
|
2041
|
+
await updateCommand(options);
|
|
2042
|
+
});
|
|
2043
|
+
program.argument("[prompt...]", "Natural language prompt for AI").action(async (promptParts) => {
|
|
2044
|
+
if (promptParts.length === 0) {
|
|
2045
|
+
program.help();
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const prompt = promptParts.join(" ");
|
|
2049
|
+
const opts = program.opts();
|
|
2050
|
+
await promptCommand(prompt, opts);
|
|
2051
|
+
});
|
|
2052
|
+
program.exitOverride();
|
|
2053
|
+
try {
|
|
2054
|
+
await program.parseAsync(process.argv);
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
if (error.code === "commander.help" || error.code === "commander.version") {
|
|
2057
|
+
process.exit(0);
|
|
2058
|
+
}
|
|
2059
|
+
logger.error("An error occurred", error);
|
|
2060
|
+
if (process.env.DEBUG) {
|
|
2061
|
+
console.error(error);
|
|
2062
|
+
}
|
|
2063
|
+
process.exit(1);
|
|
2064
|
+
}
|
|
2065
|
+
//# sourceMappingURL=index.js.map
|
|
2066
|
+
//# sourceMappingURL=index.js.map
|