@l4yercak3/cli 1.2.21 → 1.3.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/.claude/settings.local.json +8 -1
- package/bin/cli.js +25 -0
- package/docs/CLI_PAGE_DETECTION_REQUIREMENTS.md +519 -0
- package/package.json +1 -1
- package/src/api/backend-client.js +149 -0
- package/src/commands/connect.js +243 -0
- package/src/commands/pages.js +317 -0
- package/src/commands/scaffold.js +409 -0
- package/src/commands/spread.js +89 -190
- package/src/commands/sync.js +169 -0
- package/src/detectors/index.js +13 -0
- package/src/detectors/mapping-suggestor.js +119 -0
- package/src/detectors/model-detector.js +318 -0
- package/src/detectors/page-detector.js +480 -0
- package/src/generators/manifest-generator.js +154 -0
- package/src/utils/init-helpers.js +243 -0
- package/tests/page-detector.test.js +371 -0
|
@@ -294,6 +294,155 @@ class BackendClient {
|
|
|
294
294
|
async syncApplication(applicationId, syncData) {
|
|
295
295
|
return await this.request('POST', `/api/v1/cli/applications/${applicationId}/sync`, syncData);
|
|
296
296
|
}
|
|
297
|
+
|
|
298
|
+
// ============================================
|
|
299
|
+
// Activity Protocol / Page Detection API
|
|
300
|
+
// ============================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Bulk register pages for an application
|
|
304
|
+
* @param {string} applicationId - The application ID
|
|
305
|
+
* @param {Array<{path: string, name: string, pageType?: string}>} pages - Pages to register
|
|
306
|
+
* @returns {Promise<{success: boolean, results: Array, total: number, created: number, updated: number}>}
|
|
307
|
+
*/
|
|
308
|
+
async bulkRegisterPages(applicationId, pages) {
|
|
309
|
+
return await this.request('POST', '/api/v1/activity/pages/bulk', {
|
|
310
|
+
applicationId,
|
|
311
|
+
pages,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Register a single page
|
|
317
|
+
* @param {string} applicationId - The application ID
|
|
318
|
+
* @param {object} pageData - Page data
|
|
319
|
+
* @returns {Promise<{success: boolean, pageId: string, created: boolean}>}
|
|
320
|
+
*/
|
|
321
|
+
async registerPage(applicationId, pageData) {
|
|
322
|
+
return await this.request('POST', '/api/v1/activity/pages', {
|
|
323
|
+
applicationId,
|
|
324
|
+
...pageData,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get all pages for an application
|
|
330
|
+
* @param {string} applicationId - The application ID
|
|
331
|
+
* @param {object} options - Query options (status)
|
|
332
|
+
* @returns {Promise<{success: boolean, pages: Array, total: number}>}
|
|
333
|
+
*/
|
|
334
|
+
async getApplicationPages(applicationId, options = {}) {
|
|
335
|
+
let url = `/api/v1/activity/pages?applicationId=${applicationId}`;
|
|
336
|
+
if (options.status) {
|
|
337
|
+
url += `&status=${options.status}`;
|
|
338
|
+
}
|
|
339
|
+
return await this.request('GET', url);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Log an activity event
|
|
344
|
+
* @param {string} applicationId - The application ID
|
|
345
|
+
* @param {object} eventData - Event data
|
|
346
|
+
* @returns {Promise<{success: boolean, eventId: string, timestamp: number}>}
|
|
347
|
+
*/
|
|
348
|
+
async logActivityEvent(applicationId, eventData) {
|
|
349
|
+
return await this.request('POST', '/api/v1/activity/events', {
|
|
350
|
+
applicationId,
|
|
351
|
+
...eventData,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get activity events for an application
|
|
357
|
+
* @param {string} applicationId - The application ID
|
|
358
|
+
* @param {object} options - Query options (severity, category, limit)
|
|
359
|
+
* @returns {Promise<{success: boolean, events: Array, hasMore: boolean, nextCursor: string|null}>}
|
|
360
|
+
*/
|
|
361
|
+
async getActivityEvents(applicationId, options = {}) {
|
|
362
|
+
let url = `/api/v1/activity/events?applicationId=${applicationId}`;
|
|
363
|
+
if (options.severity) {
|
|
364
|
+
url += `&severity=${options.severity}`;
|
|
365
|
+
}
|
|
366
|
+
if (options.category) {
|
|
367
|
+
url += `&category=${options.category}`;
|
|
368
|
+
}
|
|
369
|
+
if (options.limit) {
|
|
370
|
+
url += `&limit=${options.limit}`;
|
|
371
|
+
}
|
|
372
|
+
return await this.request('GET', url);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get activity statistics for an application
|
|
377
|
+
* @param {string} applicationId - The application ID
|
|
378
|
+
* @param {string} timeRange - Time range ('1h', '24h', '7d')
|
|
379
|
+
* @returns {Promise<{success: boolean, stats: object}>}
|
|
380
|
+
*/
|
|
381
|
+
async getActivityStats(applicationId, timeRange = '24h') {
|
|
382
|
+
return await this.request(
|
|
383
|
+
'GET',
|
|
384
|
+
`/api/v1/activity/stats?applicationId=${applicationId}&timeRange=${timeRange}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get activity settings for an application
|
|
390
|
+
* @param {string} applicationId - The application ID
|
|
391
|
+
* @returns {Promise<object>}
|
|
392
|
+
*/
|
|
393
|
+
async getActivitySettings(applicationId) {
|
|
394
|
+
return await this.request('GET', `/api/v1/activity/settings?applicationId=${applicationId}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Update activity settings for an application
|
|
399
|
+
* @param {string} applicationId - The application ID
|
|
400
|
+
* @param {object} settings - Settings to update
|
|
401
|
+
* @returns {Promise<object>}
|
|
402
|
+
*/
|
|
403
|
+
async updateActivitySettings(applicationId, settings) {
|
|
404
|
+
return await this.request('PATCH', '/api/v1/activity/settings', {
|
|
405
|
+
applicationId,
|
|
406
|
+
...settings,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================
|
|
411
|
+
// Repository Connection API
|
|
412
|
+
// ============================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Connect a GitHub repository to a connected application
|
|
416
|
+
* Triggers repo analysis on the platform side
|
|
417
|
+
* @param {string} applicationId - The application ID
|
|
418
|
+
* @param {object} repoData - Repository connection data
|
|
419
|
+
* @param {string} repoData.repoUrl - Full GitHub repo URL
|
|
420
|
+
* @param {string} repoData.branch - Branch to analyze (e.g., 'main')
|
|
421
|
+
* @param {string} [repoData.githubConnectionId] - Optional GitHub OAuth connection ID
|
|
422
|
+
* @returns {Promise<object>}
|
|
423
|
+
*/
|
|
424
|
+
async connectRepo(applicationId, repoData) {
|
|
425
|
+
return await this.request(
|
|
426
|
+
'POST',
|
|
427
|
+
`/api/v1/cli/applications/${applicationId}/connect-repo`,
|
|
428
|
+
repoData
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Sync project manifest data to the platform
|
|
434
|
+
* Pushes the .l4yercak3.json content for platform-side analysis
|
|
435
|
+
* @param {string} applicationId - The application ID
|
|
436
|
+
* @param {object} manifest - The manifest object from .l4yercak3.json
|
|
437
|
+
* @returns {Promise<object>}
|
|
438
|
+
*/
|
|
439
|
+
async syncManifest(applicationId, manifest) {
|
|
440
|
+
return await this.request(
|
|
441
|
+
'POST',
|
|
442
|
+
`/api/v1/cli/applications/${applicationId}/sync-manifest`,
|
|
443
|
+
manifest
|
|
444
|
+
);
|
|
445
|
+
}
|
|
297
446
|
}
|
|
298
447
|
|
|
299
448
|
module.exports = new BackendClient();
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect Command
|
|
3
|
+
* Connects the project to the L4YERCAK3 platform by:
|
|
4
|
+
* 1. Loading the .l4yercak3.json manifest
|
|
5
|
+
* 2. Registering the app (if not already registered)
|
|
6
|
+
* 3. Connecting the GitHub repository for platform-side analysis
|
|
7
|
+
* 4. Writing env vars for platform integration
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const configManager = require('../config/config-manager');
|
|
13
|
+
const backendClient = require('../api/backend-client');
|
|
14
|
+
const manifestGenerator = require('../generators/manifest-generator');
|
|
15
|
+
const githubDetector = require('../detectors/github-detector');
|
|
16
|
+
const { generateProjectPathHash } = require('../utils/file-utils');
|
|
17
|
+
const {
|
|
18
|
+
requireAuth,
|
|
19
|
+
selectOrganization,
|
|
20
|
+
generateNewApiKey,
|
|
21
|
+
} = require('../utils/init-helpers');
|
|
22
|
+
const inquirer = require('inquirer');
|
|
23
|
+
const chalk = require('chalk');
|
|
24
|
+
const pkg = require('../../package.json');
|
|
25
|
+
|
|
26
|
+
async function handleConnect() {
|
|
27
|
+
requireAuth(configManager);
|
|
28
|
+
|
|
29
|
+
console.log(chalk.cyan(' 🔗 Connecting project to L4YERCAK3...\n'));
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const projectPath = process.cwd();
|
|
33
|
+
|
|
34
|
+
// Step 1: Load manifest
|
|
35
|
+
const manifest = manifestGenerator.loadManifest(projectPath);
|
|
36
|
+
if (!manifest) {
|
|
37
|
+
console.log(chalk.yellow(' ⚠️ No .l4yercak3.json manifest found.'));
|
|
38
|
+
console.log(chalk.gray(' Run "l4yercak3 init" first to scan your project.\n'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(chalk.green(` ✅ Loaded manifest (${manifest.framework}, ${manifest.detectedModels?.length || 0} models, ${manifest.detectedRoutes?.length || 0} routes)`));
|
|
43
|
+
|
|
44
|
+
// Step 2: Organization selection
|
|
45
|
+
console.log(chalk.cyan('\n 📦 Organization Setup\n'));
|
|
46
|
+
const existingConfig = configManager.getProjectConfig(projectPath);
|
|
47
|
+
let organizationId, organizationName;
|
|
48
|
+
|
|
49
|
+
if (existingConfig && existingConfig.organizationId) {
|
|
50
|
+
console.log(chalk.green(` ✅ Using organization: ${existingConfig.organizationName || existingConfig.organizationId}`));
|
|
51
|
+
organizationId = existingConfig.organizationId;
|
|
52
|
+
organizationName = existingConfig.organizationName;
|
|
53
|
+
} else {
|
|
54
|
+
const defaultName = manifest.framework || 'My Organization';
|
|
55
|
+
const result = await selectOrganization(defaultName);
|
|
56
|
+
organizationId = result.organizationId;
|
|
57
|
+
organizationName = result.organizationName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 3: Register application if not already registered
|
|
61
|
+
let applicationId = existingConfig?.applicationId || null;
|
|
62
|
+
|
|
63
|
+
if (!applicationId) {
|
|
64
|
+
console.log(chalk.cyan('\n 📱 Registering application...\n'));
|
|
65
|
+
const projectPathHash = generateProjectPathHash(projectPath);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const existingApp = await backendClient.checkExistingApplication(organizationId, projectPathHash);
|
|
69
|
+
|
|
70
|
+
if (existingApp.found && existingApp.application) {
|
|
71
|
+
applicationId = existingApp.application.id;
|
|
72
|
+
console.log(chalk.green(` ✅ Found existing registration: ${existingApp.application.name}\n`));
|
|
73
|
+
} else {
|
|
74
|
+
const folderName = path.basename(projectPath);
|
|
75
|
+
const registrationData = {
|
|
76
|
+
organizationId,
|
|
77
|
+
name: folderName,
|
|
78
|
+
description: `Connected via CLI from ${manifest.framework} project`,
|
|
79
|
+
source: {
|
|
80
|
+
type: 'cli',
|
|
81
|
+
projectPathHash,
|
|
82
|
+
cliVersion: pkg.version,
|
|
83
|
+
framework: manifest.framework,
|
|
84
|
+
routerType: manifest.routerType,
|
|
85
|
+
hasTypeScript: manifest.typescript || false,
|
|
86
|
+
},
|
|
87
|
+
connection: {
|
|
88
|
+
features: [],
|
|
89
|
+
},
|
|
90
|
+
deployment: {},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result = await backendClient.registerApplication(registrationData);
|
|
94
|
+
applicationId = result.applicationId;
|
|
95
|
+
console.log(chalk.green(` ✅ Application registered with L4YERCAK3\n`));
|
|
96
|
+
}
|
|
97
|
+
} catch (regError) {
|
|
98
|
+
console.log(chalk.yellow(` ⚠️ Could not register application: ${regError.message}`));
|
|
99
|
+
console.log(chalk.gray(' Continuing with GitHub connection...\n'));
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.green(` ✅ Application already registered (${applicationId})`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 4: API key setup
|
|
106
|
+
let apiKey = null;
|
|
107
|
+
const envPath = path.join(projectPath, '.env.local');
|
|
108
|
+
|
|
109
|
+
if (fs.existsSync(envPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
112
|
+
const match = envContent.match(/^L4YERCAK3_API_KEY=(.+)$/m);
|
|
113
|
+
if (match && match[1] && match[1].startsWith('sk_')) {
|
|
114
|
+
console.log(chalk.green(` ✅ API key found in .env.local`));
|
|
115
|
+
} else {
|
|
116
|
+
apiKey = await generateNewApiKey(organizationId);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
apiKey = await generateNewApiKey(organizationId);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
apiKey = await generateNewApiKey(organizationId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Step 5: Detect and connect GitHub repository
|
|
126
|
+
console.log(chalk.cyan('\n 🐙 GitHub Repository Connection\n'));
|
|
127
|
+
const githubInfo = githubDetector.detect(projectPath);
|
|
128
|
+
|
|
129
|
+
if (githubInfo.isGitHub) {
|
|
130
|
+
console.log(chalk.green(` ✅ Detected: ${githubInfo.owner}/${githubInfo.repo}`));
|
|
131
|
+
console.log(chalk.gray(` Branch: ${githubInfo.branch || 'main'}`));
|
|
132
|
+
console.log(chalk.gray(` URL: ${githubInfo.url}\n`));
|
|
133
|
+
|
|
134
|
+
const { connectRepo } = await inquirer.prompt([
|
|
135
|
+
{
|
|
136
|
+
type: 'confirm',
|
|
137
|
+
name: 'connectRepo',
|
|
138
|
+
message: 'Connect this repository to L4YERCAK3 for platform analysis?',
|
|
139
|
+
default: true,
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
if (connectRepo && applicationId) {
|
|
144
|
+
try {
|
|
145
|
+
await backendClient.connectRepo(applicationId, {
|
|
146
|
+
repoUrl: githubInfo.url,
|
|
147
|
+
branch: githubInfo.branch || 'main',
|
|
148
|
+
});
|
|
149
|
+
console.log(chalk.green('\n ✅ Repository connected! The platform will analyze your codebase.'));
|
|
150
|
+
console.log(chalk.gray(' Your app will appear in the builder once analysis completes.\n'));
|
|
151
|
+
} catch (repoError) {
|
|
152
|
+
console.log(chalk.yellow(`\n ⚠️ Could not connect repository: ${repoError.message}`));
|
|
153
|
+
console.log(chalk.gray(' This feature may not be available yet on the platform.\n'));
|
|
154
|
+
}
|
|
155
|
+
} else if (connectRepo && !applicationId) {
|
|
156
|
+
console.log(chalk.yellow('\n ⚠️ Cannot connect repo without a registered application.'));
|
|
157
|
+
console.log(chalk.gray(' Try running "l4yercak3 connect" again after resolving registration.\n'));
|
|
158
|
+
}
|
|
159
|
+
} else if (githubInfo.hasGit) {
|
|
160
|
+
console.log(chalk.yellow(' ⚠️ Git repository detected but not hosted on GitHub.'));
|
|
161
|
+
console.log(chalk.gray(' GitHub connection is required for platform analysis.\n'));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.yellow(' ⚠️ No git repository detected.'));
|
|
164
|
+
console.log(chalk.gray(' Initialize a git repo and push to GitHub for platform analysis.\n'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Step 6: Sync manifest to platform
|
|
168
|
+
if (applicationId) {
|
|
169
|
+
try {
|
|
170
|
+
await backendClient.syncManifest(applicationId, manifest);
|
|
171
|
+
console.log(chalk.green(' ✅ Manifest synced to platform'));
|
|
172
|
+
} catch (syncError) {
|
|
173
|
+
console.log(chalk.yellow(` ⚠️ Could not sync manifest: ${syncError.message}`));
|
|
174
|
+
console.log(chalk.gray(' This feature may not be available yet.\n'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 7: Update .env.local
|
|
179
|
+
if (apiKey || applicationId || organizationId) {
|
|
180
|
+
const envVars = {};
|
|
181
|
+
if (apiKey) envVars.L4YERCAK3_API_KEY = apiKey;
|
|
182
|
+
if (organizationId) envVars.L4YERCAK3_ORG_ID = organizationId;
|
|
183
|
+
if (applicationId) envVars.L4YERCAK3_APP_ID = applicationId;
|
|
184
|
+
|
|
185
|
+
updateEnvFile(envPath, envVars);
|
|
186
|
+
console.log(chalk.green(' ✅ Environment variables written to .env.local\n'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step 8: Save project config
|
|
190
|
+
const projectConfig = {
|
|
191
|
+
...(existingConfig || {}),
|
|
192
|
+
organizationId,
|
|
193
|
+
organizationName,
|
|
194
|
+
applicationId,
|
|
195
|
+
projectPathHash: generateProjectPathHash(projectPath),
|
|
196
|
+
connectedAt: Date.now(),
|
|
197
|
+
githubRepo: githubInfo.isGitHub ? `${githubInfo.owner}/${githubInfo.repo}` : undefined,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
configManager.saveProjectConfig(projectPath, projectConfig);
|
|
201
|
+
|
|
202
|
+
console.log(chalk.cyan(' 🎉 Connection complete!\n'));
|
|
203
|
+
console.log(chalk.gray(' Next steps:'));
|
|
204
|
+
console.log(chalk.gray(' • Run "l4yercak3 scaffold" to generate integration files'));
|
|
205
|
+
console.log(chalk.gray(' • Run "l4yercak3 sync" after changing your project structure\n'));
|
|
206
|
+
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(chalk.red(`\n ❌ Error: ${error.message}\n`));
|
|
209
|
+
if (process.env.L4YERCAK3_DEBUG && error.stack) {
|
|
210
|
+
console.error(chalk.gray(error.stack));
|
|
211
|
+
}
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update .env.local file, merging new vars with existing content
|
|
218
|
+
* @param {string} envPath - Path to .env.local
|
|
219
|
+
* @param {Object<string, string>} vars - Key-value pairs to set
|
|
220
|
+
*/
|
|
221
|
+
function updateEnvFile(envPath, vars) {
|
|
222
|
+
let content = '';
|
|
223
|
+
if (fs.existsSync(envPath)) {
|
|
224
|
+
content = fs.readFileSync(envPath, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
228
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
229
|
+
if (regex.test(content)) {
|
|
230
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
231
|
+
} else {
|
|
232
|
+
content = `${content.trimEnd()}${content ? '\n' : ''}${key}=${value}\n`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(envPath, content, 'utf8');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
command: 'connect',
|
|
241
|
+
description: 'Connect your project to the L4YERCAK3 platform',
|
|
242
|
+
handler: handleConnect,
|
|
243
|
+
};
|