langctl 0.1.2 → 0.3.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +1 -1
  3. package/README.md +159 -1151
  4. package/dist/commands/auth.js +82 -24
  5. package/dist/commands/config.js +36 -25
  6. package/dist/commands/export.js +11 -112
  7. package/dist/commands/import.js +32 -164
  8. package/dist/commands/init.js +59 -80
  9. package/dist/commands/keys.js +145 -381
  10. package/dist/commands/org.js +45 -140
  11. package/dist/commands/projects.js +125 -390
  12. package/dist/commands/pull.js +78 -183
  13. package/dist/commands/push.js +125 -0
  14. package/dist/commands/team.js +68 -272
  15. package/dist/core/config.js +130 -0
  16. package/dist/core/errors.js +67 -0
  17. package/dist/core/files.js +38 -0
  18. package/dist/core/http.js +121 -0
  19. package/dist/core/output.js +96 -0
  20. package/dist/core/project.js +58 -0
  21. package/dist/core/prompts.js +56 -0
  22. package/dist/formats/index.js +285 -0
  23. package/dist/index.js +261 -508
  24. package/dist/version.js +4 -0
  25. package/package.json +22 -16
  26. package/.gitattributes +0 -2
  27. package/NEXT_PHASE.md +0 -618
  28. package/WORKING_INSTRUCTIONS.md +0 -1389
  29. package/dist/auth.d.ts +0 -37
  30. package/dist/auth.js +0 -112
  31. package/dist/commands/auth.d.ts +0 -3
  32. package/dist/commands/config.d.ts +0 -2
  33. package/dist/commands/debug.d.ts +0 -3
  34. package/dist/commands/debug.js +0 -57
  35. package/dist/commands/export.d.ts +0 -14
  36. package/dist/commands/import.d.ts +0 -11
  37. package/dist/commands/init.d.ts +0 -2
  38. package/dist/commands/keys.d.ts +0 -25
  39. package/dist/commands/org.d.ts +0 -13
  40. package/dist/commands/projects.d.ts +0 -30
  41. package/dist/commands/pull.d.ts +0 -9
  42. package/dist/commands/team.d.ts +0 -29
  43. package/dist/config.d.ts +0 -47
  44. package/dist/config.js +0 -69
  45. package/dist/exporters/index.d.ts +0 -36
  46. package/dist/exporters/index.js +0 -214
  47. package/dist/index.d.ts +0 -3
  48. package/dist/supabase.d.ts +0 -14
  49. package/dist/supabase.js +0 -30
  50. package/dist/utils/banner.d.ts +0 -9
  51. package/dist/utils/banner.js +0 -24
  52. package/translations/en.json +0 -5
  53. package/translations/en.xml +0 -9
  54. package/translations/fr.json +0 -4
  55. package/translations/fr.xml +0 -7
  56. package/translations/hi.json +0 -4
  57. package/translations/hi.xml +0 -7
  58. package/translations/ios/en.strings +0 -9
  59. package/translations/ios/fr.strings +0 -6
  60. package/translations/ios/hi.strings +0 -6
  61. package/translations/ios/ja.strings +0 -6
  62. package/translations/ja.json +0 -4
  63. package/translations/ja.xml +0 -7
