langctl 0.2.0 → 0.4.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +158 -1188
  3. package/dist/commands/auth.js +85 -19
  4. package/dist/commands/config.js +36 -26
  5. package/dist/commands/export.js +11 -122
  6. package/dist/commands/import.js +32 -143
  7. package/dist/commands/init.js +61 -72
  8. package/dist/commands/keys.js +145 -279
  9. package/dist/commands/org.js +45 -111
  10. package/dist/commands/projects.js +124 -312
  11. package/dist/commands/pull.js +86 -129
  12. package/dist/commands/push.js +125 -0
  13. package/dist/commands/review.js +65 -0
  14. package/dist/commands/team.js +68 -257
  15. package/dist/commands/translate.js +99 -0
  16. package/dist/core/cache.js +48 -0
  17. package/dist/core/config.js +130 -0
  18. package/dist/core/errors.js +67 -0
  19. package/dist/core/files.js +38 -0
  20. package/dist/core/http.js +156 -0
  21. package/dist/core/output.js +96 -0
  22. package/dist/core/project.js +58 -0
  23. package/dist/core/prompts.js +56 -0
  24. package/dist/formats/index.js +285 -0
  25. package/dist/index.js +277 -508
  26. package/dist/version.js +4 -0
  27. package/package.json +22 -15
  28. package/.gitattributes +0 -2
  29. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -26
  30. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  31. package/CONTRIBUTING.md +0 -33
  32. package/dist/api.d.ts +0 -20
  33. package/dist/api.js +0 -75
  34. package/dist/auth.d.ts +0 -38
  35. package/dist/auth.js +0 -106
  36. package/dist/commands/auth.d.ts +0 -3
  37. package/dist/commands/config.d.ts +0 -2
  38. package/dist/commands/debug.d.ts +0 -3
  39. package/dist/commands/debug.js +0 -61
  40. package/dist/commands/export.d.ts +0 -13
  41. package/dist/commands/import.d.ts +0 -12
  42. package/dist/commands/init.d.ts +0 -2
  43. package/dist/commands/keys.d.ts +0 -25
  44. package/dist/commands/org.d.ts +0 -13
  45. package/dist/commands/projects.d.ts +0 -33
  46. package/dist/commands/pull.d.ts +0 -9
  47. package/dist/commands/team.d.ts +0 -29
  48. package/dist/config.d.ts +0 -48
  49. package/dist/config.js +0 -69
  50. package/dist/exporters/index.d.ts +0 -36
  51. package/dist/exporters/index.js +0 -214
  52. package/dist/index.d.ts +0 -3
  53. package/dist/utils/banner.d.ts +0 -9
  54. package/dist/utils/banner.js +0 -24
  55. package/translations/en.json +0 -5
  56. package/translations/en.xml +0 -9
  57. package/translations/fr.json +0 -4
  58. package/translations/fr.xml +0 -7
  59. package/translations/hi.json +0 -4
  60. package/translations/hi.xml +0 -7
  61. package/translations/ios/en.strings +0 -9
  62. package/translations/ios/fr.strings +0 -6
  63. package/translations/ios/hi.strings +0 -6
  64. package/translations/ios/ja.strings +0 -6
  65. package/translations/ja.json +0 -4
  66. package/translations/ja.xml +0 -7
