langctl 0.1.0 → 0.1.2

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/dist/auth.d.ts CHANGED
@@ -26,6 +26,10 @@ export declare function isAuthenticated(): boolean;
26
26
  * Get current organization ID
27
27
  */
28
28
  export declare function getOrganizationId(): string | undefined;
29
+ /**
30
+ * Get current API key
31
+ */
32
+ export declare function getApiKey(): string | undefined;
29
33
  /**
30
34
  * Logout (clear authentication)
31
35
  */
package/dist/auth.js CHANGED
@@ -94,6 +94,12 @@ export function isAuthenticated() {
94
94
  export function getOrganizationId() {
95
95
  return config.get('organizationId');
96
96
  }
97
+ /**
98
+ * Get current API key
99
+ */
100
+ export function getApiKey() {
101
+ return config.get('apiKey');
102
+ }
97
103
  /**
98
104
  * Logout (clear authentication)
99
105
  */
@@ -3,16 +3,8 @@ import ora from 'ora';
3
3
  import { authenticate, logout } from '../auth.js';
4
4
  import { config } from '../config.js';
5
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
6
  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
7
+ // Initialize Supabase (with hardcoded credentials)
16
8
  try {
17
9
  initializeSupabase();
18
10
  }
@@ -9,9 +9,11 @@ export function configCommand() {
9
9
  }
10
10
  // Display config with masked sensitive values
11
11
  const displayConfig = {
12
- ...allConfig,
13
12
  apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
14
- supabaseAnonKey: allConfig.supabaseAnonKey ? maskSensitive(allConfig.supabaseAnonKey) : undefined
13
+ organizationId: allConfig.organizationId,
14
+ organizationName: allConfig.organizationName,
15
+ defaultProject: allConfig.defaultProject,
16
+ defaultLanguage: allConfig.defaultLanguage
15
17
  };
16
18
  Object.entries(displayConfig).forEach(([key, value]) => {
17
19
  if (value !== undefined) {
@@ -25,9 +27,4 @@ function maskApiKey(key) {
25
27
  return '***';
26
28
  return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
27
29
  }
28
- function maskSensitive(value) {
29
- if (value.length < 8)
30
- return '***';
31
- return `${value.substring(0, 8)}...`;
32
- }
33
30
  //# sourceMappingURL=config.js.map
@@ -1,16 +1,13 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { getSupabase, initializeSupabase } from '../supabase.js';
4
- import { config } from '../config.js';
5
4
  export async function debugListApiKeys() {
6
- // Ensure supabase is initialized
7
- const url = config.get('supabaseUrl');
8
- const key = config.get('supabaseAnonKey');
5
+ // Initialize supabase (with hardcoded credentials)
9
6
  try {
10
- initializeSupabase(url, key);
7
+ initializeSupabase();
11
8
  }
12
9
  catch (e) {
13
- console.error(chalk.red('✗ Supabase not initialized. Run "langctl init" first.'));
10
+ console.error(chalk.red('✗ Failed to connect to Langctl servers.'));
14
11
  return;
15
12
  }
16
13
  const supabase = getSupabase();
@@ -21,14 +18,12 @@ export async function debugListApiKeys() {
21
18
  .limit(100);
22
19
  if (error) {
23
20
  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.'));
21
+ console.log(chalk.yellow('Hint: This is a debug command and may require elevated permissions.'));
25
22
  return;
26
23
  }
27
24
  spinner.succeed(chalk.green(`Found ${data?.length || 0} API keys`));
28
25
  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)}...`));
26
+ console.log(chalk.yellow('No API keys visible with current permissions.'));
32
27
  return;
33
28
  }
34
29
  for (const row of data) {
@@ -36,13 +31,12 @@ export async function debugListApiKeys() {
36
31
  }
37
32
  }
38
33
  export async function debugListOrganizations() {
39
- const url = config.get('supabaseUrl');
40
- const key = config.get('supabaseAnonKey');
34
+ // Initialize supabase (with hardcoded credentials)
41
35
  try {
42
- initializeSupabase(url, key);
36
+ initializeSupabase();
43
37
  }
44
38
  catch (e) {
45
- console.error(chalk.red('✗ Supabase not initialized. Run "langctl init" first.'));
39
+ console.error(chalk.red('✗ Failed to connect to Langctl servers.'));
46
40
  return;
47
41
  }
48
42
  const supabase = getSupabase();
@@ -0,0 +1,14 @@
1
+ type ExportFormat = 'i18n-json' | 'android-xml' | 'ios-strings' | 'flutter-arb' | 'flat-json' | 'nested-json';
2
+ interface ExportOptions {
3
+ language?: string;
4
+ format?: ExportFormat;
5
+ output?: string;
6
+ module?: string;
7
+ includeUnpublished?: boolean;
8
+ }
9
+ /**
10
+ * Export translations command
11
+ */
12
+ export declare function exportCommand(projectSlug: string, options: ExportOptions): Promise<void>;
13
+ export {};
14
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,115 @@
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, getApiKey } from '../auth.js';
6
+ const EDGE_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/export-translations';
7
+ const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
8
+ /**
9
+ * Get project by slug
10
+ */
11
+ async function getProjectBySlug(apiKey, slug) {
12
+ try {
13
+ const response = await fetch(LIST_PROJECTS_URL, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ 'X-API-Key': apiKey
18
+ }
19
+ });
20
+ if (!response.ok) {
21
+ return null;
22
+ }
23
+ const data = await response.json();
24
+ if (!data.success || !data.projects) {
25
+ return null;
26
+ }
27
+ return data.projects.find((p) => p.slug === slug) || null;
28
+ }
29
+ catch (error) {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Export translations command
35
+ */
36
+ export async function exportCommand(projectSlug, options) {
37
+ // Check authentication
38
+ if (!isAuthenticated()) {
39
+ console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
40
+ return;
41
+ }
42
+ const apiKey = getApiKey();
43
+ if (!apiKey) {
44
+ console.log(chalk.red('✗ API key not found. Please run "langctl auth <api-key>" again.\n'));
45
+ return;
46
+ }
47
+ const spinner = ora('Fetching project...').start();
48
+ try {
49
+ // Get project by slug
50
+ const project = await getProjectBySlug(apiKey, projectSlug);
51
+ if (!project) {
52
+ spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
53
+ console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
54
+ return;
55
+ }
56
+ spinner.text = 'Exporting translations...';
57
+ // Determine languages to export
58
+ const languages = options.language
59
+ ? [options.language]
60
+ : project.languages;
61
+ // Validate language if specified
62
+ if (options.language && !project.languages.includes(options.language)) {
63
+ spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
64
+ console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
65
+ return;
66
+ }
67
+ const format = options.format || 'flat-json';
68
+ const publishedOnly = !options.includeUnpublished;
69
+ // Export each language
70
+ for (const language of languages) {
71
+ spinner.text = `Exporting ${language}...`;
72
+ const response = await fetch(EDGE_FUNCTION_URL, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ 'X-API-Key': apiKey
77
+ },
78
+ body: JSON.stringify({
79
+ projectId: project.id,
80
+ language,
81
+ format,
82
+ publishedOnly,
83
+ module: options.module
84
+ })
85
+ });
86
+ if (!response.ok) {
87
+ const errorData = await response.json().catch(() => ({}));
88
+ throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
89
+ }
90
+ const data = await response.json();
91
+ if (!data.success || !data.content) {
92
+ throw new Error(data.error || 'Failed to export translations');
93
+ }
94
+ // Determine output path
95
+ const outputPath = options.output
96
+ ? options.output
97
+ : join(process.cwd(), 'translations', data.filename || `${language}.json`);
98
+ // Ensure directory exists
99
+ mkdirSync(dirname(outputPath), { recursive: true });
100
+ // Write file
101
+ writeFileSync(outputPath, data.content, 'utf-8');
102
+ spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
103
+ // Show spinner again if there are more languages
104
+ if (languages.indexOf(language) < languages.length - 1) {
105
+ spinner.start();
106
+ }
107
+ }
108
+ console.log(chalk.blue(`\n✓ Exported ${languages.length} language(s) successfully\n`));
109
+ }
110
+ catch (error) {
111
+ spinner.fail(chalk.red('Failed to export translations'));
112
+ console.error(chalk.red(`Error: ${error.message}\n`));
113
+ }
114
+ }
115
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,11 @@
1
+ interface ImportOptions {
2
+ language: string;
3
+ overwrite?: boolean;
4
+ publish?: boolean;
5
+ }
6
+ /**
7
+ * Import translations command
8
+ */
9
+ export declare function importCommand(projectSlug: string, filePath: string, options: ImportOptions): Promise<void>;
10
+ export {};
11
+ //# sourceMappingURL=import.d.ts.map
@@ -0,0 +1,171 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { isAuthenticated, getApiKey } from '../auth.js';
6
+ const PUSH_TRANSLATIONS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/push-translations';
7
+ const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
8
+ /**
9
+ * Get project by slug
10
+ */
11
+ async function getProjectBySlug(apiKey, slug) {
12
+ try {
13
+ const response = await fetch(LIST_PROJECTS_URL, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ 'X-API-Key': apiKey
18
+ }
19
+ });
20
+ if (!response.ok) {
21
+ return null;
22
+ }
23
+ const data = await response.json();
24
+ if (!data.success || !data.projects) {
25
+ return null;
26
+ }
27
+ return data.projects.find((p) => p.slug === slug) || null;
28
+ }
29
+ catch (error) {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Parse translation file based on extension
35
+ */
36
+ function parseTranslationFile(filePath) {
37
+ const content = readFileSync(filePath, 'utf-8');
38
+ const ext = filePath.split('.').pop()?.toLowerCase();
39
+ if (ext === 'json') {
40
+ const parsed = JSON.parse(content);
41
+ // Check if it's nested JSON and flatten it
42
+ if (isNestedObject(parsed)) {
43
+ return flattenObject(parsed);
44
+ }
45
+ return parsed;
46
+ }
47
+ throw new Error(`Unsupported file format: .${ext}. Only JSON files are currently supported.`);
48
+ }
49
+ /**
50
+ * Check if object has nested objects
51
+ */
52
+ function isNestedObject(obj) {
53
+ return Object.values(obj).some(value => typeof value === 'object' && value !== null && !Array.isArray(value));
54
+ }
55
+ /**
56
+ * Flatten nested object to dot-notation keys
57
+ */
58
+ function flattenObject(obj, prefix = '') {
59
+ const result = {};
60
+ Object.entries(obj).forEach(([key, value]) => {
61
+ const fullKey = prefix ? `${prefix}.${key}` : key;
62
+ if (typeof value === 'string') {
63
+ result[fullKey] = value;
64
+ }
65
+ else if (typeof value === 'object' && value !== null) {
66
+ Object.assign(result, flattenObject(value, fullKey));
67
+ }
68
+ });
69
+ return result;
70
+ }
71
+ /**
72
+ * Import translations command
73
+ */
74
+ export async function importCommand(projectSlug, filePath, options) {
75
+ // Check authentication
76
+ if (!isAuthenticated()) {
77
+ console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
78
+ return;
79
+ }
80
+ const apiKey = getApiKey();
81
+ if (!apiKey) {
82
+ console.log(chalk.red('✗ API key not found. Please run "langctl auth <api-key>" again.\n'));
83
+ return;
84
+ }
85
+ const spinner = ora('Loading file...').start();
86
+ try {
87
+ // Resolve file path
88
+ const resolvedPath = resolve(filePath);
89
+ // Parse translation file
90
+ spinner.text = 'Parsing translation file...';
91
+ const translations = parseTranslationFile(resolvedPath);
92
+ const keyCount = Object.keys(translations).length;
93
+ if (keyCount === 0) {
94
+ spinner.warn(chalk.yellow('No translations found in file'));
95
+ return;
96
+ }
97
+ spinner.text = 'Fetching project...';
98
+ // Get project by slug
99
+ const project = await getProjectBySlug(apiKey, projectSlug);
100
+ if (!project) {
101
+ spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
102
+ console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
103
+ return;
104
+ }
105
+ // Validate language
106
+ if (!project.languages.includes(options.language)) {
107
+ spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
108
+ console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
109
+ return;
110
+ }
111
+ spinner.text = `Importing ${keyCount} translations...`;
112
+ // Push translations
113
+ const response = await fetch(PUSH_TRANSLATIONS_URL, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ 'X-API-Key': apiKey
118
+ },
119
+ body: JSON.stringify({
120
+ projectId: project.id,
121
+ language: options.language,
122
+ translations,
123
+ publish: options.publish || false,
124
+ createMissing: true
125
+ })
126
+ });
127
+ if (!response.ok) {
128
+ const errorData = await response.json().catch(() => ({}));
129
+ throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
130
+ }
131
+ const data = await response.json();
132
+ if (!data.success && data.errors.length > 0) {
133
+ spinner.fail(chalk.red('Import completed with errors'));
134
+ console.log(chalk.green(`\n✓ Created: ${data.created}`));
135
+ console.log(chalk.blue(`✓ Updated: ${data.updated}`));
136
+ console.log(chalk.yellow(`⊘ Skipped: ${data.skipped}`));
137
+ console.log(chalk.red(`✗ Errors: ${data.errors.length}\n`));
138
+ if (data.errors.length > 0) {
139
+ console.log(chalk.red('Errors:'));
140
+ data.errors.slice(0, 5).forEach(err => {
141
+ console.log(chalk.red(` - ${err.key}: ${err.error}`));
142
+ });
143
+ if (data.errors.length > 5) {
144
+ console.log(chalk.gray(` ... and ${data.errors.length - 5} more errors\n`));
145
+ }
146
+ }
147
+ return;
148
+ }
149
+ spinner.succeed(chalk.green('Import completed successfully'));
150
+ console.log(chalk.green(`\n✓ Created: ${data.created}`));
151
+ console.log(chalk.blue(`✓ Updated: ${data.updated}`));
152
+ console.log(chalk.yellow(`⊘ Skipped: ${data.skipped}`));
153
+ if (options.publish) {
154
+ console.log(chalk.green('✓ Keys published'));
155
+ }
156
+ console.log('');
157
+ }
158
+ catch (error) {
159
+ spinner.fail(chalk.red('Failed to import translations'));
160
+ if (error.code === 'ENOENT') {
161
+ console.error(chalk.red(`Error: File not found: ${filePath}\n`));
162
+ }
163
+ else if (error instanceof SyntaxError) {
164
+ console.error(chalk.red(`Error: Invalid JSON file\n`));
165
+ }
166
+ else {
167
+ console.error(chalk.red(`Error: ${error.message}\n`));
168
+ }
169
+ }
170
+ }
171
+ //# sourceMappingURL=import.js.map
@@ -5,9 +5,6 @@ import { config } from '../config.js';
5
5
  import { authenticate } from '../auth.js';
6
6
  import { initializeSupabase } from '../supabase.js';
7
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
8
  export async function initCommand() {
12
9
  // Show logo
13
10
  showLogo();
@@ -28,12 +25,9 @@ export async function initCommand() {
28
25
  return;
29
26
  }
30
27
  }
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
28
+ // Initialize Supabase client (with hardcoded credentials - user doesn't need to know)
35
29
  try {
36
- initializeSupabase(SUPABASE_URL, SUPABASE_ANON_KEY);
30
+ initializeSupabase();
37
31
  }
38
32
  catch (error) {
39
33
  console.error(chalk.red('✗ Failed to connect to Langctl servers'));
@@ -0,0 +1,25 @@
1
+ /**
2
+ * List translation keys
3
+ */
4
+ export declare function listKeysCommand(projectSlug: string, options: any): Promise<void>;
5
+ /**
6
+ * Get single key
7
+ */
8
+ export declare function getKeyCommand(projectSlug: string, keyName: string): Promise<void>;
9
+ /**
10
+ * Create translation key
11
+ */
12
+ export declare function createKeyCommand(projectSlug: string, keyName: string, options: any): Promise<void>;
13
+ /**
14
+ * Delete translation key
15
+ */
16
+ export declare function deleteKeyCommand(projectSlug: string, keyName: string): Promise<void>;
17
+ /**
18
+ * Update single translation
19
+ */
20
+ export declare function translateKeyCommand(projectSlug: string, keyName: string, options: any): Promise<void>;
21
+ /**
22
+ * Publish/unpublish keys
23
+ */
24
+ export declare function publishKeysCommand(projectSlug: string, keys: string[], options: any): Promise<void>;
25
+ //# sourceMappingURL=keys.d.ts.map