langctl 0.0.1 → 0.1.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 (43) hide show
  1. package/NPM_PUBLISH_GUIDE.md +293 -0
  2. package/QUICK_PUBLISH.md +64 -0
  3. package/README.md +287 -32
  4. package/dist/auth.d.ts +33 -0
  5. package/dist/auth.js +106 -0
  6. package/dist/commands/auth.d.ts +3 -0
  7. package/dist/commands/auth.js +41 -0
  8. package/dist/commands/config.d.ts +2 -0
  9. package/dist/commands/config.js +33 -0
  10. package/dist/commands/debug.d.ts +3 -0
  11. package/dist/commands/debug.js +63 -0
  12. package/dist/commands/init.d.ts +2 -0
  13. package/dist/commands/init.js +93 -0
  14. package/dist/commands/projects.d.ts +2 -0
  15. package/dist/commands/projects.js +54 -0
  16. package/dist/commands/pull.d.ts +9 -0
  17. package/dist/commands/pull.js +194 -0
  18. package/dist/config.d.ts +49 -0
  19. package/dist/config.js +71 -0
  20. package/dist/exporters/index.d.ts +36 -0
  21. package/dist/exporters/index.js +214 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +166 -0
  24. package/dist/supabase.d.ts +14 -0
  25. package/dist/supabase.js +33 -0
  26. package/dist/utils/banner.d.ts +9 -0
  27. package/dist/utils/banner.js +24 -0
  28. package/package.json +22 -3
  29. package/translations/en.json +5 -0
  30. package/translations/en.xml +9 -0
  31. package/translations/fr.json +4 -0
  32. package/translations/fr.xml +7 -0
  33. package/translations/hi.json +4 -0
  34. package/translations/hi.xml +7 -0
  35. package/translations/ios/en.strings +9 -0
  36. package/translations/ios/fr.strings +6 -0
  37. package/translations/ios/hi.strings +6 -0
  38. package/translations/ios/ja.strings +6 -0
  39. package/translations/ja.json +4 -0
  40. package/translations/ja.xml +7 -0
  41. package/PUBLISH_GUIDE.md +0 -209
  42. package/bin/langctl.js +0 -19
  43. package/index.js +0 -26
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,41 @@
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
+ // Hardcoded Supabase credentials
7
+ const SUPABASE_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co';
8
+ const SUPABASE_ANON_KEY = 'sb_publishable_WCpKr-NwubrPtOj2E0cvAQ_TzvfXsNB';
9
+ export async function authCommand(apiKey) {
10
+ // Ensure Supabase is configured
11
+ if (!config.get('supabaseUrl')) {
12
+ config.set('supabaseUrl', SUPABASE_URL);
13
+ config.set('supabaseAnonKey', SUPABASE_ANON_KEY);
14
+ }
15
+ // Initialize Supabase
16
+ try {
17
+ initializeSupabase();
18
+ }
19
+ catch (error) {
20
+ console.log(chalk.red('✗ Failed to connect to Langctl servers.\n'));
21
+ return;
22
+ }
23
+ const spinner = ora('Authenticating...').start();
24
+ const result = await authenticate(apiKey);
25
+ if (result.success) {
26
+ spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
27
+ console.log(chalk.gray(`\nAPI Key saved to: ${config.getConfigPath()}\n`));
28
+ }
29
+ else {
30
+ spinner.fail(chalk.red(result.message));
31
+ console.log(chalk.yellow('\nTroubleshooting:'));
32
+ console.log(chalk.white(' - Check that your API key is correct'));
33
+ console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
34
+ console.log(chalk.white(' - Generate a new key: https://langctl.com/dashboard/settings/api-keys\n'));
35
+ }
36
+ }
37
+ export async function logoutCommand() {
38
+ logout();
39
+ console.log(chalk.green('✓ Logged out successfully\n'));
40
+ }
41
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,2 @@
1
+ export declare function configCommand(): void;
2
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,33 @@
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
+ ...allConfig,
13
+ apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
14
+ supabaseAnonKey: allConfig.supabaseAnonKey ? maskSensitive(allConfig.supabaseAnonKey) : undefined
15
+ };
16
+ Object.entries(displayConfig).forEach(([key, value]) => {
17
+ if (value !== undefined) {
18
+ console.log(chalk.white(`${key}: ${chalk.cyan(value)}`));
19
+ }
20
+ });
21
+ console.log(chalk.gray(`\nConfig file: ${config.getConfigPath()}\n`));
22
+ }
23
+ function maskApiKey(key) {
24
+ if (key.length < 10)
25
+ return '***';
26
+ return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
27
+ }
28
+ function maskSensitive(value) {
29
+ if (value.length < 8)
30
+ return '***';
31
+ return `${value.substring(0, 8)}...`;
32
+ }
33
+ //# 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,63 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getSupabase, initializeSupabase } from '../supabase.js';
4
+ import { config } from '../config.js';
5
+ export async function debugListApiKeys() {
6
+ // Ensure supabase is initialized
7
+ const url = config.get('supabaseUrl');
8
+ const key = config.get('supabaseAnonKey');
9
+ try {
10
+ initializeSupabase(url, key);
11
+ }
12
+ catch (e) {
13
+ console.error(chalk.red('✗ Supabase not initialized. Run "langctl init" first.'));
14
+ return;
15
+ }
16
+ const supabase = getSupabase();
17
+ const spinner = ora('Fetching API keys (debug)...').start();
18
+ const { data, error } = await supabase
19
+ .from('api_keys')
20
+ .select('id, name, key_prefix, organization_id, revoked, created_at, last_used_at')
21
+ .limit(100);
22
+ if (error) {
23
+ spinner.fail(chalk.red(`Error fetching API keys: ${error.message}`));
24
+ console.log(chalk.yellow('Hint: Check that the anon key has SELECT access to api_keys in this project.'));
25
+ return;
26
+ }
27
+ spinner.succeed(chalk.green(`Found ${data?.length || 0} API keys`));
28
+ if (!data || data.length === 0) {
29
+ console.log(chalk.yellow('No API keys visible with current Supabase credentials.'));
30
+ console.log(chalk.white(`Supabase URL: ${url}`));
31
+ console.log(chalk.white(`Anon key prefix: ${(key || '').substring(0, 12)}...`));
32
+ return;
33
+ }
34
+ for (const row of data) {
35
+ 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')}`);
36
+ }
37
+ }
38
+ export async function debugListOrganizations() {
39
+ const url = config.get('supabaseUrl');
40
+ const key = config.get('supabaseAnonKey');
41
+ try {
42
+ initializeSupabase(url, key);
43
+ }
44
+ catch (e) {
45
+ console.error(chalk.red('✗ Supabase not initialized. Run "langctl init" first.'));
46
+ return;
47
+ }
48
+ const supabase = getSupabase();
49
+ const spinner = ora('Fetching organizations (debug)...').start();
50
+ const { data, error } = await supabase
51
+ .from('organizations')
52
+ .select('id, name, plan, created_at')
53
+ .limit(100);
54
+ if (error) {
55
+ spinner.fail(chalk.red(`Error fetching organizations: ${error.message}`));
56
+ return;
57
+ }
58
+ spinner.succeed(chalk.green(`Found ${data?.length || 0} organizations`));
59
+ (data || []).forEach(o => {
60
+ console.log(`${chalk.cyan(o.id)} ${chalk.white(o.name)} plan=${chalk.magenta(o.plan)} created=${chalk.gray(o.created_at)}`);
61
+ });
62
+ }
63
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1,2 @@
1
+ export declare function initCommand(): Promise<void>;
2
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1,93 @@
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
+ // Hardcoded Supabase credentials - users don't need to know about our backend
9
+ const SUPABASE_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co';
10
+ const SUPABASE_ANON_KEY = 'sb_publishable_WCpKr-NwubrPtOj2E0cvAQ_TzvfXsNB';
11
+ export async function initCommand() {
12
+ // Show logo
13
+ showLogo();
14
+ console.log(chalk.blue.bold('🚀 CLI Setup\n'));
15
+ console.log(chalk.gray('Get your API key from: https://app.langctl.com/dashboard/settings/api-keys\n'));
16
+ // Check if already configured
17
+ if (config.isConfigured()) {
18
+ const { confirm } = await inquirer.prompt([
19
+ {
20
+ type: 'confirm',
21
+ name: 'confirm',
22
+ message: 'Configuration already exists. Do you want to reconfigure?',
23
+ default: false
24
+ }
25
+ ]);
26
+ if (!confirm) {
27
+ console.log(chalk.yellow('\nSetup cancelled.'));
28
+ return;
29
+ }
30
+ }
31
+ // Automatically save Supabase credentials (user doesn't need to know)
32
+ config.set('supabaseUrl', SUPABASE_URL);
33
+ config.set('supabaseAnonKey', SUPABASE_ANON_KEY);
34
+ // Initialize Supabase client
35
+ try {
36
+ initializeSupabase(SUPABASE_URL, SUPABASE_ANON_KEY);
37
+ }
38
+ catch (error) {
39
+ console.error(chalk.red('✗ Failed to connect to Langctl servers'));
40
+ console.error(chalk.red('Please check your internet connection and try again.\n'));
41
+ return;
42
+ }
43
+ // Ask for API key
44
+ const { apiKey } = await inquirer.prompt([
45
+ {
46
+ type: 'password',
47
+ name: 'apiKey',
48
+ message: 'Enter your API key:',
49
+ mask: '*',
50
+ filter: (input) => input?.trim(),
51
+ validate: (input) => {
52
+ const value = (input || '').trim();
53
+ if (!value || !value.startsWith('lc_')) {
54
+ return 'Please enter a valid API key (starts with lc_)';
55
+ }
56
+ if (value.length !== 67) {
57
+ return 'Invalid API key format (should be 67 characters)';
58
+ }
59
+ return true;
60
+ }
61
+ }
62
+ ]);
63
+ // Authenticate
64
+ const spinner = ora('Verifying API key...').start();
65
+ const result = await authenticate(apiKey);
66
+ if (result.success) {
67
+ spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
68
+ }
69
+ else {
70
+ spinner.fail(chalk.red(result.message));
71
+ console.log(chalk.yellow('\nTroubleshooting:'));
72
+ console.log(chalk.white(' - Check that your API key is correct'));
73
+ console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
74
+ console.log(chalk.white(' - Generate a new key if needed: https://app.langctl.com/dashboard/settings/api-keys\n'));
75
+ return;
76
+ }
77
+ // Ask for default settings
78
+ const { defaultLanguage } = await inquirer.prompt([
79
+ {
80
+ type: 'input',
81
+ name: 'defaultLanguage',
82
+ message: 'Default language code:',
83
+ default: 'en'
84
+ }
85
+ ]);
86
+ config.set('defaultLanguage', defaultLanguage);
87
+ console.log(chalk.green.bold('\n✓ Setup complete!\n'));
88
+ console.log(chalk.gray(`Configuration saved to: ${config.getConfigPath()}\n`));
89
+ console.log(chalk.blue('Next steps:'));
90
+ console.log(chalk.white(' 1. Run "langctl projects list" to see your projects'));
91
+ console.log(chalk.white(' 2. Run "langctl pull <project-slug>" to download translations\n'));
92
+ }
93
+ //# 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
@@ -0,0 +1,194 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { writeFileSync, mkdirSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { getSupabase } from '../supabase.js';
6
+ import { isAuthenticated, getOrganizationId } from '../auth.js';
7
+ import { exportTranslations } from '../exporters/index.js';
8
+ /**
9
+ * Get platform-specific output directory
10
+ */
11
+ function getPlatformDirectory(format) {
12
+ const platformDirs = {
13
+ 'json': 'i18n',
14
+ 'json-nested': 'i18n',
15
+ 'ios': 'ios',
16
+ 'android': 'android',
17
+ 'flutter': 'flutter'
18
+ };
19
+ return platformDirs[format] || 'i18n';
20
+ }
21
+ /**
22
+ * Resolve project identifier (name, slug, or UUID) to project UUID
23
+ */
24
+ async function resolveProjectId(identifier, organizationId) {
25
+ const supabase = getSupabase();
26
+ // Check if it's already a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
27
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
+ if (uuidRegex.test(identifier)) {
29
+ // Verify it exists
30
+ const { data } = await supabase
31
+ .from('projects')
32
+ .select('id')
33
+ .eq('id', identifier)
34
+ .eq('organization_id', organizationId)
35
+ .is('deleted_at', null)
36
+ .single();
37
+ return data ? identifier : null;
38
+ }
39
+ // Try to match by slug (case-insensitive)
40
+ const { data: bySlug } = await supabase
41
+ .from('projects')
42
+ .select('id')
43
+ .eq('organization_id', organizationId)
44
+ .ilike('slug', identifier)
45
+ .is('deleted_at', null)
46
+ .single();
47
+ if (bySlug) {
48
+ return bySlug.id;
49
+ }
50
+ // Try to match by name (case-insensitive)
51
+ const { data: byName } = await supabase
52
+ .from('projects')
53
+ .select('id')
54
+ .eq('organization_id', organizationId)
55
+ .ilike('name', identifier)
56
+ .is('deleted_at', null)
57
+ .single();
58
+ if (byName) {
59
+ return byName.id;
60
+ }
61
+ return null;
62
+ }
63
+ export async function pullCommand(projectIdentifier, options) {
64
+ // Check authentication
65
+ if (!isAuthenticated()) {
66
+ console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
67
+ return;
68
+ }
69
+ const organizationId = getOrganizationId();
70
+ if (!organizationId) {
71
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl init" again.\n'));
72
+ return;
73
+ }
74
+ // Set defaults
75
+ const format = (options.format || 'json');
76
+ const publishedOnly = options.publishedOnly !== false; // Default to true
77
+ const specificLanguage = options.language; // If not specified, pull all languages
78
+ const baseDir = options.dir || './translations'; // Default to ./translations
79
+ // Validate format
80
+ const validFormats = ['json', 'json-nested', 'ios', 'android', 'flutter'];
81
+ if (!validFormats.includes(format)) {
82
+ console.log(chalk.red(`✗ Invalid format: ${format}`));
83
+ console.log(chalk.yellow(`Valid formats: ${validFormats.join(', ')}\n`));
84
+ return;
85
+ }
86
+ const spinner = ora('Resolving project...').start();
87
+ try {
88
+ const supabase = getSupabase();
89
+ // Resolve project identifier to UUID
90
+ const projectId = await resolveProjectId(projectIdentifier, organizationId);
91
+ if (!projectId) {
92
+ spinner.fail(chalk.red(`Project "${projectIdentifier}" not found`));
93
+ console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
94
+ return;
95
+ }
96
+ // Fetch project details
97
+ spinner.text = 'Fetching project...';
98
+ const { data: project, error: projectError } = await supabase
99
+ .from('projects')
100
+ .select('id, name, slug, languages, default_language')
101
+ .eq('id', projectId)
102
+ .eq('organization_id', organizationId)
103
+ .is('deleted_at', null)
104
+ .single();
105
+ if (projectError || !project) {
106
+ spinner.fail(chalk.red('Project not found or access denied'));
107
+ return;
108
+ }
109
+ // Determine which languages to pull
110
+ let languagesToPull;
111
+ if (specificLanguage) {
112
+ // Check if specified language exists in project
113
+ if (!project.languages.includes(specificLanguage)) {
114
+ spinner.fail(chalk.red(`Language "${specificLanguage}" not found in project`));
115
+ console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
116
+ return;
117
+ }
118
+ languagesToPull = [specificLanguage];
119
+ }
120
+ else {
121
+ // Pull all languages
122
+ languagesToPull = project.languages;
123
+ }
124
+ spinner.text = `Fetching translations for "${project.name}"...`;
125
+ // Fetch translation keys
126
+ let query = supabase
127
+ .from('translation_keys')
128
+ .select('key, translations, description, module')
129
+ .eq('project_id', projectId)
130
+ .is('deleted_at', null);
131
+ if (publishedOnly) {
132
+ query = query.eq('published', true);
133
+ }
134
+ const { data: keys, error: keysError } = await query;
135
+ if (keysError) {
136
+ spinner.fail(chalk.red('Failed to fetch translations'));
137
+ console.error(chalk.red(`Error: ${keysError.message}\n`));
138
+ return;
139
+ }
140
+ if (!keys || keys.length === 0) {
141
+ spinner.fail(chalk.yellow('No translations found'));
142
+ console.log(chalk.yellow(`\nNo ${publishedOnly ? 'published ' : ''}translations found for this project.\n`));
143
+ return;
144
+ }
145
+ spinner.succeed(chalk.green(`Found ${keys.length} translation keys`));
146
+ // Get platform directory
147
+ const platformDir = getPlatformDirectory(format);
148
+ // Export translations for each language
149
+ const exportedFiles = [];
150
+ for (const language of languagesToPull) {
151
+ spinner.start(`Exporting ${language}...`);
152
+ const result = exportTranslations(keys, language, format);
153
+ // Determine output path
154
+ let outputPath;
155
+ if (options.output) {
156
+ // If custom output is specified and multiple languages, append language code
157
+ if (languagesToPull.length > 1) {
158
+ const ext = result.filename.split('.').pop();
159
+ const base = options.output.replace(/\.[^.]+$/, '');
160
+ outputPath = `${base}-${language}.${ext}`;
161
+ }
162
+ else {
163
+ outputPath = options.output;
164
+ }
165
+ }
166
+ else {
167
+ // Default output path: <baseDir>/<platform>/<language>.<ext>
168
+ const ext = result.filename.split('.').pop();
169
+ outputPath = join(baseDir, platformDir, `${language}.${ext}`);
170
+ }
171
+ // Create directory if it doesn't exist
172
+ const dir = dirname(outputPath);
173
+ mkdirSync(dir, { recursive: true });
174
+ writeFileSync(outputPath, result.content, 'utf-8');
175
+ exportedFiles.push(outputPath);
176
+ spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
177
+ }
178
+ // Summary
179
+ console.log(chalk.hex('#10b981').bold('\n📦 Export Summary:'));
180
+ console.log(chalk.white(` Project: ${project.name}`));
181
+ console.log(chalk.white(` Format: ${format}`));
182
+ console.log(chalk.white(` Platform: ${platformDir}`));
183
+ console.log(chalk.white(` Output: ${baseDir}`));
184
+ console.log(chalk.white(` Keys: ${keys.length}`));
185
+ console.log(chalk.white(` Languages: ${languagesToPull.join(', ')}`));
186
+ console.log(chalk.white(` Files: ${exportedFiles.length}`));
187
+ console.log('');
188
+ }
189
+ catch (error) {
190
+ spinner.fail(chalk.red('Failed to pull translations'));
191
+ console.error(chalk.red(`Error: ${error.message}\n`));
192
+ }
193
+ }
194
+ //# sourceMappingURL=pull.js.map