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.
@@ -2,8 +2,9 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { writeFileSync, mkdirSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
- import { getSupabase } from '../supabase.js';
6
- import { isAuthenticated, getOrganizationId } from '../auth.js';
5
+ import { isAuthenticated } from '../auth.js';
6
+ import { getApiClient } from '../api.js';
7
+ import { config } from '../config.js';
7
8
  import { exportTranslations } from '../exporters/index.js';
8
9
  /**
9
10
  * Get platform-specific output directory
@@ -18,64 +19,24 @@ function getPlatformDirectory(format) {
18
19
  };
19
20
  return platformDirs[format] || 'i18n';
20
21
  }
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
22
  export async function pullCommand(projectIdentifier, options) {
64
23
  // Check authentication
65
24
  if (!isAuthenticated()) {
66
25
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
26
+ process.exitCode = 1;
67
27
  return;
68
28
  }
69
- const organizationId = getOrganizationId();
70
- if (!organizationId) {
71
- console.log(chalk.red('✗ Organization ID not found. Please run "langctl init" again.\n'));
29
+ const orgId = config.get('organizationId');
30
+ if (!orgId) {
31
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
32
+ process.exitCode = 1;
72
33
  return;
73
34
  }
74
35
  // Set defaults
75
36
  const format = (options.format || 'json');
76
37
  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
38
+ const specificLanguage = options.language;
39
+ const baseDir = options.dir || './translations';
79
40
  // Validate format
80
41
  const validFormats = ['json', 'json-nested', 'ios', 'android', 'flutter'];
81
42
  if (!validFormats.includes(format)) {
@@ -85,31 +46,17 @@ export async function pullCommand(projectIdentifier, options) {
85
46
  }
86
47
  const spinner = ora('Resolving project...').start();
87
48
  try {
88
- const supabase = getSupabase();
89
- // Resolve project identifier to UUID
90
- const projectId = await resolveProjectId(projectIdentifier, organizationId);
91
- if (!projectId) {
49
+ const api = getApiClient();
50
+ // Resolve project by slug
51
+ const project = await api.get(`/orgs/${orgId}/projects/by-slug/${projectIdentifier}`);
52
+ if (!project) {
92
53
  spinner.fail(chalk.red(`Project "${projectIdentifier}" not found`));
93
54
  console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
94
55
  return;
95
56
  }
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
57
  // Determine which languages to pull
110
58
  let languagesToPull;
111
59
  if (specificLanguage) {
112
- // Check if specified language exists in project
113
60
  if (!project.languages.includes(specificLanguage)) {
114
61
  spinner.fail(chalk.red(`Language "${specificLanguage}" not found in project`));
115
62
  console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
@@ -118,44 +65,37 @@ export async function pullCommand(projectIdentifier, options) {
118
65
  languagesToPull = [specificLanguage];
119
66
  }
120
67
  else {
121
- // Pull all languages
122
68
  languagesToPull = project.languages;
123
69
  }
124
70
  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
71
  // Get platform directory
147
72
  const platformDir = getPlatformDirectory(format);
148
73
  // Export translations for each language
149
74
  const exportedFiles = [];
75
+ let totalKeyCount = 0;
150
76
  for (const language of languagesToPull) {
151
- spinner.start(`Exporting ${language}...`);
152
- const result = exportTranslations(keys, language, format);
77
+ spinner.text = `Exporting ${language}...`;
78
+ // Fetch flat translations from the API
79
+ const result = await api.get(`/orgs/${orgId}/projects/${project.id}/export`, {
80
+ language,
81
+ publishedOnly: publishedOnly ? 'true' : 'false'
82
+ });
83
+ // Convert flat translations to TranslationKey array for the exporter
84
+ const flatTranslations = result.translations || {};
85
+ const translationKeys = Object.entries(flatTranslations).map(([key, value]) => ({
86
+ key,
87
+ translations: { [language]: value }
88
+ }));
89
+ if (totalKeyCount === 0) {
90
+ totalKeyCount = translationKeys.length;
91
+ }
92
+ const exportResult = exportTranslations(translationKeys, language, format);
153
93
  // Determine output path
154
94
  let outputPath;
155
95
  if (options.output) {
156
96
  // If custom output is specified and multiple languages, append language code
157
97
  if (languagesToPull.length > 1) {
158
- const ext = result.filename.split('.').pop();
98
+ const ext = exportResult.filename.split('.').pop();
159
99
  const base = options.output.replace(/\.[^.]+$/, '');
160
100
  outputPath = `${base}-${language}.${ext}`;
161
101
  }
@@ -165,15 +105,19 @@ export async function pullCommand(projectIdentifier, options) {
165
105
  }
166
106
  else {
167
107
  // Default output path: <baseDir>/<platform>/<language>.<ext>
168
- const ext = result.filename.split('.').pop();
108
+ const ext = exportResult.filename.split('.').pop();
169
109
  outputPath = join(baseDir, platformDir, `${language}.${ext}`);
170
110
  }
171
111
  // Create directory if it doesn't exist
172
112
  const dir = dirname(outputPath);
173
113
  mkdirSync(dir, { recursive: true });
174
- writeFileSync(outputPath, result.content, 'utf-8');
114
+ writeFileSync(outputPath, exportResult.content, 'utf-8');
175
115
  exportedFiles.push(outputPath);
176
116
  spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
117
+ // Restart spinner if more languages remain
118
+ if (languagesToPull.indexOf(language) < languagesToPull.length - 1) {
119
+ spinner.start();
120
+ }
177
121
  }
178
122
  // Summary
179
123
  console.log(chalk.hex('#10b981').bold('\n📦 Export Summary:'));
@@ -181,7 +125,7 @@ export async function pullCommand(projectIdentifier, options) {
181
125
  console.log(chalk.white(` Format: ${format}`));
182
126
  console.log(chalk.white(` Platform: ${platformDir}`));
183
127
  console.log(chalk.white(` Output: ${baseDir}`));
184
- console.log(chalk.white(` Keys: ${keys.length}`));
128
+ console.log(chalk.white(` Keys: ${totalKeyCount}`));
185
129
  console.log(chalk.white(` Languages: ${languagesToPull.join(', ')}`));
186
130
  console.log(chalk.white(` Files: ${exportedFiles.length}`));
187
131
  console.log('');
@@ -189,6 +133,7 @@ export async function pullCommand(projectIdentifier, options) {
189
133
  catch (error) {
190
134
  spinner.fail(chalk.red('Failed to pull translations'));
191
135
  console.error(chalk.red(`Error: ${error.message}\n`));
136
+ process.exitCode = 1;
192
137
  }
193
138
  }
194
139
  //# sourceMappingURL=pull.js.map
@@ -1,43 +1,38 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { isAuthenticated, getApiKey } from '../auth.js';
4
- const TEAM_MANAGEMENT_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/manage-team-members';
3
+ import { isAuthenticated } from '../auth.js';
4
+ import { getApiClient } from '../api.js';
5
+ import { config } from '../config.js';
5
6
  /**
6
7
  * List all team members
7
8
  */
8
9
  export async function listTeamCommand() {
9
10
  if (!isAuthenticated()) {
10
11
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ const orgId = config.get('organizationId');
16
+ if (!orgId) {
17
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
18
+ process.exitCode = 1;
11
19
  return;
12
20
  }
13
- const apiKey = getApiKey();
14
21
  const spinner = ora('Fetching team members...').start();
15
22
  try {
16
- const response = await fetch(TEAM_MANAGEMENT_URL, {
17
- method: 'POST',
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- 'X-API-Key': apiKey
21
- },
22
- body: JSON.stringify({
23
- action: 'list'
24
- })
25
- });
26
- const data = await response.json();
27
- if (!data.success) {
28
- throw new Error(data.error);
29
- }
23
+ const api = getApiClient();
24
+ const members = await api.get(`/orgs/${orgId}/members`);
30
25
  spinner.stop();
31
- if (!data.members || data.members.length === 0) {
26
+ if (!members || members.length === 0) {
32
27
  console.log(chalk.yellow('\nNo team members found\n'));
33
28
  return;
34
29
  }
35
- console.log(chalk.blue.bold(`\n👥 Team Members (${data.members.length})\n`));
36
- data.members.forEach((member) => {
37
- console.log(chalk.white.bold(member.email || member.user_id));
30
+ console.log(chalk.blue.bold(`\n👥 Team Members (${members.length})\n`));
31
+ members.forEach((member) => {
32
+ console.log(chalk.white.bold(member.email));
38
33
  console.log(chalk.gray(` Role: ${member.role}`));
39
- if (member.joined_at) {
40
- console.log(chalk.gray(` Joined: ${new Date(member.joined_at).toLocaleDateString()}`));
34
+ if (member.joinedAt) {
35
+ console.log(chalk.gray(` Joined: ${new Date(member.joinedAt).toLocaleDateString()}`));
41
36
  }
42
37
  console.log('');
43
38
  });
@@ -45,6 +40,7 @@ export async function listTeamCommand() {
45
40
  catch (error) {
46
41
  spinner.fail(chalk.red('Failed to list team members'));
47
42
  console.error(chalk.red(`Error: ${error.message}\n`));
43
+ process.exitCode = 1;
48
44
  }
49
45
  }
50
46
  /**
@@ -53,40 +49,37 @@ export async function listTeamCommand() {
53
49
  export async function getTeamMemberCommand(email) {
54
50
  if (!isAuthenticated()) {
55
51
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ const orgId = config.get('organizationId');
56
+ if (!orgId) {
57
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
58
+ process.exitCode = 1;
56
59
  return;
57
60
  }
58
- const apiKey = getApiKey();
59
61
  const spinner = ora('Fetching member details...').start();
60
62
  try {
61
- const response = await fetch(TEAM_MANAGEMENT_URL, {
62
- method: 'POST',
63
- headers: {
64
- 'Content-Type': 'application/json',
65
- 'X-API-Key': apiKey
66
- },
67
- body: JSON.stringify({
68
- action: 'get',
69
- email
70
- })
71
- });
72
- const data = await response.json();
73
- if (!data.success) {
74
- throw new Error(data.error);
63
+ const api = getApiClient();
64
+ const members = await api.get(`/orgs/${orgId}/members`);
65
+ const member = members.find((m) => m.email === email);
66
+ if (!member) {
67
+ throw new Error(`Member "${email}" not found`);
75
68
  }
76
69
  spinner.stop();
77
- const member = data.member;
78
70
  console.log(chalk.blue.bold('\n👤 Team Member Details\n'));
79
71
  console.log(chalk.white.bold(`Email: ${member.email}`));
80
72
  console.log(chalk.gray(`Role: ${member.role}`));
81
- console.log(chalk.gray(`User ID: ${member.user_id}`));
82
- if (member.joined_at) {
83
- console.log(chalk.gray(`Joined: ${new Date(member.joined_at).toLocaleString()}`));
73
+ console.log(chalk.gray(`User ID: ${member.userId}`));
74
+ if (member.joinedAt) {
75
+ console.log(chalk.gray(`Joined: ${new Date(member.joinedAt).toLocaleString()}`));
84
76
  }
85
77
  console.log('');
86
78
  }
87
79
  catch (error) {
88
80
  spinner.fail(chalk.red('Failed to get member details'));
89
81
  console.error(chalk.red(`Error: ${error.message}\n`));
82
+ process.exitCode = 1;
90
83
  }
91
84
  }
92
85
  /**
@@ -95,33 +88,29 @@ export async function getTeamMemberCommand(email) {
95
88
  export async function inviteTeamMemberCommand(email, options) {
96
89
  if (!isAuthenticated()) {
97
90
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ const orgId = config.get('organizationId');
95
+ if (!orgId) {
96
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
97
+ process.exitCode = 1;
98
98
  return;
99
99
  }
100
- const apiKey = getApiKey();
101
100
  const spinner = ora(`Inviting ${email}...`).start();
102
101
  try {
103
- const response = await fetch(TEAM_MANAGEMENT_URL, {
104
- method: 'POST',
105
- headers: {
106
- 'Content-Type': 'application/json',
107
- 'X-API-Key': apiKey
108
- },
109
- body: JSON.stringify({
110
- action: 'invite',
111
- email,
112
- role: options.role || 'member'
113
- })
102
+ const api = getApiClient();
103
+ await api.post(`/orgs/${orgId}/invitations`, {
104
+ email,
105
+ role: options.role || 'member'
114
106
  });
115
- const data = await response.json();
116
- if (!data.success) {
117
- throw new Error(data.error);
118
- }
119
- spinner.succeed(chalk.green(data.message));
107
+ spinner.succeed(chalk.green(`Invitation sent to ${email}`));
120
108
  console.log('');
121
109
  }
122
110
  catch (error) {
123
111
  spinner.fail(chalk.red('Failed to invite member'));
124
112
  console.error(chalk.red(`Error: ${error.message}\n`));
113
+ process.exitCode = 1;
125
114
  }
126
115
  }
127
116
  /**
@@ -130,32 +119,31 @@ export async function inviteTeamMemberCommand(email, options) {
130
119
  export async function removeTeamMemberCommand(email) {
131
120
  if (!isAuthenticated()) {
132
121
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const orgId = config.get('organizationId');
126
+ if (!orgId) {
127
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
128
+ process.exitCode = 1;
133
129
  return;
134
130
  }
135
- const apiKey = getApiKey();
136
131
  const spinner = ora(`Removing ${email}...`).start();
137
132
  try {
138
- const response = await fetch(TEAM_MANAGEMENT_URL, {
139
- method: 'POST',
140
- headers: {
141
- 'Content-Type': 'application/json',
142
- 'X-API-Key': apiKey
143
- },
144
- body: JSON.stringify({
145
- action: 'remove',
146
- email
147
- })
148
- });
149
- const data = await response.json();
150
- if (!data.success) {
151
- throw new Error(data.error);
133
+ const api = getApiClient();
134
+ const members = await api.get(`/orgs/${orgId}/members`);
135
+ const member = members.find((m) => m.email === email);
136
+ if (!member) {
137
+ throw new Error(`Member "${email}" not found`);
152
138
  }
153
- spinner.succeed(chalk.green(data.message));
139
+ await api.delete(`/orgs/${orgId}/members/${member.id}`);
140
+ spinner.succeed(chalk.green(`Removed ${email} from organization`));
154
141
  console.log('');
155
142
  }
156
143
  catch (error) {
157
144
  spinner.fail(chalk.red('Failed to remove member'));
158
145
  console.error(chalk.red(`Error: ${error.message}\n`));
146
+ process.exitCode = 1;
159
147
  }
160
148
  }
161
149
  /**
@@ -164,38 +152,37 @@ export async function removeTeamMemberCommand(email) {
164
152
  export async function updateTeamRoleCommand(email, role) {
165
153
  if (!isAuthenticated()) {
166
154
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
155
+ process.exitCode = 1;
167
156
  return;
168
157
  }
169
158
  const validRoles = ['viewer', 'member', 'admin', 'owner'];
170
159
  if (!validRoles.includes(role)) {
171
160
  console.log(chalk.red(`✗ Invalid role. Must be one of: ${validRoles.join(', ')}\n`));
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ const orgId = config.get('organizationId');
165
+ if (!orgId) {
166
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
167
+ process.exitCode = 1;
172
168
  return;
173
169
  }
174
- const apiKey = getApiKey();
175
170
  const spinner = ora(`Updating role for ${email}...`).start();
176
171
  try {
177
- const response = await fetch(TEAM_MANAGEMENT_URL, {
178
- method: 'POST',
179
- headers: {
180
- 'Content-Type': 'application/json',
181
- 'X-API-Key': apiKey
182
- },
183
- body: JSON.stringify({
184
- action: 'update-role',
185
- email,
186
- role
187
- })
188
- });
189
- const data = await response.json();
190
- if (!data.success) {
191
- throw new Error(data.error);
172
+ const api = getApiClient();
173
+ const members = await api.get(`/orgs/${orgId}/members`);
174
+ const member = members.find((m) => m.email === email);
175
+ if (!member) {
176
+ throw new Error(`Member "${email}" not found`);
192
177
  }
193
- spinner.succeed(chalk.green(data.message));
178
+ await api.patch(`/orgs/${orgId}/members/${member.id}`, { role });
179
+ spinner.succeed(chalk.green(`Updated ${email} role to ${role}`));
194
180
  console.log('');
195
181
  }
196
182
  catch (error) {
197
183
  spinner.fail(chalk.red('Failed to update role'));
198
184
  console.error(chalk.red(`Error: ${error.message}\n`));
185
+ process.exitCode = 1;
199
186
  }
200
187
  }
201
188
  /**
@@ -204,41 +191,39 @@ export async function updateTeamRoleCommand(email, role) {
204
191
  export async function listInvitationsCommand(options) {
205
192
  if (!isAuthenticated()) {
206
193
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+ const orgId = config.get('organizationId');
198
+ if (!orgId) {
199
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
200
+ process.exitCode = 1;
207
201
  return;
208
202
  }
209
- const apiKey = getApiKey();
210
203
  const spinner = ora('Fetching invitations...').start();
211
204
  try {
212
- const response = await fetch(TEAM_MANAGEMENT_URL, {
213
- method: 'POST',
214
- headers: {
215
- 'Content-Type': 'application/json',
216
- 'X-API-Key': apiKey
217
- },
218
- body: JSON.stringify({
219
- action: 'list-invitations',
220
- pending: options.pending || false
221
- })
222
- });
223
- const data = await response.json();
224
- if (!data.success) {
225
- throw new Error(data.error);
205
+ const api = getApiClient();
206
+ const params = {};
207
+ if (options?.pending) {
208
+ params.status = 'pending';
226
209
  }
210
+ const result = await api.get(`/orgs/${orgId}/invitations`, params);
211
+ const invitations = result.invitations || [];
227
212
  spinner.stop();
228
- if (!data.invitations || data.invitations.length === 0) {
213
+ if (invitations.length === 0) {
229
214
  console.log(chalk.yellow('\nNo invitations found\n'));
230
215
  return;
231
216
  }
232
- console.log(chalk.blue.bold(`\n✉️ Invitations (${data.invitations.length})\n`));
233
- data.invitations.forEach((invitation) => {
217
+ console.log(chalk.blue.bold(`\n✉️ Invitations (${invitations.length})\n`));
218
+ invitations.forEach((invitation) => {
234
219
  console.log(chalk.white.bold(invitation.email));
235
220
  console.log(chalk.gray(` Role: ${invitation.role}`));
236
221
  console.log(chalk.gray(` Status: ${invitation.status}`));
237
- if (invitation.invited_at) {
238
- console.log(chalk.gray(` Invited: ${new Date(invitation.invited_at).toLocaleDateString()}`));
222
+ if (invitation.createdAt) {
223
+ console.log(chalk.gray(` Invited: ${new Date(invitation.createdAt).toLocaleDateString()}`));
239
224
  }
240
- if (invitation.expires_at) {
241
- console.log(chalk.gray(` Expires: ${new Date(invitation.expires_at).toLocaleDateString()}`));
225
+ if (invitation.expiresAt) {
226
+ console.log(chalk.gray(` Expires: ${new Date(invitation.expiresAt).toLocaleDateString()}`));
242
227
  }
243
228
  console.log('');
244
229
  });
@@ -246,6 +231,7 @@ export async function listInvitationsCommand(options) {
246
231
  catch (error) {
247
232
  spinner.fail(chalk.red('Failed to list invitations'));
248
233
  console.error(chalk.red(`Error: ${error.message}\n`));
234
+ process.exitCode = 1;
249
235
  }
250
236
  }
251
237
  /**
@@ -254,32 +240,31 @@ export async function listInvitationsCommand(options) {
254
240
  export async function revokeInvitationCommand(email) {
255
241
  if (!isAuthenticated()) {
256
242
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
243
+ process.exitCode = 1;
244
+ return;
245
+ }
246
+ const orgId = config.get('organizationId');
247
+ if (!orgId) {
248
+ console.log(chalk.red('✗ No organization configured. Please run "langctl init" first.\n'));
249
+ process.exitCode = 1;
257
250
  return;
258
251
  }
259
- const apiKey = getApiKey();
260
252
  const spinner = ora(`Revoking invitation for ${email}...`).start();
261
253
  try {
262
- const response = await fetch(TEAM_MANAGEMENT_URL, {
263
- method: 'POST',
264
- headers: {
265
- 'Content-Type': 'application/json',
266
- 'X-API-Key': apiKey
267
- },
268
- body: JSON.stringify({
269
- action: 'revoke-invitation',
270
- email
271
- })
272
- });
273
- const data = await response.json();
274
- if (!data.success) {
275
- throw new Error(data.error);
254
+ const api = getApiClient();
255
+ const result = await api.get(`/orgs/${orgId}/invitations`);
256
+ const invitation = (result.invitations || []).find((inv) => inv.email === email && inv.status === 'pending');
257
+ if (!invitation) {
258
+ throw new Error(`No pending invitation found for "${email}"`);
276
259
  }
277
- spinner.succeed(chalk.green(data.message));
260
+ await api.post(`/orgs/${orgId}/invitations/${invitation.id}/revoke`);
261
+ spinner.succeed(chalk.green(`Invitation for ${email} revoked`));
278
262
  console.log('');
279
263
  }
280
264
  catch (error) {
281
265
  spinner.fail(chalk.red('Failed to revoke invitation'));
282
266
  console.error(chalk.red(`Error: ${error.message}\n`));
267
+ process.exitCode = 1;
283
268
  }
284
269
  }
285
270
  //# sourceMappingURL=team.js.map
package/dist/config.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface LangctlConfig {
2
2
  apiKey?: string;
3
3
  organizationId?: string;
4
4
  organizationName?: string;
5
+ apiBaseUrl?: string;
5
6
  defaultProject?: string;
6
7
  defaultLanguage?: string;
7
8
  }
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ const program = new Command();
17
17
  // Package info
18
18
  const packageJson = {
19
19
  name: 'langctl',
20
- version: '0.1.0',
20
+ version: '0.2.0',
21
21
  description: 'CLI-first translation management for developers'
22
22
  };
23
23
  program
@@ -462,7 +462,7 @@ const debug = program
462
462
  .description('Debug helpers');
463
463
  debug
464
464
  .command('list-api-keys')
465
- .description('List API keys visible to current Supabase credentials')
465
+ .description('Test API connection and show debug info')
466
466
  .action(async () => {
467
467
  try {
468
468
  await debugListApiKeys();
@@ -474,7 +474,7 @@ debug
474
474
  });
475
475
  debug
476
476
  .command('list-organizations')
477
- .description('List organizations visible to current Supabase credentials')
477
+ .description('Show current organization details')
478
478
  .action(async () => {
479
479
  try {
480
480
  await debugListOrganizations();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langctl",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Translation management CLI for developers. Manage i18n translations, export to 6 formats (iOS, Android, Flutter, i18next), import translations, and collaborate with teams. Affordable alternative to Lokalise and Crowdin.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "langctl": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "build": "rm -rf dist && tsc",
12
12
  "dev": "tsx src/index.ts",
13
13
  "start": "node dist/index.js",
14
14
  "prepublishOnly": "npm run build",
@@ -87,7 +87,6 @@
87
87
  "node": ">=16.0.0"
88
88
  },
89
89
  "dependencies": {
90
- "@supabase/supabase-js": "^2.89.0",
91
90
  "chalk": "^5.6.2",
92
91
  "commander": "^14.0.2",
93
92
  "conf": "^15.0.2",