langctl 0.0.1 → 0.1.1

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 (44) hide show
  1. package/NPM_PUBLISH_GUIDE.md +293 -0
  2. package/QUICK_PUBLISH.md +64 -0
  3. package/README.md +300 -30
  4. package/SECURITY_FIX.md +167 -0
  5. package/dist/auth.d.ts +33 -0
  6. package/dist/auth.js +106 -0
  7. package/dist/commands/auth.d.ts +3 -0
  8. package/dist/commands/auth.js +33 -0
  9. package/dist/commands/config.d.ts +2 -0
  10. package/dist/commands/config.js +30 -0
  11. package/dist/commands/debug.d.ts +3 -0
  12. package/dist/commands/debug.js +57 -0
  13. package/dist/commands/init.d.ts +2 -0
  14. package/dist/commands/init.js +87 -0
  15. package/dist/commands/projects.d.ts +2 -0
  16. package/dist/commands/projects.js +54 -0
  17. package/dist/commands/pull.d.ts +9 -0
  18. package/dist/commands/pull.js +194 -0
  19. package/dist/config.d.ts +47 -0
  20. package/dist/config.js +69 -0
  21. package/dist/exporters/index.d.ts +36 -0
  22. package/dist/exporters/index.js +214 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +166 -0
  25. package/dist/supabase.d.ts +14 -0
  26. package/dist/supabase.js +30 -0
  27. package/dist/utils/banner.d.ts +9 -0
  28. package/dist/utils/banner.js +24 -0
  29. package/package.json +22 -3
  30. package/translations/en.json +5 -0
  31. package/translations/en.xml +9 -0
  32. package/translations/fr.json +4 -0
  33. package/translations/fr.xml +7 -0
  34. package/translations/hi.json +4 -0
  35. package/translations/hi.xml +7 -0
  36. package/translations/ios/en.strings +9 -0
  37. package/translations/ios/fr.strings +6 -0
  38. package/translations/ios/hi.strings +6 -0
  39. package/translations/ios/ja.strings +6 -0
  40. package/translations/ja.json +4 -0
  41. package/translations/ja.xml +7 -0
  42. package/PUBLISH_GUIDE.md +0 -209
  43. package/bin/langctl.js +0 -19
  44. package/index.js +0 -26
