langctl 0.1.2 → 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.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Report a bug or unexpected behavior
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ **Describe the bug**
10
+ A clear description of what the bug is.
11
+
12
+ **To reproduce**
13
+ Steps to reproduce:
14
+ 1. Run `langctl ...`
15
+ 2. ...
16
+
17
+ **Expected behavior**
18
+ What you expected to happen.
19
+
20
+ **Environment**
21
+ - OS: [e.g. macOS 14, Ubuntu 22.04]
22
+ - Node.js version: [e.g. 18.17.0]
23
+ - langctl version: [output of `langctl --version`]
24
+
25
+ **Additional context**
26
+ Add any other context, logs, or screenshots.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a new feature or improvement
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ **Problem**
10
+ What problem does this feature solve?
11
+
12
+ **Proposed solution**
13
+ Describe how you'd like this to work.
14
+
15
+ **Alternatives considered**
16
+ Any alternative solutions or workarounds you've tried.
17
+
18
+ **Additional context**
19
+ Any other context, examples, or references.
@@ -0,0 +1,33 @@
1
+ # Contributing to langctl
2
+
3
+ Thanks for your interest in contributing! Here's how to get started.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/siddharthsaxena0/langctl.git
9
+ cd langctl
10
+ npm install
11
+ npm run dev # runs tsx watch
12
+ ```
13
+
14
+ ## Making Changes
15
+
16
+ 1. Fork the repo and create a feature branch from `main`
17
+ 2. Make your changes
18
+ 3. Test locally with `npm run dev`
19
+ 4. Submit a pull request
20
+
21
+ ## Reporting Bugs
22
+
23
+ Use the [bug report template](https://github.com/siddharthsaxena0/langctl/issues/new?template=bug_report.md) and include your `langctl --version`, Node.js version, and OS.
24
+
25
+ ## Code Style
26
+
27
+ - TypeScript with strict mode
28
+ - Use `commander` for CLI commands
29
+ - Keep commands in `src/commands/`
30
+
31
+ ## License
32
+
33
+ By contributing, you agree that your contributions will be licensed under the MIT License.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Langctl
3
+ Copyright (c) 2026 Litcode Private Limited
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,21 @@
1
- # Langctl CLI
1
+ <div align="center">
2
2
 
3
- > CLI-first translation management for developers
3
+ # Langctl
4
+
5
+ **CLI-first translation management for developers**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/langctl.svg)](https://www.npmjs.com/package/langctl)
8
+ [![license](https://img.shields.io/npm/l/langctl.svg)](https://github.com/siddharthsaxena0/langctl/blob/main/LICENSE)
9
+ [![npm downloads](https://img.shields.io/npm/dm/langctl.svg)](https://www.npmjs.com/package/langctl)
10
+
11
+ **[Website](https://langctl.com)** · **[Documentation](https://langctl.com/docs)** · **[Get Started](https://app.langctl.com/signup)**
12
+
13
+ <!-- Add a demo GIF here: -->
14
+ <!-- ![langctl demo](assets/demo.gif) -->
15
+
16
+ </div>
17
+
18
+ ---
4
19
 
5
20
  Langctl is a powerful command-line tool that lets you manage translations directly from your terminal. Create projects, manage translation keys, invite team members, export in multiple formats, and integrate seamlessly into your CI/CD pipeline.
6
21
 
@@ -29,13 +44,17 @@ npx langctl --help
29
44
 
30
45
  ## Quick Start
31
46
 
47
+ > **📌 First time here?** Check out our [Getting Started Guide](https://langctl.com/docs/getting-started/introduction) on the website for a complete walkthrough!
48
+
32
49
  ### 1. Get Your API Key
33
50
 
34
- 1. Visit [app.langctl.com](https://app.langctl.com) and sign in
51
+ 1. Visit **[app.langctl.com](https://app.langctl.com/signup)** and create a free account
35
52
  2. Go to **Settings → API Keys**
36
53
  3. Click **"Generate New Key"**
37
54
  4. Copy the key (it's only shown once!)
38
55
 
56
+ > **💡 Tip:** Learn more about API keys and authentication at [langctl.com/docs/cli/authentication](https://langctl.com/docs/cli/authentication)
57
+
39
58
  ### 2. Authenticate
40
59
 
41
60
  ```bash
