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.
@@ -1,62 +1,60 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { isAuthenticated, getApiKey } from '../auth.js';
4
- const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
5
- const MANAGE_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/manage-projects';
3
+ import { isAuthenticated } from '../auth.js';
4
+ import { getApiClient } from '../api.js';
5
+ import { config } from '../config.js';
6
+ /**
7
+ * Resolve a project slug to a full project object
8
+ */
9
+ async function resolveProject(orgId, slug) {
10
+ const api = getApiClient();
11
+ return api.get(`/orgs/${orgId}/projects/by-slug/${slug}`);
12
+ }
13
+ /**
14
+ * List all projects
15
+ */
6
16
  export async function projectsListCommand() {
7
- // Check authentication
8
17
  if (!isAuthenticated()) {
9
18
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
19
+ process.exitCode = 1;
10
20
  return;
11
21
  }
12
- const apiKey = getApiKey();
13
- if (!apiKey) {
14
- console.log(chalk.red('✗ API key not found. Please run "langctl auth <api-key>" again.\n'));
22
+ const orgId = config.get('organizationId');
23
+ if (!orgId) {
24
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
25
+ process.exitCode = 1;
15
26
  return;
16
27
  }
17
28
  const spinner = ora('Fetching projects...').start();
18
29
  try {
19
- // Call list-projects Edge Function
20
- const response = await fetch(LIST_PROJECTS_URL, {
21
- method: 'POST',
22
- headers: {
23
- 'Content-Type': 'application/json',
24
- 'X-API-Key': apiKey
25
- }
26
- });
27
- if (!response.ok) {
28
- const errorData = await response.json().catch(() => ({}));
29
- throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
30
- }
31
- const data = await response.json();
32
- if (!data.success) {
33
- throw new Error(data.error || 'Failed to fetch projects');
34
- }
30
+ const api = getApiClient();
31
+ const projects = await api.get(`/orgs/${orgId}/projects`);
35
32
  spinner.stop();
36
- if (!data.projects || data.projects.length === 0) {
33
+ if (!projects || projects.length === 0) {
37
34
  console.log(chalk.yellow('\nNo projects found. Create one in the dashboard first.\n'));
38
35
  return;
39
36
  }
40
- console.log(chalk.blue.bold(`\n📦 Projects (${data.projects.length})\n`));
41
- data.projects.forEach((project) => {
37
+ console.log(chalk.blue.bold(`\n📦 Projects (${projects.length})\n`));
38
+ projects.forEach((project) => {
42
39
  console.log(chalk.white.bold(project.name));
43
40
  console.log(chalk.gray(` Slug: ${project.slug}`));
44
41
  if (project.description) {
45
42
  console.log(chalk.gray(` Description: ${project.description}`));
46
43
  }
47
44
  console.log(chalk.gray(` Languages: ${project.languages.join(', ')}`));
48
- console.log(chalk.gray(` Default: ${project.default_language}`));
45
+ console.log(chalk.gray(` Default: ${project.defaultLanguage}`));
49
46
  if (project.modules && project.modules.length > 0) {
50
47
  console.log(chalk.gray(` Modules: ${project.modules.join(', ')}`));
51
48
  }
52
49
  console.log('');
53
50
  });
54
51
  console.log(chalk.blue('To export translations, run:'));
55
- console.log(chalk.white(` langctl export ${data.projects[0].slug} -l en\n`));
52
+ console.log(chalk.white(` langctl export ${projects[0].slug} -l en\n`));
56
53
  }
57
54
  catch (error) {
58
55
  spinner.fail(chalk.red('Failed to fetch projects'));
59
56
  console.error(chalk.red(`Error: ${error.message}\n`));
57
+ process.exitCode = 1;
60
58
  }
61
59
  }
62
60
  /**
@@ -65,37 +63,33 @@ export async function projectsListCommand() {
65
63
  export async function projectsCreateCommand(name, options) {
66
64
  if (!isAuthenticated()) {
67
65
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ const orgId = config.get('organizationId');
70
+ if (!orgId) {
71
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
72
+ process.exitCode = 1;
68
73
  return;
69
74
  }
70
- const apiKey = getApiKey();
71
75
  const spinner = ora('Creating project...').start();
72
76
  try {
77
+ const api = getApiClient();
73
78
  const languages = options.languages ? options.languages.split(',') : ['en'];
74
79
  const defaultLanguage = options.defaultLanguage || languages[0];
75
- const response = await fetch(MANAGE_PROJECTS_URL, {
76
- method: 'POST',
77
- headers: {
78
- 'Content-Type': 'application/json',
79
- 'X-API-Key': apiKey
80
- },
81
- body: JSON.stringify({
82
- action: 'create',
83
- name,
84
- description: options.description,
85
- languages,
86
- defaultLanguage
87
- })
80
+ const project = await api.post(`/orgs/${orgId}/projects`, {
81
+ name,
82
+ description: options.description,
83
+ languages,
84
+ defaultLanguage
88
85
  });
89
- const data = await response.json();
90
- if (!data.success) {
91
- throw new Error(data.error);
92
- }
93
- spinner.succeed(chalk.green(`Created project: ${name} (${data.project.slug})`));
86
+ spinner.succeed(chalk.green(`Created project: ${name} (${project.slug})`));
94
87
  console.log('');
95
88
  }
96
89
  catch (error) {
97
90
  spinner.fail(chalk.red('Failed to create project'));
98
91
  console.error(chalk.red(`Error: ${error.message}\n`));
92
+ process.exitCode = 1;
99
93
  }
100
94
  }
101
95
  /**
@@ -104,25 +98,18 @@ export async function projectsCreateCommand(name, options) {
104
98
  export async function projectsGetCommand(slug) {
105
99
  if (!isAuthenticated()) {
106
100
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ const orgId = config.get('organizationId');
105
+ if (!orgId) {
106
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
107
+ process.exitCode = 1;
107
108
  return;
108
109
  }
109
- const apiKey = getApiKey();
110
110
  const spinner = ora('Fetching project...').start();
111
111
  try {
112
- // Get project from list first
113
- const listResponse = await fetch(LIST_PROJECTS_URL, {
114
- method: 'POST',
115
- headers: {
116
- 'Content-Type': 'application/json',
117
- 'X-API-Key': apiKey
118
- }
119
- });
120
- const listData = await listResponse.json();
121
- const project = listData.projects?.find((p) => p.slug === slug);
122
- if (!project) {
123
- spinner.fail(chalk.red(`Project "${slug}" not found`));
124
- return;
125
- }
112
+ const project = await resolveProject(orgId, slug);
126
113
  spinner.stop();
127
114
  console.log(chalk.blue.bold('\n📦 Project Details\n'));
128
115
  console.log(chalk.white.bold(project.name));
@@ -132,15 +119,16 @@ export async function projectsGetCommand(slug) {
132
119
  console.log(chalk.gray(`Description: ${project.description}`));
133
120
  }
134
121
  console.log(chalk.gray(`Languages: ${project.languages.join(', ')}`));
135
- console.log(chalk.gray(`Default Language: ${project.default_language}`));
122
+ console.log(chalk.gray(`Default Language: ${project.defaultLanguage}`));
136
123
  if (project.modules && project.modules.length > 0) {
137
124
  console.log(chalk.gray(`Modules: ${project.modules.join(', ')}`));
138
125
  }
139
126
  console.log('');
140
127
  }
141
128
  catch (error) {
142
- spinner.fail(chalk.red('Failed to fetch project'));
129
+ spinner.fail(chalk.red(`Failed to fetch project "${slug}"`));
143
130
  console.error(chalk.red(`Error: ${error.message}\n`));
131
+ process.exitCode = 1;
144
132
  }
145
133
  }
146
134
  /**
@@ -149,45 +137,56 @@ export async function projectsGetCommand(slug) {
149
137
  export async function projectsUpdateCommand(slug, options) {
150
138
  if (!isAuthenticated()) {
151
139
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ const orgId = config.get('organizationId');
144
+ if (!orgId) {
145
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
146
+ process.exitCode = 1;
152
147
  return;
153
148
  }
154
- const apiKey = getApiKey();
155
149
  const spinner = ora('Updating project...').start();
156
150
  try {
157
- // Get project ID from slug
158
- const listResponse = await fetch(LIST_PROJECTS_URL, {
159
- method: 'POST',
160
- headers: {
161
- 'Content-Type': 'application/json',
162
- 'X-API-Key': apiKey
163
- }
164
- });
165
- const listData = await listResponse.json();
166
- const project = listData.projects?.find((p) => p.slug === slug);
167
- if (!project) {
168
- spinner.fail(chalk.red(`Project "${slug}" not found`));
169
- return;
170
- }
171
- const updateData = { projectId: project.id, action: 'update' };
151
+ const project = await resolveProject(orgId, slug);
152
+ const api = getApiClient();
153
+ const updateData = {};
172
154
  if (options.name)
173
155
  updateData.name = options.name;
174
156
  if (options.description !== undefined)
175
157
  updateData.description = options.description;
176
- if (options.languages)
177
- updateData.languages = options.languages.split(',');
178
158
  if (options.defaultLanguage)
179
159
  updateData.defaultLanguage = options.defaultLanguage;
180
- const response = await fetch(MANAGE_PROJECTS_URL, {
181
- method: 'POST',
182
- headers: {
183
- 'Content-Type': 'application/json',
184
- 'X-API-Key': apiKey
185
- },
186
- body: JSON.stringify(updateData)
187
- });
188
- const data = await response.json();
189
- if (!data.success) {
190
- throw new Error(data.error);
160
+ // Keep support for --languages by reconciling desired list through add/remove-language APIs.
161
+ let targetLanguages = null;
162
+ if (options.languages) {
163
+ const parsedLanguages = options.languages
164
+ .split(',')
165
+ .map((lang) => lang.trim())
166
+ .filter((lang) => lang.length > 0);
167
+ if (parsedLanguages.length === 0) {
168
+ throw new Error('At least one language must be provided when using --languages');
169
+ }
170
+ const effectiveDefaultLanguage = options.defaultLanguage || project.defaultLanguage;
171
+ if (!parsedLanguages.includes(effectiveDefaultLanguage)) {
172
+ throw new Error(`Default language "${effectiveDefaultLanguage}" must be included in --languages`);
173
+ }
174
+ targetLanguages = parsedLanguages;
175
+ }
176
+ if (Object.keys(updateData).length > 0) {
177
+ await api.patch(`/orgs/${orgId}/projects/${project.id}`, updateData);
178
+ }
179
+ if (targetLanguages) {
180
+ const desiredLanguages = targetLanguages;
181
+ const currentLanguages = project.languages || [];
182
+ const toAdd = desiredLanguages.filter((lang) => !currentLanguages.includes(lang));
183
+ const toRemove = currentLanguages.filter((lang) => !desiredLanguages.includes(lang));
184
+ for (const lang of toAdd) {
185
+ await api.post(`/orgs/${orgId}/projects/${project.id}/languages`, { code: lang });
186
+ }
187
+ for (const lang of toRemove) {
188
+ await api.delete(`/orgs/${orgId}/projects/${project.id}/languages/${lang}`);
189
+ }
191
190
  }
192
191
  spinner.succeed(chalk.green(`Updated project: ${slug}`));
193
192
  console.log('');
@@ -195,6 +194,7 @@ export async function projectsUpdateCommand(slug, options) {
195
194
  catch (error) {
196
195
  spinner.fail(chalk.red('Failed to update project'));
197
196
  console.error(chalk.red(`Error: ${error.message}\n`));
197
+ process.exitCode = 1;
198
198
  }
199
199
  }
200
200
  /**
@@ -203,46 +203,27 @@ export async function projectsUpdateCommand(slug, options) {
203
203
  export async function projectsDeleteCommand(slug) {
204
204
  if (!isAuthenticated()) {
205
205
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+ const orgId = config.get('organizationId');
210
+ if (!orgId) {
211
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
212
+ process.exitCode = 1;
206
213
  return;
207
214
  }
208
- const apiKey = getApiKey();
209
215
  const spinner = ora('Deleting project...').start();
210
216
  try {
211
- // Get project ID from slug
212
- const listResponse = await fetch(LIST_PROJECTS_URL, {
213
- method: 'POST',
214
- headers: {
215
- 'Content-Type': 'application/json',
216
- 'X-API-Key': apiKey
217
- }
218
- });
219
- const listData = await listResponse.json();
220
- const project = listData.projects?.find((p) => p.slug === slug);
221
- if (!project) {
222
- spinner.fail(chalk.red(`Project "${slug}" not found`));
223
- return;
224
- }
225
- const response = await fetch(MANAGE_PROJECTS_URL, {
226
- method: 'POST',
227
- headers: {
228
- 'Content-Type': 'application/json',
229
- 'X-API-Key': apiKey
230
- },
231
- body: JSON.stringify({
232
- action: 'delete',
233
- projectId: project.id
234
- })
235
- });
236
- const data = await response.json();
237
- if (!data.success) {
238
- throw new Error(data.error);
239
- }
217
+ const project = await resolveProject(orgId, slug);
218
+ const api = getApiClient();
219
+ await api.delete(`/orgs/${orgId}/projects/${project.id}`);
240
220
  spinner.succeed(chalk.green(`Deleted project: ${slug}`));
241
221
  console.log('');
242
222
  }
243
223
  catch (error) {
244
224
  spinner.fail(chalk.red('Failed to delete project'));
245
225
  console.error(chalk.red(`Error: ${error.message}\n`));
226
+ process.exitCode = 1;
246
227
  }
247
228
  }
248
229
  /**
@@ -251,47 +232,27 @@ export async function projectsDeleteCommand(slug) {
251
232
  export async function projectsAddLanguageCommand(slug, language) {
252
233
  if (!isAuthenticated()) {
253
234
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
235
+ process.exitCode = 1;
236
+ return;
237
+ }
238
+ const orgId = config.get('organizationId');
239
+ if (!orgId) {
240
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
241
+ process.exitCode = 1;
254
242
  return;
255
243
  }
256
- const apiKey = getApiKey();
257
244
  const spinner = ora(`Adding language ${language}...`).start();
258
245
  try {
259
- // Get project ID from slug
260
- const listResponse = await fetch(LIST_PROJECTS_URL, {
261
- method: 'POST',
262
- headers: {
263
- 'Content-Type': 'application/json',
264
- 'X-API-Key': apiKey
265
- }
266
- });
267
- const listData = await listResponse.json();
268
- const project = listData.projects?.find((p) => p.slug === slug);
269
- if (!project) {
270
- spinner.fail(chalk.red(`Project "${slug}" not found`));
271
- return;
272
- }
273
- const response = await fetch(MANAGE_PROJECTS_URL, {
274
- method: 'POST',
275
- headers: {
276
- 'Content-Type': 'application/json',
277
- 'X-API-Key': apiKey
278
- },
279
- body: JSON.stringify({
280
- action: 'add-language',
281
- projectId: project.id,
282
- language
283
- })
284
- });
285
- const data = await response.json();
286
- if (!data.success) {
287
- throw new Error(data.error);
288
- }
289
- spinner.succeed(chalk.green(data.message));
246
+ const project = await resolveProject(orgId, slug);
247
+ const api = getApiClient();
248
+ await api.post(`/orgs/${orgId}/projects/${project.id}/languages`, { code: language });
249
+ spinner.succeed(chalk.green(`Added language "${language}" to project "${slug}"`));
290
250
  console.log('');
291
251
  }
292
252
  catch (error) {
293
253
  spinner.fail(chalk.red('Failed to add language'));
294
254
  console.error(chalk.red(`Error: ${error.message}\n`));
255
+ process.exitCode = 1;
295
256
  }
296
257
  }
297
258
  /**
@@ -300,47 +261,27 @@ export async function projectsAddLanguageCommand(slug, language) {
300
261
  export async function projectsRemoveLanguageCommand(slug, language) {
301
262
  if (!isAuthenticated()) {
302
263
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+ const orgId = config.get('organizationId');
268
+ if (!orgId) {
269
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
270
+ process.exitCode = 1;
303
271
  return;
304
272
  }
305
- const apiKey = getApiKey();
306
273
  const spinner = ora(`Removing language ${language}...`).start();
307
274
  try {
308
- // Get project ID from slug
309
- const listResponse = await fetch(LIST_PROJECTS_URL, {
310
- method: 'POST',
311
- headers: {
312
- 'Content-Type': 'application/json',
313
- 'X-API-Key': apiKey
314
- }
315
- });
316
- const listData = await listResponse.json();
317
- const project = listData.projects?.find((p) => p.slug === slug);
318
- if (!project) {
319
- spinner.fail(chalk.red(`Project "${slug}" not found`));
320
- return;
321
- }
322
- const response = await fetch(MANAGE_PROJECTS_URL, {
323
- method: 'POST',
324
- headers: {
325
- 'Content-Type': 'application/json',
326
- 'X-API-Key': apiKey
327
- },
328
- body: JSON.stringify({
329
- action: 'remove-language',
330
- projectId: project.id,
331
- language
332
- })
333
- });
334
- const data = await response.json();
335
- if (!data.success) {
336
- throw new Error(data.error);
337
- }
338
- spinner.succeed(chalk.green(data.message));
275
+ const project = await resolveProject(orgId, slug);
276
+ const api = getApiClient();
277
+ await api.delete(`/orgs/${orgId}/projects/${project.id}/languages/${language}`);
278
+ spinner.succeed(chalk.green(`Removed language "${language}" from project "${slug}"`));
339
279
  console.log('');
340
280
  }
341
281
  catch (error) {
342
282
  spinner.fail(chalk.red('Failed to remove language'));
343
283
  console.error(chalk.red(`Error: ${error.message}\n`));
284
+ process.exitCode = 1;
344
285
  }
345
286
  }
346
287
  /**
@@ -349,54 +290,36 @@ export async function projectsRemoveLanguageCommand(slug, language) {
349
290
  export async function projectsStatsCommand(slug) {
350
291
  if (!isAuthenticated()) {
351
292
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
293
+ process.exitCode = 1;
294
+ return;
295
+ }
296
+ const orgId = config.get('organizationId');
297
+ if (!orgId) {
298
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
299
+ process.exitCode = 1;
352
300
  return;
353
301
  }
354
- const apiKey = getApiKey();
355
302
  const spinner = ora('Fetching statistics...').start();
356
303
  try {
357
- // Get project ID from slug
358
- const listResponse = await fetch(LIST_PROJECTS_URL, {
359
- method: 'POST',
360
- headers: {
361
- 'Content-Type': 'application/json',
362
- 'X-API-Key': apiKey
363
- }
364
- });
365
- const listData = await listResponse.json();
366
- const project = listData.projects?.find((p) => p.slug === slug);
367
- if (!project) {
368
- spinner.fail(chalk.red(`Project "${slug}" not found`));
369
- return;
370
- }
371
- const response = await fetch(MANAGE_PROJECTS_URL, {
372
- method: 'POST',
373
- headers: {
374
- 'Content-Type': 'application/json',
375
- 'X-API-Key': apiKey
376
- },
377
- body: JSON.stringify({
378
- action: 'stats',
379
- projectId: project.id
380
- })
381
- });
382
- const data = await response.json();
383
- if (!data.success) {
384
- throw new Error(data.error);
385
- }
304
+ const project = await resolveProject(orgId, slug);
305
+ const api = getApiClient();
306
+ const stats = await api.get(`/orgs/${orgId}/projects/${project.id}/stats`);
386
307
  spinner.stop();
387
308
  console.log(chalk.blue.bold('\n📊 Project Statistics\n'));
388
- console.log(chalk.white(`Total Keys: ${data.stats.total_keys}`));
389
- console.log(chalk.green(`Published Keys: ${data.stats.published_keys}`));
390
- console.log(chalk.yellow(`Unpublished Keys: ${data.stats.unpublished_keys}`));
391
- console.log(chalk.white(`Modules: ${data.stats.modules}`));
392
- if (data.stats.module_names && data.stats.module_names.length > 0) {
393
- console.log(chalk.gray(` ${data.stats.module_names.join(', ')}`));
309
+ console.log(chalk.white(`Total Keys: ${stats.totalKeys}`));
310
+ console.log(chalk.green(`Published Keys: ${stats.publishedKeys}`));
311
+ console.log(chalk.yellow(`Unpublished Keys: ${stats.unpublishedKeys}`));
312
+ console.log(chalk.white(`Language Count: ${stats.languageCount}`));
313
+ if (stats.modules && stats.modules.length > 0) {
314
+ console.log(chalk.white(`Modules: ${stats.modules.length}`));
315
+ console.log(chalk.gray(` ${stats.modules.join(', ')}`));
394
316
  }
395
317
  console.log('');
396
318
  }
397
319
  catch (error) {
398
320
  spinner.fail(chalk.red('Failed to fetch statistics'));
399
321
  console.error(chalk.red(`Error: ${error.message}\n`));
322
+ process.exitCode = 1;
400
323
  }
401
324
  }
402
325
  //# sourceMappingURL=projects.js.map