langctl 0.1.1 → 0.2.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/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/CONTRIBUTING.md +33 -0
- package/LICENSE +1 -1
- package/README.md +1018 -113
- package/dist/api.d.ts +20 -0
- package/dist/api.js +75 -0
- package/dist/auth.d.ts +7 -2
- package/dist/auth.js +27 -27
- package/dist/commands/auth.js +2 -10
- package/dist/commands/config.js +1 -0
- package/dist/commands/debug.js +45 -41
- package/dist/commands/export.d.ts +13 -0
- package/dist/commands/export.js +125 -0
- package/dist/commands/import.d.ts +12 -0
- package/dist/commands/import.js +150 -0
- package/dist/commands/init.js +0 -10
- package/dist/commands/keys.d.ts +25 -0
- package/dist/commands/keys.js +294 -0
- package/dist/commands/org.d.ts +13 -0
- package/dist/commands/org.js +118 -0
- package/dist/commands/projects.d.ts +31 -0
- package/dist/commands/projects.js +295 -24
- package/dist/commands/pull.js +40 -95
- package/dist/commands/team.d.ts +29 -0
- package/dist/commands/team.js +270 -0
- package/dist/config.d.ts +1 -0
- package/dist/index.js +357 -6
- package/package.json +53 -7
- package/NPM_PUBLISH_GUIDE.md +0 -293
- package/QUICK_PUBLISH.md +0 -64
- package/SECURITY_FIX.md +0 -167
- package/dist/supabase.d.ts +0 -14
- package/dist/supabase.js +0 -30
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class ApiClient {
|
|
2
|
+
private baseUrl;
|
|
3
|
+
private apiKey;
|
|
4
|
+
constructor(baseUrl: string, apiKey: string);
|
|
5
|
+
private request;
|
|
6
|
+
get<T>(path: string, params?: Record<string, string>): Promise<T>;
|
|
7
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
8
|
+
patch<T>(path: string, body: unknown): Promise<T>;
|
|
9
|
+
delete<T>(path: string): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create an API client from the current config.
|
|
13
|
+
* Throws if not authenticated.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getApiClient(): ApiClient;
|
|
16
|
+
/**
|
|
17
|
+
* Create an API client with a specific API key (for auth flow before key is saved).
|
|
18
|
+
*/
|
|
19
|
+
export declare function createApiClient(apiKey: string): ApiClient;
|
|
20
|
+
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
const DEFAULT_API_URL = 'https://api.langctl.com/api/v1';
|
|
3
|
+
export class ApiClient {
|
|
4
|
+
constructor(baseUrl, apiKey) {
|
|
5
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
6
|
+
this.apiKey = apiKey;
|
|
7
|
+
}
|
|
8
|
+
async request(method, path, body, params) {
|
|
9
|
+
let url = `${this.baseUrl}${path}`;
|
|
10
|
+
if (params && Object.keys(params).length > 0) {
|
|
11
|
+
const searchParams = new URLSearchParams();
|
|
12
|
+
for (const [key, value] of Object.entries(params)) {
|
|
13
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
14
|
+
searchParams.append(key, value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const qs = searchParams.toString();
|
|
18
|
+
if (qs)
|
|
19
|
+
url += `?${qs}`;
|
|
20
|
+
}
|
|
21
|
+
const headers = {
|
|
22
|
+
'X-API-Key': this.apiKey,
|
|
23
|
+
};
|
|
24
|
+
if (body !== undefined) {
|
|
25
|
+
headers['Content-Type'] = 'application/json';
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method,
|
|
29
|
+
headers,
|
|
30
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
31
|
+
signal: AbortSignal.timeout(30000),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const errorData = await response.json().catch(() => ({}));
|
|
35
|
+
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
// Handle 204 No Content
|
|
38
|
+
if (response.status === 204) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return response.json();
|
|
42
|
+
}
|
|
43
|
+
async get(path, params) {
|
|
44
|
+
return this.request('GET', path, undefined, params);
|
|
45
|
+
}
|
|
46
|
+
async post(path, body) {
|
|
47
|
+
return this.request('POST', path, body);
|
|
48
|
+
}
|
|
49
|
+
async patch(path, body) {
|
|
50
|
+
return this.request('PATCH', path, body);
|
|
51
|
+
}
|
|
52
|
+
async delete(path) {
|
|
53
|
+
return this.request('DELETE', path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create an API client from the current config.
|
|
58
|
+
* Throws if not authenticated.
|
|
59
|
+
*/
|
|
60
|
+
export function getApiClient() {
|
|
61
|
+
const apiKey = config.get('apiKey');
|
|
62
|
+
const baseUrl = config.get('apiBaseUrl') || DEFAULT_API_URL;
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
throw new Error('Not authenticated. Please run "langctl auth <api-key>" first.');
|
|
65
|
+
}
|
|
66
|
+
return new ApiClient(baseUrl, apiKey);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create an API client with a specific API key (for auth flow before key is saved).
|
|
70
|
+
*/
|
|
71
|
+
export function createApiClient(apiKey) {
|
|
72
|
+
const baseUrl = config.get('apiBaseUrl') || DEFAULT_API_URL;
|
|
73
|
+
return new ApiClient(baseUrl, apiKey);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=api.js.map
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export interface ApiKeyData {
|
|
2
|
-
|
|
2
|
+
organizationId: string;
|
|
3
3
|
organization: {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
|
6
|
+
slug: string;
|
|
6
7
|
plan: string;
|
|
7
8
|
};
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
|
-
* Verify API key via
|
|
11
|
+
* Verify API key via Fastify API
|
|
11
12
|
*/
|
|
12
13
|
export declare function verifyApiKey(plainTextKey: string): Promise<ApiKeyData | null>;
|
|
13
14
|
/**
|
|
@@ -26,6 +27,10 @@ export declare function isAuthenticated(): boolean;
|
|
|
26
27
|
* Get current organization ID
|
|
27
28
|
*/
|
|
28
29
|
export declare function getOrganizationId(): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Get current API key
|
|
32
|
+
*/
|
|
33
|
+
export declare function getApiKey(): string | undefined;
|
|
29
34
|
/**
|
|
30
35
|
* Logout (clear authentication)
|
|
31
36
|
*/
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { config } from './config.js';
|
|
2
|
-
|
|
3
|
-
const EDGE_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/verify-api-key';
|
|
2
|
+
import { createApiClient } from './api.js';
|
|
4
3
|
// Normalize and validate API key input
|
|
5
4
|
function sanitizeApiKey(input) {
|
|
6
5
|
const raw = (input || '').trim().toLowerCase();
|
|
@@ -15,7 +14,7 @@ function sanitizeApiKey(input) {
|
|
|
15
14
|
return { ok: true, key: `lc_${hexOnly}` };
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
|
-
* Verify API key via
|
|
17
|
+
* Verify API key via Fastify API
|
|
19
18
|
*/
|
|
20
19
|
export async function verifyApiKey(plainTextKey) {
|
|
21
20
|
try {
|
|
@@ -23,33 +22,28 @@ export async function verifyApiKey(plainTextKey) {
|
|
|
23
22
|
if (!sanitized.ok || !sanitized.key) {
|
|
24
23
|
return null;
|
|
25
24
|
}
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify({ apiKey: sanitized.key })
|
|
25
|
+
// Create a temporary API client with the key to validate
|
|
26
|
+
const client = createApiClient(sanitized.key);
|
|
27
|
+
// Validate the API key
|
|
28
|
+
const validateResult = await client.post('/api-keys/validate', {
|
|
29
|
+
apiKey: sanitized.key,
|
|
33
30
|
});
|
|
34
|
-
if (!
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
const data = await response.json();
|
|
38
|
-
if (!data.success) {
|
|
31
|
+
if (!validateResult.valid) {
|
|
39
32
|
return null;
|
|
40
33
|
}
|
|
41
|
-
//
|
|
34
|
+
// Fetch organization details
|
|
35
|
+
const org = await client.get(`/orgs/${validateResult.organizationId}`);
|
|
42
36
|
return {
|
|
43
|
-
|
|
37
|
+
organizationId: validateResult.organizationId,
|
|
44
38
|
organization: {
|
|
45
|
-
id:
|
|
46
|
-
name:
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
id: org.id,
|
|
40
|
+
name: org.name,
|
|
41
|
+
slug: org.slug,
|
|
42
|
+
plan: org.plan,
|
|
43
|
+
},
|
|
49
44
|
};
|
|
50
45
|
}
|
|
51
46
|
catch (error) {
|
|
52
|
-
console.error('Error verifying API key:', error);
|
|
53
47
|
return null;
|
|
54
48
|
}
|
|
55
49
|
}
|
|
@@ -61,25 +55,25 @@ export async function authenticate(apiKey) {
|
|
|
61
55
|
if (!sanitized.ok || !sanitized.key) {
|
|
62
56
|
return {
|
|
63
57
|
success: false,
|
|
64
|
-
message: sanitized.message || 'Invalid API key format. Expected format: lc_[64 hex characters]'
|
|
58
|
+
message: sanitized.message || 'Invalid API key format. Expected format: lc_[64 hex characters]',
|
|
65
59
|
};
|
|
66
60
|
}
|
|
67
|
-
// Verify via
|
|
61
|
+
// Verify via Fastify API
|
|
68
62
|
const keyData = await verifyApiKey(sanitized.key);
|
|
69
63
|
if (!keyData) {
|
|
70
64
|
return {
|
|
71
65
|
success: false,
|
|
72
|
-
message: 'Invalid or revoked API key. Please check your key and try again.'
|
|
66
|
+
message: 'Invalid or revoked API key. Please check your key and try again.',
|
|
73
67
|
};
|
|
74
68
|
}
|
|
75
69
|
// Save to config
|
|
76
70
|
config.set('apiKey', sanitized.key);
|
|
77
|
-
config.set('organizationId', keyData.
|
|
71
|
+
config.set('organizationId', keyData.organizationId);
|
|
78
72
|
config.set('organizationName', keyData.organization.name);
|
|
79
73
|
return {
|
|
80
74
|
success: true,
|
|
81
75
|
message: `Successfully authenticated as ${keyData.organization.name}`,
|
|
82
|
-
data: keyData
|
|
76
|
+
data: keyData,
|
|
83
77
|
};
|
|
84
78
|
}
|
|
85
79
|
/**
|
|
@@ -94,6 +88,12 @@ export function isAuthenticated() {
|
|
|
94
88
|
export function getOrganizationId() {
|
|
95
89
|
return config.get('organizationId');
|
|
96
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Get current API key
|
|
93
|
+
*/
|
|
94
|
+
export function getApiKey() {
|
|
95
|
+
return config.get('apiKey');
|
|
96
|
+
}
|
|
97
97
|
/**
|
|
98
98
|
* Logout (clear authentication)
|
|
99
99
|
*/
|
package/dist/commands/auth.js
CHANGED
|
@@ -2,16 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { authenticate, logout } from '../auth.js';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
|
-
import { initializeSupabase } from '../supabase.js';
|
|
6
5
|
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
6
|
const spinner = ora('Authenticating...').start();
|
|
16
7
|
const result = await authenticate(apiKey);
|
|
17
8
|
if (result.success) {
|
|
@@ -20,10 +11,11 @@ export async function authCommand(apiKey) {
|
|
|
20
11
|
}
|
|
21
12
|
else {
|
|
22
13
|
spinner.fail(chalk.red(result.message));
|
|
14
|
+
process.exitCode = 1;
|
|
23
15
|
console.log(chalk.yellow('\nTroubleshooting:'));
|
|
24
16
|
console.log(chalk.white(' - Check that your API key is correct'));
|
|
25
17
|
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'));
|
|
18
|
+
console.log(chalk.white(' - Generate a new key: https://app.langctl.com/dashboard/settings/api-keys\n'));
|
|
27
19
|
}
|
|
28
20
|
}
|
|
29
21
|
export async function logoutCommand() {
|
package/dist/commands/config.js
CHANGED
|
@@ -9,6 +9,7 @@ export function configCommand() {
|
|
|
9
9
|
}
|
|
10
10
|
// Display config with masked sensitive values
|
|
11
11
|
const displayConfig = {
|
|
12
|
+
apiBaseUrl: allConfig.apiBaseUrl || 'https://api.langctl.com/api/v1 (default)',
|
|
12
13
|
apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
|
|
13
14
|
organizationId: allConfig.organizationId,
|
|
14
15
|
organizationName: allConfig.organizationName,
|
package/dist/commands/debug.js
CHANGED
|
@@ -1,57 +1,61 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import {
|
|
3
|
+
import { isAuthenticated } from '../auth.js';
|
|
4
|
+
import { getApiClient } from '../api.js';
|
|
5
|
+
import { config } from '../config.js';
|
|
4
6
|
export async function debugListApiKeys() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
catch (e) {
|
|
10
|
-
console.error(chalk.red('✗ Failed to connect to Langctl servers.'));
|
|
7
|
+
if (!isAuthenticated()) {
|
|
8
|
+
console.log(chalk.red('✗ Not authenticated.\n'));
|
|
9
|
+
process.exitCode = 1;
|
|
11
10
|
return;
|
|
12
11
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
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.'));
|
|
12
|
+
const orgId = config.get('organizationId');
|
|
13
|
+
if (!orgId) {
|
|
14
|
+
console.log(chalk.red('✗ No organization configured.\n'));
|
|
15
|
+
process.exitCode = 1;
|
|
22
16
|
return;
|
|
23
17
|
}
|
|
24
|
-
spinner
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log(
|
|
18
|
+
const spinner = ora('Testing API connection...').start();
|
|
19
|
+
try {
|
|
20
|
+
const api = getApiClient();
|
|
21
|
+
const org = await api.get(`/orgs/${orgId}`);
|
|
22
|
+
spinner.succeed(chalk.green(`API connection successful`));
|
|
23
|
+
console.log(chalk.blue.bold('\n🔧 Debug Info\n'));
|
|
24
|
+
console.log(chalk.white(`Organization: ${org.name}`));
|
|
25
|
+
console.log(chalk.white(`Plan: ${org.plan}`));
|
|
26
|
+
console.log(chalk.gray(`API URL: ${config.get('apiBaseUrl') || 'https://api.langctl.com/api/v1'}`));
|
|
27
|
+
console.log(chalk.gray(`Config: ${config.getConfigPath()}`));
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
spinner.fail(chalk.red('API connection failed'));
|
|
32
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
33
|
+
process.exitCode = 1;
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
export async function debugListOrganizations() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
console.error(chalk.red('✗ Failed to connect to Langctl servers.'));
|
|
37
|
+
if (!isAuthenticated()) {
|
|
38
|
+
console.log(chalk.red('✗ Not authenticated.\n'));
|
|
39
|
+
process.exitCode = 1;
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.select('id, name, plan, created_at')
|
|
47
|
-
.limit(100);
|
|
48
|
-
if (error) {
|
|
49
|
-
spinner.fail(chalk.red(`Error fetching organizations: ${error.message}`));
|
|
42
|
+
const orgId = config.get('organizationId');
|
|
43
|
+
if (!orgId) {
|
|
44
|
+
console.log(chalk.red('✗ No organization configured.\n'));
|
|
45
|
+
process.exitCode = 1;
|
|
50
46
|
return;
|
|
51
47
|
}
|
|
52
|
-
spinner
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const spinner = ora('Fetching organization...').start();
|
|
49
|
+
try {
|
|
50
|
+
const api = getApiClient();
|
|
51
|
+
const org = await api.get(`/orgs/${orgId}`);
|
|
52
|
+
spinner.succeed(chalk.green('Found organization'));
|
|
53
|
+
console.log(`${chalk.cyan(org.id)} ${chalk.white(org.name)} plan=${chalk.magenta(org.plan)} created=${chalk.gray(org.createdAt)}`);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
spinner.fail(chalk.red('Failed to fetch organization'));
|
|
57
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
56
60
|
}
|
|
57
61
|
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface ExportOptions {
|
|
2
|
+
language?: string;
|
|
3
|
+
format?: string;
|
|
4
|
+
output?: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
includeUnpublished?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Export translations command
|
|
10
|
+
*/
|
|
11
|
+
export declare function exportCommand(projectSlug: string, options: ExportOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { isAuthenticated } from '../auth.js';
|
|
6
|
+
import { getApiClient } from '../api.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
import { exportTranslations } from '../exporters/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Map CLI format names to exporter format names
|
|
11
|
+
*/
|
|
12
|
+
const formatMap = {
|
|
13
|
+
'flat-json': 'json',
|
|
14
|
+
'nested-json': 'json-nested',
|
|
15
|
+
'i18n-json': 'json',
|
|
16
|
+
'android-xml': 'android',
|
|
17
|
+
'ios-strings': 'ios',
|
|
18
|
+
'flutter-arb': 'flutter',
|
|
19
|
+
// Also accept exporter format names directly
|
|
20
|
+
'json': 'json',
|
|
21
|
+
'json-nested': 'json-nested',
|
|
22
|
+
'android': 'android',
|
|
23
|
+
'ios': 'ios',
|
|
24
|
+
'flutter': 'flutter',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get file extension for a given export format
|
|
28
|
+
*/
|
|
29
|
+
function getFileExtension(format, language) {
|
|
30
|
+
switch (format) {
|
|
31
|
+
case 'json':
|
|
32
|
+
case 'json-nested':
|
|
33
|
+
return `${language}.json`;
|
|
34
|
+
case 'android':
|
|
35
|
+
return `strings.xml`;
|
|
36
|
+
case 'ios':
|
|
37
|
+
return `Localizable.strings`;
|
|
38
|
+
case 'flutter':
|
|
39
|
+
return `intl_${language}.arb`;
|
|
40
|
+
default:
|
|
41
|
+
return `${language}.json`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Export translations command
|
|
46
|
+
*/
|
|
47
|
+
export async function exportCommand(projectSlug, options) {
|
|
48
|
+
if (!isAuthenticated()) {
|
|
49
|
+
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const orgId = config.get('organizationId');
|
|
54
|
+
if (!orgId) {
|
|
55
|
+
console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const spinner = ora('Fetching project...').start();
|
|
60
|
+
try {
|
|
61
|
+
const api = getApiClient();
|
|
62
|
+
// Resolve project by slug
|
|
63
|
+
const project = await api.get(`/orgs/${orgId}/projects/by-slug/${projectSlug}`);
|
|
64
|
+
spinner.text = 'Exporting translations...';
|
|
65
|
+
// Determine languages to export
|
|
66
|
+
const languages = options.language
|
|
67
|
+
? [options.language]
|
|
68
|
+
: project.languages;
|
|
69
|
+
// Validate language if specified
|
|
70
|
+
if (options.language && !project.languages.includes(options.language)) {
|
|
71
|
+
spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
|
|
72
|
+
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Resolve format
|
|
76
|
+
const cliFormat = options.format || 'flat-json';
|
|
77
|
+
const exportFormat = formatMap[cliFormat];
|
|
78
|
+
if (!exportFormat) {
|
|
79
|
+
spinner.fail(chalk.red(`Unsupported format: ${cliFormat}`));
|
|
80
|
+
console.log(chalk.yellow(`Valid formats: ${Object.keys(formatMap).join(', ')}\n`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const publishedOnly = !options.includeUnpublished;
|
|
84
|
+
// Export each language
|
|
85
|
+
for (const language of languages) {
|
|
86
|
+
spinner.text = `Exporting ${language}...`;
|
|
87
|
+
// Fetch flat translations from the API
|
|
88
|
+
const params = {
|
|
89
|
+
language,
|
|
90
|
+
publishedOnly: publishedOnly ? 'true' : 'false'
|
|
91
|
+
};
|
|
92
|
+
if (options.module)
|
|
93
|
+
params.module = options.module;
|
|
94
|
+
const result = await api.get(`/orgs/${orgId}/projects/${project.id}/export`, params);
|
|
95
|
+
// Convert flat translations to TranslationKey array for the exporter
|
|
96
|
+
const flatTranslations = result.translations || {};
|
|
97
|
+
const translationKeys = Object.entries(flatTranslations).map(([key, value]) => ({
|
|
98
|
+
key,
|
|
99
|
+
translations: { [language]: value }
|
|
100
|
+
}));
|
|
101
|
+
// Use client-side exporter for format conversion
|
|
102
|
+
const exportResult = exportTranslations(translationKeys, language, exportFormat);
|
|
103
|
+
// Determine output path
|
|
104
|
+
const outputPath = options.output
|
|
105
|
+
? options.output
|
|
106
|
+
: join(process.cwd(), 'translations', getFileExtension(exportFormat, language));
|
|
107
|
+
// Ensure directory exists
|
|
108
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
109
|
+
// Write file
|
|
110
|
+
writeFileSync(outputPath, exportResult.content, 'utf-8');
|
|
111
|
+
spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
|
|
112
|
+
// Show spinner again if there are more languages
|
|
113
|
+
if (languages.indexOf(language) < languages.length - 1) {
|
|
114
|
+
spinner.start();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
console.log(chalk.blue(`\n✓ Exported ${languages.length} language(s) successfully\n`));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
spinner.fail(chalk.red('Failed to export translations'));
|
|
121
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface ImportOptions {
|
|
2
|
+
language: string;
|
|
3
|
+
overwrite?: boolean;
|
|
4
|
+
module?: string;
|
|
5
|
+
publish?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Import translations command
|
|
9
|
+
*/
|
|
10
|
+
export declare function importCommand(projectSlug: string, filePath: string, options: ImportOptions): Promise<void>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=import.d.ts.map
|