@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l4yercak3/cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Icing on the L4yercak3 - The sweet finishing touch for your Layer Cake integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 = configManager.getBackendUrl();
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 `${backendUrl}/api/auth/oauth-signup?provider=${provider}&sessionType=cli&state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
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 `${backendUrl}/auth/cli-login?state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
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/list?organizationId=${organizationId}`);
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/generate`, {
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();
@@ -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. Registration can be retried later.\n'));
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
- console.log(chalk.cyan('\n 🎉 Setup complete!\n'));
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('https://backend.test.com');
25
+ configManager.getBackendUrl.mockReturnValue(API_BASE_URL);
21
26
  configManager.getSession.mockReturnValue(null);
22
- // Reset baseUrl since the module was already instantiated
23
- BackendClient.baseUrl = 'https://backend.test.com';
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
- 'https://backend.test.com/api/test',
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
- 'https://backend.test.com/api/test',
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
- expect(url).toContain('https://backend.test.com');
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/generate'),
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
- expect.stringContaining('/api/v1/cli/applications/by-path?organizationId=org-123&hash=hash123'),
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
- expect.stringContaining('/api/v1/cli/applications'),
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
- expect.stringContaining('/api/v1/cli/applications/app-123'),
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
- expect.stringContaining('/api/v1/cli/applications/app-123'),
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
- expect.stringContaining('/api/v1/cli/applications?organizationId=org-123'),
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
  });