langctl 0.0.1 ā 0.1.1
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 +300 -30
- package/SECURITY_FIX.md +167 -0
- 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 +33 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +57 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +87 -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 +47 -0
- package/dist/config.js +69 -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 +30 -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/SECURITY_FIX.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# š Security Fix - Supabase Credentials
|
|
2
|
+
|
|
3
|
+
**Date:** January 5, 2026
|
|
4
|
+
**Issue:** Supabase credentials were being stored in user config files
|
|
5
|
+
**Status:** ā
FIXED
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## šØ What Was Wrong
|
|
10
|
+
|
|
11
|
+
Previously, the CLI was:
|
|
12
|
+
- ā Storing Supabase URL in `~/.langctl/config.json`
|
|
13
|
+
- ā Storing Supabase anon key in `~/.langctl/config.json`
|
|
14
|
+
- ā Asking users about "Supabase configuration" during init
|
|
15
|
+
- ā Exposing backend implementation details to users
|
|
16
|
+
|
|
17
|
+
**Why this was bad:**
|
|
18
|
+
- Users could see our backend credentials
|
|
19
|
+
- Users could manipulate Supabase connection
|
|
20
|
+
- Exposed unnecessary implementation details
|
|
21
|
+
- Confused users with technical jargon
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ā
What's Fixed
|
|
26
|
+
|
|
27
|
+
Now the CLI:
|
|
28
|
+
- ā
Hardcodes Supabase credentials in source code
|
|
29
|
+
- ā
Only stores user credentials (API key, org ID, preferences)
|
|
30
|
+
- ā
Users never see or interact with Supabase
|
|
31
|
+
- ā
Clean, simple user experience
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## š Files Changed
|
|
36
|
+
|
|
37
|
+
### 1. `src/config.ts`
|
|
38
|
+
**Removed from interface:**
|
|
39
|
+
```typescript
|
|
40
|
+
// REMOVED - no longer stored
|
|
41
|
+
supabaseUrl?: string;
|
|
42
|
+
supabaseAnonKey?: string;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Now only stores:**
|
|
46
|
+
```typescript
|
|
47
|
+
apiKey?: string; // User's API key
|
|
48
|
+
organizationId?: string; // User's org ID
|
|
49
|
+
organizationName?: string; // User's org name
|
|
50
|
+
defaultProject?: string; // User preference
|
|
51
|
+
defaultLanguage?: string; // User preference
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. `src/supabase.ts`
|
|
55
|
+
**Before:**
|
|
56
|
+
```typescript
|
|
57
|
+
// BAD - pulled from user config
|
|
58
|
+
const supabaseUrl = config.get('supabaseUrl');
|
|
59
|
+
const supabaseAnonKey = config.get('supabaseAnonKey');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**After:**
|
|
63
|
+
```typescript
|
|
64
|
+
// GOOD - hardcoded in source
|
|
65
|
+
const SUPABASE_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co';
|
|
66
|
+
const SUPABASE_ANON_KEY = 'eyJhbGci...';
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. `src/commands/init.ts`
|
|
70
|
+
**Before:**
|
|
71
|
+
```typescript
|
|
72
|
+
// BAD - saved to user config
|
|
73
|
+
config.set('supabaseUrl', SUPABASE_URL);
|
|
74
|
+
config.set('supabaseAnonKey', SUPABASE_ANON_KEY);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**After:**
|
|
78
|
+
```typescript
|
|
79
|
+
// GOOD - just initializes, doesn't save
|
|
80
|
+
initializeSupabase();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. `src/commands/config.ts`
|
|
84
|
+
**Before:**
|
|
85
|
+
```typescript
|
|
86
|
+
// BAD - displayed Supabase credentials
|
|
87
|
+
supabaseAnonKey: allConfig.supabaseAnonKey ? maskSensitive(...) : undefined
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**After:**
|
|
91
|
+
```typescript
|
|
92
|
+
// GOOD - only displays user data
|
|
93
|
+
apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
|
|
94
|
+
organizationId: allConfig.organizationId,
|
|
95
|
+
organizationName: allConfig.organizationName
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## š Security Notes
|
|
101
|
+
|
|
102
|
+
**Is it safe to hardcode Supabase credentials in the CLI?**
|
|
103
|
+
|
|
104
|
+
ā
**YES!** This is perfectly safe because:
|
|
105
|
+
|
|
106
|
+
1. **Anon key is designed for client-side use**
|
|
107
|
+
- It's meant to be public-facing
|
|
108
|
+
- Row Level Security (RLS) protects all data
|
|
109
|
+
- Users can only access what they're authorized for
|
|
110
|
+
|
|
111
|
+
2. **RLS policies enforce security**
|
|
112
|
+
- Users can only see their own organization data
|
|
113
|
+
- API key verification happens server-side (Edge Function)
|
|
114
|
+
- All operations are protected by RLS
|
|
115
|
+
|
|
116
|
+
3. **This is standard practice**
|
|
117
|
+
- Same as mobile apps that embed API credentials
|
|
118
|
+
- Same as web apps that expose client keys
|
|
119
|
+
- Backend security, not client security
|
|
120
|
+
|
|
121
|
+
**What if someone decompiles the CLI?**
|
|
122
|
+
- They'll see the Supabase URL and anon key
|
|
123
|
+
- But they CANNOT access data without a valid API key
|
|
124
|
+
- API keys are user-specific and verified server-side
|
|
125
|
+
- RLS prevents unauthorized access
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## š What Users See Now
|
|
130
|
+
|
|
131
|
+
### Config File (`~/.langctl/config.json`)
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"apiKey": "lc_abc123...",
|
|
135
|
+
"organizationId": "uuid-here",
|
|
136
|
+
"organizationName": "My Company",
|
|
137
|
+
"defaultLanguage": "en"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Clean and simple!** No backend implementation details.
|
|
142
|
+
|
|
143
|
+
### Running `langctl config`
|
|
144
|
+
```
|
|
145
|
+
š Langctl Configuration
|
|
146
|
+
|
|
147
|
+
apiKey: lc_abc123...wxyz
|
|
148
|
+
organizationId: 550e8400-e29b-41d4-a716-446655440000
|
|
149
|
+
organizationName: My Company
|
|
150
|
+
defaultLanguage: en
|
|
151
|
+
|
|
152
|
+
Config file: /Users/username/.langctl/config.json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Only user-relevant information!**
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## ā
Ready to Publish
|
|
160
|
+
|
|
161
|
+
This security fix is complete. The CLI now:
|
|
162
|
+
- ā
Keeps backend credentials private
|
|
163
|
+
- ā
Only exposes user data to users
|
|
164
|
+
- ā
Maintains full security with RLS
|
|
165
|
+
- ā
Provides clean user experience
|
|
166
|
+
|
|
167
|
+
**Safe to publish to npm!** š
|
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,33 @@
|
|
|
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
|
+
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
|
+
const spinner = ora('Authenticating...').start();
|
|
16
|
+
const result = await authenticate(apiKey);
|
|
17
|
+
if (result.success) {
|
|
18
|
+
spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
|
|
19
|
+
console.log(chalk.gray(`\nAPI Key saved to: ${config.getConfigPath()}\n`));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
spinner.fail(chalk.red(result.message));
|
|
23
|
+
console.log(chalk.yellow('\nTroubleshooting:'));
|
|
24
|
+
console.log(chalk.white(' - Check that your API key is correct'));
|
|
25
|
+
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'));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function logoutCommand() {
|
|
30
|
+
logout();
|
|
31
|
+
console.log(chalk.green('ā Logged out successfully\n'));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
|
|
13
|
+
organizationId: allConfig.organizationId,
|
|
14
|
+
organizationName: allConfig.organizationName,
|
|
15
|
+
defaultProject: allConfig.defaultProject,
|
|
16
|
+
defaultLanguage: allConfig.defaultLanguage
|
|
17
|
+
};
|
|
18
|
+
Object.entries(displayConfig).forEach(([key, value]) => {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
console.log(chalk.white(`${key}: ${chalk.cyan(value)}`));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
console.log(chalk.gray(`\nConfig file: ${config.getConfigPath()}\n`));
|
|
24
|
+
}
|
|
25
|
+
function maskApiKey(key) {
|
|
26
|
+
if (key.length < 10)
|
|
27
|
+
return '***';
|
|
28
|
+
return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getSupabase, initializeSupabase } from '../supabase.js';
|
|
4
|
+
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.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
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.'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
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')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
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.'));
|
|
40
|
+
return;
|
|
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}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
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
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
export async function initCommand() {
|
|
9
|
+
// Show logo
|
|
10
|
+
showLogo();
|
|
11
|
+
console.log(chalk.blue.bold('š CLI Setup\n'));
|
|
12
|
+
console.log(chalk.gray('Get your API key from: https://app.langctl.com/dashboard/settings/api-keys\n'));
|
|
13
|
+
// Check if already configured
|
|
14
|
+
if (config.isConfigured()) {
|
|
15
|
+
const { confirm } = await inquirer.prompt([
|
|
16
|
+
{
|
|
17
|
+
type: 'confirm',
|
|
18
|
+
name: 'confirm',
|
|
19
|
+
message: 'Configuration already exists. Do you want to reconfigure?',
|
|
20
|
+
default: false
|
|
21
|
+
}
|
|
22
|
+
]);
|
|
23
|
+
if (!confirm) {
|
|
24
|
+
console.log(chalk.yellow('\nSetup cancelled.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Initialize Supabase client (with hardcoded credentials - user doesn't need to know)
|
|
29
|
+
try {
|
|
30
|
+
initializeSupabase();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error(chalk.red('ā Failed to connect to Langctl servers'));
|
|
34
|
+
console.error(chalk.red('Please check your internet connection and try again.\n'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Ask for API key
|
|
38
|
+
const { apiKey } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'password',
|
|
41
|
+
name: 'apiKey',
|
|
42
|
+
message: 'Enter your API key:',
|
|
43
|
+
mask: '*',
|
|
44
|
+
filter: (input) => input?.trim(),
|
|
45
|
+
validate: (input) => {
|
|
46
|
+
const value = (input || '').trim();
|
|
47
|
+
if (!value || !value.startsWith('lc_')) {
|
|
48
|
+
return 'Please enter a valid API key (starts with lc_)';
|
|
49
|
+
}
|
|
50
|
+
if (value.length !== 67) {
|
|
51
|
+
return 'Invalid API key format (should be 67 characters)';
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
]);
|
|
57
|
+
// Authenticate
|
|
58
|
+
const spinner = ora('Verifying API key...').start();
|
|
59
|
+
const result = await authenticate(apiKey);
|
|
60
|
+
if (result.success) {
|
|
61
|
+
spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
spinner.fail(chalk.red(result.message));
|
|
65
|
+
console.log(chalk.yellow('\nTroubleshooting:'));
|
|
66
|
+
console.log(chalk.white(' - Check that your API key is correct'));
|
|
67
|
+
console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
|
|
68
|
+
console.log(chalk.white(' - Generate a new key if needed: https://app.langctl.com/dashboard/settings/api-keys\n'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Ask for default settings
|
|
72
|
+
const { defaultLanguage } = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'defaultLanguage',
|
|
76
|
+
message: 'Default language code:',
|
|
77
|
+
default: 'en'
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
config.set('defaultLanguage', defaultLanguage);
|
|
81
|
+
console.log(chalk.green.bold('\nā Setup complete!\n'));
|
|
82
|
+
console.log(chalk.gray(`Configuration saved to: ${config.getConfigPath()}\n`));
|
|
83
|
+
console.log(chalk.blue('Next steps:'));
|
|
84
|
+
console.log(chalk.white(' 1. Run "langctl projects list" to see your projects'));
|
|
85
|
+
console.log(chalk.white(' 2. Run "langctl pull <project-slug>" to download translations\n'));
|
|
86
|
+
}
|
|
87
|
+
//# 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
|