@@ -1,1389 +0,0 @@
1
- # Langctl CLI - Internal Working Instructions
2
-
3
- **For Developers & AI Agents**
4
-
5
- This document explains the internal architecture and working of the Langctl CLI. Use this to understand how each component works, how to extend functionality, and troubleshoot issues.
6
-
7
- ---
8
-
9
- ## Table of Contents
10
-
11
- 1. [Architecture Overview](#architecture-overview)
12
- 2. [Authentication System](#authentication-system)
13
- 3. [Edge Functions Integration](#edge-functions-integration)
14
- 4. [Command Structure](#command-structure)
15
- 5. [File Organization](#file-organization)
16
- 6. [How Each Command Works](#how-each-command-works)
17
- 7. [Configuration Management](#configuration-management)
18
- 8. [Error Handling](#error-handling)
19
- 9. [Testing & Deployment](#testing--deployment)
20
- 10. [Adding New Commands](#adding-new-commands)
21
-
22
- ---
23
-
24
- ## Architecture Overview
25
-
26
- ### Core Principles
27
-
28
- 1. **CLI-First Design**: Never expose Supabase credentials to users
29
- 2. **Edge Function Gateway**: All operations go through Supabase Edge Functions
30
- 3. **API Key Authentication**: Users authenticate with organization-scoped API keys
31
- 4. **Stateless Operations**: Each command is independent and self-contained
32
-
33
- ### Technology Stack
34
-
35
- - **Commander.js**: CLI framework for command parsing and routing
36
- - **Chalk**: Terminal string styling and colors
37
- - **Ora**: Loading spinners for async operations
38
- - **Node-fetch**: HTTP requests to Edge Functions
39
- - **TypeScript**: Type safety and better DX
40
-
41
- ### Data Flow
42
-
43
- ```
44
- User Command
45
- ↓
46
- Commander.js (index.ts)
47
- ↓
48
- Command Handler (commands/*.ts)
49
- ↓
50
- Authentication Check (auth.ts)
51
- ↓
52
- Edge Function Request (fetch)
53
- ↓
54
- Edge Function (Supabase)
55
- ↓
56
- Database Query/Mutation
57
- ↓
58
- Response to CLI
59
- ↓
60
- Formatted Output (chalk)
61
- ```
62
-
63
- ---
64
-
65
- ## Authentication System
66
-
67
- ### File: `src/auth.ts`
68
-
69
- #### How API Keys Work
70
-
71
- 1. **Format**: `lc_[64 hex characters]` (67 chars total)
72
- 2. **Storage**: SHA-256 hash stored in database
73
- 3. **Validation**: Keys validated via `verify-api-key` Edge Function
74
- 4. **Local Storage**: Plain text key stored in `~/.langctl/config.json`
75
-
76
- #### Key Functions
77
-
78
- ##### `sanitizeApiKey(input: string)`
79
- ```typescript
80
- // Purpose: Validate and normalize API key format
81
- // Input: User-provided API key (may have extra spaces, wrong case)
82
- // Output: { ok: boolean, key?: string, message?: string }
83
- // Logic:
84
- // 1. Trim and lowercase
85
- // 2. Check 'lc_' prefix
86
- // 3. Extract hex part (everything after 'lc_')
87
- // 4. Remove non-hex characters
88
- // 5. Verify exactly 64 hex characters
89
- ```
90
-
91
- ##### `verifyApiKey(plainTextKey: string)`
92
- ```typescript
93
- // Purpose: Verify API key with server
94
- // Calls: verify-api-key Edge Function
95
- // Returns: ApiKeyData | null
96
- // Process:
97
- // 1. Sanitize input key
98
- // 2. POST to verify-api-key function
99
- // 3. Parse response (organizationId, name, plan)
100
- // 4. Return null if invalid
101
- ```
102
-
103
- ##### `authenticate(apiKey: string)`
104
- ```typescript
105
- // Purpose: Full authentication flow
106
- // Process:
107
- // 1. Sanitize key
108
- // 2. Verify with server
109
- // 3. Save to config.json
110
- // 4. Return success/failure
111
- // Side Effects: Writes to ~/.langctl/config.json
112
- ```
113
-
114
- ##### `isAuthenticated()`
115
- ```typescript
116
- // Purpose: Check if user has valid stored credentials
117
- // Logic: Checks if apiKey and organizationId exist in config
118
- // Note: Does NOT verify key validity with server
119
- ```
120
-
121
- ##### `getApiKey()` / `getOrganizationId()`
122
- ```typescript
123
- // Purpose: Retrieve stored credentials
124
- // Returns: string | undefined
125
- ```
126
-
127
- ##### `logout()`
128
- ```typescript
129
- // Purpose: Clear all stored credentials
130
- // Side Effects: Deletes apiKey, organizationId, organizationName, defaultProject from config
131
- ```
132
-
133
- ---
134
-
135
- ## Edge Functions Integration
136
-
137
- ### Base URL
138
- All Edge Functions are at: `https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/`
139
-
140
- ### Authentication Pattern
141
-
142
- Every Edge Function follows this pattern:
143
-
144
- ```typescript
145
- const response = await fetch(EDGE_FUNCTION_URL, {
146
- method: 'POST',
147
- headers: {
148
- 'Content-Type': 'application/json',
149
- 'X-API-Key': apiKey // User's API key
150
- },
151
- body: JSON.stringify({
152
- action: 'action-name',
153
- // ... other parameters
154
- })
155
- });
156
- ```
157
-
158
- ### Edge Functions List
159
-
160
- #### 1. `verify-api-key`
161
- - **Purpose**: Validate API key and get organization info
162
- - **Input**: `{ apiKey: string }`
163
- - **Output**: `{ success: boolean, organizationId: string, organizationName: string, plan: string }`
164
- - **Used By**: `auth.ts`
165
-
166
- #### 2. `list-projects`
167
- - **Purpose**: List all projects for authenticated organization
168
- - **Input**: (none - uses X-API-Key header)
169
- - **Output**: `{ success: boolean, projects: Project[] }`
170
- - **Used By**: `projects.ts`
171
-
172
- #### 3. `manage-projects`
173
- - **Purpose**: CRUD operations on projects
174
- - **Actions**:
175
- - `create`: Create new project
176
- - `get`: Get project details (NOT USED - use list-projects instead)
177
- - `update`: Update project
178
- - `delete`: Soft delete project
179
- - `add-language`: Add language to project
180
- - `remove-language`: Remove language from project
181
- - `stats`: Get project statistics
182
- - **Used By**: `projects.ts`
183
-
184
- #### 4. `manage-translation-keys`
185
- - **Purpose**: CRUD operations on translation keys
186
- - **Actions**:
187
- - `list`: List keys with filtering
188
- - `get`: Get single key details (NOT USED directly)
189
- - `create`: Create new key
190
- - `update`: Update key metadata (NOT USED)
191
- - `delete`: Delete key
192
- - `translate`: Update single language translation
193
- - `publish`: Bulk publish/unpublish keys
194
- - **Used By**: `keys.ts`
195
-
196
- #### 5. `manage-team-members`
197
- - **Purpose**: Team member management
198
- - **Actions**:
199
- - `list`: List team members
200
- - `get`: Get member details
201
- - `invite`: Invite new member
202
- - `remove`: Remove member
203
- - `update-role`: Update member role
204
- - `list-invitations`: List invitations
205
- - `revoke-invitation`: Revoke invitation
206
- - **Used By**: `team.ts`
207
-
208
- #### 6. `get-organization-info`
209
- - **Purpose**: Organization information and statistics
210
- - **Actions**:
211
- - `info`: Get org details
212
- - `stats`: Get org statistics
213
- - `plan`: Get subscription plan and limits
214
- - **Used By**: `org.ts`
215
-
216
- #### 7. `export-translations`
217
- - **Purpose**: Export translations in various formats
218
- - **Input**: `{ projectSlug: string, language?: string, format: string, module?: string, includeUnpublished: boolean }`
219
- - **Output**: `{ success: boolean, translations: object/string, format: string }`
220
- - **Used By**: `export.ts`
221
-
222
- #### 8. `push-translations`
223
- - **Purpose**: Import/push translations to server
224
- - **Input**: `{ projectSlug: string, language: string, translations: object, overwrite: boolean, publish: boolean }`
225
- - **Output**: `{ success: boolean, created: number, updated: number, skipped: number }`
226
- - **Used By**: `import.ts`
227
-
228
- ---
229
-
230
- ## Command Structure
231
-
232
- ### File: `src/index.ts`
233
-
234
- This is the main entry point that registers all commands using Commander.js.
235
-
236
- #### Command Registration Pattern
237
-
238
- ```typescript
239
- program
240
- .command('command-name <required> [optional]')
241
- .description('What this command does')
242
- .option('-f, --flag <value>', 'Flag description', 'default')
243
- .action(async (requiredArg, options) => {
244
- try {
245
- await commandFunction(requiredArg, options);
246
- } catch (error: any) {
247
- console.error(chalk.red(`Error: ${error.message}\n`));
248
- process.exit(1);
249
- }
250
- });
251
- ```
252
-
253
- #### Subcommand Groups
254
-
255
- Commands are organized into groups:
256
-
257
- ```typescript
258
- const projects = program.command('projects').description('Manage projects');
259
- projects.command('list').action(...);
260
- projects.command('create <name>').action(...);
261
-
262
- const keys = program.command('keys').description('Manage translation keys');
263
- keys.command('list <project>').action(...);
264
- keys.command('create <project> <key>').action(...);
265
- ```
266
-
267
- ---
268
-
269
- ## File Organization
270
-
271
- ```
272
- langctl-cli/
273
- ├── src/
274
- │ ├── index.ts # Main entry point, command registration
275
- │ ├── auth.ts # Authentication utilities
276
- │ ├── config.ts # Configuration management
277
- │ ├── commands/
278
- │ │ ├── auth.ts # Auth commands (init, logout)
279
- │ │ ├── projects.ts # Project CRUD commands
280
- │ │ ├── keys.ts # Translation key commands
281
- │ │ ├── team.ts # Team management commands
282
- │ │ ├── org.ts # Organization commands
283
- │ │ ├── export.ts # Export translations
284
- │ │ ├── import.ts # Import translations
285
- │ │ ├── pull.ts # Legacy pull command
286
- │ │ ├── init.ts # Interactive setup
287
- │ │ ├── config.ts # Config viewer
288
- │ │ └── debug.ts # Debug utilities
289
- │ └── utils/
290
- │ └── banner.ts # CLI banner/logo
291
- ├── dist/ # Compiled JavaScript
292
- ├── bin/
293
- │ └── langctl.js # Executable entry point
294
- ├── package.json
295
- ├── tsconfig.json
296
- └── README.md
297
- ```
298
-
299
- ---
300
-
301
- ## How Each Command Works
302
-
303
- ### Authentication Commands
304
-
305
- #### `langctl init`
306
- **File**: `src/commands/init.ts`
307
-
308
- ```typescript
309
- // Process:
310
- 1. Show welcome banner
311
- 2. Prompt for API key
312
- 3. Call authenticate() from auth.ts
313
- 4. Prompt for default language preference
314
- 5. Save to config
315
- 6. Show success message
316
- ```
317
-
318
- #### `langctl auth <api-key>`
319
- **File**: `src/commands/auth.ts`
320
-
321
- ```typescript
322
- // Process:
323
- 1. Get API key from argument
324
- 2. Call authenticate() from auth.ts
325
- 3. authenticate() does:
326
- - Sanitize key
327
- - POST to verify-api-key Edge Function
328
- - Save to config on success
329
- 4. Show success/error message
330
- ```
331
-
332
- #### `langctl logout`
333
- **File**: `src/commands/auth.ts`
334
-
335
- ```typescript
336
- // Process:
337
- 1. Call logout() from auth.ts
338
- 2. logout() deletes all keys from config
339
- 3. Show confirmation message
340
- ```
341
-
342
- #### `langctl config`
343
- **File**: `src/commands/config.ts`
344
-
345
- ```typescript
346
- // Process:
347
- 1. Read config from ~/.langctl/config.json
348
- 2. Display masked API key (first 10 chars + ...)
349
- 3. Display organizationId, organizationName
350
- 4. Display config file path
351
- ```
352
-
353
- ---
354
-
355
- ### Organization Commands
356
-
357
- #### `langctl org info`
358
- **File**: `src/commands/org.ts`
359
-
360
- ```typescript
361
- // Process:
362
- 1. Check authentication (isAuthenticated())
363
- 2. Get API key (getApiKey())
364
- 3. Start spinner
365
- 4. POST to get-organization-info with action: 'info'
366
- 5. Parse response
367
- 6. Display:
368
- - Organization name
369
- - ID and slug
370
- - Subscription plan
371
- - Creation date
372
- ```
373
-
374
- #### `langctl org stats`
375
- **File**: `src/commands/org.ts`
376
-
377
- ```typescript
378
- // Process:
379
- 1. Check authentication
380
- 2. POST to get-organization-info with action: 'stats'
381
- 3. Parse response with stats:
382
- - members: number of team members
383
- - projects: number of projects
384
- - total_keys, published_keys, unpublished_keys
385
- - languages: count and codes
386
- - api_keys: active API keys
387
- - webhooks: active webhooks
388
- 4. Display formatted statistics
389
- ```
390
-
391
- #### `langctl org plan`
392
- **File**: `src/commands/org.ts`
393
-
394
- ```typescript
395
- // Process:
396
- 1. Check authentication
397
- 2. POST to get-organization-info with action: 'plan'
398
- 3. Parse response with limits:
399
- - max_members
400
- - max_projects
401
- - max_keys_per_project
402
- - max_api_keys
403
- 4. Format limits (null or -1 = "Unlimited")
404
- 5. Display plan name and limits
405
- ```
406
-
407
- ---
408
-
409
- ### Project Commands
410
-
411
- #### `langctl projects list`
412
- **File**: `src/commands/projects.ts`
413
-
414
- ```typescript
415
- // Process:
416
- 1. Check authentication
417
- 2. POST to list-projects Edge Function
418
- 3. Parse response with projects array
419
- 4. For each project, display:
420
- - Name (bold)
421
- - Slug
422
- - Description
423
- - Languages (comma-separated)
424
- - Default language
425
- - Modules (if any)
426
- 5. Show export command example
427
- ```
428
-
429
- #### `langctl projects create <name>`
430
- **File**: `src/commands/projects.ts`
431
-
432
- ```typescript
433
- // Process:
434
- 1. Check authentication
435
- 2. Parse options:
436
- - languages: split by comma, default 'en'
437
- - defaultLanguage: default to first language
438
- - description: optional
439
- 3. POST to manage-projects with action: 'create'
440
- 4. Send: name, description, languages array, defaultLanguage
441
- 5. Display success with project slug
442
- ```
443
-
444
- #### `langctl projects get <slug>`
445
- **File**: `src/commands/projects.ts`
446
-
447
- ```typescript
448
- // Process:
449
- 1. Check authentication
450
- 2. POST to list-projects
451
- 3. Find project with matching slug
452
- 4. Display detailed info:
453
- - Name, slug, ID
454
- - Description
455
- - All languages
456
- - Default language
457
- - Modules list
458
- ```
459
-
460
- **Note**: We use list-projects and filter client-side because manage-projects 'get' action requires project ID, but users only know slugs.
461
-
462
- #### `langctl projects update <slug>`
463
- **File**: `src/commands/projects.ts`
464
-
465
- ```typescript
466
- // Process:
467
- 1. Check authentication
468
- 2. POST to list-projects to get project ID from slug
469
- 3. Build updateData object with:
470
- - name (if provided)
471
- - description (if provided)
472
- - languages array (if provided, split by comma)
473
- - defaultLanguage (if provided)
474
- 4. POST to manage-projects with action: 'update'
475
- 5. Send projectId + updateData
476
- 6. Display success
477
- ```
478
-
479
- #### `langctl projects delete <slug>`
480
- **File**: `src/commands/projects.ts`
481
-
482
- ```typescript
483
- // Process:
484
- 1. Check authentication
485
- 2. Get project ID from slug (via list-projects)
486
- 3. POST to manage-projects with action: 'delete'
487
- 4. Send projectId
488
- 5. Display success
489
- ```
490
-
491
- **Note**: This is a soft delete. Project is marked deleted but not removed from database.
492
-
493
- #### `langctl projects add-language <slug> <language>`
494
- **File**: `src/commands/projects.ts`
495
-
496
- ```typescript
497
- // Process:
498
- 1. Check authentication
499
- 2. Get project ID from slug
500
- 3. POST to manage-projects with action: 'add-language'
501
- 4. Send: projectId, language
502
- 5. Edge Function validates:
503
- - Project exists
504
- - Language not already in project
505
- 6. Display success message from server
506
- ```
507
-
508
- #### `langctl projects remove-language <slug> <language>`
509
- **File**: `src/commands/projects.ts`
510
-
511
- ```typescript
512
- // Process:
513
- 1. Check authentication
514
- 2. Get project ID from slug
515
- 3. POST to manage-projects with action: 'remove-language'
516
- 4. Send: projectId, language
517
- 5. Edge Function validates:
518
- - Project exists
519
- - Language exists in project
520
- - Language is not the default language
521
- 6. Display success message from server
522
- ```
523
-
524
- #### `langctl projects stats <slug>`
525
- **File**: `src/commands/projects.ts`
526
-
527
- ```typescript
528
- // Process:
529
- 1. Check authentication
530
- 2. Get project ID from slug
531
- 3. POST to manage-projects with action: 'stats'
532
- 4. Parse response with:
533
- - total_keys
534
- - published_keys
535
- - unpublished_keys
536
- - modules (count)
537
- - module_names (array)
538
- 5. Display formatted statistics
539
- ```
540
-
541
- ---
542
-
543
- ### Translation Key Commands
544
-
545
- #### Helper Function: `getProjectBySlug()`
546
- **File**: `src/commands/keys.ts`
547
-
548
- ```typescript
549
- // Purpose: Convert project slug to project ID
550
- // Process:
551
- 1. POST to list-projects
552
- 2. Find project where project.slug === slug
553
- 3. Return Project object or null
554
- // Why: Edge Functions need project ID, users only know slugs
555
- ```
556
-
557
- #### `langctl keys list <project>`
558
- **File**: `src/commands/keys.ts`
559
-
560
- ```typescript
561
- // Process:
562
- 1. Check authentication
563
- 2. Get project object from slug
564
- 3. POST to manage-translation-keys with action: 'list'
565
- 4. Send filters:
566
- - projectId
567
- - module (optional)
568
- - published (optional boolean)
569
- - search (optional string)
570
- - limit (default 100)
571
- - offset (default 0)
572
- 5. Parse response with keys array
573
- 6. For each key, display:
574
- - Key name (bold)
575
- - Description
576
- - Module
577
- - Published status (colored)
578
- - Languages available
579
- ```
580
-
581
- #### `langctl keys get <project> <key>`
582
- **File**: `src/commands/keys.ts`
583
-
584
- ```typescript
585
- // Process:
586
- 1. Check authentication
587
- 2. Get project ID from slug
588
- 3. POST to manage-translation-keys with action: 'list'
589
- 4. Send: projectId, search: keyName, limit: 1
590
- 5. Find exact match in results
591
- 6. Display detailed info:
592
- - Key name and ID
593
- - Description
594
- - Module
595
- - Published status
596
- - All translations (all languages)
597
- ```
598
-
599
- **Note**: We use 'list' action with search instead of 'get' to match by key name (users don't know key IDs).
600
-
601
- #### `langctl keys create <project> <key>`
602
- **File**: `src/commands/keys.ts`
603
-
604
- ```typescript
605
- // Process:
606
- 1. Check authentication
607
- 2. Get project ID from slug
608
- 3. Parse options:
609
- - Build translations object from --value-* options
610
- - Example: --value-en → translations.en
611
- - Example: --value-es → translations.es
612
- 4. POST to manage-translation-keys with action: 'create'
613
- 5. Send:
614
- - projectId
615
- - key (key name)
616
- - translations (object)
617
- - description (optional)
618
- - module (optional)
619
- - tags (array, optional)
620
- 6. Display success
621
- ```
622
-
623
- **Implementation Detail**: Options starting with 'value' are converted to language codes:
624
- ```typescript
625
- Object.keys(options).forEach(opt => {
626
- if (opt.startsWith('value')) {
627
- const lang = opt.replace('value', '').toLowerCase();
628
- if (lang) translations[lang] = options[opt];
629
- }
630
- });
631
- ```
632
-
633
- #### `langctl keys delete <project> <key>`
634
- **File**: `src/commands/keys.ts`
635
-
636
- ```typescript
637
- // Process:
638
- 1. Check authentication
639
- 2. Get project ID from slug
640
- 3. Find key ID (list with search, find exact match)
641
- 4. POST to manage-translation-keys with action: 'delete'
642
- 5. Send: projectId, keyId
643
- 6. Display success
644
- ```
645
-
646
- **Note**: This is a soft delete.
647
-
648
- #### `langctl keys translate <project> <key>`
649
- **File**: `src/commands/keys.ts`
650
-
651
- ```typescript
652
- // Process:
653
- 1. Check authentication
654
- 2. Get project ID from slug
655
- 3. Find key ID
656
- 4. POST to manage-translation-keys with action: 'translate'
657
- 5. Send:
658
- - projectId
659
- - keyId
660
- - language (from --language option)
661
- - value (from --value option)
662
- 6. Display success with language and key name
663
- ```
664
-
665
- #### `langctl keys publish <project> <keys...>`
666
- **File**: `src/commands/keys.ts`
667
-
668
- ```typescript
669
- // Process:
670
- 1. Check authentication
671
- 2. Get project ID from slug
672
- 3. List all keys (limit: 1000)
673
- 4. Filter keys by name (match against keys... argument)
674
- 5. Extract key IDs
675
- 6. POST to manage-translation-keys with action: 'publish'
676
- 7. Send:
677
- - projectId
678
- - keyIds (array)
679
- - published (boolean, true unless --unpublish)
680
- 8. Display success message from server
681
- ```
682
-
683
- **Note**: Can publish/unpublish multiple keys at once.
684
-
685
- ---
686
-
687
- ### Team Commands
688
-
689
- #### `langctl team list`
690
- **File**: `src/commands/team.ts`
691
-
692
- ```typescript
693
- // Process:
694
- 1. Check authentication
695
- 2. POST to manage-team-members with action: 'list'
696
- 3. Parse response with members array
697
- 4. For each member, display:
698
- - Email (or user_id if no email)
699
- - Role
700
- - Join date
701
- ```
702
-
703
- **Note**: Edge Function uses RPC function `get_organization_members_with_users` to join with auth.users.
704
-
705
- #### `langctl team get <email>`
706
- **File**: `src/commands/team.ts`
707
-
708
- ```typescript
709
- // Process:
710
- 1. Check authentication
711
- 2. POST to manage-team-members with action: 'get'
712
- 3. Send: email
713
- 4. Display:
714
- - Email
715
- - Role
716
- - User ID
717
- - Join date
718
- ```
719
-
720
- #### `langctl team invite <email>`
721
- **File**: `src/commands/team.ts`
722
-
723
- ```typescript
724
- // Process:
725
- 1. Check authentication
726
- 2. POST to manage-team-members with action: 'invite'
727
- 3. Send:
728
- - email
729
- - role (default: 'member', from --role option)
730
- 4. Edge Function:
731
- - Calls RPC function invite_user_to_organization
732
- - Creates invitation record
733
- - Sends email (via database trigger)
734
- 5. Display success message
735
- ```
736
-
737
- **Roles**: viewer, member, admin (not owner - that's assigned at org creation).
738
-
739
- #### `langctl team remove <email>`
740
- **File**: `src/commands/team.ts`
741
-
742
- ```typescript
743
- // Process:
744
- 1. Check authentication
745
- 2. POST to manage-team-members with action: 'remove'
746
- 3. Send: email
747
- 4. Edge Function validates:
748
- - User exists
749
- - User is member of organization
750
- - User is not the owner
751
- - User is not self (cannot remove yourself)
752
- 5. Deletes organization_members record
753
- 6. Display success
754
- ```
755
-
756
- #### `langctl team update-role <email> <role>`
757
- **File**: `src/commands/team.ts`
758
-
759
- ```typescript
760
- // Process:
761
- 1. Check authentication
762
- 2. Validate role (viewer, member, admin)
763
- 3. POST to manage-team-members with action: 'update-role'
764
- 4. Send: email, role
765
- 5. Edge Function validates:
766
- - User exists and is member
767
- - Cannot change to/from owner role
768
- 6. Updates organization_members.role
769
- 7. Display success
770
- ```
771
-
772
- #### `langctl team invitations`
773
- **File**: `src/commands/team.ts`
774
-
775
- ```typescript
776
- // Process:
777
- 1. Check authentication
778
- 2. POST to manage-team-members with action: 'list-invitations'
779
- 3. Send: pending (boolean from --pending option)
780
- 4. Parse response with invitations array
781
- 5. For each invitation, display:
782
- - Email
783
- - Role
784
- - Status (pending, accepted, cancelled)
785
- - Invited date
786
- - Expires date
787
- ```
788
-
789
- #### `langctl team revoke-invitation <email>`
790
- **File**: `src/commands/team.ts`
791
-
792
- ```typescript
793
- // Process:
794
- 1. Check authentication
795
- 2. POST to manage-team-members with action: 'revoke-invitation'
796
- 3. Send: email
797
- 4. Edge Function:
798
- - Finds pending invitation for email
799
- - Updates status to 'cancelled'
800
- 5. Display success
801
- ```
802
-
803
- ---
804
-
805
- ### Export/Import Commands
806
-
807
- #### `langctl export <project>`
808
- **File**: `src/commands/export.ts`
809
-
810
- ```typescript
811
- // Process:
812
- 1. Check authentication
813
- 2. Get project object from slug
814
- 3. Validate format (flat-json, nested-json, i18n-json, android-xml, ios-strings, flutter-arb)
815
- 4. POST to export-translations
816
- 5. Send:
817
- - projectSlug
818
- - language (optional)
819
- - format
820
- - module (optional)
821
- - includeUnpublished (boolean)
822
- 6. Parse response with translations
823
- 7. If language specified:
824
- - Write to file (output path or auto-generated)
825
- 8. If no language (export all):
826
- - Loop through all project languages
827
- - Export each language
828
- - Write to separate files
829
- 9. Display success with file paths
830
- ```
831
-
832
- **Auto-generated paths**:
833
- - JSON formats: `./translations/<slug>.<language>.json`
834
- - iOS: `./translations/<slug>.<language>.strings`
835
- - Android: `./translations/<slug>.<language>.xml`
836
- - Flutter: `./translations/<slug>.<language>.arb`
837
-
838
- **Format Conversion**: Done server-side in Edge Function:
839
- - Converts `{{param}}` to platform-specific format
840
- - Android: `%1$s` (positional)
841
- - iOS: `%@` (positional)
842
- - Flutter: `{param}` (named)
843
-
844
- #### `langctl import <project> <file>`
845
- **File**: `src/commands/import.ts`
846
-
847
- ```typescript
848
- // Process:
849
- 1. Check authentication
850
- 2. Get project object from slug
851
- 3. Validate language option is provided
852
- 4. Read file:
853
- - Check file exists
854
- - Parse JSON
855
- 5. Flatten JSON if nested:
856
- - Converts { "home": { "title": "..." } } to { "home.title": "..." }
857
- 6. POST to push-translations
858
- 7. Send:
859
- - projectSlug
860
- - language
861
- - translations (flattened object)
862
- - overwrite (boolean from --overwrite)
863
- - publish (boolean from --publish)
864
- 8. Parse response:
865
- - created: number of new keys
866
- - updated: number of updated keys
867
- - skipped: number of skipped keys
868
- 9. Display summary
869
- ```
870
-
871
- **Flattening Logic**:
872
- ```typescript
873
- function flattenObject(obj: any, prefix = ''): Record<string, string> {
874
- let result: Record<string, string> = {};
875
- for (const key in obj) {
876
- const value = obj[key];
877
- const newKey = prefix ? `${prefix}.${key}` : key;
878
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
879
- Object.assign(result, flattenObject(value, newKey));
880
- } else {
881
- result[newKey] = String(value);
882
- }
883
- }
884
- return result;
885
- }
886
- ```
887
-
888
- ---
889
-
890
- ## Configuration Management
891
-
892
- ### File: `src/config.ts`
893
-
894
- ```typescript
895
- class Config {
896
- private configPath: string;
897
- private configDir: string;
898
- private data: Record<string, any>;
899
-
900
- constructor() {
901
- // Config stored at: ~/.langctl/config.json
902
- this.configDir = path.join(os.homedir(), '.langctl');
903
- this.configPath = path.join(this.configDir, 'config.json');
904
- this.load();
905
- }
906
-
907
- private load() {
908
- // Create directory if doesn't exist
909
- // Read config.json or initialize empty object
910
- }
911
-
912
- private save() {
913
- // Write config.json with pretty formatting
914
- }
915
-
916
- get(key: string): any {
917
- return this.data[key];
918
- }
919
-
920
- set(key: string, value: any) {
921
- this.data[key] = value;
922
- this.save();
923
- }
924
-
925
- delete(key: string) {
926
- delete this.data[key];
927
- this.save();
928
- }
929
-
930
- getAll(): Record<string, any> {
931
- return { ...this.data };
932
- }
933
- }
934
-
935
- export const config = new Config();
936
- ```
937
-
938
- ### Configuration Keys
939
-
940
- - `apiKey`: User's API key (plain text)
941
- - `organizationId`: UUID of organization
942
- - `organizationName`: Display name
943
- - `defaultLanguage`: User preference for language (not currently used)
944
- - `defaultProject`: User's default project slug (not currently used)
945
-
946
- ---
947
-
948
- ## Error Handling
949
-
950
- ### Common Error Patterns
951
-
952
- #### Network Errors
953
- ```typescript
954
- try {
955
- const response = await fetch(url, options);
956
- if (!response.ok) {
957
- const errorData = await response.json().catch(() => ({})) as any;
958
- throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
959
- }
960
- } catch (error: any) {
961
- spinner.fail(chalk.red('Operation failed'));
962
- console.error(chalk.red(`Error: ${error.message}\n`));
963
- }
964
- ```
965
-
966
- #### Authentication Errors
967
- ```typescript
968
- if (!isAuthenticated()) {
969
- console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
970
- return;
971
- }
972
- ```
973
-
974
- #### Validation Errors
975
- ```typescript
976
- if (!validRoles.includes(role)) {
977
- console.log(chalk.red(`✗ Invalid role. Must be one of: ${validRoles.join(', ')}\n`));
978
- return;
979
- }
980
- ```
981
-
982
- ### Error Display
983
-
984
- - Use `chalk.red()` for errors
985
- - Use `chalk.yellow()` for warnings
986
- - Use `chalk.green()` for success
987
- - Use `spinner.fail()` for operation failures
988
- - Always include newline at end for readability
989
-
990
- ---
991
-
992
- ## Testing & Deployment
993
-
994
- ### Building
995
-
996
- ```bash
997
- cd langctl-cli
998
- npm run build
999
- ```
1000
-
1001
- This compiles TypeScript to JavaScript in `dist/` directory.
1002
-
1003
- ### Testing Locally
1004
-
1005
- ```bash
1006
- # Test specific command
1007
- node dist/index.js org info
1008
-
1009
- # Or use npm link for global testing
1010
- npm link
1011
- langctl org info
1012
- ```
1013
-
1014
- ### Deploying Edge Functions
1015
-
1016
- ```bash
1017
- # Deploy all functions
1018
- supabase functions deploy --no-verify-jwt
1019
-
1020
- # Deploy specific function
1021
- supabase functions deploy manage-projects --no-verify-jwt
1022
- ```
1023
-
1024
- **IMPORTANT**: Always use `--no-verify-jwt` flag. Edge Functions authenticate using custom X-API-Key header, not Supabase JWT.
1025
-
1026
- ### Publishing to npm
1027
-
1028
- ```bash
1029
- npm version patch # or minor, or major
1030
- npm publish
1031
- ```
1032
-
1033
- ---
1034
-
1035
- ## Adding New Commands
1036
-
1037
- ### Step 1: Create Edge Function
1038
-
1039
- 1. Create function directory:
1040
- ```bash
1041
- mkdir -p supabase/functions/my-function
1042
- ```
1043
-
1044
- 2. Create `index.ts`:
1045
- ```typescript
1046
- import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
1047
- import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
1048
-
1049
- const corsHeaders = {
1050
- 'Access-Control-Allow-Origin': '*',
1051
- 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-api-key',
1052
- }
1053
-
1054
- async function hashApiKey(key: string): Promise<string> {
1055
- const encoder = new TextEncoder()
1056
- const data = encoder.encode(key)
1057
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
1058
- const hashArray = Array.from(new Uint8Array(hashBuffer))
1059
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
1060
- }
1061
-
1062
- async function verifyApiKey(supabase: any, apiKey: string) {
1063
- try {
1064
- const keyHash = await hashApiKey(apiKey.trim().toLowerCase())
1065
- const { data, error } = await supabase
1066
- .from('api_keys')
1067
- .select('organization_id, created_by, scopes, revoked')
1068
- .eq('key_hash', keyHash)
1069
- .eq('revoked', false)
1070
- .single()
1071
-
1072
- if (error || !data) {
1073
- return { valid: false, error: 'Invalid or revoked API key' }
1074
- }
1075
-
1076
- return {
1077
- valid: true,
1078
- organizationId: data.organization_id,
1079
- userId: data.created_by,
1080
- scopes: data.scopes
1081
- }
1082
- } catch (error) {
1083
- return { valid: false, error: 'Authentication failed' }
1084
- }
1085
- }
1086
-
1087
- serve(async (req) => {
1088
- if (req.method === 'OPTIONS') {
1089
- return new Response('ok', { headers: corsHeaders })
1090
- }
1091
-
1092
- try {
1093
- const apiKey = req.headers.get('X-API-Key')
1094
- if (!apiKey) {
1095
- return new Response(
1096
- JSON.stringify({ success: false, error: 'X-API-Key header is required' }),
1097
- { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
1098
- )
1099
- }
1100
-
1101
- const supabaseUrl = Deno.env.get('SUPABASE_URL')!
1102
- const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
1103
- const supabase = createClient(supabaseUrl, supabaseServiceKey)
1104
-
1105
- const auth = await verifyApiKey(supabase, apiKey)
1106
- if (!auth.valid) {
1107
- return new Response(
1108
- JSON.stringify({ success: false, error: auth.error }),
1109
- { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
1110
- )
1111
- }
1112
-
1113
- const { action, ...params } = await req.json()
1114
-
1115
- let result
1116
-
1117
- switch (action) {
1118
- case 'my-action':
1119
- // Your logic here
1120
- result = { success: true, data: {} }
1121
- break
1122
- default:
1123
- return new Response(
1124
- JSON.stringify({ success: false, error: 'Invalid action' }),
1125
- { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
1126
- )
1127
- }
1128
-
1129
- return new Response(
1130
- JSON.stringify(result),
1131
- { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
1132
- )
1133
-
1134
- } catch (error) {
1135
- return new Response(
1136
- JSON.stringify({ success: false, error: error.message }),
1137
- { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
1138
- )
1139
- }
1140
- })
1141
- ```
1142
-
1143
- 3. Create `deno.json`:
1144
- ```json
1145
- {
1146
- "imports": {}
1147
- }
1148
- ```
1149
-
1150
- 4. Add to `supabase/config.toml`:
1151
- ```toml
1152
- [functions.my-function]
1153
- enabled = true
1154
- verify_jwt = false
1155
- import_map = "./functions/my-function/deno.json"
1156
- entrypoint = "./functions/my-function/index.ts"
1157
- ```
1158
-
1159
- 5. Deploy:
1160
- ```bash
1161
- supabase functions deploy my-function --no-verify-jwt
1162
- ```
1163
-
1164
- ### Step 2: Create CLI Command
1165
-
1166
- 1. Create command file:
1167
- ```typescript
1168
- // src/commands/mycommand.ts
1169
- import chalk from 'chalk';
1170
- import ora from 'ora';
1171
- import { isAuthenticated, getApiKey } from '../auth.js';
1172
-
1173
- const MY_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/my-function';
1174
-
1175
- export async function myCommand(arg: string, options: any): Promise<void> {
1176
- if (!isAuthenticated()) {
1177
- console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
1178
- return;
1179
- }
1180
-
1181
- const apiKey = getApiKey();
1182
- const spinner = ora('Processing...').start();
1183
-
1184
- try {
1185
- const response = await fetch(MY_FUNCTION_URL, {
1186
- method: 'POST',
1187
- headers: {
1188
- 'Content-Type': 'application/json',
1189
- 'X-API-Key': apiKey!
1190
- },
1191
- body: JSON.stringify({
1192
- action: 'my-action',
1193
- arg,
1194
- ...options
1195
- })
1196
- });
1197
-
1198
- const data = await response.json() as any;
1199
-
1200
- if (!data.success) {
1201
- throw new Error(data.error);
1202
- }
1203
-
1204
- spinner.succeed(chalk.green('Success!'));
1205
- console.log(data.result);
1206
-
1207
- } catch (error: any) {
1208
- spinner.fail(chalk.red('Failed'));
1209
- console.error(chalk.red(`Error: ${error.message}\n`));
1210
- }
1211
- }
1212
- ```
1213
-
1214
- 2. Register in `src/index.ts`:
1215
- ```typescript
1216
- import { myCommand } from './commands/mycommand.js';
1217
-
1218
- program
1219
- .command('mycommand <arg>')
1220
- .description('Description of my command')
1221
- .option('-f, --flag <value>', 'Flag description')
1222
- .action(async (arg: string, options: any) => {
1223
- try {
1224
- await myCommand(arg, options);
1225
- } catch (error: any) {
1226
- console.error(chalk.red(`Error: ${error.message}\n`));
1227
- process.exit(1);
1228
- }
1229
- });
1230
- ```
1231
-
1232
- 3. Build and test:
1233
- ```bash
1234
- npm run build
1235
- node dist/index.js mycommand test-arg --flag value
1236
- ```
1237
-
1238
- ---
1239
-
1240
- ## Security Considerations
1241
-
1242
- ### What Users Can Access
1243
-
1244
- - ✅ Their API key (plain text, stored locally)
1245
- - ✅ Their organization data (via API key auth)
1246
- - ✅ Projects within their organization
1247
-
1248
- ### What Users CANNOT Access
1249
-
1250
- - ❌ Supabase credentials
1251
- - ❌ Other organizations' data
1252
- - ❌ Database connection strings
1253
- - ❌ Service role keys
1254
- - ❌ Edge Function URLs (hardcoded, but not secret)
1255
-
1256
- ### API Key Security
1257
-
1258
- 1. **Format Validation**: Strict format enforcement prevents injection
1259
- 2. **SHA-256 Hashing**: Keys hashed before storage
1260
- 3. **Server-Side Validation**: All validation happens on server
1261
- 4. **Organization Scoping**: Keys tied to specific organization
1262
- 5. **Revocable**: Keys can be revoked from dashboard
1263
-
1264
- ### Edge Function Security
1265
-
1266
- 1. **No JWT Verification**: Uses custom X-API-Key header
1267
- 2. **Service Role Key**: Edge Functions use service role for database access
1268
- 3. **Input Validation**: All inputs validated before database queries
1269
- 4. **Row Level Security**: Database RLS policies provide additional security layer
1270
- 5. **CORS Enabled**: Allows CLI to call functions
1271
-
1272
- ---
1273
-
1274
- ## Common Debugging Patterns
1275
-
1276
- ### Check Authentication
1277
- ```bash
1278
- langctl config # View stored credentials
1279
- ```
1280
-
1281
- ### Test Edge Function Directly
1282
- ```bash
1283
- curl -X POST https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/my-function \
1284
- -H "Content-Type: application/json" \
1285
- -H "X-API-Key: lc_..." \
1286
- -d '{"action":"test"}'
1287
- ```
1288
-
1289
- ### Check Edge Function Logs
1290
- ```bash
1291
- supabase functions logs my-function
1292
- ```
1293
-
1294
- ### Verify API Key
1295
- ```bash
1296
- curl -X POST https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/verify-api-key \
1297
- -H "Content-Type: application/json" \
1298
- -d '{"apiKey":"lc_..."}'
1299
- ```
1300
-
1301
- ### Test Command Locally
1302
- ```bash
1303
- cd langctl-cli
1304
- npm run build
1305
- node dist/index.js <command> <args>
1306
- ```
1307
-
1308
- ---
1309
-
1310
- ## Performance Considerations
1311
-
1312
- ### Avoiding Multiple API Calls
1313
-
1314
- **Bad**:
1315
- ```typescript
1316
- // Get project ID
1317
- const projects = await listProjects();
1318
- const project = projects.find(p => p.slug === slug);
1319
-
1320
- // Then use project ID for each key
1321
- for (const key of keys) {
1322
- await getKey(project.id, key);
1323
- }
1324
- ```
1325
-
1326
- **Good**:
1327
- ```typescript
1328
- // Get project ID once
1329
- const projects = await listProjects();
1330
- const project = projects.find(p => p.slug === slug);
1331
-
1332
- // Batch operation
1333
- await publishKeys(project.id, keys);
1334
- ```
1335
-
1336
- ### Caching Project Data
1337
-
1338
- The `getProjectBySlug()` helper is called frequently. Consider:
1339
- - Caching project list for session
1340
- - Storing last-used project slug
1341
- - Implementing project shortcuts
1342
-
1343
- **Current**: Each command calls list-projects
1344
- **Future**: Cache in config with TTL
1345
-
1346
- ---
1347
-
1348
- ## Future Improvements
1349
-
1350
- See `NEXT_PHASE.md` for Phase 3 features.
1351
-
1352
- ---
1353
-
1354
- ## Summary
1355
-
1356
- ### Request Flow
1357
-
1358
- 1. User runs command: `langctl keys list my-app`
1359
- 2. Commander parses: command='keys list', args=['my-app'], options={}
1360
- 3. Calls: `listKeysCommand('my-app', {})`
1361
- 4. Checks: `isAuthenticated()` → reads ~/.langctl/config.json
1362
- 5. Gets: `getApiKey()` → reads from config
1363
- 6. Fetches: `list-projects` → converts slug to project ID
1364
- 7. Posts: `manage-translation-keys` with action='list'
1365
- 8. Edge Function: Validates API key, queries database
1366
- 9. Response: JSON with keys array
1367
- 10. Format: Chalk colors + console.log
1368
- 11. Exit: Process completes
1369
-
1370
- ### Key Files to Know
1371
-
1372
- - `src/index.ts` - Command registration
1373
- - `src/auth.ts` - Authentication logic
1374
- - `src/config.ts` - Configuration management
1375
- - `src/commands/*.ts` - Command implementations
1376
- - `supabase/functions/*/index.ts` - Edge Functions
1377
- - `supabase/config.toml` - Edge Function configuration
1378
-
1379
- ### Architecture Principles
1380
-
1381
- 1. **CLI Never Touches Database**: All through Edge Functions
1382
- 2. **User-Friendly Arguments**: Use slugs/names, not UUIDs
1383
- 3. **Consistent Error Handling**: Always use try/catch with spinners
1384
- 4. **Colored Output**: Success=green, Error=red, Info=blue/white
1385
- 5. **Helpful Messages**: Guide users to next steps
1386
-
1387
- ---
1388
-
1389
- **Last Updated**: Phase 1 & 2 Complete (January 2026)