@l4yercak3/cli 1.1.1 → 1.1.3
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/docs/microsass_production_machine/CLI_API_REFERENCE.md +1 -1
- package/package.json +1 -1
- package/src/api/backend-client.js +26 -9
- package/src/commands/spread.js +2 -18
- package/src/config/config-manager.js +4 -3
- package/tests/backend-client.test.js +59 -19
- package/tests/config-manager.test.js +2 -2
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
|
@@ -411,24 +411,8 @@ async function handleSpread() {
|
|
|
411
411
|
oauthProviders = providers;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
// Step 6: Backend URL
|
|
415
|
-
const
|
|
416
|
-
const { backendUrl } = await inquirer.prompt([
|
|
417
|
-
{
|
|
418
|
-
type: 'input',
|
|
419
|
-
name: 'backendUrl',
|
|
420
|
-
message: 'Backend API URL:',
|
|
421
|
-
default: defaultBackendUrl,
|
|
422
|
-
validate: (input) => {
|
|
423
|
-
try {
|
|
424
|
-
new URL(input);
|
|
425
|
-
return true;
|
|
426
|
-
} catch {
|
|
427
|
-
return 'Please enter a valid URL';
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
]);
|
|
414
|
+
// Step 6: Backend URL (fixed to Convex HTTP endpoint)
|
|
415
|
+
const backendUrl = 'https://aromatic-akita-723.convex.site';
|
|
432
416
|
|
|
433
417
|
// Step 7: Production domain (for OAuth redirect URIs)
|
|
434
418
|
let productionDomain = null;
|
|
@@ -27,13 +27,13 @@ class ConfigManager {
|
|
|
27
27
|
*/
|
|
28
28
|
getConfig() {
|
|
29
29
|
this.ensureConfigDir();
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
if (!fs.existsSync(this.configFile)) {
|
|
32
32
|
return {
|
|
33
33
|
session: null,
|
|
34
34
|
organizations: [],
|
|
35
35
|
settings: {
|
|
36
|
-
backendUrl: process.env.L4YERCAK3_BACKEND_URL || 'https://
|
|
36
|
+
backendUrl: process.env.L4YERCAK3_BACKEND_URL || 'https://aromatic-akita-723.convex.site',
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
}
|
|
@@ -115,10 +115,11 @@ class ConfigManager {
|
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Get backend URL from config or env
|
|
118
|
+
* This is the Convex HTTP URL for all API calls
|
|
118
119
|
*/
|
|
119
120
|
getBackendUrl() {
|
|
120
121
|
const config = this.getConfig();
|
|
121
|
-
return config.settings?.backendUrl || process.env.L4YERCAK3_BACKEND_URL || 'https://
|
|
122
|
+
return config.settings?.backendUrl || process.env.L4YERCAK3_BACKEND_URL || 'https://aromatic-akita-723.convex.site';
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/**
|
|
@@ -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
|
});
|
|
@@ -36,7 +36,7 @@ describe('ConfigManager', () => {
|
|
|
36
36
|
session: null,
|
|
37
37
|
organizations: [],
|
|
38
38
|
settings: {
|
|
39
|
-
backendUrl: 'https://
|
|
39
|
+
backendUrl: 'https://aromatic-akita-723.convex.site',
|
|
40
40
|
},
|
|
41
41
|
});
|
|
42
42
|
});
|
|
@@ -204,7 +204,7 @@ describe('ConfigManager', () => {
|
|
|
204
204
|
|
|
205
205
|
const url = ConfigManager.getBackendUrl();
|
|
206
206
|
|
|
207
|
-
expect(url).toBe('https://
|
|
207
|
+
expect(url).toBe('https://aromatic-akita-723.convex.site');
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
it('returns configured URL from settings', () => {
|