aerocoding 0.0.3 → 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 +127 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1034 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -17
package/dist/index.js
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import chalk4 from "chalk";
|
|
8
|
+
|
|
9
|
+
// src/auth/device-flow.ts
|
|
10
|
+
import axios from "axios";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
import open from "open";
|
|
14
|
+
var API_URL = process.env.API_URL || "http://localhost:3000";
|
|
15
|
+
var MAX_POLL_TIME = 15 * 60 * 1e3;
|
|
16
|
+
var DeviceFlow = class {
|
|
17
|
+
/**
|
|
18
|
+
* Initiate the complete device authorization flow
|
|
19
|
+
*/
|
|
20
|
+
async initiateAuth() {
|
|
21
|
+
const deviceAuth = await this.requestDeviceCode();
|
|
22
|
+
this.displayUserCode(deviceAuth);
|
|
23
|
+
await this.openBrowser(deviceAuth.verification_uri_complete);
|
|
24
|
+
const tokens = await this.pollForToken(deviceAuth);
|
|
25
|
+
return tokens;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Step 1: Request device code from server
|
|
29
|
+
*/
|
|
30
|
+
async requestDeviceCode() {
|
|
31
|
+
try {
|
|
32
|
+
const response = await axios.post(`${API_URL}/api/device/authorize`, {
|
|
33
|
+
client_id: "aerocoding-cli",
|
|
34
|
+
scope: "generate:code projects:read"
|
|
35
|
+
});
|
|
36
|
+
return response.data;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(chalk.red("Failed to initiate device authorization"));
|
|
39
|
+
if (error.response) {
|
|
40
|
+
console.error(
|
|
41
|
+
chalk.red(`Error: ${error.response.data.error_description}`)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
throw new Error("Failed to initiate device authorization");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Step 2: Display user code and instructions
|
|
49
|
+
*/
|
|
50
|
+
displayUserCode(auth) {
|
|
51
|
+
console.log("\n");
|
|
52
|
+
console.log(chalk.cyan("\u2501".repeat(60)));
|
|
53
|
+
console.log(chalk.bold.white(" \u{1F680} AeroCoding CLI - Device Activation"));
|
|
54
|
+
console.log(chalk.cyan("\u2501".repeat(60)));
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log(chalk.white(" 1. Open this URL in your browser:"));
|
|
57
|
+
console.log(chalk.cyan.bold(` ${auth.verification_uri}`));
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(chalk.white(" 2. Enter this code:"));
|
|
60
|
+
console.log(chalk.green.bold(` ${auth.user_code}`));
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log(chalk.white(" 3. Confirm you see this code on the website:"));
|
|
63
|
+
console.log(chalk.yellow.bold(` ${auth.confirmation_code}`));
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(
|
|
66
|
+
chalk.gray(` Code expires in ${auth.expires_in / 60} minutes`)
|
|
67
|
+
);
|
|
68
|
+
console.log(chalk.cyan("\u2501".repeat(60)));
|
|
69
|
+
console.log("");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Step 2.5: Open browser automatically
|
|
73
|
+
*/
|
|
74
|
+
async openBrowser(url) {
|
|
75
|
+
try {
|
|
76
|
+
await open(url);
|
|
77
|
+
console.log(chalk.gray(" \u2713 Opening browser..."));
|
|
78
|
+
} catch {
|
|
79
|
+
console.log(chalk.yellow(" \u26A0 Could not open browser automatically"));
|
|
80
|
+
console.log(chalk.gray(" Please open the URL manually\n"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Step 3: Poll for token
|
|
85
|
+
*/
|
|
86
|
+
async pollForToken(auth) {
|
|
87
|
+
const spinner2 = ora({
|
|
88
|
+
text: "Waiting for authorization...",
|
|
89
|
+
color: "cyan"
|
|
90
|
+
}).start();
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
let currentInterval = auth.interval * 1e3;
|
|
93
|
+
while (true) {
|
|
94
|
+
if (Date.now() - startTime > MAX_POLL_TIME) {
|
|
95
|
+
spinner2.fail(chalk.red("Authorization timeout"));
|
|
96
|
+
throw new Error("Device authorization timed out");
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const response = await axios.post(`${API_URL}/api/device/token`, {
|
|
100
|
+
device_code: auth.device_code,
|
|
101
|
+
client_id: "aerocoding-cli"
|
|
102
|
+
});
|
|
103
|
+
spinner2.succeed(chalk.green("\u2713 Successfully authenticated!"));
|
|
104
|
+
return response.data;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const errorCode = error.response?.data?.error;
|
|
107
|
+
const errorDescription = error.response?.data?.error_description;
|
|
108
|
+
if (errorCode === "authorization_pending") {
|
|
109
|
+
await this.sleep(currentInterval);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (errorCode === "slow_down") {
|
|
113
|
+
currentInterval += 5e3;
|
|
114
|
+
spinner2.text = "Polling... (slowed down to avoid spam)";
|
|
115
|
+
await this.sleep(currentInterval);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (errorCode === "expired_token") {
|
|
119
|
+
spinner2.fail(chalk.red("Authorization code expired"));
|
|
120
|
+
throw new Error("Device code expired. Please try again.");
|
|
121
|
+
}
|
|
122
|
+
if (errorCode === "access_denied") {
|
|
123
|
+
spinner2.fail(chalk.red("Authorization denied"));
|
|
124
|
+
throw new Error("You denied the authorization request");
|
|
125
|
+
}
|
|
126
|
+
spinner2.fail(chalk.red("Authorization failed"));
|
|
127
|
+
console.error(
|
|
128
|
+
chalk.red(`Error: ${errorDescription || "Unknown error"}`)
|
|
129
|
+
);
|
|
130
|
+
throw new Error(errorDescription || "Unknown error");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Helper: Sleep for specified milliseconds
|
|
136
|
+
*/
|
|
137
|
+
sleep(ms) {
|
|
138
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/auth/token-manager.ts
|
|
143
|
+
import { Entry } from "@napi-rs/keyring";
|
|
144
|
+
import { createClient } from "@supabase/supabase-js";
|
|
145
|
+
import chalk2 from "chalk";
|
|
146
|
+
var SERVICE_NAME = "aerocoding-cli";
|
|
147
|
+
var SUPABASE_URL = process.env.SUPABASE_URL || "https://your-project.supabase.co";
|
|
148
|
+
var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "";
|
|
149
|
+
function getPassword(service, account) {
|
|
150
|
+
try {
|
|
151
|
+
const entry = new Entry(service, account);
|
|
152
|
+
return entry.getPassword();
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function setPassword(service, account, password) {
|
|
158
|
+
const entry = new Entry(service, account);
|
|
159
|
+
entry.setPassword(password);
|
|
160
|
+
}
|
|
161
|
+
function deletePassword(service, account) {
|
|
162
|
+
try {
|
|
163
|
+
const entry = new Entry(service, account);
|
|
164
|
+
entry.deletePassword();
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var TokenManager = class {
|
|
169
|
+
supabase;
|
|
170
|
+
constructor() {
|
|
171
|
+
this.supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
172
|
+
auth: {
|
|
173
|
+
autoRefreshToken: false,
|
|
174
|
+
persistSession: false
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get access token, automatically refreshing if expired
|
|
180
|
+
*/
|
|
181
|
+
async getAccessToken() {
|
|
182
|
+
try {
|
|
183
|
+
let accessToken = getPassword(SERVICE_NAME, "access_token");
|
|
184
|
+
if (!accessToken) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (this.isTokenExpired(accessToken)) {
|
|
188
|
+
console.log(chalk2.yellow("\u23F1\uFE0F Token expired, refreshing..."));
|
|
189
|
+
accessToken = await this.refreshTokens();
|
|
190
|
+
}
|
|
191
|
+
return accessToken;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(chalk2.red("Failed to get access token:"), error);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Refresh tokens using stored refresh token
|
|
199
|
+
*/
|
|
200
|
+
async refreshTokens() {
|
|
201
|
+
try {
|
|
202
|
+
const refreshToken = getPassword(SERVICE_NAME, "refresh_token");
|
|
203
|
+
if (!refreshToken) {
|
|
204
|
+
throw new Error("No refresh token available");
|
|
205
|
+
}
|
|
206
|
+
const { data, error } = await this.supabase.auth.refreshSession({
|
|
207
|
+
refresh_token: refreshToken
|
|
208
|
+
});
|
|
209
|
+
if (error || !data.session) {
|
|
210
|
+
throw new Error("Failed to refresh session");
|
|
211
|
+
}
|
|
212
|
+
setPassword(SERVICE_NAME, "access_token", data.session.access_token);
|
|
213
|
+
setPassword(SERVICE_NAME, "refresh_token", data.session.refresh_token);
|
|
214
|
+
console.log(chalk2.green("\u2713 Token refreshed successfully"));
|
|
215
|
+
return data.session.access_token;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(chalk2.red("Token refresh failed. Please login again."));
|
|
218
|
+
await this.logout();
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Check if JWT token is expired (or expires in < 5 min)
|
|
224
|
+
*/
|
|
225
|
+
isTokenExpired(token) {
|
|
226
|
+
try {
|
|
227
|
+
const payload = JSON.parse(
|
|
228
|
+
Buffer.from(token.split(".")[1], "base64").toString()
|
|
229
|
+
);
|
|
230
|
+
const expiresAt = payload.exp * 1e3;
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
return expiresAt - now < 5 * 60 * 1e3;
|
|
233
|
+
} catch {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Save tokens and user metadata after successful login
|
|
239
|
+
*/
|
|
240
|
+
async saveTokens(accessToken, refreshToken, user) {
|
|
241
|
+
try {
|
|
242
|
+
setPassword(SERVICE_NAME, "access_token", accessToken);
|
|
243
|
+
setPassword(SERVICE_NAME, "refresh_token", refreshToken);
|
|
244
|
+
setPassword(SERVICE_NAME, "user_metadata", JSON.stringify(user));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error(chalk2.red("Failed to save tokens:"), error);
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get stored user metadata
|
|
252
|
+
*/
|
|
253
|
+
async getUserMetadata() {
|
|
254
|
+
try {
|
|
255
|
+
const metadata = getPassword(SERVICE_NAME, "user_metadata");
|
|
256
|
+
return metadata ? JSON.parse(metadata) : null;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Logout - clear all stored credentials
|
|
263
|
+
*/
|
|
264
|
+
async logout() {
|
|
265
|
+
try {
|
|
266
|
+
deletePassword(SERVICE_NAME, "access_token");
|
|
267
|
+
deletePassword(SERVICE_NAME, "refresh_token");
|
|
268
|
+
deletePassword(SERVICE_NAME, "user_metadata");
|
|
269
|
+
} catch (error) {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Check if user is currently authenticated
|
|
274
|
+
*/
|
|
275
|
+
async isAuthenticated() {
|
|
276
|
+
const token = await this.getAccessToken();
|
|
277
|
+
return token !== null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/api/client.ts
|
|
282
|
+
import axios2 from "axios";
|
|
283
|
+
var API_URL2 = process.env.API_URL || "http://localhost:3000";
|
|
284
|
+
var ApiClient = class {
|
|
285
|
+
client;
|
|
286
|
+
constructor(accessToken, options) {
|
|
287
|
+
this.client = axios2.create({
|
|
288
|
+
baseURL: API_URL2,
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: `Bearer ${accessToken}`,
|
|
291
|
+
"Content-Type": "application/json"
|
|
292
|
+
},
|
|
293
|
+
timeout: 3e4
|
|
294
|
+
// 30 seconds
|
|
295
|
+
});
|
|
296
|
+
this.client.interceptors.response.use(
|
|
297
|
+
(response) => response,
|
|
298
|
+
async (error) => {
|
|
299
|
+
if (error?.response?.status === 401) {
|
|
300
|
+
await options?.onUnauthorized?.();
|
|
301
|
+
}
|
|
302
|
+
return Promise.reject(error);
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get current authenticated user
|
|
308
|
+
*/
|
|
309
|
+
async getCurrentUser() {
|
|
310
|
+
const response = await this.client.get("/api/user/me");
|
|
311
|
+
return response.data;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* List user's projects
|
|
315
|
+
*/
|
|
316
|
+
async listProjects(organizationId) {
|
|
317
|
+
const response = await this.client.get("/api/projects", {
|
|
318
|
+
params: { organizationId }
|
|
319
|
+
});
|
|
320
|
+
return response.data;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get project by ID
|
|
324
|
+
*/
|
|
325
|
+
async getProject(projectId) {
|
|
326
|
+
const response = await this.client.get(`/api/projects/${projectId}`);
|
|
327
|
+
return response.data;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Generate code from project schema
|
|
331
|
+
*/
|
|
332
|
+
async generateCode(payload) {
|
|
333
|
+
const response = await this.client.post("/api/generate", payload);
|
|
334
|
+
return response.data.data;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get credit usage for organization
|
|
338
|
+
*/
|
|
339
|
+
async getCreditUsage(organizationId) {
|
|
340
|
+
const response = await this.client.get("/api/credits/usage", {
|
|
341
|
+
params: { organizationId }
|
|
342
|
+
});
|
|
343
|
+
return response.data;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get available architectures for a framework
|
|
347
|
+
*/
|
|
348
|
+
async getArchitectures(framework) {
|
|
349
|
+
const response = await this.client.get("/api/architectures", {
|
|
350
|
+
params: { framework }
|
|
351
|
+
});
|
|
352
|
+
return response.data;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/commands/_shared/create-api-client.ts
|
|
357
|
+
function createApiClientWithAutoLogout(accessToken, tokenManager) {
|
|
358
|
+
return new ApiClient(accessToken, {
|
|
359
|
+
onUnauthorized: async () => {
|
|
360
|
+
await tokenManager.logout();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/commands/_shared/unauthorized.ts
|
|
366
|
+
import chalk3 from "chalk";
|
|
367
|
+
async function handleUnauthorized(tokenManager) {
|
|
368
|
+
await tokenManager.logout();
|
|
369
|
+
console.error(
|
|
370
|
+
chalk3.yellow(
|
|
371
|
+
" Your session is invalid or expired. You have been logged out."
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
console.error(chalk3.gray(" Run 'aerocoding login' to authenticate."));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/commands/login.ts
|
|
379
|
+
async function loginCommand() {
|
|
380
|
+
console.log(chalk4.bold("\n\u{1F680} AeroCoding CLI Login\n"));
|
|
381
|
+
const tokenManager = new TokenManager();
|
|
382
|
+
const existingToken = await tokenManager.getAccessToken();
|
|
383
|
+
if (existingToken) {
|
|
384
|
+
const metadata = await tokenManager.getUserMetadata();
|
|
385
|
+
console.log(
|
|
386
|
+
chalk4.yellow("\u26A0\uFE0F Already logged in as:"),
|
|
387
|
+
chalk4.white(metadata?.email || "unknown")
|
|
388
|
+
);
|
|
389
|
+
console.log(
|
|
390
|
+
chalk4.gray(
|
|
391
|
+
" Run 'aerocoding logout' to login with a different account\n"
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const deviceFlow = new DeviceFlow();
|
|
398
|
+
const tokens = await deviceFlow.initiateAuth();
|
|
399
|
+
const apiClient = createApiClientWithAutoLogout(
|
|
400
|
+
tokens.access_token,
|
|
401
|
+
tokenManager
|
|
402
|
+
);
|
|
403
|
+
let user;
|
|
404
|
+
try {
|
|
405
|
+
user = await apiClient.getCurrentUser();
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error.response?.status === 401) {
|
|
408
|
+
await handleUnauthorized(tokenManager);
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
await tokenManager.saveTokens(tokens.access_token, tokens.refresh_token, {
|
|
413
|
+
id: user.id,
|
|
414
|
+
email: user.email,
|
|
415
|
+
name: user.name || void 0,
|
|
416
|
+
tier: user.tier
|
|
417
|
+
});
|
|
418
|
+
console.log("");
|
|
419
|
+
console.log(
|
|
420
|
+
chalk4.green("\u2713 Successfully logged in as:"),
|
|
421
|
+
chalk4.white(user.email)
|
|
422
|
+
);
|
|
423
|
+
console.log(chalk4.gray(" Plan:"), chalk4.cyan(user.tier.toUpperCase()));
|
|
424
|
+
console.log(chalk4.gray(" Run 'aerocoding whoami' to verify\n"));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(
|
|
427
|
+
chalk4.red("\n\u2717 Login failed:"),
|
|
428
|
+
error.message || "Unknown error"
|
|
429
|
+
);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/commands/logout.ts
|
|
435
|
+
import chalk5 from "chalk";
|
|
436
|
+
async function logoutCommand() {
|
|
437
|
+
const tokenManager = new TokenManager();
|
|
438
|
+
const metadata = await tokenManager.getUserMetadata();
|
|
439
|
+
if (!metadata) {
|
|
440
|
+
console.log(chalk5.yellow("\u26A0\uFE0F Not logged in"));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const email = metadata.email;
|
|
444
|
+
await tokenManager.logout();
|
|
445
|
+
console.log(chalk5.green("\u2713 Logged out successfully"));
|
|
446
|
+
console.log(chalk5.gray(` Cleared credentials for ${email}
|
|
447
|
+
`));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/whoami.ts
|
|
451
|
+
import chalk6 from "chalk";
|
|
452
|
+
async function whoamiCommand() {
|
|
453
|
+
const tokenManager = new TokenManager();
|
|
454
|
+
const token = await tokenManager.getAccessToken();
|
|
455
|
+
if (!token) {
|
|
456
|
+
console.log(chalk6.red("\u2717 Not logged in"));
|
|
457
|
+
console.log(chalk6.gray(" Run 'aerocoding login' to authenticate\n"));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const apiClient = createApiClientWithAutoLogout(token, tokenManager);
|
|
462
|
+
const user = await apiClient.getCurrentUser();
|
|
463
|
+
console.log("");
|
|
464
|
+
console.log(chalk6.white("Logged in as:"), chalk6.cyan.bold(user.email));
|
|
465
|
+
console.log(chalk6.gray("User ID:"), user.id);
|
|
466
|
+
console.log(chalk6.gray("Plan:"), chalk6.cyan(user.tier.toUpperCase()));
|
|
467
|
+
if (user.name) {
|
|
468
|
+
console.log(chalk6.gray("Name:"), user.name);
|
|
469
|
+
}
|
|
470
|
+
console.log("");
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error(chalk6.red("\u2717 Failed to get user info"));
|
|
473
|
+
if (error.response?.status === 401) {
|
|
474
|
+
await handleUnauthorized(tokenManager);
|
|
475
|
+
}
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/commands/generate.ts
|
|
481
|
+
import chalk9 from "chalk";
|
|
482
|
+
import ora2 from "ora";
|
|
483
|
+
|
|
484
|
+
// src/utils/file-writer.ts
|
|
485
|
+
import fs from "fs/promises";
|
|
486
|
+
import path from "path";
|
|
487
|
+
import chalk7 from "chalk";
|
|
488
|
+
function isPathSafe(outputDir, filePath) {
|
|
489
|
+
const resolvedOutput = path.resolve(outputDir);
|
|
490
|
+
const resolvedFile = path.resolve(outputDir, filePath);
|
|
491
|
+
return resolvedFile.startsWith(resolvedOutput + path.sep) || resolvedFile === resolvedOutput;
|
|
492
|
+
}
|
|
493
|
+
async function writeGeneratedFiles(files, outputDir) {
|
|
494
|
+
for (const file of files) {
|
|
495
|
+
if (!isPathSafe(outputDir, file.path)) {
|
|
496
|
+
console.error(
|
|
497
|
+
chalk7.red(` \u2717 Skipping unsafe path: ${file.path}`)
|
|
498
|
+
);
|
|
499
|
+
console.error(
|
|
500
|
+
chalk7.gray(` Path traversal detected - file path must be within output directory`)
|
|
501
|
+
);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const fullPath = path.resolve(outputDir, file.path);
|
|
505
|
+
const dir = path.dirname(fullPath);
|
|
506
|
+
try {
|
|
507
|
+
await fs.mkdir(dir, { recursive: true });
|
|
508
|
+
await fs.writeFile(fullPath, file.content, "utf-8");
|
|
509
|
+
console.log(chalk7.gray(` \u2713 ${file.path}`));
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error(chalk7.red(` \u2717 Failed to write ${file.path}`));
|
|
512
|
+
console.error(chalk7.gray(` ${error.message}`));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/utils/prompt.ts
|
|
518
|
+
import readline from "readline";
|
|
519
|
+
import chalk8 from "chalk";
|
|
520
|
+
function promptConfirm(message) {
|
|
521
|
+
return new Promise((resolve) => {
|
|
522
|
+
const rl = readline.createInterface({
|
|
523
|
+
input: process.stdin,
|
|
524
|
+
output: process.stdout
|
|
525
|
+
});
|
|
526
|
+
rl.question(chalk8.yellow(`${message} [Y/n] `), (answer) => {
|
|
527
|
+
rl.close();
|
|
528
|
+
const normalized = answer.trim().toLowerCase();
|
|
529
|
+
resolve(normalized !== "n" && normalized !== "no");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/config/loader.ts
|
|
535
|
+
import fs2 from "fs/promises";
|
|
536
|
+
import path2 from "path";
|
|
537
|
+
|
|
538
|
+
// src/config/schema.ts
|
|
539
|
+
import { z } from "zod";
|
|
540
|
+
var configSchema = z.object({
|
|
541
|
+
$schema: z.string().optional(),
|
|
542
|
+
project: z.string().uuid(),
|
|
543
|
+
output: z.string().default("./generated"),
|
|
544
|
+
backend: z.object({
|
|
545
|
+
preset: z.string(),
|
|
546
|
+
layers: z.array(z.string())
|
|
547
|
+
}).optional(),
|
|
548
|
+
frontend: z.object({
|
|
549
|
+
preset: z.string(),
|
|
550
|
+
layers: z.array(z.string())
|
|
551
|
+
}).optional(),
|
|
552
|
+
codeStyle: z.object({
|
|
553
|
+
includeValidations: z.boolean().default(true),
|
|
554
|
+
includeComments: z.boolean().default(true),
|
|
555
|
+
includeAnnotations: z.boolean().default(true),
|
|
556
|
+
includeLogging: z.boolean().default(true),
|
|
557
|
+
includeTesting: z.boolean().default(true)
|
|
558
|
+
}).default({}),
|
|
559
|
+
libraries: z.object({
|
|
560
|
+
validation: z.string().optional(),
|
|
561
|
+
logging: z.string().optional()
|
|
562
|
+
}).optional(),
|
|
563
|
+
excludePatterns: z.array(z.string()).optional()
|
|
564
|
+
});
|
|
565
|
+
var CONFIG_FILENAME = ".aerocodingrc.json";
|
|
566
|
+
|
|
567
|
+
// src/config/loader.ts
|
|
568
|
+
async function loadConfig(dir = process.cwd()) {
|
|
569
|
+
const configPath = path2.join(dir, CONFIG_FILENAME);
|
|
570
|
+
try {
|
|
571
|
+
const content = await fs2.readFile(configPath, "utf-8");
|
|
572
|
+
const parsed = JSON.parse(content);
|
|
573
|
+
return configSchema.parse(parsed);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.code === "ENOENT") {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function saveConfig(config, dir = process.cwd()) {
|
|
582
|
+
const configPath = path2.join(dir, CONFIG_FILENAME);
|
|
583
|
+
const content = JSON.stringify(
|
|
584
|
+
{
|
|
585
|
+
$schema: "https://aerocoding.app/schemas/aerocodingrc.json",
|
|
586
|
+
...config
|
|
587
|
+
},
|
|
588
|
+
null,
|
|
589
|
+
2
|
|
590
|
+
);
|
|
591
|
+
await fs2.writeFile(configPath, content, "utf-8");
|
|
592
|
+
}
|
|
593
|
+
async function configExists(dir = process.cwd()) {
|
|
594
|
+
const configPath = path2.join(dir, CONFIG_FILENAME);
|
|
595
|
+
try {
|
|
596
|
+
await fs2.access(configPath);
|
|
597
|
+
return true;
|
|
598
|
+
} catch {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/commands/generate.ts
|
|
604
|
+
function buildTargetsFromConfig(config) {
|
|
605
|
+
if (!config) return [];
|
|
606
|
+
const targets = [];
|
|
607
|
+
if (config.backend?.preset) {
|
|
608
|
+
targets.push("backend");
|
|
609
|
+
}
|
|
610
|
+
if (config.frontend?.preset) {
|
|
611
|
+
targets.push("frontend");
|
|
612
|
+
}
|
|
613
|
+
return targets;
|
|
614
|
+
}
|
|
615
|
+
async function generateCommand(options) {
|
|
616
|
+
const tokenManager = new TokenManager();
|
|
617
|
+
const token = await tokenManager.getAccessToken();
|
|
618
|
+
if (!token) {
|
|
619
|
+
console.log(chalk9.red("\n Not authenticated"));
|
|
620
|
+
console.log(chalk9.gray(" Run 'aerocoding login' to get started\n"));
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
const config = await loadConfig();
|
|
624
|
+
const projectId = options.project || config?.project;
|
|
625
|
+
if (!projectId) {
|
|
626
|
+
console.log(chalk9.red("\n Project ID required"));
|
|
627
|
+
console.log(chalk9.gray(" Run 'aerocoding init' to create a config file"));
|
|
628
|
+
console.log(chalk9.gray(" Or use --project <id> to specify a project\n"));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
const targets = options.targets || buildTargetsFromConfig(config);
|
|
632
|
+
const output = options.output || config?.output || "./generated";
|
|
633
|
+
const backendPreset = options.backendPreset || config?.backend?.preset;
|
|
634
|
+
const frontendPreset = options.frontendPreset || config?.frontend?.preset;
|
|
635
|
+
const backendLayers = options.backendLayers || config?.backend?.layers;
|
|
636
|
+
const frontendLayers = options.frontendLayers || config?.frontend?.layers;
|
|
637
|
+
const validationLib = options.validationLib || config?.libraries?.validation;
|
|
638
|
+
const includeValidations = options.validations ?? config?.codeStyle?.includeValidations ?? true;
|
|
639
|
+
const includeComments = options.comments ?? config?.codeStyle?.includeComments ?? true;
|
|
640
|
+
const includeAnnotations = options.annotations ?? config?.codeStyle?.includeAnnotations ?? true;
|
|
641
|
+
const includeLogging = options.logging ?? config?.codeStyle?.includeLogging ?? true;
|
|
642
|
+
const includeTesting = options.testing ?? config?.codeStyle?.includeTesting ?? true;
|
|
643
|
+
const apiClient = createApiClientWithAutoLogout(token, tokenManager);
|
|
644
|
+
let spinner2 = ora2({ text: "Fetching project...", color: "cyan" }).start();
|
|
645
|
+
try {
|
|
646
|
+
const project = await apiClient.getProject(projectId);
|
|
647
|
+
spinner2.succeed(chalk9.gray(`Project: ${project.name}`));
|
|
648
|
+
spinner2 = ora2({ text: "Checking credits...", color: "cyan" }).start();
|
|
649
|
+
const credits = await apiClient.getCreditUsage(project.organizationId);
|
|
650
|
+
spinner2.succeed(chalk9.gray(`Credits: ${credits.remaining} / ${credits.limit} available`));
|
|
651
|
+
console.log("");
|
|
652
|
+
console.log(chalk9.bold(" Generation Summary"));
|
|
653
|
+
console.log(chalk9.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
654
|
+
console.log(chalk9.gray(" Project:"), chalk9.white(project.name));
|
|
655
|
+
if (config) {
|
|
656
|
+
console.log(chalk9.gray(" Config:"), chalk9.cyan(".aerocodingrc.json"));
|
|
657
|
+
}
|
|
658
|
+
console.log(
|
|
659
|
+
chalk9.gray(" Targets:"),
|
|
660
|
+
chalk9.cyan(targets.length > 0 ? targets.join(", ") : "default")
|
|
661
|
+
);
|
|
662
|
+
if (backendPreset) {
|
|
663
|
+
console.log(chalk9.gray(" Backend Preset:"), chalk9.cyan(backendPreset));
|
|
664
|
+
}
|
|
665
|
+
if (frontendPreset) {
|
|
666
|
+
console.log(chalk9.gray(" Frontend Preset:"), chalk9.cyan(frontendPreset));
|
|
667
|
+
}
|
|
668
|
+
const disabledOptions = [];
|
|
669
|
+
if (!includeValidations) disabledOptions.push("validations");
|
|
670
|
+
if (!includeComments) disabledOptions.push("comments");
|
|
671
|
+
if (!includeAnnotations) disabledOptions.push("annotations");
|
|
672
|
+
if (!includeLogging) disabledOptions.push("logging");
|
|
673
|
+
if (!includeTesting) disabledOptions.push("testing");
|
|
674
|
+
if (disabledOptions.length > 0) {
|
|
675
|
+
console.log(chalk9.gray(" Disabled:"), chalk9.yellow(disabledOptions.join(", ")));
|
|
676
|
+
}
|
|
677
|
+
if (validationLib) {
|
|
678
|
+
console.log(chalk9.gray(" Validation Lib:"), chalk9.cyan(validationLib));
|
|
679
|
+
}
|
|
680
|
+
console.log(
|
|
681
|
+
chalk9.gray(" Credits:"),
|
|
682
|
+
credits.remaining > 50 ? chalk9.green(`${credits.remaining} available`) : chalk9.yellow(`${credits.remaining} available (low)`)
|
|
683
|
+
);
|
|
684
|
+
console.log(chalk9.gray(" Output:"), chalk9.cyan(output));
|
|
685
|
+
console.log("");
|
|
686
|
+
if (!options.yes) {
|
|
687
|
+
const confirmed = await promptConfirm(" Proceed with generation?");
|
|
688
|
+
if (!confirmed) {
|
|
689
|
+
console.log(chalk9.yellow("\n Generation cancelled\n"));
|
|
690
|
+
process.exit(0);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
console.log("");
|
|
694
|
+
spinner2 = ora2({ text: "Generating code...", color: "cyan" }).start();
|
|
695
|
+
const result = await apiClient.generateCode({
|
|
696
|
+
projectId,
|
|
697
|
+
targets,
|
|
698
|
+
options: {
|
|
699
|
+
includeValidations,
|
|
700
|
+
includeComments,
|
|
701
|
+
includeAnnotations,
|
|
702
|
+
includeLogging,
|
|
703
|
+
includeTesting,
|
|
704
|
+
outputDir: output,
|
|
705
|
+
backendPreset,
|
|
706
|
+
frontendPreset,
|
|
707
|
+
backendLayers,
|
|
708
|
+
frontendLayers,
|
|
709
|
+
validationLib
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
spinner2.succeed(chalk9.green("Code generated successfully!"));
|
|
713
|
+
console.log("");
|
|
714
|
+
console.log(chalk9.bold(" Results"));
|
|
715
|
+
console.log(chalk9.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
716
|
+
console.log(chalk9.gray(" Files:"), chalk9.cyan(result.stats.totalFiles));
|
|
717
|
+
console.log(chalk9.gray(" Entities:"), chalk9.cyan(result.stats.totalEntities));
|
|
718
|
+
console.log(
|
|
719
|
+
chalk9.gray(" Languages:"),
|
|
720
|
+
chalk9.cyan(result.stats.languages.join(", "))
|
|
721
|
+
);
|
|
722
|
+
if (result.creditsUsed !== void 0) {
|
|
723
|
+
console.log("");
|
|
724
|
+
console.log(chalk9.bold(" Credits"));
|
|
725
|
+
console.log(chalk9.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
726
|
+
console.log(chalk9.gray(" Used:"), chalk9.yellow(result.creditsUsed));
|
|
727
|
+
console.log(
|
|
728
|
+
chalk9.gray(" Remaining:"),
|
|
729
|
+
result.creditsRemaining !== void 0 && result.creditsRemaining > 50 ? chalk9.green(result.creditsRemaining) : chalk9.yellow(result.creditsRemaining)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
733
|
+
console.log("");
|
|
734
|
+
console.log(chalk9.yellow(" Warnings:"));
|
|
735
|
+
for (const warning of result.warnings) {
|
|
736
|
+
console.log(chalk9.yellow(` - ${warning}`));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
console.log("");
|
|
740
|
+
await writeGeneratedFiles(result.files, output);
|
|
741
|
+
console.log(chalk9.green(` Files written to ${chalk9.white(output)}`));
|
|
742
|
+
console.log("");
|
|
743
|
+
} catch (error) {
|
|
744
|
+
spinner2.fail(chalk9.red("Generation failed"));
|
|
745
|
+
if (error.response?.status === 401) {
|
|
746
|
+
await handleUnauthorized(tokenManager);
|
|
747
|
+
} else if (error.response?.status === 403) {
|
|
748
|
+
console.log(chalk9.yellow("\n You don't have permission to access this project."));
|
|
749
|
+
console.log(chalk9.gray(" Check if you're part of the organization.\n"));
|
|
750
|
+
} else if (error.response?.status === 404) {
|
|
751
|
+
console.log(chalk9.yellow("\n Project not found."));
|
|
752
|
+
console.log(chalk9.gray(" Check if the project ID is correct.\n"));
|
|
753
|
+
} else if (error.response?.status === 429) {
|
|
754
|
+
const data = error.response.data;
|
|
755
|
+
console.log(chalk9.red("\n Insufficient credits"));
|
|
756
|
+
if (data.requiredCredits) {
|
|
757
|
+
console.log(chalk9.yellow(` Required: ${data.requiredCredits} credits`));
|
|
758
|
+
}
|
|
759
|
+
console.log(chalk9.yellow(` Available: ${data.remaining ?? 0} credits`));
|
|
760
|
+
console.log(chalk9.gray("\n Upgrade your plan or wait for credit reset.\n"));
|
|
761
|
+
} else if (error.response?.data?.message) {
|
|
762
|
+
console.log(chalk9.red(`
|
|
763
|
+
${error.response.data.message}
|
|
764
|
+
`));
|
|
765
|
+
} else {
|
|
766
|
+
console.log(chalk9.red(`
|
|
767
|
+
${error.message}
|
|
768
|
+
`));
|
|
769
|
+
}
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/commands/init.ts
|
|
775
|
+
import * as p from "@clack/prompts";
|
|
776
|
+
import chalk10 from "chalk";
|
|
777
|
+
async function initCommand(options) {
|
|
778
|
+
p.intro(chalk10.bgCyan.black(" AeroCoding CLI "));
|
|
779
|
+
if (!options.force && await configExists()) {
|
|
780
|
+
const overwrite = await p.confirm({
|
|
781
|
+
message: "Config file already exists. Overwrite?",
|
|
782
|
+
initialValue: false
|
|
783
|
+
});
|
|
784
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
785
|
+
p.cancel("Operation cancelled.");
|
|
786
|
+
process.exit(0);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const tokenManager = new TokenManager();
|
|
790
|
+
const token = await tokenManager.getAccessToken();
|
|
791
|
+
if (!token) {
|
|
792
|
+
p.cancel("Not logged in. Run 'aerocoding login' first.");
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
const apiClient = createApiClientWithAutoLogout(token, tokenManager);
|
|
796
|
+
try {
|
|
797
|
+
let projectId = options.project;
|
|
798
|
+
if (!projectId) {
|
|
799
|
+
const spinner3 = p.spinner();
|
|
800
|
+
spinner3.start("Loading projects...");
|
|
801
|
+
const projects = await apiClient.listProjects();
|
|
802
|
+
spinner3.stop("Projects loaded");
|
|
803
|
+
if (projects.length === 0) {
|
|
804
|
+
p.cancel("No projects found. Create a project on aerocoding.app first.");
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
const selectedProject = await p.select({
|
|
808
|
+
message: "Select project",
|
|
809
|
+
options: projects.map((proj) => ({
|
|
810
|
+
value: proj.id,
|
|
811
|
+
label: proj.name,
|
|
812
|
+
hint: [proj.backendFramework, proj.frontendFramework].filter(Boolean).join(" + ")
|
|
813
|
+
}))
|
|
814
|
+
});
|
|
815
|
+
if (p.isCancel(selectedProject)) {
|
|
816
|
+
p.cancel("Operation cancelled.");
|
|
817
|
+
process.exit(0);
|
|
818
|
+
}
|
|
819
|
+
projectId = selectedProject;
|
|
820
|
+
}
|
|
821
|
+
const spinner2 = p.spinner();
|
|
822
|
+
spinner2.start("Fetching project details...");
|
|
823
|
+
const project = await apiClient.getProject(projectId);
|
|
824
|
+
spinner2.stop(`Project: ${project.name}`);
|
|
825
|
+
const targetOptions = [];
|
|
826
|
+
if (project.backendFramework) {
|
|
827
|
+
targetOptions.push({
|
|
828
|
+
value: "backend",
|
|
829
|
+
label: `Backend (${project.backendFramework})`
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (project.frontendFramework) {
|
|
833
|
+
targetOptions.push({
|
|
834
|
+
value: "frontend",
|
|
835
|
+
label: `Frontend (${project.frontendFramework})`
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (targetOptions.length === 0) {
|
|
839
|
+
p.cancel("Project has no frameworks configured. Update it on aerocoding.app first.");
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
const targets = await p.multiselect({
|
|
843
|
+
message: "What do you want to generate?",
|
|
844
|
+
options: targetOptions,
|
|
845
|
+
required: true
|
|
846
|
+
});
|
|
847
|
+
if (p.isCancel(targets)) {
|
|
848
|
+
p.cancel("Operation cancelled.");
|
|
849
|
+
process.exit(0);
|
|
850
|
+
}
|
|
851
|
+
const selectedTargets = targets;
|
|
852
|
+
const config = {
|
|
853
|
+
project: projectId,
|
|
854
|
+
output: "./generated",
|
|
855
|
+
codeStyle: {
|
|
856
|
+
includeValidations: true,
|
|
857
|
+
includeComments: true,
|
|
858
|
+
includeAnnotations: true,
|
|
859
|
+
includeLogging: true,
|
|
860
|
+
includeTesting: true
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
if (selectedTargets.includes("backend") && project.backendFramework) {
|
|
864
|
+
const archSpinner = p.spinner();
|
|
865
|
+
archSpinner.start("Loading backend architectures...");
|
|
866
|
+
const architectures = await apiClient.getArchitectures(project.backendFramework);
|
|
867
|
+
archSpinner.stop("Architectures loaded");
|
|
868
|
+
if (architectures.length > 0) {
|
|
869
|
+
const preset = await p.select({
|
|
870
|
+
message: "Backend architecture",
|
|
871
|
+
options: architectures.map((arch) => ({
|
|
872
|
+
value: arch.id,
|
|
873
|
+
label: arch.name,
|
|
874
|
+
hint: arch.description
|
|
875
|
+
}))
|
|
876
|
+
});
|
|
877
|
+
if (p.isCancel(preset)) {
|
|
878
|
+
p.cancel("Operation cancelled.");
|
|
879
|
+
process.exit(0);
|
|
880
|
+
}
|
|
881
|
+
const selectedArch = architectures.find((a) => a.id === preset);
|
|
882
|
+
if (selectedArch && selectedArch.layers.length > 0) {
|
|
883
|
+
const layers = await p.multiselect({
|
|
884
|
+
message: "Select backend layers to include",
|
|
885
|
+
options: selectedArch.layers.map((layer) => ({
|
|
886
|
+
value: layer.id,
|
|
887
|
+
label: layer.name,
|
|
888
|
+
hint: layer.category
|
|
889
|
+
})),
|
|
890
|
+
initialValues: selectedArch.layers.map((l) => l.id)
|
|
891
|
+
});
|
|
892
|
+
if (p.isCancel(layers)) {
|
|
893
|
+
p.cancel("Operation cancelled.");
|
|
894
|
+
process.exit(0);
|
|
895
|
+
}
|
|
896
|
+
config.backend = {
|
|
897
|
+
preset,
|
|
898
|
+
layers
|
|
899
|
+
};
|
|
900
|
+
} else {
|
|
901
|
+
config.backend = {
|
|
902
|
+
preset,
|
|
903
|
+
layers: []
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (selectedTargets.includes("frontend") && project.frontendFramework) {
|
|
909
|
+
const archSpinner = p.spinner();
|
|
910
|
+
archSpinner.start("Loading frontend architectures...");
|
|
911
|
+
const architectures = await apiClient.getArchitectures(project.frontendFramework);
|
|
912
|
+
archSpinner.stop("Architectures loaded");
|
|
913
|
+
if (architectures.length > 0) {
|
|
914
|
+
const preset = await p.select({
|
|
915
|
+
message: "Frontend architecture",
|
|
916
|
+
options: architectures.map((arch) => ({
|
|
917
|
+
value: arch.id,
|
|
918
|
+
label: arch.name,
|
|
919
|
+
hint: arch.description
|
|
920
|
+
}))
|
|
921
|
+
});
|
|
922
|
+
if (p.isCancel(preset)) {
|
|
923
|
+
p.cancel("Operation cancelled.");
|
|
924
|
+
process.exit(0);
|
|
925
|
+
}
|
|
926
|
+
const selectedArch = architectures.find((a) => a.id === preset);
|
|
927
|
+
if (selectedArch && selectedArch.layers.length > 0) {
|
|
928
|
+
const layers = await p.multiselect({
|
|
929
|
+
message: "Select frontend layers to include",
|
|
930
|
+
options: selectedArch.layers.map((layer) => ({
|
|
931
|
+
value: layer.id,
|
|
932
|
+
label: layer.name,
|
|
933
|
+
hint: layer.category
|
|
934
|
+
})),
|
|
935
|
+
initialValues: selectedArch.layers.map((l) => l.id)
|
|
936
|
+
});
|
|
937
|
+
if (p.isCancel(layers)) {
|
|
938
|
+
p.cancel("Operation cancelled.");
|
|
939
|
+
process.exit(0);
|
|
940
|
+
}
|
|
941
|
+
config.frontend = {
|
|
942
|
+
preset,
|
|
943
|
+
layers
|
|
944
|
+
};
|
|
945
|
+
} else {
|
|
946
|
+
config.frontend = {
|
|
947
|
+
preset,
|
|
948
|
+
layers: []
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const codeStyleOptions = await p.multiselect({
|
|
954
|
+
message: "Code style options",
|
|
955
|
+
options: [
|
|
956
|
+
{ value: "validations", label: "Include validations", hint: "Add validation rules" },
|
|
957
|
+
{ value: "comments", label: "Include comments", hint: "Add code documentation" },
|
|
958
|
+
{ value: "annotations", label: "Include annotations", hint: "Add decorators/attributes" },
|
|
959
|
+
{ value: "logging", label: "Include logging", hint: "Add log statements" },
|
|
960
|
+
{ value: "testing", label: "Include tests", hint: "Generate test files" }
|
|
961
|
+
],
|
|
962
|
+
initialValues: ["validations", "comments", "annotations", "logging", "testing"]
|
|
963
|
+
});
|
|
964
|
+
if (p.isCancel(codeStyleOptions)) {
|
|
965
|
+
p.cancel("Operation cancelled.");
|
|
966
|
+
process.exit(0);
|
|
967
|
+
}
|
|
968
|
+
const selectedStyles = codeStyleOptions;
|
|
969
|
+
config.codeStyle = {
|
|
970
|
+
includeValidations: selectedStyles.includes("validations"),
|
|
971
|
+
includeComments: selectedStyles.includes("comments"),
|
|
972
|
+
includeAnnotations: selectedStyles.includes("annotations"),
|
|
973
|
+
includeLogging: selectedStyles.includes("logging"),
|
|
974
|
+
includeTesting: selectedStyles.includes("testing")
|
|
975
|
+
};
|
|
976
|
+
const output = await p.text({
|
|
977
|
+
message: "Output directory",
|
|
978
|
+
initialValue: "./generated",
|
|
979
|
+
validate: (value) => {
|
|
980
|
+
if (!value) return "Output directory is required";
|
|
981
|
+
return void 0;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
if (p.isCancel(output)) {
|
|
985
|
+
p.cancel("Operation cancelled.");
|
|
986
|
+
process.exit(0);
|
|
987
|
+
}
|
|
988
|
+
config.output = output;
|
|
989
|
+
await saveConfig(config);
|
|
990
|
+
p.outro(
|
|
991
|
+
chalk10.green("Config saved to .aerocodingrc.json") + "\n\n" + chalk10.gray(" Run ") + chalk10.cyan("aerocoding generate") + chalk10.gray(" to generate code!")
|
|
992
|
+
);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
if (error.response?.status === 401) {
|
|
995
|
+
await handleUnauthorized(tokenManager);
|
|
996
|
+
} else if (error.response?.data?.message) {
|
|
997
|
+
p.cancel(error.response.data.message);
|
|
998
|
+
} else {
|
|
999
|
+
p.cancel(error.message || "An unexpected error occurred");
|
|
1000
|
+
}
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/commands/pull.ts
|
|
1006
|
+
import chalk11 from "chalk";
|
|
1007
|
+
async function pullCommand(options) {
|
|
1008
|
+
console.log(chalk11.yellow("\u26A0\uFE0F Command not yet implemented"));
|
|
1009
|
+
if (options.project) {
|
|
1010
|
+
console.log(chalk11.gray(` Requested project: ${options.project}`));
|
|
1011
|
+
}
|
|
1012
|
+
console.log(chalk11.gray(" Coming soon in v0.2.0\n"));
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/commands/status.ts
|
|
1016
|
+
import chalk12 from "chalk";
|
|
1017
|
+
async function statusCommand() {
|
|
1018
|
+
console.log(chalk12.yellow("\u26A0\uFE0F Command not yet implemented"));
|
|
1019
|
+
console.log(chalk12.gray(" Coming soon in v0.2.0\n"));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/index.ts
|
|
1023
|
+
import "dotenv/config";
|
|
1024
|
+
var program = new Command();
|
|
1025
|
+
program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.0");
|
|
1026
|
+
program.command("login").description("Authenticate with AeroCoding").action(loginCommand);
|
|
1027
|
+
program.command("logout").description("Logout and clear stored credentials").action(logoutCommand);
|
|
1028
|
+
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
1029
|
+
program.command("init").description("Initialize AeroCoding in current directory").option("-p, --project <id>", "Project ID (skip selection)").option("-f, --force", "Overwrite existing config without asking").action(initCommand);
|
|
1030
|
+
program.command("pull").description("Pull schema from cloud").option("-p, --project <id>", "Project ID").action(pullCommand);
|
|
1031
|
+
program.command("status").description("Show local schema status").action(statusCommand);
|
|
1032
|
+
program.command("generate").alias("gen").description("Generate code from schema").option("-p, --project <id>", "Project ID").option("-t, --targets <targets...>", "Generation targets (e.g., dotnet-entity)").option("-e, --entities <entities...>", "Filter by entity names").option("-o, --output <dir>", "Output directory", "./generated").option("--backend-preset <preset>", "Backend architecture preset").option("--frontend-preset <preset>", "Frontend architecture preset").option("--backend-layers <layers...>", "Backend layers to generate").option("--frontend-layers <layers...>", "Frontend layers to generate").option("--no-validations", "Exclude validations").option("--no-comments", "Exclude comments").option("--no-annotations", "Exclude annotations").option("--no-logging", "Exclude logging statements").option("--no-testing", "Exclude test files").option("--validation-lib <framework>", "Validation library (e.g., fluentvalidation, zod, formz)").option("-y, --yes", "Skip confirmation prompt").action(generateCommand);
|
|
1033
|
+
program.parse();
|
|
1034
|
+
//# sourceMappingURL=index.js.map
|