@l4yercak3/cli 1.1.0 → 1.1.2
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/package.json +1 -1
- package/src/api/backend-client.js +26 -9
- package/src/commands/spread.js +10 -2
- package/tests/backend-client.test.js +59 -19
package/package.json
CHANGED
|
@@ -7,9 +7,16 @@ const crypto = require('crypto');
|
|
|
7
7
|
const fetch = require('node-fetch');
|
|
8
8
|
const configManager = require('../config/config-manager');
|
|
9
9
|
|
|
10
|
+
// API Base URL - All CLI API endpoints go through Convex HTTP
|
|
11
|
+
const API_BASE_URL = 'https://aromatic-akita-723.convex.site';
|
|
12
|
+
|
|
13
|
+
// App URL - Only used for browser login page (Next.js serves the OAuth UI)
|
|
14
|
+
const APP_URL = 'https://app.l4yercak3.com';
|
|
15
|
+
|
|
10
16
|
class BackendClient {
|
|
11
17
|
constructor() {
|
|
12
|
-
this.baseUrl =
|
|
18
|
+
this.baseUrl = API_BASE_URL;
|
|
19
|
+
this.appUrl = APP_URL;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
@@ -37,6 +44,7 @@ class BackendClient {
|
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* Make API request
|
|
47
|
+
* All CLI API endpoints go through the Convex HTTP base URL
|
|
40
48
|
* Returns response data with error details preserved for specific handling
|
|
41
49
|
*/
|
|
42
50
|
async request(method, endpoint, data = null) {
|
|
@@ -115,20 +123,20 @@ class BackendClient {
|
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
125
|
* Get CLI login URL with state parameter for CSRF protection
|
|
126
|
+
* Browser login is served by Next.js at APP_URL (not the Convex API URL)
|
|
118
127
|
* @param {string} state - The state token generated by the CLI
|
|
119
128
|
* @param {string|null} provider - Optional OAuth provider for direct auth
|
|
120
129
|
* @returns {string} The login URL
|
|
121
130
|
*/
|
|
122
131
|
getLoginUrl(state, provider = null) {
|
|
123
|
-
const backendUrl = configManager.getBackendUrl();
|
|
124
132
|
const callbackUrl = 'http://localhost:3000/callback';
|
|
125
133
|
|
|
126
134
|
if (provider) {
|
|
127
135
|
// Direct OAuth provider URL
|
|
128
|
-
return `${
|
|
136
|
+
return `${this.appUrl}/api/auth/oauth-signup?provider=${provider}&sessionType=cli&state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
|
|
129
137
|
} else {
|
|
130
138
|
// Provider selection page
|
|
131
|
-
return `${
|
|
139
|
+
return `${this.appUrl}/auth/cli-login?state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -137,15 +145,14 @@ class BackendClient {
|
|
|
137
145
|
* Returns: { keys, limit, currentCount, canCreateMore, limitDescription }
|
|
138
146
|
*/
|
|
139
147
|
async listApiKeys(organizationId) {
|
|
140
|
-
return await this.request('GET', `/api/v1/api-keys
|
|
148
|
+
return await this.request('GET', `/api/v1/auth/cli/api-keys?organizationId=${organizationId}`);
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/**
|
|
144
152
|
* Generate API key for organization
|
|
145
|
-
* Note: This calls Convex action directly, requires session
|
|
146
153
|
*/
|
|
147
154
|
async generateApiKey(organizationId, name, scopes = ['*']) {
|
|
148
|
-
return await this.request('POST', `/api/v1/api-keys
|
|
155
|
+
return await this.request('POST', `/api/v1/auth/cli/api-keys`, {
|
|
149
156
|
organizationId,
|
|
150
157
|
name,
|
|
151
158
|
scopes,
|
|
@@ -156,14 +163,14 @@ class BackendClient {
|
|
|
156
163
|
* Get organizations for current user
|
|
157
164
|
*/
|
|
158
165
|
async getOrganizations() {
|
|
159
|
-
return await this.request('GET', '/api/v1/organizations');
|
|
166
|
+
return await this.request('GET', '/api/v1/auth/cli/organizations');
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
/**
|
|
163
170
|
* Create organization
|
|
164
171
|
*/
|
|
165
172
|
async createOrganization(name) {
|
|
166
|
-
return await this.request('POST', '/api/v1/organizations', {
|
|
173
|
+
return await this.request('POST', '/api/v1/auth/cli/organizations', {
|
|
167
174
|
name,
|
|
168
175
|
});
|
|
169
176
|
}
|
|
@@ -229,6 +236,16 @@ class BackendClient {
|
|
|
229
236
|
async listApplications(organizationId) {
|
|
230
237
|
return await this.request('GET', `/api/v1/cli/applications?organizationId=${organizationId}`);
|
|
231
238
|
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sync application data
|
|
242
|
+
* @param {string} applicationId - The application ID
|
|
243
|
+
* @param {object} syncData - Sync configuration (direction, models)
|
|
244
|
+
* @returns {Promise<object>}
|
|
245
|
+
*/
|
|
246
|
+
async syncApplication(applicationId, syncData) {
|
|
247
|
+
return await this.request('POST', `/api/v1/cli/applications/${applicationId}/sync`, syncData);
|
|
248
|
+
}
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
module.exports = new BackendClient();
|
package/src/commands/spread.js
CHANGED
|
@@ -592,7 +592,8 @@ async function handleSpread() {
|
|
|
592
592
|
} catch (regError) {
|
|
593
593
|
// Registration failed but files were generated - warn but don't fail
|
|
594
594
|
console.log(chalk.yellow(` ⚠️ Could not register with backend: ${regError.message}`));
|
|
595
|
-
console.log(chalk.gray(' Your files were generated.
|
|
595
|
+
console.log(chalk.gray(' Your files were generated successfully.'));
|
|
596
|
+
console.log(chalk.gray(' Backend registration will be available in a future update.\n'));
|
|
596
597
|
}
|
|
597
598
|
|
|
598
599
|
// Save project configuration
|
|
@@ -614,7 +615,14 @@ async function handleSpread() {
|
|
|
614
615
|
configManager.saveProjectConfig(detection.projectPath, projectConfig);
|
|
615
616
|
console.log(chalk.gray(` 📝 Configuration saved to ~/.l4yercak3/config.json\n`));
|
|
616
617
|
|
|
617
|
-
|
|
618
|
+
// Show appropriate completion message based on registration status
|
|
619
|
+
if (applicationId) {
|
|
620
|
+
console.log(chalk.cyan('\n 🎉 Setup complete!\n'));
|
|
621
|
+
} else {
|
|
622
|
+
console.log(chalk.cyan('\n 🎉 Local setup complete!\n'));
|
|
623
|
+
console.log(chalk.yellow(' ⚠️ Note: Backend registration pending - your app works locally but'));
|
|
624
|
+
console.log(chalk.yellow(' won\'t appear in the L4YERCAK3 dashboard until endpoints are available.\n'));
|
|
625
|
+
}
|
|
618
626
|
|
|
619
627
|
if (features.includes('oauth')) {
|
|
620
628
|
console.log(chalk.yellow(' 📋 Next steps:\n'));
|
|
@@ -14,13 +14,19 @@ configManager.getBackendUrl.mockReturnValue('https://backend.test.com');
|
|
|
14
14
|
// Need to require after mocking
|
|
15
15
|
const BackendClient = require('../src/api/backend-client');
|
|
16
16
|
|
|
17
|
+
// API Base URL for all CLI endpoints (Convex HTTP)
|
|
18
|
+
const API_BASE_URL = 'https://aromatic-akita-723.convex.site';
|
|
19
|
+
// App URL only for browser login
|
|
20
|
+
const APP_URL = 'https://app.l4yercak3.com';
|
|
21
|
+
|
|
17
22
|
describe('BackendClient', () => {
|
|
18
23
|
beforeEach(() => {
|
|
19
24
|
jest.clearAllMocks();
|
|
20
|
-
configManager.getBackendUrl.mockReturnValue(
|
|
25
|
+
configManager.getBackendUrl.mockReturnValue(API_BASE_URL);
|
|
21
26
|
configManager.getSession.mockReturnValue(null);
|
|
22
|
-
// Reset
|
|
23
|
-
BackendClient.baseUrl =
|
|
27
|
+
// Reset URLs since the module was already instantiated
|
|
28
|
+
BackendClient.baseUrl = API_BASE_URL;
|
|
29
|
+
BackendClient.appUrl = APP_URL;
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
describe('getHeaders', () => {
|
|
@@ -61,7 +67,7 @@ describe('BackendClient', () => {
|
|
|
61
67
|
const result = await BackendClient.request('GET', '/api/test');
|
|
62
68
|
|
|
63
69
|
expect(fetch).toHaveBeenCalledWith(
|
|
64
|
-
|
|
70
|
+
`${API_BASE_URL}/api/test`,
|
|
65
71
|
expect.objectContaining({
|
|
66
72
|
method: 'GET',
|
|
67
73
|
})
|
|
@@ -79,7 +85,7 @@ describe('BackendClient', () => {
|
|
|
79
85
|
await BackendClient.request('POST', '/api/test', { name: 'test' });
|
|
80
86
|
|
|
81
87
|
expect(fetch).toHaveBeenCalledWith(
|
|
82
|
-
|
|
88
|
+
`${API_BASE_URL}/api/test`,
|
|
83
89
|
expect.objectContaining({
|
|
84
90
|
method: 'POST',
|
|
85
91
|
body: JSON.stringify({ name: 'test' }),
|
|
@@ -262,7 +268,8 @@ describe('BackendClient', () => {
|
|
|
262
268
|
const state = 'test-state-token';
|
|
263
269
|
const url = BackendClient.getLoginUrl(state);
|
|
264
270
|
|
|
265
|
-
|
|
271
|
+
// Login URL uses APP_URL (Next.js), not API_BASE_URL
|
|
272
|
+
expect(url).toContain(APP_URL);
|
|
266
273
|
expect(url).toContain('/auth/cli-login');
|
|
267
274
|
expect(url).toContain('state=test-state-token');
|
|
268
275
|
expect(url).toContain('callback=');
|
|
@@ -272,6 +279,7 @@ describe('BackendClient', () => {
|
|
|
272
279
|
const state = 'test-state-token';
|
|
273
280
|
const url = BackendClient.getLoginUrl(state, 'google');
|
|
274
281
|
|
|
282
|
+
expect(url).toContain(APP_URL);
|
|
275
283
|
expect(url).toContain('/api/auth/oauth-signup');
|
|
276
284
|
expect(url).toContain('provider=google');
|
|
277
285
|
expect(url).toContain('sessionType=cli');
|
|
@@ -300,7 +308,7 @@ describe('BackendClient', () => {
|
|
|
300
308
|
const result = await BackendClient.generateApiKey('org-123', 'My Key', ['read', 'write']);
|
|
301
309
|
|
|
302
310
|
expect(fetch).toHaveBeenCalledWith(
|
|
303
|
-
expect.stringContaining('/api/v1/api-keys
|
|
311
|
+
expect.stringContaining('/api/v1/auth/cli/api-keys'),
|
|
304
312
|
expect.objectContaining({
|
|
305
313
|
method: 'POST',
|
|
306
314
|
body: JSON.stringify({
|
|
@@ -348,7 +356,7 @@ describe('BackendClient', () => {
|
|
|
348
356
|
const result = await BackendClient.getOrganizations();
|
|
349
357
|
|
|
350
358
|
expect(fetch).toHaveBeenCalledWith(
|
|
351
|
-
expect.stringContaining('/api/v1/organizations'),
|
|
359
|
+
expect.stringContaining('/api/v1/auth/cli/organizations'),
|
|
352
360
|
expect.objectContaining({ method: 'GET' })
|
|
353
361
|
);
|
|
354
362
|
expect(result.organizations).toHaveLength(1);
|
|
@@ -369,7 +377,7 @@ describe('BackendClient', () => {
|
|
|
369
377
|
const result = await BackendClient.createOrganization('New Org');
|
|
370
378
|
|
|
371
379
|
expect(fetch).toHaveBeenCalledWith(
|
|
372
|
-
expect.stringContaining('/api/v1/organizations'),
|
|
380
|
+
expect.stringContaining('/api/v1/auth/cli/organizations'),
|
|
373
381
|
expect.objectContaining({
|
|
374
382
|
method: 'POST',
|
|
375
383
|
body: JSON.stringify({ name: 'New Org' }),
|
|
@@ -380,7 +388,7 @@ describe('BackendClient', () => {
|
|
|
380
388
|
});
|
|
381
389
|
|
|
382
390
|
// ============================================
|
|
383
|
-
// Connected Applications API Tests
|
|
391
|
+
// Connected Applications API Tests (Convex HTTP)
|
|
384
392
|
// ============================================
|
|
385
393
|
|
|
386
394
|
describe('checkExistingApplication', () => {
|
|
@@ -399,8 +407,9 @@ describe('BackendClient', () => {
|
|
|
399
407
|
|
|
400
408
|
const result = await BackendClient.checkExistingApplication('org-123', 'hash123');
|
|
401
409
|
|
|
410
|
+
// Should use Convex URL, not main backend
|
|
402
411
|
expect(fetch).toHaveBeenCalledWith(
|
|
403
|
-
|
|
412
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications/by-path?organizationId=org-123&hash=hash123',
|
|
404
413
|
expect.objectContaining({ method: 'GET' })
|
|
405
414
|
);
|
|
406
415
|
expect(result.found).toBe(true);
|
|
@@ -434,7 +443,7 @@ describe('BackendClient', () => {
|
|
|
434
443
|
});
|
|
435
444
|
|
|
436
445
|
describe('registerApplication', () => {
|
|
437
|
-
it('registers new application', async () => {
|
|
446
|
+
it('registers new application via Convex URL', async () => {
|
|
438
447
|
const mockResponse = {
|
|
439
448
|
ok: true,
|
|
440
449
|
json: jest.fn().mockResolvedValue({
|
|
@@ -459,8 +468,9 @@ describe('BackendClient', () => {
|
|
|
459
468
|
|
|
460
469
|
const result = await BackendClient.registerApplication(registrationData);
|
|
461
470
|
|
|
471
|
+
// Should use Convex URL, not main backend
|
|
462
472
|
expect(fetch).toHaveBeenCalledWith(
|
|
463
|
-
|
|
473
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications',
|
|
464
474
|
expect.objectContaining({
|
|
465
475
|
method: 'POST',
|
|
466
476
|
body: JSON.stringify(registrationData),
|
|
@@ -471,7 +481,7 @@ describe('BackendClient', () => {
|
|
|
471
481
|
});
|
|
472
482
|
|
|
473
483
|
describe('updateApplication', () => {
|
|
474
|
-
it('updates existing application', async () => {
|
|
484
|
+
it('updates existing application via Convex URL', async () => {
|
|
475
485
|
const mockResponse = {
|
|
476
486
|
ok: true,
|
|
477
487
|
json: jest.fn().mockResolvedValue({
|
|
@@ -490,7 +500,7 @@ describe('BackendClient', () => {
|
|
|
490
500
|
const result = await BackendClient.updateApplication('app-123', updates);
|
|
491
501
|
|
|
492
502
|
expect(fetch).toHaveBeenCalledWith(
|
|
493
|
-
|
|
503
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications/app-123',
|
|
494
504
|
expect.objectContaining({
|
|
495
505
|
method: 'PATCH',
|
|
496
506
|
body: JSON.stringify(updates),
|
|
@@ -501,7 +511,7 @@ describe('BackendClient', () => {
|
|
|
501
511
|
});
|
|
502
512
|
|
|
503
513
|
describe('getApplication', () => {
|
|
504
|
-
it('fetches application details', async () => {
|
|
514
|
+
it('fetches application details via Convex URL', async () => {
|
|
505
515
|
const mockResponse = {
|
|
506
516
|
ok: true,
|
|
507
517
|
json: jest.fn().mockResolvedValue({
|
|
@@ -515,7 +525,7 @@ describe('BackendClient', () => {
|
|
|
515
525
|
const result = await BackendClient.getApplication('app-123');
|
|
516
526
|
|
|
517
527
|
expect(fetch).toHaveBeenCalledWith(
|
|
518
|
-
|
|
528
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications/app-123',
|
|
519
529
|
expect.objectContaining({ method: 'GET' })
|
|
520
530
|
);
|
|
521
531
|
expect(result.id).toBe('app-123');
|
|
@@ -524,7 +534,7 @@ describe('BackendClient', () => {
|
|
|
524
534
|
});
|
|
525
535
|
|
|
526
536
|
describe('listApplications', () => {
|
|
527
|
-
it('lists applications for organization', async () => {
|
|
537
|
+
it('lists applications for organization via Convex URL', async () => {
|
|
528
538
|
const mockResponse = {
|
|
529
539
|
ok: true,
|
|
530
540
|
json: jest.fn().mockResolvedValue({
|
|
@@ -539,10 +549,40 @@ describe('BackendClient', () => {
|
|
|
539
549
|
const result = await BackendClient.listApplications('org-123');
|
|
540
550
|
|
|
541
551
|
expect(fetch).toHaveBeenCalledWith(
|
|
542
|
-
|
|
552
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications?organizationId=org-123',
|
|
543
553
|
expect.objectContaining({ method: 'GET' })
|
|
544
554
|
);
|
|
545
555
|
expect(result.applications).toHaveLength(2);
|
|
546
556
|
});
|
|
547
557
|
});
|
|
558
|
+
|
|
559
|
+
describe('syncApplication', () => {
|
|
560
|
+
it('syncs application data via Convex URL', async () => {
|
|
561
|
+
const mockResponse = {
|
|
562
|
+
ok: true,
|
|
563
|
+
json: jest.fn().mockResolvedValue({
|
|
564
|
+
success: true,
|
|
565
|
+
syncedRecords: 42,
|
|
566
|
+
}),
|
|
567
|
+
};
|
|
568
|
+
fetch.mockResolvedValue(mockResponse);
|
|
569
|
+
|
|
570
|
+
const syncData = {
|
|
571
|
+
direction: 'bidirectional',
|
|
572
|
+
models: ['contacts', 'organizations'],
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const result = await BackendClient.syncApplication('app-123', syncData);
|
|
576
|
+
|
|
577
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
578
|
+
'https://aromatic-akita-723.convex.site/api/v1/cli/applications/app-123/sync',
|
|
579
|
+
expect.objectContaining({
|
|
580
|
+
method: 'POST',
|
|
581
|
+
body: JSON.stringify(syncData),
|
|
582
|
+
})
|
|
583
|
+
);
|
|
584
|
+
expect(result.success).toBe(true);
|
|
585
|
+
expect(result.syncedRecords).toBe(42);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
548
588
|
});
|