@@ -1163,20 +1182,34 @@ A: Yes! Store your API key as a secret and use it in your workflow. See [CI/CD I
1163
1182
 
1164
1183
  ---
1165
1184
 
1166
- ## Links
1185
+ ## 🌟 Why Langctl?
1167
1186
 
1168
- - **Website:** [langctl.com](https://langctl.com)
1169
- - **Dashboard:** [app.langctl.com](https://app.langctl.com)
1170
- - **Documentation:** [langctl.com/docs](https://langctl.com/docs)
1171
- - **GitHub:** [github.com/siddharthsaxena0/langctl](https://github.com/siddharthsaxena0/langctl)
1172
- - **Report Issues:** [github.com/siddharthsaxena0/langctl/issues](https://github.com/siddharthsaxena0/langctl/issues)
1173
- - **Email Support:** [hello@langctl.com](mailto:hello@langctl.com)
1187
+ Tired of expensive translation management tools? Langctl offers:
1188
+ - ✅ **Free to start** - No credit card required
1189
+ - ✅ **90% cheaper** than enterprise alternatives
1190
+ - ✅ **Developer-first** - Built for your workflow
1191
+ - ✅ **AI-powered** - Context-aware translations
1192
+ - ✅ **Open source CLI** - Inspect, fork, contribute
1193
+
1194
+ **[See pricing and compare features →](https://langctl.com/pricing)**
1195
+
1196
+ ---
1197
+
1198
+ ## 📚 Resources & Support
1199
+
1200
+ - 🌐 **[Website](https://langctl.com)** - Features, pricing, and comparisons
1201
+ - 📖 **[Full Documentation](https://langctl.com/docs)** - Guides, tutorials, and best practices
1202
+ - 🎯 **[Quick Start Tutorial](https://langctl.com/docs/getting-started/quickstart)** - Step-by-step walkthrough
1203
+ - 💡 **[Integration Guides](https://langctl.com/docs/integrations/overview)** - React, Angular, Vue, iOS, Android, Flutter
1204
+ - 📊 **[Dashboard](https://app.langctl.com)** - Manage translations visually
1205
+ - 💬 **[Get Support](https://langctl.com/contact)** - Email us at [hello@langctl.com](mailto:hello@langctl.com)
1206
+ - 🐛 **[Report Issues](https://github.com/siddharthsaxena0/langctl/issues)** - Bug reports and feature requests
1174
1207
 
1175
1208
  ---
1176
1209
 
1177
1210
  ## Contributing
1178
1211
 
1179
- We welcome contributions! Please see our [contributing guidelines](CONTRIBUTING.md) for details.
1212
+ We welcome contributions! This is an open-source project and we'd love your help making it better.
1180
1213
 
1181
1214
  ---
1182
1215
 
@@ -1184,8 +1217,16 @@ We welcome contributions! Please see our [contributing guidelines](CONTRIBUTING.
1184
1217
 
1185
1218
  MIT License - see [LICENSE](LICENSE) file for details.
1186
1219
 
1220
+ Copyright © 2026 Litcode Private Limited. All rights reserved.
1221
+
1187
1222
  ---
1188
1223
 
1224
+ <div align="center">
1225
+
1189
1226
  **Built with ❤️ by the Langctl team**
1190
1227
 
1191
1228
  *Making translation management simple, fast, and developer-friendly.*
1229
+
1230
+ **[Get Started Free →](https://app.langctl.com/signup)**
1231
+
1232
+ </div>
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
- organization_id: string;
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 Edge Function
11
+ * Verify API key via Fastify API
11
12
  */
12
13
  export declare function verifyApiKey(plainTextKey: string): Promise<ApiKeyData | null>;
13
14
  /**
package/dist/auth.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { config } from './config.js';
2
- // Edge Function URL
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 Edge Function
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
- // 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 })
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 (!response.ok) {
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
- // Map Edge Function response to ApiKeyData format
34
+ // Fetch organization details
35
+ const org = await client.get(`/orgs/${validateResult.organizationId}`);
42
36
  return {
43
- organization_id: data.organizationId,
37
+ organizationId: validateResult.organizationId,
44
38
  organization: {
45
- id: data.organizationId,
46
- name: data.organizationName,
47
- plan: data.plan
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 Edge Function
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.organization_id);
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
  /**
@@ -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() {
@@ -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,
@@ -1,57 +1,61 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { getSupabase, initializeSupabase } from '../supabase.js';
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
- // Initialize supabase (with hardcoded credentials)
6
- try {
7
- initializeSupabase();
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 supabase = getSupabase();
14
- const spinner = ora('Fetching API keys (debug)...').start();
15
- const { data, error } = await supabase
16
- .from('api_keys')
17
- .select('id, name, key_prefix, organization_id, revoked, created_at, last_used_at')
18
- .limit(100);
19
- if (error) {
20
- spinner.fail(chalk.red(`Error fetching API keys: ${error.message}`));
21
- console.log(chalk.yellow('Hint: This is a debug command and may require elevated permissions.'));
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.succeed(chalk.green(`Found ${data?.length || 0} API keys`));
25
- if (!data || data.length === 0) {
26
- console.log(chalk.yellow('No API keys visible with current permissions.'));
27
- return;
28
- }
29
- for (const row of data) {
30
- console.log(`${chalk.cyan(row.id)} ${chalk.white(row.name)} prefix=${chalk.magenta(row.key_prefix)} org=${chalk.yellow(row.organization_id)} revoked=${row.revoked ? chalk.red('true') : chalk.green('false')} last_used=${chalk.gray(row.last_used_at || 'never')}`);
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
- // Initialize supabase (with hardcoded credentials)
35
- try {
36
- initializeSupabase();
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 supabase = getSupabase();
43
- const spinner = ora('Fetching organizations (debug)...').start();
44
- const { data, error } = await supabase
45
- .from('organizations')
46
- .select('id, name, plan, created_at')
47
- .limit(100);
48
- if (error) {
49
- spinner.fail(chalk.red(`Error fetching organizations: ${error.message}`));
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.succeed(chalk.green(`Found ${data?.length || 0} organizations`));
53
- (data || []).forEach(o => {
54
- console.log(`${chalk.cyan(o.id)} ${chalk.white(o.name)} plan=${chalk.magenta(o.plan)} created=${chalk.gray(o.created_at)}`);
55
- });
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
@@ -1,7 +1,6 @@
1
- type ExportFormat = 'i18n-json' | 'android-xml' | 'ios-strings' | 'flutter-arb' | 'flat-json' | 'nested-json';
2
1
  interface ExportOptions {
3
2
  language?: string;
4
- format?: ExportFormat;
3
+ format?: string;
5
4
  output?: string;
6
5
  module?: string;
7
6
  includeUnpublished?: boolean;