@@ -0,0 +1,167 @@
1
+ # šŸ”’ Security Fix - Supabase Credentials
2
+
3
+ **Date:** January 5, 2026
4
+ **Issue:** Supabase credentials were being stored in user config files
5
+ **Status:** āœ… FIXED
6
+
7
+ ---
8
+
9
+ ## 🚨 What Was Wrong
10
+
11
+ Previously, the CLI was:
12
+ - āŒ Storing Supabase URL in `~/.langctl/config.json`
13
+ - āŒ Storing Supabase anon key in `~/.langctl/config.json`
14
+ - āŒ Asking users about "Supabase configuration" during init
15
+ - āŒ Exposing backend implementation details to users
16
+
17
+ **Why this was bad:**
18
+ - Users could see our backend credentials
19
+ - Users could manipulate Supabase connection
20
+ - Exposed unnecessary implementation details
21
+ - Confused users with technical jargon
22
+
23
+ ---
24
+
25
+ ## āœ… What's Fixed
26
+
27
+ Now the CLI:
28
+ - āœ… Hardcodes Supabase credentials in source code
29
+ - āœ… Only stores user credentials (API key, org ID, preferences)
30
+ - āœ… Users never see or interact with Supabase
31
+ - āœ… Clean, simple user experience
32
+
33
+ ---
34
+
35
+ ## šŸ“„ Files Changed
36
+
37
+ ### 1. `src/config.ts`
38
+ **Removed from interface:**
39
+ ```typescript
40
+ // REMOVED - no longer stored
41
+ supabaseUrl?: string;
42
+ supabaseAnonKey?: string;
43
+ ```
44
+
45
+ **Now only stores:**
46
+ ```typescript
47
+ apiKey?: string; // User's API key
48
+ organizationId?: string; // User's org ID
49
+ organizationName?: string; // User's org name
50
+ defaultProject?: string; // User preference
51
+ defaultLanguage?: string; // User preference
52
+ ```
53
+
54
+ ### 2. `src/supabase.ts`
55
+ **Before:**
56
+ ```typescript
57
+ // BAD - pulled from user config
58
+ const supabaseUrl = config.get('supabaseUrl');
59
+ const supabaseAnonKey = config.get('supabaseAnonKey');
60
+ ```
61
+
62
+ **After:**
63
+ ```typescript
64
+ // GOOD - hardcoded in source
65
+ const SUPABASE_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co';
66
+ const SUPABASE_ANON_KEY = 'eyJhbGci...';
67
+ ```
68
+
69
+ ### 3. `src/commands/init.ts`
70
+ **Before:**
71
+ ```typescript
72
+ // BAD - saved to user config
73
+ config.set('supabaseUrl', SUPABASE_URL);
74
+ config.set('supabaseAnonKey', SUPABASE_ANON_KEY);
75
+ ```
76
+
77
+ **After:**
78
+ ```typescript
79
+ // GOOD - just initializes, doesn't save
80
+ initializeSupabase();
81
+ ```
82
+
83
+ ### 4. `src/commands/config.ts`
84
+ **Before:**
85
+ ```typescript
86
+ // BAD - displayed Supabase credentials
87
+ supabaseAnonKey: allConfig.supabaseAnonKey ? maskSensitive(...) : undefined
88
+ ```
89
+
90
+ **After:**
91
+ ```typescript
92
+ // GOOD - only displays user data
93
+ apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
94
+ organizationId: allConfig.organizationId,
95
+ organizationName: allConfig.organizationName
96
+ ```
97
+
98
+ ---
99
+
100
+ ## šŸ” Security Notes
101
+
102
+ **Is it safe to hardcode Supabase credentials in the CLI?**
103
+
104
+ āœ… **YES!** This is perfectly safe because:
105
+
106
+ 1. **Anon key is designed for client-side use**
107
+ - It's meant to be public-facing
108
+ - Row Level Security (RLS) protects all data
109
+ - Users can only access what they're authorized for
110
+
111
+ 2. **RLS policies enforce security**
112
+ - Users can only see their own organization data
113
+ - API key verification happens server-side (Edge Function)
114
+ - All operations are protected by RLS
115
+
116
+ 3. **This is standard practice**
117
+ - Same as mobile apps that embed API credentials
118
+ - Same as web apps that expose client keys
119
+ - Backend security, not client security
120
+
121
+ **What if someone decompiles the CLI?**
122
+ - They'll see the Supabase URL and anon key
123
+ - But they CANNOT access data without a valid API key
124
+ - API keys are user-specific and verified server-side
125
+ - RLS prevents unauthorized access
126
+
127
+ ---
128
+
129
+ ## šŸ“Š What Users See Now
130
+
131
+ ### Config File (`~/.langctl/config.json`)
132
+ ```json
133
+ {
134
+ "apiKey": "lc_abc123...",
135
+ "organizationId": "uuid-here",
136
+ "organizationName": "My Company",
137
+ "defaultLanguage": "en"
138
+ }
139
+ ```
140
+
141
+ **Clean and simple!** No backend implementation details.
142
+
143
+ ### Running `langctl config`
144
+ ```
145
+ šŸ“‹ Langctl Configuration
146
+
147
+ apiKey: lc_abc123...wxyz
148
+ organizationId: 550e8400-e29b-41d4-a716-446655440000
149
+ organizationName: My Company
150
+ defaultLanguage: en
151
+
152
+ Config file: /Users/username/.langctl/config.json
153
+ ```
154
+
155
+ **Only user-relevant information!**
156
+
157
+ ---
158
+
159
+ ## āœ… Ready to Publish
160
+
161
+ This security fix is complete. The CLI now:
162
+ - āœ… Keeps backend credentials private
163
+ - āœ… Only exposes user data to users
164
+ - āœ… Maintains full security with RLS
165
+ - āœ… Provides clean user experience
166
+
167
+ **Safe to publish to npm!** šŸš€
package/dist/auth.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export interface ApiKeyData {
2
+ organization_id: string;
3
+ organization: {
4
+ id: string;
5
+ name: string;
6
+ plan: string;
7
+ };
8
+ }
9
+ /**
10
+ * Verify API key via Edge Function
11
+ */
12
+ export declare function verifyApiKey(plainTextKey: string): Promise<ApiKeyData | null>;
13
+ /**
14
+ * Authenticate and save credentials to config
15
+ */
16
+ export declare function authenticate(apiKey: string): Promise<{
17
+ success: boolean;
18
+ message: string;
19
+ data?: ApiKeyData;
20
+ }>;
21
+ /**
22
+ * Check if user is authenticated
23
+ */
24
+ export declare function isAuthenticated(): boolean;
25
+ /**
26
+ * Get current organization ID
27
+ */
28
+ export declare function getOrganizationId(): string | undefined;
29
+ /**
30
+ * Logout (clear authentication)
31
+ */
32
+ export declare function logout(): void;
33
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,106 @@
1
+ import { config } from './config.js';
2
+ // Edge Function URL
3
+ const EDGE_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/verify-api-key';
4
+ // Normalize and validate API key input
5
+ function sanitizeApiKey(input) {
6
+ const raw = (input || '').trim().toLowerCase();
7
+ if (!raw.startsWith('lc_')) {
8
+ return { ok: false, message: 'API key must start with lc_' };
9
+ }
10
+ const hex = raw.slice(3); // after lc_
11
+ const hexOnly = hex.replace(/[^a-f0-9]/g, '');
12
+ if (hexOnly.length !== 64) {
13
+ return { ok: false, message: 'Invalid API key format. Expected lc_ followed by 64 hex characters' };
14
+ }
15
+ return { ok: true, key: `lc_${hexOnly}` };
16
+ }
17
+ /**
18
+ * Verify API key via Edge Function
19
+ */
20
+ export async function verifyApiKey(plainTextKey) {
21
+ try {
22
+ const sanitized = sanitizeApiKey(plainTextKey);
23
+ if (!sanitized.ok || !sanitized.key) {
24
+ return null;
25
+ }
26
+ // Call Edge Function
27
+ const response = await fetch(EDGE_FUNCTION_URL, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify({ apiKey: sanitized.key })
33
+ });
34
+ if (!response.ok) {
35
+ return null;
36
+ }
37
+ const data = await response.json();
38
+ if (!data.success) {
39
+ return null;
40
+ }
41
+ // Map Edge Function response to ApiKeyData format
42
+ return {
43
+ organization_id: data.organizationId,
44
+ organization: {
45
+ id: data.organizationId,
46
+ name: data.organizationName,
47
+ plan: data.plan
48
+ }
49
+ };
50
+ }
51
+ catch (error) {
52
+ console.error('Error verifying API key:', error);
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * Authenticate and save credentials to config
58
+ */
59
+ export async function authenticate(apiKey) {
60
+ const sanitized = sanitizeApiKey(apiKey);
61
+ if (!sanitized.ok || !sanitized.key) {
62
+ return {
63
+ success: false,
64
+ message: sanitized.message || 'Invalid API key format. Expected format: lc_[64 hex characters]'
65
+ };
66
+ }
67
+ // Verify via Edge Function
68
+ const keyData = await verifyApiKey(sanitized.key);
69
+ if (!keyData) {
70
+ return {
71
+ success: false,
72
+ message: 'Invalid or revoked API key. Please check your key and try again.'
73
+ };
74
+ }
75
+ // Save to config
76
+ config.set('apiKey', sanitized.key);
77
+ config.set('organizationId', keyData.organization_id);
78
+ config.set('organizationName', keyData.organization.name);
79
+ return {
80
+ success: true,
81
+ message: `Successfully authenticated as ${keyData.organization.name}`,
82
+ data: keyData
83
+ };
84
+ }
85
+ /**
86
+ * Check if user is authenticated
87
+ */
88
+ export function isAuthenticated() {
89
+ return !!(config.get('apiKey') && config.get('organizationId'));
90
+ }
91
+ /**
92
+ * Get current organization ID
93
+ */
94
+ export function getOrganizationId() {
95
+ return config.get('organizationId');
96
+ }
97
+ /**
98
+ * Logout (clear authentication)
99
+ */
100
+ export function logout() {
101
+ config.delete('apiKey');
102
+ config.delete('organizationId');
103
+ config.delete('organizationName');
104
+ config.delete('defaultProject');
105
+ }
106
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,3 @@
1
+ export declare function authCommand(apiKey: string): Promise<void>;
2
+ export declare function logoutCommand(): Promise<void>;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { authenticate, logout } from '../auth.js';
4
+ import { config } from '../config.js';
5
+ import { initializeSupabase } from '../supabase.js';
6
+ export async function authCommand(apiKey) {
7
+ // Initialize Supabase (with hardcoded credentials)
8
+ try {
9
+ initializeSupabase();
10
+ }
11
+ catch (error) {
12
+ console.log(chalk.red('āœ— Failed to connect to Langctl servers.\n'));
13
+ return;
14
+ }
15
+ const spinner = ora('Authenticating...').start();
16
+ const result = await authenticate(apiKey);
17
+ if (result.success) {
18
+ spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
19
+ console.log(chalk.gray(`\nAPI Key saved to: ${config.getConfigPath()}\n`));
20
+ }
21
+ else {
22
+ spinner.fail(chalk.red(result.message));
23
+ console.log(chalk.yellow('\nTroubleshooting:'));
24
+ console.log(chalk.white(' - Check that your API key is correct'));
25
+ console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
26
+ console.log(chalk.white(' - Generate a new key: https://langctl.com/dashboard/settings/api-keys\n'));
27
+ }
28
+ }
29
+ export async function logoutCommand() {
30
+ logout();
31
+ console.log(chalk.green('āœ“ Logged out successfully\n'));
32
+ }
33
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,2 @@
1
+ export declare function configCommand(): void;
2
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import { config } from '../config.js';
3
+ export function configCommand() {
4
+ console.log(chalk.blue.bold('\nšŸ“‹ Langctl Configuration\n'));
5
+ const allConfig = config.getAll();
6
+ if (Object.keys(allConfig).length === 0) {
7
+ console.log(chalk.yellow('No configuration found. Run "langctl init" to set up.\n'));
8
+ return;
9
+ }
10
+ // Display config with masked sensitive values
11
+ const displayConfig = {
12
+ apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
13
+ organizationId: allConfig.organizationId,
14
+ organizationName: allConfig.organizationName,
15
+ defaultProject: allConfig.defaultProject,
16
+ defaultLanguage: allConfig.defaultLanguage
17
+ };
18
+ Object.entries(displayConfig).forEach(([key, value]) => {
19
+ if (value !== undefined) {
20
+ console.log(chalk.white(`${key}: ${chalk.cyan(value)}`));
21
+ }
22
+ });
23
+ console.log(chalk.gray(`\nConfig file: ${config.getConfigPath()}\n`));
24
+ }
25
+ function maskApiKey(key) {
26
+ if (key.length < 10)
27
+ return '***';
28
+ return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
29
+ }
30
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ export declare function debugListApiKeys(): Promise<void>;
2
+ export declare function debugListOrganizations(): Promise<void>;
3
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getSupabase, initializeSupabase } from '../supabase.js';
4
+ export async function debugListApiKeys() {
5
+ // Initialize supabase (with hardcoded credentials)
6
+ try {
7
+ initializeSupabase();
8
+ }
9
+ catch (e) {
10
+ console.error(chalk.red('āœ— Failed to connect to Langctl servers.'));
11
+ return;
12
+ }
13
+ const supabase = getSupabase();
14
+ const spinner = ora('Fetching API keys (debug)...').start();
15
+ const { data, error } = await supabase
16
+ .from('api_keys')
17
+ .select('id, name, key_prefix, organization_id, revoked, created_at, last_used_at')
18
+ .limit(100);
19
+ if (error) {
20
+ spinner.fail(chalk.red(`Error fetching API keys: ${error.message}`));
21
+ console.log(chalk.yellow('Hint: This is a debug command and may require elevated permissions.'));
22
+ return;
23
+ }
24
+ spinner.succeed(chalk.green(`Found ${data?.length || 0} API keys`));
25
+ if (!data || data.length === 0) {
26
+ console.log(chalk.yellow('No API keys visible with current permissions.'));
27
+ return;
28
+ }
29
+ for (const row of data) {
30
+ console.log(`${chalk.cyan(row.id)} ${chalk.white(row.name)} prefix=${chalk.magenta(row.key_prefix)} org=${chalk.yellow(row.organization_id)} revoked=${row.revoked ? chalk.red('true') : chalk.green('false')} last_used=${chalk.gray(row.last_used_at || 'never')}`);
31
+ }
32
+ }
33
+ export async function debugListOrganizations() {
34
+ // Initialize supabase (with hardcoded credentials)
35
+ try {
36
+ initializeSupabase();
37
+ }
38
+ catch (e) {
39
+ console.error(chalk.red('āœ— Failed to connect to Langctl servers.'));
40
+ return;
41
+ }
42
+ const supabase = getSupabase();
43
+ const spinner = ora('Fetching organizations (debug)...').start();
44
+ const { data, error } = await supabase
45
+ .from('organizations')
46
+ .select('id, name, plan, created_at')
47
+ .limit(100);
48
+ if (error) {
49
+ spinner.fail(chalk.red(`Error fetching organizations: ${error.message}`));
50
+ return;
51
+ }
52
+ spinner.succeed(chalk.green(`Found ${data?.length || 0} organizations`));
53
+ (data || []).forEach(o => {
54
+ console.log(`${chalk.cyan(o.id)} ${chalk.white(o.name)} plan=${chalk.magenta(o.plan)} created=${chalk.gray(o.created_at)}`);
55
+ });
56
+ }
57
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1,2 @@
1
+ export declare function initCommand(): Promise<void>;
2
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1,87 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { config } from '../config.js';
5
+ import { authenticate } from '../auth.js';
6
+ import { initializeSupabase } from '../supabase.js';
7
+ import { showLogo } from '../utils/banner.js';
8
+ export async function initCommand() {
9
+ // Show logo
10
+ showLogo();
11
+ console.log(chalk.blue.bold('šŸš€ CLI Setup\n'));
12
+ console.log(chalk.gray('Get your API key from: https://app.langctl.com/dashboard/settings/api-keys\n'));
13
+ // Check if already configured
14
+ if (config.isConfigured()) {
15
+ const { confirm } = await inquirer.prompt([
16
+ {
17
+ type: 'confirm',
18
+ name: 'confirm',
19
+ message: 'Configuration already exists. Do you want to reconfigure?',
20
+ default: false
21
+ }
22
+ ]);
23
+ if (!confirm) {
24
+ console.log(chalk.yellow('\nSetup cancelled.'));
25
+ return;
26
+ }
27
+ }
28
+ // Initialize Supabase client (with hardcoded credentials - user doesn't need to know)
29
+ try {
30
+ initializeSupabase();
31
+ }
32
+ catch (error) {
33
+ console.error(chalk.red('āœ— Failed to connect to Langctl servers'));
34
+ console.error(chalk.red('Please check your internet connection and try again.\n'));
35
+ return;
36
+ }
37
+ // Ask for API key
38
+ const { apiKey } = await inquirer.prompt([
39
+ {
40
+ type: 'password',
41
+ name: 'apiKey',
42
+ message: 'Enter your API key:',
43
+ mask: '*',
44
+ filter: (input) => input?.trim(),
45
+ validate: (input) => {
46
+ const value = (input || '').trim();
47
+ if (!value || !value.startsWith('lc_')) {
48
+ return 'Please enter a valid API key (starts with lc_)';
49
+ }
50
+ if (value.length !== 67) {
51
+ return 'Invalid API key format (should be 67 characters)';
52
+ }
53
+ return true;
54
+ }
55
+ }
56
+ ]);
57
+ // Authenticate
58
+ const spinner = ora('Verifying API key...').start();
59
+ const result = await authenticate(apiKey);
60
+ if (result.success) {
61
+ spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
62
+ }
63
+ else {
64
+ spinner.fail(chalk.red(result.message));
65
+ console.log(chalk.yellow('\nTroubleshooting:'));
66
+ console.log(chalk.white(' - Check that your API key is correct'));
67
+ console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
68
+ console.log(chalk.white(' - Generate a new key if needed: https://app.langctl.com/dashboard/settings/api-keys\n'));
69
+ return;
70
+ }
71
+ // Ask for default settings
72
+ const { defaultLanguage } = await inquirer.prompt([
73
+ {
74
+ type: 'input',
75
+ name: 'defaultLanguage',
76
+ message: 'Default language code:',
77
+ default: 'en'
78
+ }
79
+ ]);
80
+ config.set('defaultLanguage', defaultLanguage);
81
+ console.log(chalk.green.bold('\nāœ“ Setup complete!\n'));
82
+ console.log(chalk.gray(`Configuration saved to: ${config.getConfigPath()}\n`));
83
+ console.log(chalk.blue('Next steps:'));
84
+ console.log(chalk.white(' 1. Run "langctl projects list" to see your projects'));
85
+ console.log(chalk.white(' 2. Run "langctl pull <project-slug>" to download translations\n'));
86
+ }
87
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,2 @@
1
+ export declare function projectsListCommand(): Promise<void>;
2
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1,54 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getSupabase } from '../supabase.js';
4
+ import { isAuthenticated, getOrganizationId } from '../auth.js';
5
+ export async function projectsListCommand() {
6
+ // Check authentication
7
+ if (!isAuthenticated()) {
8
+ console.log(chalk.red('āœ— Not authenticated. Please run "langctl auth <api-key>" first.\n'));
9
+ return;
10
+ }
11
+ const organizationId = getOrganizationId();
12
+ if (!organizationId) {
13
+ console.log(chalk.red('āœ— Organization ID not found. Please run "langctl init" again.\n'));
14
+ return;
15
+ }
16
+ const spinner = ora('Fetching projects...').start();
17
+ try {
18
+ const supabase = getSupabase();
19
+ const { data, error } = await supabase
20
+ .from('projects')
21
+ .select('*')
22
+ .eq('organization_id', organizationId)
23
+ .is('deleted_at', null)
24
+ .order('created_at', { ascending: false });
25
+ if (error) {
26
+ spinner.fail(chalk.red('Failed to fetch projects'));
27
+ console.error(chalk.red(`Error: ${error.message}\n`));
28
+ return;
29
+ }
30
+ spinner.stop();
31
+ if (!data || data.length === 0) {
32
+ console.log(chalk.yellow('\nNo projects found. Create one in the dashboard first.\n'));
33
+ return;
34
+ }
35
+ console.log(chalk.blue.bold(`\nšŸ“¦ Projects (${data.length})\n`));
36
+ data.forEach((project) => {
37
+ console.log(chalk.white.bold(project.name));
38
+ console.log(chalk.gray(` Slug: ${project.slug}`));
39
+ if (project.description) {
40
+ console.log(chalk.gray(` Description: ${project.description}`));
41
+ }
42
+ console.log(chalk.gray(` Languages: ${project.languages.join(', ')}`));
43
+ console.log(chalk.gray(` Default: ${project.default_language}`));
44
+ console.log('');
45
+ });
46
+ console.log(chalk.blue('To pull translations, run:'));
47
+ console.log(chalk.white(` langctl pull ${data[0].slug}\n`));
48
+ }
49
+ catch (error) {
50
+ spinner.fail(chalk.red('Failed to fetch projects'));
51
+ console.error(chalk.red(`Error: ${error.message}\n`));
52
+ }
53
+ }
54
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1,9 @@
1
+ export interface PullOptions {
2
+ language?: string;
3
+ format?: string;
4
+ output?: string;
5
+ dir?: string;
6
+ publishedOnly?: boolean;
7
+ }
8
+ export declare function pullCommand(projectIdentifier: string, options: PullOptions): Promise<void>;
9
+ //# sourceMappingURL=pull.d.ts.map