@@ -1,325 +1,137 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
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
- */
2
+ import { CliError, ExitCode, usageError } from '../core/errors.js';
3
+ import { getSession } from '../core/http.js';
4
+ import { log, printJson, runtime, table } from '../core/output.js';
5
+ import { confirm } from '../core/prompts.js';
6
+ import { getProject } from '../core/project.js';
7
+ import { splitList } from './pull.js';
16
8
  export async function projectsListCommand() {
17
- if (!isAuthenticated()) {
18
- console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
19
- process.exitCode = 1;
20
- return;
21
- }
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;
26
- return;
27
- }
28
- const spinner = ora('Fetching projects...').start();
29
- try {
30
- const api = getApiClient();
31
- const projects = await api.get(`/orgs/${orgId}/projects`);
32
- spinner.stop();
33
- if (!projects || projects.length === 0) {
34
- console.log(chalk.yellow('\nNo projects found. Create one in the dashboard first.\n'));
35
- return;
36
- }
37
- console.log(chalk.blue.bold(`\n📦 Projects (${projects.length})\n`));
38
- projects.forEach((project) => {
39
- console.log(chalk.white.bold(project.name));
40
- console.log(chalk.gray(` Slug: ${project.slug}`));
41
- if (project.description) {
42
- console.log(chalk.gray(` Description: ${project.description}`));
43
- }
44
- console.log(chalk.gray(` Languages: ${project.languages.join(', ')}`));
45
- console.log(chalk.gray(` Default: ${project.defaultLanguage}`));
46
- if (project.modules && project.modules.length > 0) {
47
- console.log(chalk.gray(` Modules: ${project.modules.join(', ')}`));
48
- }
49
- console.log('');
50
- });
51
- console.log(chalk.blue('To export translations, run:'));
52
- console.log(chalk.white(` langctl export ${projects[0].slug} -l en\n`));
53
- }
54
- catch (error) {
55
- spinner.fail(chalk.red('Failed to fetch projects'));
56
- console.error(chalk.red(`Error: ${error.message}\n`));
57
- process.exitCode = 1;
58
- }
59
- }
60
- /**
61
- * Create new project
62
- */
63
- export async function projectsCreateCommand(name, options) {
64
- if (!isAuthenticated()) {
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;
73
- return;
74
- }
75
- const spinner = ora('Creating project...').start();
76
- try {
77
- const api = getApiClient();
78
- const languages = options.languages ? options.languages.split(',') : ['en'];
79
- const defaultLanguage = options.defaultLanguage || languages[0];
80
- const project = await api.post(`/orgs/${orgId}/projects`, {
81
- name,
82
- description: options.description,
83
- languages,
84
- defaultLanguage
85
- });
86
- spinner.succeed(chalk.green(`Created project: ${name} (${project.slug})`));
87
- console.log('');
88
- }
89
- catch (error) {
90
- spinner.fail(chalk.red('Failed to create project'));
91
- console.error(chalk.red(`Error: ${error.message}\n`));
92
- process.exitCode = 1;
93
- }
9
+ const session = await getSession();
10
+ const projects = await session.api.get(`/orgs/${session.orgId}/projects`);
11
+ if (runtime.json)
12
+ return printJson(projects);
13
+ if (projects.length === 0)
14
+ return log.info('No projects yet. Create one with "langctl projects create <name>".');
15
+ table(projects.map(p => [p.slug, p.name, p.languages.join(','), p.defaultLanguage]), ['SLUG', 'NAME', 'LANGUAGES', 'DEFAULT']);
94
16
  }
95
- /**
96
- * Get project details
97
- */
98
17
  export async function projectsGetCommand(slug) {
99
- if (!isAuthenticated()) {
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;
108
- return;
109
- }
110
- const spinner = ora('Fetching project...').start();
111
- try {
112
- const project = await resolveProject(orgId, slug);
113
- spinner.stop();
114
- console.log(chalk.blue.bold('\n📦 Project Details\n'));
115
- console.log(chalk.white.bold(project.name));
116
- console.log(chalk.gray(`Slug: ${project.slug}`));
117
- console.log(chalk.gray(`ID: ${project.id}`));
118
- if (project.description) {
119
- console.log(chalk.gray(`Description: ${project.description}`));
120
- }
121
- console.log(chalk.gray(`Languages: ${project.languages.join(', ')}`));
122
- console.log(chalk.gray(`Default Language: ${project.defaultLanguage}`));
123
- if (project.modules && project.modules.length > 0) {
124
- console.log(chalk.gray(`Modules: ${project.modules.join(', ')}`));
125
- }
126
- console.log('');
127
- }
128
- catch (error) {
129
- spinner.fail(chalk.red(`Failed to fetch project "${slug}"`));
130
- console.error(chalk.red(`Error: ${error.message}\n`));
131
- process.exitCode = 1;
132
- }
18
+ const session = await getSession();
19
+ const project = await getProject(session, slug);
20
+ if (runtime.json)
21
+ return printJson(project);
22
+ log.out(`${chalk.bold(project.name)} ${chalk.dim(`(${project.slug})`)}`);
23
+ if (project.description)
24
+ log.out(chalk.dim(project.description));
25
+ log.out(` languages ${project.languages.join(', ')} ${chalk.dim(`default ${project.defaultLanguage}`)}`);
26
+ if (project.modules?.length)
27
+ log.out(` modules ${project.modules.join(', ')}`);
28
+ log.out(` id ${project.id}`);
133
29
  }
