@l4yercak3/cli 1.0.3 → 1.0.5

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/bin/cli.js CHANGED
@@ -14,6 +14,8 @@ const loginCommand = require('../src/commands/login');
14
14
  const logoutCommand = require('../src/commands/logout');
15
15
  const statusCommand = require('../src/commands/status');
16
16
  const spreadCommand = require('../src/commands/spread');
17
+ const apiKeysCommand = require('../src/commands/api-keys');
18
+ const upgradeCommand = require('../src/commands/upgrade');
17
19
 
18
20
  // Create CLI program
19
21
  const program = new Command();
@@ -48,6 +50,16 @@ program
48
50
  .description(spreadCommand.description)
49
51
  .action(spreadCommand.handler);
50
52
 
53
+ program
54
+ .command(apiKeysCommand.command)
55
+ .description(apiKeysCommand.description)
56
+ .action(apiKeysCommand.handler);
57
+
58
+ program
59
+ .command(upgradeCommand.command)
60
+ .description(upgradeCommand.description)
61
+ .action(upgradeCommand.handler);
62
+
51
63
  // Show logo and welcome message if no command provided
52
64
  if (process.argv.length === 2) {
53
65
  console.log(''); // initial spacing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l4yercak3/cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {
@@ -37,6 +37,7 @@ class BackendClient {
37
37
 
38
38
  /**
39
39
  * Make API request
40
+ * Returns response data with error details preserved for specific handling
40
41
  */
41
42
  async request(method, endpoint, data = null) {
42
43
  const url = `${this.baseUrl}${endpoint}`;
@@ -54,7 +55,14 @@ class BackendClient {
54
55
  const responseData = await response.json();
55
56
 
56
57
  if (!response.ok) {
57
- throw new Error(responseData.message || `API request failed: ${response.status}`);
58
+ // Create error with additional details from backend
59
+ const error = new Error(responseData.message || responseData.error || `API request failed: ${response.status}`);
60
+ error.code = responseData.code || 'UNKNOWN_ERROR';
61
+ error.suggestion = responseData.suggestion || null;
62
+ error.upgradeUrl = responseData.upgradeUrl || null;
63
+ error.upgradeCommand = responseData.upgradeCommand || null;
64
+ error.status = response.status;
65
+ throw error;
58
66
  }
59
67
 
60
68
  return responseData;
@@ -124,13 +132,19 @@ class BackendClient {
124
132
  }
125
133
  }
126
134
 
135
+ /**
136
+ * List API keys for an organization
137
+ * Returns: { keys, limit, currentCount, canCreateMore, limitDescription }
138
+ */
139
+ async listApiKeys(organizationId) {
140
+ return await this.request('GET', `/api/v1/api-keys/list?organizationId=${organizationId}`);
141
+ }
142
+
127
143
  /**
128
144
  * Generate API key for organization
129
145
  * Note: This calls Convex action directly, requires session
130
146
  */
131
147
  async generateApiKey(organizationId, name, scopes = ['*']) {
132
- // This will need to call Convex action via backend API wrapper
133
- // For now, placeholder
134
148
  return await this.request('POST', `/api/v1/api-keys/generate`, {
135
149
  organizationId,
136
150
  name,
@@ -0,0 +1,119 @@
1
+ /**
2
+ * API Keys Command
3
+ * List and manage API keys for organizations
4
+ */
5
+
6
+ const configManager = require('../config/config-manager');
7
+ const backendClient = require('../api/backend-client');
8
+ const inquirer = require('inquirer');
9
+ const chalk = require('chalk');
10
+
11
+ /**
12
+ * API Keys list command handler
13
+ */
14
+ async function handleApiKeysList() {
15
+ // Check if logged in
16
+ if (!configManager.isLoggedIn()) {
17
+ console.log(chalk.yellow(' ⚠️ You must be logged in first'));
18
+ console.log(chalk.gray('\n Run "l4yercak3 login" to authenticate\n'));
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(chalk.cyan(' 🔑 API Keys\n'));
23
+
24
+ try {
25
+ // Get organizations
26
+ const orgsResponse = await backendClient.getOrganizations();
27
+ const organizations = Array.isArray(orgsResponse)
28
+ ? orgsResponse
29
+ : orgsResponse.organizations || orgsResponse.data || [];
30
+
31
+ if (organizations.length === 0) {
32
+ console.log(chalk.yellow(' ⚠️ No organizations found'));
33
+ console.log(chalk.gray('\n Run "l4yercak3 spread" to create an organization\n'));
34
+ return;
35
+ }
36
+
37
+ // Select organization if multiple
38
+ let organizationId;
39
+ let organizationName;
40
+
41
+ if (organizations.length === 1) {
42
+ organizationId = organizations[0].id;
43
+ organizationName = organizations[0].name;
44
+ } else {
45
+ const { orgChoice } = await inquirer.prompt([
46
+ {
47
+ type: 'list',
48
+ name: 'orgChoice',
49
+ message: 'Select organization:',
50
+ choices: organizations.map(org => ({
51
+ name: `${org.name} (${org.id})`,
52
+ value: org.id,
53
+ })),
54
+ },
55
+ ]);
56
+ organizationId = orgChoice;
57
+ organizationName = organizations.find(org => org.id === orgChoice)?.name;
58
+ }
59
+
60
+ console.log(chalk.gray(` Organization: ${organizationName}\n`));
61
+
62
+ // List API keys
63
+ const keysResponse = await backendClient.listApiKeys(organizationId);
64
+ const keys = keysResponse.keys || [];
65
+
66
+ if (keys.length === 0) {
67
+ console.log(chalk.yellow(' No API keys found for this organization'));
68
+ console.log(chalk.gray('\n Run "l4yercak3 spread" to generate an API key\n'));
69
+ return;
70
+ }
71
+
72
+ // Display keys
73
+ console.log(chalk.green(` Found ${keys.length} API key(s):\n`));
74
+
75
+ keys.forEach((key, i) => {
76
+ const maskedKey = key.key ? `${key.key.substring(0, 15)}...` : '[hidden]';
77
+ const name = key.name || `Key ${i + 1}`;
78
+ const created = key.createdAt ? new Date(key.createdAt).toLocaleDateString() : 'Unknown';
79
+
80
+ console.log(chalk.white(` ${i + 1}. ${name}`));
81
+ console.log(chalk.gray(` Key: ${maskedKey}`));
82
+ console.log(chalk.gray(` Created: ${created}`));
83
+ if (key.scopes) {
84
+ console.log(chalk.gray(` Scopes: ${key.scopes.join(', ')}`));
85
+ }
86
+ console.log('');
87
+ });
88
+
89
+ // Show limit info
90
+ if (keysResponse.limitDescription) {
91
+ console.log(chalk.gray(` Limit: ${keysResponse.limitDescription}`));
92
+ }
93
+ if (keysResponse.canCreateMore !== undefined) {
94
+ if (keysResponse.canCreateMore) {
95
+ console.log(chalk.green(` ✅ You can create more API keys`));
96
+ } else {
97
+ console.log(chalk.yellow(` ⚠️ You've reached your API key limit`));
98
+ console.log(chalk.gray(' Upgrade at: https://app.l4yercak3.com/settings/billing'));
99
+ }
100
+ }
101
+ console.log('');
102
+
103
+ } catch (error) {
104
+ if (error.code === 'SESSION_EXPIRED') {
105
+ console.log(chalk.red(`\n ❌ Session expired. Please run "l4yercak3 login" again.\n`));
106
+ } else if (error.code === 'NOT_AUTHORIZED') {
107
+ console.log(chalk.red(`\n ❌ You don't have permission to view API keys for this organization.\n`));
108
+ } else {
109
+ console.error(chalk.red(` ❌ Error listing API keys: ${error.message}\n`));
110
+ }
111
+ process.exit(1);
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ command: 'api-keys',
117
+ description: 'List API keys for your organizations',
118
+ handler: handleApiKeysList,
119
+ };
@@ -9,6 +9,7 @@ const backendClient = require('../api/backend-client');
9
9
  const chalk = require('chalk');
10
10
  const inquirer = require('inquirer');
11
11
  const projectDetector = require('../detectors');
12
+ const { showLogo } = require('../logo');
12
13
 
13
14
  /**
14
15
  * Generate retro Windows 95 style HTML page
@@ -123,10 +124,19 @@ async function handleLogin() {
123
124
  // Check if already logged in
124
125
  if (configManager.isLoggedIn()) {
125
126
  const session = configManager.getSession();
126
- console.log(chalk.yellow(' ⚠️ You are already logged in'));
127
- console.log(chalk.gray(` Email: ${session.email}`));
128
- console.log(chalk.gray(` Session expires: ${new Date(session.expiresAt).toLocaleString()}`));
129
- console.log(chalk.gray('\n Run "l4yercak3 logout" to log out first\n'));
127
+
128
+ // Show logo
129
+ console.log('');
130
+ showLogo(false);
131
+
132
+ console.log(chalk.green(' ✅ You are already logged in'));
133
+ if (session.email) {
134
+ console.log(chalk.gray(` Email: ${session.email}`));
135
+ }
136
+ console.log(chalk.gray(` Session expires: ${new Date(session.expiresAt).toLocaleString()}\n`));
137
+
138
+ // Still offer the setup wizard
139
+ await promptSetupWizard();
130
140
  return;
131
141
  }
132
142
 
@@ -175,7 +185,11 @@ async function handleLogin() {
175
185
  console.log(chalk.gray(' Session saved, but validation failed. You may need to log in again.'));
176
186
  }
177
187
 
178
- console.log(chalk.green('\n ✅ Successfully logged in!\n'));
188
+ // Show logo after successful login
189
+ console.log('');
190
+ showLogo(false);
191
+
192
+ console.log(chalk.green(' ✅ Successfully logged in!\n'));
179
193
 
180
194
  const finalSession = configManager.getSession();
181
195
  if (finalSession && finalSession.email) {
@@ -20,15 +20,36 @@ async function createOrganization(orgName) {
20
20
  // Handle different response formats
21
21
  const organizationId = newOrg.organizationId || newOrg.id || newOrg.data?.organizationId || newOrg.data?.id;
22
22
  const organizationName = newOrg.name || orgName;
23
-
23
+
24
24
  if (!organizationId) {
25
25
  throw new Error('Organization ID not found in response. Please check backend API endpoint.');
26
26
  }
27
-
27
+
28
28
  console.log(chalk.green(` ✅ Organization created: ${organizationName}\n`));
29
29
  return { organizationId, organizationName };
30
30
  }
31
31
 
32
+ /**
33
+ * Helper function to generate a new API key
34
+ */
35
+ async function generateNewApiKey(organizationId) {
36
+ console.log(chalk.gray(' Generating API key...'));
37
+ const apiKeyResponse = await backendClient.generateApiKey(
38
+ organizationId,
39
+ 'CLI Generated Key',
40
+ ['*']
41
+ );
42
+ // Handle different response formats
43
+ const apiKey = apiKeyResponse.key || apiKeyResponse.apiKey || apiKeyResponse.data?.key || apiKeyResponse.data?.apiKey;
44
+
45
+ if (!apiKey) {
46
+ throw new Error('API key not found in response. Please check backend API endpoint.');
47
+ }
48
+
49
+ console.log(chalk.green(` ✅ API key generated\n`));
50
+ return apiKey;
51
+ }
52
+
32
53
  async function handleSpread() {
33
54
  // Check if logged in
34
55
  if (!configManager.isLoggedIn()) {
@@ -181,28 +202,172 @@ async function handleSpread() {
181
202
  process.exit(1);
182
203
  }
183
204
 
184
- // Step 3: Generate API key
205
+ // Step 3: API Key Setup
185
206
  console.log(chalk.cyan(' 🔑 API Key Setup\n'));
186
207
  let apiKey;
187
208
 
188
209
  try {
189
- console.log(chalk.gray(' Generating API key...'));
190
- const apiKeyResponse = await backendClient.generateApiKey(
191
- organizationId,
192
- 'CLI Generated Key',
193
- ['*']
194
- );
195
- // Handle different response formats
196
- apiKey = apiKeyResponse.key || apiKeyResponse.apiKey || apiKeyResponse.data?.key || apiKeyResponse.data?.apiKey;
197
-
198
- if (!apiKey) {
199
- throw new Error('API key not found in response. Please check backend API endpoint.');
210
+ // First, check if organization already has API keys
211
+ console.log(chalk.gray(' Checking existing API keys...'));
212
+ let existingKeys = null;
213
+
214
+ try {
215
+ existingKeys = await backendClient.listApiKeys(organizationId);
216
+ } catch (listError) {
217
+ // If listing fails, continue to try generating
218
+ console.log(chalk.gray(' Could not check existing keys, attempting to generate...'));
219
+ }
220
+
221
+ if (existingKeys && existingKeys.keys && existingKeys.keys.length > 0) {
222
+ // Organization has existing keys
223
+ console.log(chalk.yellow(` ⚠️ Found ${existingKeys.keys.length} existing API key(s)`));
224
+
225
+ if (existingKeys.limitDescription) {
226
+ console.log(chalk.gray(` Limit: ${existingKeys.limitDescription}`));
227
+ }
228
+
229
+ // Show existing keys (masked)
230
+ existingKeys.keys.forEach((key, i) => {
231
+ const maskedKey = key.key ? `${key.key.substring(0, 10)}...` : key.name || `Key ${i + 1}`;
232
+ console.log(chalk.gray(` • ${key.name || 'Unnamed'}: ${maskedKey}`));
233
+ });
234
+ console.log('');
235
+
236
+ if (!existingKeys.canCreateMore) {
237
+ // At limit - offer to use existing key
238
+ const { useExisting } = await inquirer.prompt([
239
+ {
240
+ type: 'confirm',
241
+ name: 'useExisting',
242
+ message: 'You\'ve reached your API key limit. Use an existing key from your .env.local file?',
243
+ default: true,
244
+ },
245
+ ]);
246
+
247
+ if (useExisting) {
248
+ const { manualKey } = await inquirer.prompt([
249
+ {
250
+ type: 'input',
251
+ name: 'manualKey',
252
+ message: 'Enter your existing API key:',
253
+ validate: (input) => input.trim().length > 0 || 'API key is required',
254
+ },
255
+ ]);
256
+ apiKey = manualKey.trim();
257
+ console.log(chalk.green(` ✅ Using provided API key\n`));
258
+ } else {
259
+ console.log(chalk.yellow('\n ⚠️ To generate more API keys, upgrade your plan at https://app.l4yercak3.com/settings/billing\n'));
260
+ process.exit(0);
261
+ }
262
+ } else {
263
+ // Can create more - ask what to do
264
+ const { keyAction } = await inquirer.prompt([
265
+ {
266
+ type: 'list',
267
+ name: 'keyAction',
268
+ message: 'What would you like to do?',
269
+ choices: [
270
+ { name: 'Generate a new API key', value: 'generate' },
271
+ { name: 'Enter an existing API key', value: 'existing' },
272
+ ],
273
+ },
274
+ ]);
275
+
276
+ if (keyAction === 'existing') {
277
+ const { manualKey } = await inquirer.prompt([
278
+ {
279
+ type: 'input',
280
+ name: 'manualKey',
281
+ message: 'Enter your existing API key:',
282
+ validate: (input) => input.trim().length > 0 || 'API key is required',
283
+ },
284
+ ]);
285
+ apiKey = manualKey.trim();
286
+ console.log(chalk.green(` ✅ Using provided API key\n`));
287
+ } else {
288
+ // Generate new key
289
+ apiKey = await generateNewApiKey(organizationId);
290
+ }
291
+ }
292
+ } else {
293
+ // No existing keys - generate one
294
+ apiKey = await generateNewApiKey(organizationId);
200
295
  }
201
-
202
- console.log(chalk.green(` ✅ API key generated\n`));
203
296
  } catch (error) {
204
- console.error(chalk.red(` ❌ Error generating API key: ${error.message}\n`));
205
- process.exit(1);
297
+ // Handle specific error codes
298
+ if (error.code === 'API_KEY_LIMIT_REACHED') {
299
+ console.log(chalk.yellow(`\n ⚠️ ${error.message}`));
300
+ if (error.suggestion) {
301
+ console.log(chalk.gray(` ${error.suggestion}`));
302
+ }
303
+
304
+ // Show upgrade option if available
305
+ if (error.upgradeUrl) {
306
+ console.log(chalk.cyan(`\n 🚀 Upgrade your plan to get more API keys:`));
307
+ console.log(chalk.gray(` ${error.upgradeUrl}\n`));
308
+
309
+ const { action } = await inquirer.prompt([
310
+ {
311
+ type: 'list',
312
+ name: 'action',
313
+ message: 'What would you like to do?',
314
+ choices: [
315
+ { name: 'Open upgrade page in browser', value: 'upgrade' },
316
+ { name: 'Enter an existing API key', value: 'existing' },
317
+ { name: 'Exit', value: 'exit' },
318
+ ],
319
+ },
320
+ ]);
321
+
322
+ if (action === 'upgrade') {
323
+ const { default: open } = require('open');
324
+ console.log(chalk.gray(' Opening browser...'));
325
+ await open(error.upgradeUrl);
326
+ console.log(chalk.gray('\n After upgrading, run "l4yercak3 spread" again.\n'));
327
+ process.exit(0);
328
+ } else if (action === 'existing') {
329
+ const { manualKey } = await inquirer.prompt([
330
+ {
331
+ type: 'input',
332
+ name: 'manualKey',
333
+ message: 'Enter your existing API key:',
334
+ validate: (input) => input.trim().length > 0 || 'API key is required',
335
+ },
336
+ ]);
337
+ apiKey = manualKey.trim();
338
+ console.log(chalk.green(` ✅ Using provided API key\n`));
339
+ } else {
340
+ process.exit(0);
341
+ }
342
+ } else {
343
+ // No upgrade URL - fallback to manual entry
344
+ console.log(chalk.gray('\n You can enter an existing API key or upgrade your plan.\n'));
345
+
346
+ const { manualKey } = await inquirer.prompt([
347
+ {
348
+ type: 'input',
349
+ name: 'manualKey',
350
+ message: 'Enter your existing API key (or press Enter to exit):',
351
+ },
352
+ ]);
353
+
354
+ if (manualKey.trim()) {
355
+ apiKey = manualKey.trim();
356
+ console.log(chalk.green(` ✅ Using provided API key\n`));
357
+ } else {
358
+ process.exit(0);
359
+ }
360
+ }
361
+ } else if (error.code === 'SESSION_EXPIRED') {
362
+ console.log(chalk.red(`\n ❌ Session expired. Please run "l4yercak3 login" again.\n`));
363
+ process.exit(1);
364
+ } else if (error.code === 'NOT_AUTHORIZED') {
365
+ console.log(chalk.red(`\n ❌ You don't have permission to manage API keys for this organization.\n`));
366
+ process.exit(1);
367
+ } else {
368
+ console.error(chalk.red(` ❌ Error setting up API key: ${error.message}\n`));
369
+ process.exit(1);
370
+ }
206
371
  }
207
372
 
208
373
  // Step 4: Feature selection
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Upgrade Command
3
+ * Opens the upgrade page in the browser for plan upgrades
4
+ */
5
+
6
+ const configManager = require('../config/config-manager');
7
+ const chalk = require('chalk');
8
+ const { default: open } = require('open');
9
+
10
+ /**
11
+ * Upgrade command handler
12
+ */
13
+ async function handleUpgrade() {
14
+ // Check if logged in
15
+ if (!configManager.isLoggedIn()) {
16
+ console.log(chalk.yellow(' ⚠️ You must be logged in first'));
17
+ console.log(chalk.gray('\n Run "l4yercak3 login" to authenticate\n'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const session = configManager.getSession();
22
+ const backendUrl = configManager.getBackendUrl();
23
+
24
+ console.log(chalk.cyan(' 🚀 Opening upgrade page...\n'));
25
+
26
+ // Build upgrade URL with CLI token for seamless authentication
27
+ const upgradeUrl = `${backendUrl}/upgrade?token=${encodeURIComponent(session.token)}&reason=cli_upgrade`;
28
+
29
+ console.log(chalk.gray(` URL: ${upgradeUrl}\n`));
30
+
31
+ try {
32
+ await open(upgradeUrl);
33
+ console.log(chalk.green(' ✅ Upgrade page opened in your browser\n'));
34
+ console.log(chalk.gray(' Select a plan to unlock more features:'));
35
+ console.log(chalk.gray(' • More API keys'));
36
+ console.log(chalk.gray(' • Priority support'));
37
+ console.log(chalk.gray(' • Advanced features\n'));
38
+ } catch (error) {
39
+ console.log(chalk.yellow(' ⚠️ Could not open browser automatically'));
40
+ console.log(chalk.gray(`\n Please visit: ${upgradeUrl}\n`));
41
+ }
42
+ }
43
+
44
+ module.exports = {
45
+ command: 'upgrade',
46
+ description: 'Upgrade your L4YERCAK3 plan',
47
+ handler: handleUpgrade,
48
+ };
@@ -16,6 +16,9 @@ jest.mock('../../src/detectors', () => ({
16
16
  projectPath: '/test/path',
17
17
  }),
18
18
  }));
19
+ jest.mock('../../src/logo', () => ({
20
+ showLogo: jest.fn(),
21
+ }));
19
22
  jest.mock('chalk', () => ({
20
23
  cyan: (str) => str,
21
24
  yellow: (str) => str,
@@ -77,7 +80,7 @@ describe('Login Command', () => {
77
80
  });
78
81
 
79
82
  describe('handler - already logged in', () => {
80
- it('shows warning when already logged in', async () => {
83
+ it('shows success message when already logged in', async () => {
81
84
  configManager.isLoggedIn.mockReturnValue(true);
82
85
  configManager.getSession.mockReturnValue({
83
86
  email: 'user@example.com',
@@ -91,7 +94,7 @@ describe('Login Command', () => {
91
94
  expect(open).not.toHaveBeenCalled();
92
95
  });
93
96
 
94
- it('suggests logout when already logged in', async () => {
97
+ it('shows session info and offers setup wizard when already logged in', async () => {
95
98
  configManager.isLoggedIn.mockReturnValue(true);
96
99
  configManager.getSession.mockReturnValue({
97
100
  email: 'user@example.com',
@@ -100,7 +103,8 @@ describe('Login Command', () => {
100
103
 
101
104
  await loginCommand.handler();
102
105
 
103
- expect(consoleOutput.some((line) => line.includes('logout'))).toBe(true);
106
+ // Should show "What's Next" since we're not in a project (mocked)
107
+ expect(consoleOutput.some((line) => line.includes("What's Next"))).toBe(true);
104
108
  });
105
109
  });
106
110