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.
- package/NPM_PUBLISH_GUIDE.md +293 -0
- package/QUICK_PUBLISH.md +64 -0
- package/README.md +287 -32
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +106 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +33 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +63 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +54 -0
- package/dist/commands/pull.d.ts +9 -0
- package/dist/commands/pull.js +194 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +71 -0
- package/dist/exporters/index.d.ts +36 -0
- package/dist/exporters/index.js +214 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +166 -0
- package/dist/supabase.d.ts +14 -0
- package/dist/supabase.js +33 -0
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +24 -0
- package/package.json +22 -3
- package/translations/en.json +5 -0
- package/translations/en.xml +9 -0
- package/translations/fr.json +4 -0
- package/translations/fr.xml +7 -0
- package/translations/hi.json +4 -0
- package/translations/hi.xml +7 -0
- package/translations/ios/en.strings +9 -0
- package/translations/ios/fr.strings +6 -0
- package/translations/ios/hi.strings +6 -0
- package/translations/ios/ja.strings +6 -0
- package/translations/ja.json +4 -0
- package/translations/ja.xml +7 -0
- package/PUBLISH_GUIDE.md +0 -209
- package/bin/langctl.js +0 -19
- 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,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,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,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,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,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
|