134
- /**
135
- * Update project
136
- */
137
- export async function projectsUpdateCommand(slug, options) {
138
- if (!isAuthenticated()) {
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;
147
- return;
148
- }
149
- const spinner = ora('Updating project...').start();
150
- try {
151
- const project = await resolveProject(orgId, slug);
152
- const api = getApiClient();
153
- const updateData = {};
154
- if (options.name)
155
- updateData.name = options.name;
156
- if (options.description !== undefined)
157
- updateData.description = options.description;
158
- if (options.defaultLanguage)
159
- updateData.defaultLanguage = options.defaultLanguage;
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
- }
190
- }
191
- spinner.succeed(chalk.green(`Updated project: ${slug}`));
192
- console.log('');
193
- }
194
- catch (error) {
195
- spinner.fail(chalk.red('Failed to update project'));
196
- console.error(chalk.red(`Error: ${error.message}\n`));
197
- process.exitCode = 1;
198
- }
30
+ export async function projectsCreateCommand(name, opts) {
31
+ const session = await getSession();
32
+ const languages = splitList(opts.languages ?? 'en');
33
+ const defaultLanguage = opts.defaultLanguage ?? languages[0];
34
+ if (!languages.includes(defaultLanguage))
35
+ throw usageError(`Default language "${defaultLanguage}" must be one of --languages (${languages.join(',')}).`);
36
+ const project = await session.api.post(`/orgs/${session.orgId}/projects`, { name, description: opts.description, languages, defaultLanguage });
37
+ if (runtime.json)
38
+ return printJson(project);
39
+ log.success(`Created ${chalk.bold(project.name)} — slug ${chalk.cyan(project.slug)}`);
40
+ }
41
+ export async function projectsUpdateCommand(slug, opts) {
42
+ const session = await getSession();
43
+ const project = await getProject(session, slug);
44
+ const body = {};
45
+ if (opts.name)
46
+ body.name = opts.name;
47
+ if (opts.description !== undefined)
48
+ body.description = opts.description;
49
+ if (opts.defaultLanguage)
50
+ body.defaultLanguage = opts.defaultLanguage;
51
+ let toAdd = [];
52
+ let toRemove = [];
53
+ if (opts.languages) {
54
+ const desired = splitList(opts.languages);
55
+ const effectiveDefault = opts.defaultLanguage ?? project.defaultLanguage;
56
+ if (!desired.includes(effectiveDefault))
57
+ throw usageError(`--languages must include the default language "${effectiveDefault}".`);
58
+ toAdd = desired.filter(l => !project.languages.includes(l));
59
+ toRemove = project.languages.filter(l => !desired.includes(l));
60
+ }
61
+ if (Object.keys(body).length === 0 && !toAdd.length && !toRemove.length)
62
+ throw usageError('Nothing to update.');
63
+ if (toRemove.length && !(await confirm(`Remove ${toRemove.join(', ')} from ${project.slug} and delete all of their translations?`))) {
64
+ throw new CliError('Cancelled.', ExitCode.Error);
65
+ }
66
+ // Add new languages first so a new default language exists before it is set
67
+ for (const code of toAdd)
68
+ await session.api.post(`/orgs/${session.orgId}/projects/${project.id}/languages`, { code });
69
+ if (Object.keys(body).length)
70
+ await session.api.patch(`/orgs/${session.orgId}/projects/${project.id}`, body);
71
+ for (const code of toRemove)
72
+ await session.api.delete(`/orgs/${session.orgId}/projects/${project.id}/languages/${encodeURIComponent(code)}`);
73
+ const updated = await getProject(session, slug);
74
+ if (runtime.json)
75
+ return printJson(updated);
76
+ log.success(`Updated ${chalk.bold(updated.slug)}${toAdd.length ? ` +${toAdd.join(',')}` : ''}${toRemove.length ? ` -${toRemove.join(',')}` : ''}`);
199
77
  }
200
- /**
201
- * Delete project
202
- */
203
78
  export async function projectsDeleteCommand(slug) {
204
- if (!isAuthenticated()) {
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;
213
- return;
214
- }
215
- const spinner = ora('Deleting project...').start();
216
- try {
217
- const project = await resolveProject(orgId, slug);
218
- const api = getApiClient();
219
- await api.delete(`/orgs/${orgId}/projects/${project.id}`);
220
- spinner.succeed(chalk.green(`Deleted project: ${slug}`));
221
- console.log('');
222
- }
223
- catch (error) {
224
- spinner.fail(chalk.red('Failed to delete project'));
225
- console.error(chalk.red(`Error: ${error.message}\n`));
226
- process.exitCode = 1;
227
- }
79
+ const session = await getSession();
80
+ const project = await getProject(session, slug);
81
+ if (!(await confirm(`Delete project "${project.slug}" and all of its translation keys?`)))
82
+ throw new CliError('Cancelled.', ExitCode.Error);
83
+ await session.api.delete(`/orgs/${session.orgId}/projects/${project.id}`);
84
+ if (runtime.json)
85
+ return printJson({ deleted: project.slug });
86
+ log.success(`Deleted project ${chalk.bold(project.slug)}`);
228
87
  }
