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,30 +1,26 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { isAuthenticated, getApiKey } from '../auth.js';
4
- const EDGE_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/manage-translation-keys';
5
- const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
3
+ import { isAuthenticated } from '../auth.js';
4
+ import { getApiClient } from '../api.js';
5
+ import { config } from '../config.js';
6
6
  /**
7
- * Get project by slug
7
+ * Resolve a project slug to a full project object
8
8
  */
9
- async function getProjectBySlug(apiKey, slug) {
10
- try {
11
- const response = await fetch(LIST_PROJECTS_URL, {
12
- method: 'POST',
13
- headers: {
14
- 'Content-Type': 'application/json',
15
- 'X-API-Key': apiKey
16
- }
17
- });
18
- if (!response.ok)
19
- return null;
20
- const data = await response.json();
21
- if (!data.success || !data.projects)
22
- return null;
23
- return data.projects.find((p) => p.slug === slug) || null;
24
- }
25
- catch (error) {
26
- return null;
27
- }
9
+ async function resolveProject(orgId, slug) {
10
+ const api = getApiClient();
11
+ return api.get(`/orgs/${orgId}/projects/by-slug/${slug}`);
12
+ }
13
+ /**
14
+ * Find a key by exact name within a project (search + exact match)
15
+ */
16
+ async function findKeyByName(orgId, projectId, keyName) {
17
+ const api = getApiClient();
18
+ const result = await api.get(`/orgs/${orgId}/projects/${projectId}/keys`, {
19
+ search: keyName,
20
+ pageSize: '100'
21
+ });
22
+ const key = result.data?.find((k) => k.key === keyName);
23
+ return key || null;
28
24
  }
29
25
  /**
30
26
  * List translation keys
@@ -32,43 +28,48 @@ async function getProjectBySlug(apiKey, slug) {
32
28
  export async function listKeysCommand(projectSlug, options) {
33
29
  if (!isAuthenticated()) {
34
30
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ const orgId = config.get('organizationId');
35
+ if (!orgId) {
36
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
37
+ process.exitCode = 1;
35
38
  return;
36
39
  }
37
- const apiKey = getApiKey();
38
40
  const spinner = ora('Fetching keys...').start();
39
41
  try {
40
- const project = await getProjectBySlug(apiKey, projectSlug);
41
- if (!project) {
42
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
43
- return;
44
- }
45
- const response = await fetch(EDGE_FUNCTION_URL, {
46
- method: 'POST',
47
- headers: {
48
- 'Content-Type': 'application/json',
49
- 'X-API-Key': apiKey
50
- },
51
- body: JSON.stringify({
52
- action: 'list',
53
- projectId: project.id,
54
- module: options.module,
55
- published: options.published,
56
- search: options.search,
57
- limit: options.limit || 100,
58
- offset: options.offset || 0
59
- })
60
- });
61
- const data = await response.json();
62
- if (!data.success) {
63
- throw new Error(data.error);
64
- }
42
+ const project = await resolveProject(orgId, projectSlug);
43
+ const limit = Math.max(1, parseInt(options.limit || '100', 10) || 100);
44
+ const offset = Math.max(0, parseInt(options.offset || '0', 10) || 0);
45
+ const skipWithinPage = offset % limit;
46
+ const page = Math.floor(offset / limit) + 1;
47
+ const pageSize = Math.min(100, limit + skipWithinPage);
48
+ const api = getApiClient();
49
+ const params = {
50
+ page: String(page),
51
+ pageSize: String(pageSize)
52
+ };
53
+ if (options.module)
54
+ params.module = options.module;
55
+ if (options.search)
56
+ params.search = options.search;
57
+ if (options.published)
58
+ params.published = 'true';
59
+ const result = await api.get(`/orgs/${orgId}/projects/${project.id}/keys`, params);
65
60
  spinner.stop();
66
- if (!data.keys || data.keys.length === 0) {
61
+ let keys = result.data || [];
62
+ if (skipWithinPage > 0) {
63
+ keys = keys.slice(skipWithinPage);
64
+ }
65
+ keys = keys.slice(0, limit);
66
+ const total = result.pagination?.total;
67
+ if (!keys || keys.length === 0) {
67
68
  console.log(chalk.yellow('\nNo keys found\n'));
68
69
  return;
69
70
  }
70
- console.log(chalk.blue.bold(`\n🔑 Translation Keys (${data.keys.length}${data.total ? ` of ${data.total}` : ''})\n`));
71
- data.keys.forEach((key) => {
71
+ console.log(chalk.blue.bold(`\n🔑 Translation Keys (${keys.length}${total ? ` of ${total}` : ''})\n`));
72
+ keys.forEach((key) => {
72
73
  console.log(chalk.white.bold(key.key));
73
74
  if (key.description) {
74
75
  console.log(chalk.gray(` Description: ${key.description}`));
@@ -77,7 +78,7 @@ export async function listKeysCommand(projectSlug, options) {
77
78
  console.log(chalk.gray(` Module: ${key.module}`));
78
79
  }
79
80
  console.log(chalk.gray(` Published: ${key.published ? chalk.green('Yes') : chalk.yellow('No')}`));
80
- const langs = Object.keys(key.translations);
81
+ const langs = Object.keys(key.translations || {});
81
82
  if (langs.length > 0) {
82
83
  console.log(chalk.gray(` Languages: ${langs.join(', ')}`));
83
84
  }
@@ -87,6 +88,7 @@ export async function listKeysCommand(projectSlug, options) {
87
88
  catch (error) {
88
89
  spinner.fail(chalk.red('Failed to list keys'));
89
90
  console.error(chalk.red(`Error: ${error.message}\n`));
91
+ process.exitCode = 1;
90
92
  }
91
93
  }
92
94
  /**
@@ -95,36 +97,19 @@ export async function listKeysCommand(projectSlug, options) {
95
97
  export async function getKeyCommand(projectSlug, keyName) {
96
98
  if (!isAuthenticated()) {
97
99
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const orgId = config.get('organizationId');
104
+ if (!orgId) {
105
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
106
+ process.exitCode = 1;
98
107
  return;
99
108
  }
100
- const apiKey = getApiKey();
101
109
  const spinner = ora('Fetching key...').start();
102
110
  try {
103
- const project = await getProjectBySlug(apiKey, projectSlug);
104
- if (!project) {
105
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
106
- return;
107
- }
108
- // First list to find the key ID
109
- const listResponse = await fetch(EDGE_FUNCTION_URL, {
110
- method: 'POST',
111
- headers: {
112
- 'Content-Type': 'application/json',
113
- 'X-API-Key': apiKey
114
- },
115
- body: JSON.stringify({
116
- action: 'list',
117
- projectId: project.id,
118
- search: keyName,
119
- limit: 1
120
- })
121
- });
122
- const listData = await listResponse.json();
123
- if (!listData.success || !listData.keys || listData.keys.length === 0) {
124
- spinner.fail(chalk.red(`Key "${keyName}" not found`));
125
- return;
126
- }
127
- const key = listData.keys.find((k) => k.key === keyName);
111
+ const project = await resolveProject(orgId, projectSlug);
112
+ const key = await findKeyByName(orgId, project.id, keyName);
128
113
  if (!key) {
129
114
  spinner.fail(chalk.red(`Key "${keyName}" not found`));
130
115
  return;
@@ -141,7 +126,7 @@ export async function getKeyCommand(projectSlug, keyName) {
141
126
  }
142
127
  console.log(chalk.gray(`Published: ${key.published ? chalk.green('Yes') : chalk.yellow('No')}`));
143
128
  console.log(chalk.blue.bold('\nTranslations:'));
144
- Object.entries(key.translations).forEach(([lang, value]) => {
129
+ Object.entries(key.translations || {}).forEach(([lang, value]) => {
145
130
  console.log(chalk.white(` ${lang}: ${value}`));
146
131
  });
147
132
  console.log('');
@@ -149,6 +134,7 @@ export async function getKeyCommand(projectSlug, keyName) {
149
134
  catch (error) {
150
135
  spinner.fail(chalk.red('Failed to get key'));
151
136
  console.error(chalk.red(`Error: ${error.message}\n`));
137
+ process.exitCode = 1;
152
138
  }
153
139
  }
154
140
  /**
@@ -157,16 +143,18 @@ export async function getKeyCommand(projectSlug, keyName) {
157
143
  export async function createKeyCommand(projectSlug, keyName, options) {
158
144
  if (!isAuthenticated()) {
159
145
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+ const orgId = config.get('organizationId');
150
+ if (!orgId) {
151
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
152
+ process.exitCode = 1;
160
153
  return;
161
154
  }
162
- const apiKey = getApiKey();
163
155
  const spinner = ora('Creating key...').start();
164
156
  try {
165
- const project = await getProjectBySlug(apiKey, projectSlug);
166
- if (!project) {
167
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
168
- return;
169
- }
157
+ const project = await resolveProject(orgId, projectSlug);
170
158
  // Build translations object from --value-* options
171
159
  const translations = {};
172
160
  Object.keys(options).forEach(opt => {
@@ -177,32 +165,21 @@ export async function createKeyCommand(projectSlug, keyName, options) {
177
165
  }
178
166
  }
179
167
  });
180
- const response = await fetch(EDGE_FUNCTION_URL, {
181
- method: 'POST',
182
- headers: {
183
- 'Content-Type': 'application/json',
184
- 'X-API-Key': apiKey
185
- },
186
- body: JSON.stringify({
187
- action: 'create',
188
- projectId: project.id,
189
- key: keyName,
190
- translations,
191
- description: options.description,
192
- module: options.module,
193
- tags: options.tags ? options.tags.split(',') : []
194
- })
168
+ const api = getApiClient();
169
+ await api.post(`/orgs/${orgId}/projects/${project.id}/keys`, {
170
+ key: keyName,
171
+ translations: Object.keys(translations).length > 0 ? translations : undefined,
172
+ description: options.description,
173
+ module: options.module,
174
+ tags: options.tags ? options.tags.split(',') : undefined
195
175
  });
196
- const data = await response.json();
197
- if (!data.success) {
198
- throw new Error(data.error);
199
- }
200
176
  spinner.succeed(chalk.green(`Created key: ${keyName}`));
201
177
  console.log('');
202
178
  }
203
179
  catch (error) {
204
180
  spinner.fail(chalk.red('Failed to create key'));
205
181
  console.error(chalk.red(`Error: ${error.message}\n`));
182
+ process.exitCode = 1;
206
183
  }
207
184
  }
208
185
  /**
@@ -211,59 +188,32 @@ export async function createKeyCommand(projectSlug, keyName, options) {
211
188
  export async function deleteKeyCommand(projectSlug, keyName) {
212
189
  if (!isAuthenticated()) {
213
190
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
191
+ process.exitCode = 1;
192
+ return;
193
+ }
194
+ const orgId = config.get('organizationId');
195
+ if (!orgId) {
196
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
197
+ process.exitCode = 1;
214
198
  return;
215
199
  }
216
- const apiKey = getApiKey();
217
200
  const spinner = ora('Deleting key...').start();
218
201
  try {
219
- const project = await getProjectBySlug(apiKey, projectSlug);
220
- if (!project) {
221
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
222
- return;
223
- }
224
- // Find key ID
225
- const listResponse = await fetch(EDGE_FUNCTION_URL, {
226
- method: 'POST',
227
- headers: {
228
- 'Content-Type': 'application/json',
229
- 'X-API-Key': apiKey
230
- },
231
- body: JSON.stringify({
232
- action: 'list',
233
- projectId: project.id,
234
- search: keyName,
235
- limit: 1
236
- })
237
- });
238
- const listData = await listResponse.json();
239
- const key = listData.keys?.find((k) => k.key === keyName);
202
+ const project = await resolveProject(orgId, projectSlug);
203
+ const key = await findKeyByName(orgId, project.id, keyName);
240
204
  if (!key) {
241
205
  spinner.fail(chalk.red(`Key "${keyName}" not found`));
242
206
  return;
243
207
  }
244
- // Delete key
245
- const response = await fetch(EDGE_FUNCTION_URL, {
246
- method: 'POST',
247
- headers: {
248
- 'Content-Type': 'application/json',
249
- 'X-API-Key': apiKey
250
- },
251
- body: JSON.stringify({
252
- action: 'delete',
253
- projectId: project.id,
254
- keyId: key.id
255
- })
256
- });
257
- const data = await response.json();
258
- if (!data.success) {
259
- throw new Error(data.error);
260
- }
208
+ const api = getApiClient();
209
+ await api.delete(`/orgs/${orgId}/projects/${project.id}/keys/${key.id}`);
261
210
  spinner.succeed(chalk.green(`Deleted key: ${keyName}`));
262
211
  console.log('');
263
212
  }
264
213
  catch (error) {
265
214
  spinner.fail(chalk.red('Failed to delete key'));
266
215
  console.error(chalk.red(`Error: ${error.message}\n`));
216
+ process.exitCode = 1;
267
217
  }
268
218
  }
269
219
  /**
@@ -272,61 +222,32 @@ export async function deleteKeyCommand(projectSlug, keyName) {
272
222
  export async function translateKeyCommand(projectSlug, keyName, options) {
273
223
  if (!isAuthenticated()) {
274
224
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+ const orgId = config.get('organizationId');
229
+ if (!orgId) {
230
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
231
+ process.exitCode = 1;
275
232
  return;
276
233
  }
277
- const apiKey = getApiKey();
278
234
  const spinner = ora('Updating translation...').start();
279
235
  try {
280
- const project = await getProjectBySlug(apiKey, projectSlug);
281
- if (!project) {
282
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
283
- return;
284
- }
285
- // Find key ID
286
- const listResponse = await fetch(EDGE_FUNCTION_URL, {
287
- method: 'POST',
288
- headers: {
289
- 'Content-Type': 'application/json',
290
- 'X-API-Key': apiKey
291
- },
292
- body: JSON.stringify({
293
- action: 'list',
294
- projectId: project.id,
295
- search: keyName,
296
- limit: 1
297
- })
298
- });
299
- const listData = await listResponse.json();
300
- const key = listData.keys?.find((k) => k.key === keyName);
236
+ const project = await resolveProject(orgId, projectSlug);
237
+ const key = await findKeyByName(orgId, project.id, keyName);
301
238
  if (!key) {
302
239
  spinner.fail(chalk.red(`Key "${keyName}" not found`));
303
240
  return;
304
241
  }
305
- // Update translation
306
- const response = await fetch(EDGE_FUNCTION_URL, {
307
- method: 'POST',
308
- headers: {
309
- 'Content-Type': 'application/json',
310
- 'X-API-Key': apiKey
311
- },
312
- body: JSON.stringify({
313
- action: 'translate',
314
- projectId: project.id,
315
- keyId: key.id,
316
- language: options.language,
317
- value: options.value
318
- })
319
- });
320
- const data = await response.json();
321
- if (!data.success) {
322
- throw new Error(data.error);
323
- }
242
+ const api = getApiClient();
243
+ await api.patch(`/orgs/${orgId}/projects/${project.id}/keys/${key.id}/translations/${options.language}`, { value: options.value });
324
244
  spinner.succeed(chalk.green(`Updated ${options.language} translation for ${keyName}`));
325
245
  console.log('');
326
246
  }
327
247
  catch (error) {
328
248
  spinner.fail(chalk.red('Failed to update translation'));
329
249
  console.error(chalk.red(`Error: ${error.message}\n`));
250
+ process.exitCode = 1;
330
251
  }
331
252
  }
332
253
  /**
@@ -335,62 +256,39 @@ export async function translateKeyCommand(projectSlug, keyName, options) {
335
256
  export async function publishKeysCommand(projectSlug, keys, options) {
336
257
  if (!isAuthenticated()) {
337
258
  console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+ const orgId = config.get('organizationId');
263
+ if (!orgId) {
264
+ console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
265
+ process.exitCode = 1;
338
266
  return;
339
267
  }
340
- const apiKey = getApiKey();
341
268
  const action = options.unpublish ? 'unpublish' : 'publish';
342
269
  const spinner = ora(`${action === 'publish' ? 'Publishing' : 'Unpublishing'} keys...`).start();
343
270
  try {
344
- const project = await getProjectBySlug(apiKey, projectSlug);
345
- if (!project) {
346
- spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
347
- return;
348
- }
349
- // Get key IDs
350
- const listResponse = await fetch(EDGE_FUNCTION_URL, {
351
- method: 'POST',
352
- headers: {
353
- 'Content-Type': 'application/json',
354
- 'X-API-Key': apiKey
355
- },
356
- body: JSON.stringify({
357
- action: 'list',
358
- projectId: project.id,
359
- limit: 1000
360
- })
271
+ const project = await resolveProject(orgId, projectSlug);
272
+ // List all keys to match names to IDs
273
+ const api = getApiClient();
274
+ const result = await api.get(`/orgs/${orgId}/projects/${project.id}/keys`, {
275
+ pageSize: '1000'
361
276
  });
362
- const listData = await listResponse.json();
363
- const keyIds = listData.keys
277
+ const keyIds = result.data
364
278
  ?.filter((k) => keys.includes(k.key))
365
279
  .map((k) => k.id) || [];
366
280
  if (keyIds.length === 0) {
367
281
  spinner.fail(chalk.red('No matching keys found'));
368
282
  return;
369
283
  }
370
- // Publish/unpublish keys
371
- const response = await fetch(EDGE_FUNCTION_URL, {
372
- method: 'POST',
373
- headers: {
374
- 'Content-Type': 'application/json',
375
- 'X-API-Key': apiKey
376
- },
377
- body: JSON.stringify({
378
- action: 'publish',
379
- projectId: project.id,
380
- keyIds,
381
- published: !options.unpublish
382
- })
383
- });
384
- const data = await response.json();
385
- if (!data.success) {
386
- throw new Error(data.error);
387
- }
388
- spinner.succeed(chalk.green(data.message));
284
+ const response = await api.post(`/orgs/${orgId}/projects/${project.id}/keys/bulk-publish`, { keyIds, published: !options.unpublish });
285
+ spinner.succeed(chalk.green(response.message || `${action === 'publish' ? 'Published' : 'Unpublished'} ${response.count} key(s)`));
389
286
  console.log('');
390
287
  }
391
288
  catch (error) {
392
289
  spinner.fail(chalk.red(`Failed to ${action} keys`));
393
290
  console.error(chalk.red(`Error: ${error.message}\n`));
291
+ process.exitCode = 1;
394
292
  }
395
293
  }
396
294
  //# sourceMappingURL=keys.js.map