229
- /**
230
- * Add language to project
231
- */
232
- export async function projectsAddLanguageCommand(slug, language) {
233
- if (!isAuthenticated()) {
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;
242
- return;
243
- }
244
- const spinner = ora(`Adding language ${language}...`).start();
245
- try {
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}"`));
250
- console.log('');
251
- }
252
- catch (error) {
253
- spinner.fail(chalk.red('Failed to add language'));
254
- console.error(chalk.red(`Error: ${error.message}\n`));
255
- process.exitCode = 1;
88
+ export async function projectsAddLanguageCommand(slug, codes) {
89
+ const session = await getSession();
90
+ const project = await getProject(session, slug);
91
+ const added = [];
92
+ for (const code of codes) {
93
+ if (project.languages.includes(code)) {
94
+ log.info(`${code} is already in ${project.slug}`);
95
+ continue;
96
+ }
97
+ await session.api.post(`/orgs/${session.orgId}/projects/${project.id}/languages`, { code });
98
+ added.push(code);
256
99
  }
100
+ if (runtime.json)
101
+ return printJson({ project: project.slug, added });
102
+ if (added.length)
103
+ log.success(`Added ${added.join(', ')} to ${chalk.bold(project.slug)}`);
257
104
  }
258
- /**
259
- * Remove language from project
260
- */
261
- export async function projectsRemoveLanguageCommand(slug, language) {
262
- if (!isAuthenticated()) {
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;
271
- return;
272
- }
273
- const spinner = ora(`Removing language ${language}...`).start();
274
- try {
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}"`));
279
- console.log('');
280
- }
281
- catch (error) {
282
- spinner.fail(chalk.red('Failed to remove language'));
283
- console.error(chalk.red(`Error: ${error.message}\n`));
284
- process.exitCode = 1;
285
- }
105
+ export async function projectsRemoveLanguageCommand(slug, codes) {
106
+ const session = await getSession();
107
+ const project = await getProject(session, slug);
108
+ const missing = codes.filter(c => !project.languages.includes(c));
109
+ if (missing.length)
110
+ throw usageError(`Not in ${project.slug}: ${missing.join(', ')}`);
111
+ if (codes.includes(project.defaultLanguage))
112
+ throw usageError(`"${project.defaultLanguage}" is the default language and can't be removed.`);
113
+ if (!(await confirm(`Remove ${codes.join(', ')} from ${project.slug} and delete all of their translations?`)))
114
+ throw new CliError('Cancelled.', ExitCode.Error);
115
+ for (const code of codes)
116
+ await session.api.delete(`/orgs/${session.orgId}/projects/${project.id}/languages/${encodeURIComponent(code)}`);
117
+ if (runtime.json)
118
+ return printJson({ project: project.slug, removed: codes });
119
+ log.success(`Removed ${codes.join(', ')} from ${chalk.bold(project.slug)}`);
286
120
  }
287
- /**
288
- * Get project statistics
289
- */
290
121
  export async function projectsStatsCommand(slug) {
291
- if (!isAuthenticated()) {
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;
300
- return;
301
- }
302
- const spinner = ora('Fetching statistics...').start();
303
- try {
304
- const project = await resolveProject(orgId, slug);
305
- const api = getApiClient();
306
- const stats = await api.get(`/orgs/${orgId}/projects/${project.id}/stats`);
307
- spinner.stop();
308
- console.log(chalk.blue.bold('\n📊 Project Statistics\n'));
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(', ')}`));
316
- }
317
- console.log('');
318
- }
319
- catch (error) {
320
- spinner.fail(chalk.red('Failed to fetch statistics'));
321
- console.error(chalk.red(`Error: ${error.message}\n`));
322
- process.exitCode = 1;
323
- }
122
+ const session = await getSession();
123
+ const project = await getProject(session, slug);
124
+ const [stats, snapshot] = await Promise.all([
125
+ session.api.get(`/orgs/${session.orgId}/projects/${project.id}/stats`),
126
+ session.api.get(`/orgs/${session.orgId}/projects/${project.id}/export`, { publishedOnly: 'false' }),
127
+ ]);
128
+ const total = snapshot.keys.length;
129
+ const coverage = project.languages.map(lang => {
130
+ const translated = snapshot.keys.filter(k => k.translations[lang]).length;
131
+ return { language: lang, translated, missing: total - translated, percent: total ? Math.round((translated / total) * 100) : 100 };
132
+ });
133
+ if (runtime.json)
134
+ return printJson({ ...stats, coverage });
135
+ log.out(`${chalk.bold(project.slug)} ${stats.totalKeys} keys ${chalk.green(`${stats.publishedKeys} published`)} ${chalk.yellow(`${stats.unpublishedKeys} draft`)}`);
136
+ table(coverage.map(c => [c.language, `${c.translated}/${total}`, c.missing ? chalk.yellow(`${c.percent}%`) : chalk.green('100%')]), ['LANG', 'TRANSLATED', 'DONE']);
324
137
  }
325
- //# sourceMappingURL=projects.js.map