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
package/dist/index.js CHANGED
@@ -1,517 +1,286 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
2
+ import { Command, CommanderError, Option } from 'commander';
3
3
  import chalk from 'chalk';
4
- import { initCommand } from './commands/init.js';
5
- import { authCommand, logoutCommand } from './commands/auth.js';
6
- import { configCommand } from './commands/config.js';
7
- import { projectsListCommand, projectsCreateCommand, projectsGetCommand, projectsUpdateCommand, projectsDeleteCommand, projectsAddLanguageCommand, projectsRemoveLanguageCommand, projectsStatsCommand } from './commands/projects.js';
8
- import { listKeysCommand, getKeyCommand, createKeyCommand, deleteKeyCommand, translateKeyCommand, publishKeysCommand } from './commands/keys.js';
9
- import { listTeamCommand, getTeamMemberCommand, inviteTeamMemberCommand, removeTeamMemberCommand, updateTeamRoleCommand, listInvitationsCommand, revokeInvitationCommand } from './commands/team.js';
10
- import { orgInfoCommand, orgStatsCommand, orgPlanCommand } from './commands/org.js';
11
- import { pullCommand } from './commands/pull.js';
4
+ import { authCommand, logoutCommand, whoamiCommand } from './commands/auth.js';
5
+ import { configCommand, formatsCommand } from './commands/config.js';
12
6
  import { exportCommand } from './commands/export.js';
13
7
  import { importCommand } from './commands/import.js';
14
- import { debugListApiKeys, debugListOrganizations } from './commands/debug.js';
15
- import { showLogo } from './utils/banner.js';
8
+ import { initCommand } from './commands/init.js';
9
+ import { createKeyCommand, deleteKeyCommand, getKeyCommand, listKeysCommand, publishKeysCommand, updateKeyCommand } from './commands/keys.js';
10
+ import { orgInfoCommand, orgPlanCommand, orgStatsCommand } from './commands/org.js';
11
+ import { projectsAddLanguageCommand, projectsCreateCommand, projectsDeleteCommand, projectsGetCommand, projectsListCommand, projectsRemoveLanguageCommand, projectsStatsCommand, projectsUpdateCommand, } from './commands/projects.js';
12
+ import { pullCommand } from './commands/pull.js';
13
+ import { pushCommand } from './commands/push.js';
14
+ import { translateCommand } from './commands/translate.js';
15
+ import { reviewCommand } from './commands/review.js';
16
+ import { getTeamMemberCommand, inviteTeamMemberCommand, listInvitationsCommand, listTeamCommand, removeTeamMemberCommand, revokeInvitationCommand, updateTeamRoleCommand, } from './commands/team.js';
17
+ import { flagOverrides } from './core/config.js';
18
+ import { CliError, ExitCode, usageError } from './core/errors.js';
19
+ import { httpSettings } from './core/http.js';
20
+ import { log, printJson, runtime } from './core/output.js';
21
+ import { VERSION } from './version.js';
22
+ const collect = (value, previous = []) => [...previous, value];
23
+ /** Global flags, accepted before or after the subcommand (`langctl --json pull` and `langctl pull --json`). */
24
+ function withGlobals(cmd) {
25
+ return cmd
26
+ .option('--json', 'machine-readable JSON output on stdout')
27
+ .option('-q, --quiet', 'only print results and errors')
28
+ .option('--verbose', 'log HTTP requests to stderr')
29
+ .option('-y, --yes', 'skip confirmation prompts (required for destructive commands in CI)')
30
+ .option('--api-key <key>', 'API key (prefer the LANGCTL_API_KEY env var)')
31
+ .option('--api-url <url>', 'API base URL (env: LANGCTL_API_URL)')
32
+ .option('--timeout <seconds>', 'request timeout in seconds (env: LANGCTL_TIMEOUT)')
33
+ .option('--no-color', 'disable colors (also honors NO_COLOR)');
34
+ }
35
+ /** Wrap an action so every command shares the same error handling and exit codes. */
36
+ function run(fn) {
37
+ return async (...args) => {
38
+ await fn(...args);
39
+ };
40
+ }
16
41
  const program = new Command();
17
- // Package info
18
- const packageJson = {
19
- name: 'langctl',
20
- version: '0.2.0',
21
- description: 'CLI-first translation management for developers'
22
- };
23
42
  program
24
43
  .name('langctl')
25
- .description(packageJson.description)
26
- .version(packageJson.version, '-v, --version', 'Show version number');
27
- // Init command
28
- program
29
- .command('init')
30
- .description('Interactive setup wizard')
31
- .action(async () => {
32
- try {
33
- await initCommand();
34
- }
35
- catch (error) {
36
- console.error(chalk.red(`Error: ${error.message}\n`));
37
- process.exit(1);
38
- }
39
- });
40
- // Auth command
41
- program
42
- .command('auth <api-key>')
43
- .description('Authenticate and save credentials')
44
- .action(async (apiKey) => {
45
- try {
46
- await authCommand(apiKey);
47
- }
48
- catch (error) {
49
- console.error(chalk.red(`Error: ${error.message}\n`));
50
- process.exit(1);
51
- }
52
- });
53
- // Logout command
54
- program
55
- .command('logout')
56
- .description('Clear authentication credentials')
57
- .action(async () => {
58
- try {
59
- await logoutCommand();
60
- }
61
- catch (error) {
62
- console.error(chalk.red(`Error: ${error.message}\n`));
63
- process.exit(1);
64
- }
65
- });
66
- // Config command
67
- program
68
- .command('config')
69
- .description('View current configuration')
70
- .action(() => {
71
- try {
72
- configCommand();
73
- }
74
- catch (error) {
75
- console.error(chalk.red(`Error: ${error.message}\n`));
76
- process.exit(1);
77
- }
78
- });
79
- // Projects command
80
- const projects = program
81
- .command('projects')
82
- .description('Manage projects');
83
- projects
84
- .command('list')
85
- .description('Show all accessible projects')
86
- .action(async () => {
87
- try {
88
- await projectsListCommand();
89
- }
90
- catch (error) {
91
- console.error(chalk.red(`Error: ${error.message}\n`));
92
- process.exit(1);
93
- }
94
- });
95
- projects
96
- .command('create <name>')
97
- .description('Create a new project')
98
- .option('-d, --description <text>', 'Project description')
99
- .option('-l, --languages <langs>', 'Comma-separated language codes (e.g., en,es,fr)', 'en')
100
- .option('--default-language <code>', 'Default language code')
101
- .action(async (name, options) => {
102
- try {
103
- await projectsCreateCommand(name, options);
104
- }
105
- catch (error) {
106
- console.error(chalk.red(`Error: ${error.message}\n`));
107
- process.exit(1);
108
- }
109
- });
110
- projects
111
- .command('get <slug>')
112
- .description('Get project details')
113
- .action(async (slug) => {
114
- try {
115
- await projectsGetCommand(slug);
116
- }
117
- catch (error) {
118
- console.error(chalk.red(`Error: ${error.message}\n`));
119
- process.exit(1);
120
- }
121
- });
122
- projects
123
- .command('update <slug>')
124
- .description('Update project')
125
- .option('-n, --name <name>', 'New project name')
126
- .option('-d, --description <text>', 'New description')
127
- .option('-l, --languages <langs>', 'Comma-separated language codes')
128
- .option('--default-language <code>', 'Default language code')
129
- .action(async (slug, options) => {
130
- try {
131
- await projectsUpdateCommand(slug, options);
132
- }
133
- catch (error) {
134
- console.error(chalk.red(`Error: ${error.message}\n`));
135
- process.exit(1);
136
- }
137
- });
138
- projects
139
- .command('delete <slug>')
140
- .description('Delete project')
141
- .action(async (slug) => {
142
- try {
143
- await projectsDeleteCommand(slug);
144
- }
145
- catch (error) {
146
- console.error(chalk.red(`Error: ${error.message}\n`));
147
- process.exit(1);
148
- }
149
- });
150
- projects
151
- .command('add-language <slug> <language>')
152
- .description('Add language to project')
153
- .action(async (slug, language) => {
154
- try {
155
- await projectsAddLanguageCommand(slug, language);
156
- }
157
- catch (error) {
158
- console.error(chalk.red(`Error: ${error.message}\n`));
159
- process.exit(1);
160
- }
161
- });
162
- projects
163
- .command('remove-language <slug> <language>')
164
- .description('Remove language from project')
165
- .action(async (slug, language) => {
166
- try {
167
- await projectsRemoveLanguageCommand(slug, language);
168
- }
169
- catch (error) {
170
- console.error(chalk.red(`Error: ${error.message}\n`));
171
- process.exit(1);
172
- }
173
- });
174
- projects
175
- .command('stats <slug>')
176
- .description('Get project statistics')
177
- .action(async (slug) => {
178
- try {
179
- await projectsStatsCommand(slug);
180
- }
181
- catch (error) {
182
- console.error(chalk.red(`Error: ${error.message}\n`));
183
- process.exit(1);
184
- }
185
- });
186
- // Keys command
187
- const keys = program
188
- .command('keys')
189
- .description('Manage translation keys');
190
- keys
191
- .command('list <project>')
192
- .description('List translation keys')
193
- .option('-m, --module <name>', 'Filter by module')
194
- .option('-p, --published', 'Show only published keys')
195
- .option('-s, --search <term>', 'Search keys')
196
- .option('--limit <number>', 'Limit results', '100')
197
- .option('--offset <number>', 'Offset results', '0')
198
- .action(async (project, options) => {
199
- try {
200
- await listKeysCommand(project, options);
201
- }
202
- catch (error) {
203
- console.error(chalk.red(`Error: ${error.message}\n`));
204
- process.exit(1);
205
- }
206
- });
207
- keys
208
- .command('get <project> <key>')
209
- .description('Get key details')
210
- .action(async (project, key) => {
211
- try {
212
- await getKeyCommand(project, key);
213
- }
214
- catch (error) {
215
- console.error(chalk.red(`Error: ${error.message}\n`));
216
- process.exit(1);
217
- }
218
- });
219
- keys
220
- .command('create <project> <key>')
221
- .description('Create translation key')
222
- .option('-d, --description <text>', 'Key description')
223
- .option('-m, --module <name>', 'Module name')
224
- .option('--value-en <value>', 'English value')
225
- .option('--value-es <value>', 'Spanish value')
226
- .option('--value-fr <value>', 'French value')
227
- .option('--value-de <value>', 'German value')
228
- .option('--tags <tags>', 'Comma-separated tags')
229
- .action(async (project, key, options) => {
230
- try {
231
- await createKeyCommand(project, key, options);
232
- }
233
- catch (error) {
234
- console.error(chalk.red(`Error: ${error.message}\n`));
235
- process.exit(1);
236
- }
237
- });
238
- keys
239
- .command('delete <project> <key>')
240
- .description('Delete translation key')
241
- .action(async (project, key) => {
242
- try {
243
- await deleteKeyCommand(project, key);
244
- }
245
- catch (error) {
246
- console.error(chalk.red(`Error: ${error.message}\n`));
247
- process.exit(1);
248
- }
249
- });
250
- keys
251
- .command('translate <project> <key>')
252
- .description('Update translation for a key')
253
- .requiredOption('-l, --language <code>', 'Language code')
254
- .requiredOption('-v, --value <text>', 'Translation value')
255
- .action(async (project, key, options) => {
256
- try {
257
- await translateKeyCommand(project, key, options);
258
- }
259
- catch (error) {
260
- console.error(chalk.red(`Error: ${error.message}\n`));
261
- process.exit(1);
262
- }
263
- });
264
- keys
265
- .command('publish <project> <keys...>')
266
- .description('Publish translation keys')
267
- .option('--unpublish', 'Unpublish instead of publish')
268
- .action(async (project, keysList, options) => {
269
- try {
270
- await publishKeysCommand(project, keysList, options);
271
- }
272
- catch (error) {
273
- console.error(chalk.red(`Error: ${error.message}\n`));
274
- process.exit(1);
275
- }
276
- });
277
- // Team command
278
- const team = program
279
- .command('team')
280
- .description('Manage team members');
281
- team
282
- .command('list')
283
- .description('List team members')
284
- .action(async () => {
285
- try {
286
- await listTeamCommand();
287
- }
288
- catch (error) {
289
- console.error(chalk.red(`Error: ${error.message}\n`));
290
- process.exit(1);
291
- }
292
- });
293
- team
294
- .command('get <email>')
295
- .description('Get team member details')
296
- .action(async (email) => {
297
- try {
298
- await getTeamMemberCommand(email);
299
- }
300
- catch (error) {
301
- console.error(chalk.red(`Error: ${error.message}\n`));
302
- process.exit(1);
303
- }
304
- });
305
- team
306
- .command('invite <email>')
307
- .description('Invite team member')
308
- .option('-r, --role <role>', 'Member role: viewer, member, admin', 'member')
309
- .action(async (email, options) => {
310
- try {
311
- await inviteTeamMemberCommand(email, options);
312
- }
313
- catch (error) {
314
- console.error(chalk.red(`Error: ${error.message}\n`));
315
- process.exit(1);
44
+ .description('Translation management for developers — sync i18n files between your code, CI and Langctl.')
45
+ .version(VERSION, '-v, --version', 'print the version')
46
+ .showHelpAfterError('(run with --help for usage)')
47
+ .configureHelp({ sortSubcommands: false })
48
+ .exitOverride();
49
+ withGlobals(program);
50
+ program.hook('preAction', (_root, action) => {
51
+ const o = action.optsWithGlobals();
52
+ runtime.json = Boolean(o.json);
53
+ runtime.quiet = Boolean(o.quiet);
54
+ runtime.verbose = Boolean(o.verbose);
55
+ runtime.yes = Boolean(o.yes);
56
+ if (o.color === false || process.env.NO_COLOR)
57
+ chalk.level = 0;
58
+ if (typeof o.apiKey === 'string')
59
+ flagOverrides.apiKey = o.apiKey;
60
+ if (typeof o.apiUrl === 'string')
61
+ flagOverrides.apiUrl = o.apiUrl;
62
+ if (o.timeout !== undefined) {
63
+ const s = Number(o.timeout);
64
+ if (!Number.isFinite(s) || s <= 0)
65
+ throw usageError('--timeout must be a positive number of seconds');
66
+ httpSettings.timeoutMs = s * 1000;
67
+ }
68
+ });
69
+ // ── Setup & auth ────────────────────────────────────────────────
70
+ withGlobals(program.command('init'))
71
+ .description('set up this repository: authenticate if needed and write langctl.json')
72
+ .option('-p, --project <slug>', 'project slug')
73
+ .option('-f, --format <format>', 'file format (see "langctl formats")')
74
+ .option('-o, --output <template>', 'path template for files, e.g. src/locales/{lang}.json')
75
+ .option('--force', 'overwrite an existing langctl.json')
76
+ .action(run(opts => initCommand(opts)));
77
+ withGlobals(program.command('auth [api-key]'))
78
+ .description('store an API key (prefer --stdin so the key stays out of shell history)')
79
+ .option('--stdin', 'read the API key from stdin')
80
+ .action(run((key, opts) => authCommand(key, opts)));
81
+ withGlobals(program.command('logout')).description('remove the stored API key').action(run(() => logoutCommand()));
82
+ withGlobals(program.command('whoami')).alias('status').description('show the organization, key and scopes in use, and check connectivity')
83
+ .action(run(() => whoamiCommand()));
84
+ withGlobals(program.command('config')).description('show effective configuration and where it comes from').action(run(() => configCommand()));
85
+ withGlobals(program.command('formats')).description('list supported file formats').action(run(() => formatsCommand()));
86
+ // ── Sync ────────────────────────────────────────────────────────
87
+ withGlobals(program.command('pull [project]'))
88
+ .description('download translations into files (project/format/output default to langctl.json)')
89
+ .option('-l, --languages <codes>', 'comma-separated languages (default: all)')
90
+ .addOption(new Option('--language <code>').hideHelp())
91
+ .option('-f, --format <format>', 'file format (default: json)')
92
+ .option('-o, --output <template>', 'path template, e.g. "src/locales/{lang}.json"')
93
+ .addOption(new Option('-d, --dir <path>', 'legacy: base directory for the default layout').hideHelp())
94
+ .option('-m, --module <name>', 'only keys from this module')
95
+ .option('--include-drafts', 'include unpublished keys (excluded by default)')
96
+ .option('--include-unreviewed', 'include AI translations nobody has reviewed yet (excluded by default)')
97
+ .addOption(new Option('--no-published-only').hideHelp())
98
+ .option('--check', 'do not write; exit 7 if any file is out of date (for CI)')
99
+ .option('--dry-run', 'show what would change without writing')
100
+ .option('--require-complete', 'exit 1 if any language is missing translations')
101
+ .action(run((project, opts) => pullCommand(project, { ...opts, languages: opts.languages ?? opts.language })));
102
+ withGlobals(program.command('push [project]'))
103
+ .description('upload translation files (by default only the source language)')
104
+ .option('-l, --languages <codes>', 'comma-separated languages, or "all" (default: source language)')
105
+ .option('-f, --format <format>', 'file format (default: from langctl.json or file extension)')
106
+ .option('-i, --input <template>', 'path template to read, e.g. "src/locales/{lang}.json"')
107
+ .option('-m, --module <name>', 'assign new keys to this module')
108
+ .option('--overwrite', 'replace existing translations (default: only add new keys/languages)')
109
+ .option('--publish', 'publish the uploaded keys')
110
+ .option('--dry-run', 'show what would change without uploading')
111
+ .action(run((project, opts) => pushCommand(project, opts)));
112
+ withGlobals(program.command('export [project]'))
113
+ .description('export translations to a file (one-off; "pull" is the repo workflow)')
114
+ .option('-l, --language <code>', 'language to export (default: all)')
115
+ .option('--languages <codes>', 'comma-separated languages')
116
+ .option('-f, --format <format>', 'file format (default: json)')
117
+ .option('-o, --output <path>', 'output file, or template with {lang}')
118
+ .option('-m, --module <name>', 'only keys from this module')
119
+ .option('--include-drafts', 'include unpublished keys')
120
+ .addOption(new Option('--include-unpublished').hideHelp())
121
+ .action(run((project, opts) => exportCommand(project, opts)));
122
+ withGlobals(program.command('import <project-or-file> [file]'))
123
+ .description('import one translation file (project may come from langctl.json)')
124
+ .option('-l, --language <code>', 'language of the file (default: project default language)')
125
+ .option('-f, --format <format>', 'file format (default: from extension)')
126
+ .option('-m, --module <name>', 'assign new keys to this module')
127
+ .option('--overwrite', 'replace existing translations')
128
+ .option('--publish', 'publish the imported keys')
129
+ .option('--dry-run', 'show what would change without uploading')
130
+ .action(run((a, b, opts) => importCommand(a, b, opts)));
131
+ // ── Projects ────────────────────────────────────────────────────
132
+ const projects = program.command('projects').alias('project').description('manage projects');
133
+ withGlobals(projects.command('list')).alias('ls').description('list projects').action(run(() => projectsListCommand()));
134
+ withGlobals(projects.command('get <slug>')).description('show a project').action(run((slug) => projectsGetCommand(slug)));
135
+ withGlobals(projects.command('create <name>')).description('create a project')
136
+ .option('-d, --description <text>', 'description')
137
+ .option('-l, --languages <codes>', 'comma-separated languages', 'en')
138
+ .option('--default-language <code>', 'default (source) language — defaults to the first of --languages')
139
+ .action(run((name, opts) => projectsCreateCommand(name, opts)));
140
+ withGlobals(projects.command('update <slug>')).description('update a project')
141
+ .option('-n, --name <name>', 'new name (the slug never changes)')
142
+ .option('-d, --description <text>', 'new description')
143
+ .option('-l, --languages <codes>', 'set the full language list (removing a language deletes its translations)')
144
+ .option('--default-language <code>', 'default language')
145
+ .action(run((slug, opts) => projectsUpdateCommand(slug, opts)));
146
+ withGlobals(projects.command('delete <slug>')).description('delete a project and its keys (asks to confirm; --yes in CI)')
147
+ .action(run((slug) => projectsDeleteCommand(slug)));
148
+ withGlobals(projects.command('add-language <slug> <codes...>')).description('add languages to a project')
149
+ .action(run((slug, codes) => projectsAddLanguageCommand(slug, codes)));
150
+ withGlobals(projects.command('remove-language <slug> <codes...>')).description('remove languages and their translations')
151
+ .action(run((slug, codes) => projectsRemoveLanguageCommand(slug, codes)));
152
+ withGlobals(projects.command('stats <slug>')).description('key counts and translation coverage per language')
153
+ .action(run((slug) => projectsStatsCommand(slug)));
154
+ // ── Keys ────────────────────────────────────────────────────────
155
+ const keys = program.command('keys').alias('key').description('manage translation keys');
156
+ withGlobals(keys.command('list [project]')).alias('ls').description('list keys')
157
+ .option('-m, --module <name>', 'filter by module')
158
+ .option('-p, --published', 'only published keys')
159
+ .option('--drafts', 'only unpublished keys')
160
+ .option('-s, --search <text>', 'search key names and descriptions')
161
+ .option('--limit <n>', 'show at most n keys')
162
+ .addOption(new Option('--offset <n>').hideHelp())
163
+ .action(run((project, opts) => listKeysCommand(project, opts)));
164
+ withGlobals(keys.command('get <project> <key>')).description('show a key and all its translations')
165
+ .action(run((project, key) => getKeyCommand(project, key)));
166
+ withGlobals(keys.command('create <project> <key>')).description('create a key')
167
+ .option('--value <lang=text>', 'translation, repeatable: --value en="Hi" --value es="Hola"', collect)
168
+ .option('-d, --description <text>', 'context for translators')
169
+ .option('-m, --module <name>', 'module')
170
+ .option('--tags <tags>', 'comma-separated tags')
171
+ .option('--publish', 'publish immediately (default: draft)')
172
+ .addOption(new Option('--value-en <text>').hideHelp())
173
+ .addOption(new Option('--value-es <text>').hideHelp())
174
+ .addOption(new Option('--value-fr <text>').hideHelp())
175
+ .addOption(new Option('--value-de <text>').hideHelp())
176
+ .action(run((project, key, opts) => createKeyCommand(project, key, opts)));
177
+ withGlobals(keys.command('update <project> <key>')).description('update translations, description or module')
178
+ .option('--value <lang=text>', 'translation, repeatable', collect)
179
+ .option('-d, --description <text>', 'description')
180
+ .option('-m, --module <name>', 'module')
181
+ .action(run((project, key, opts) => updateKeyCommand(project, key, opts)));
182
+ withGlobals(keys.command('translate <project> <key>')).description('set one translation (same as update --value)')
183
+ .requiredOption('-l, --language <code>', 'language')
184
+ .requiredOption('-t, --text <text>', 'translation text')
185
+ .addOption(new Option('--value <text>').hideHelp())
186
+ .action(run((project, key, opts) => updateKeyCommand(project, key, { language: opts.language, text: opts.text ?? opts.value })));
187
+ withGlobals(keys.command('delete <project> <keys...>')).alias('rm').description('delete keys (asks to confirm; --yes in CI)')
188
+ .action(run((project, names) => deleteKeyCommand(project, names)));
189
+ withGlobals(keys.command('publish <project> [keys...]')).description('publish keys so pull/export include them')
190
+ .option('--all', 'publish every draft key')
191
+ .option('-m, --module <name>', 'publish every draft key in a module')
192
+ .addOption(new Option('--unpublish').hideHelp())
193
+ .action(run((project, names, opts) => publishKeysCommand(project, names, opts, true)));
194
+ withGlobals(keys.command('unpublish <project> [keys...]')).description('move keys back to draft')
195
+ .option('--all', 'unpublish every published key')
196
+ .option('-m, --module <name>', 'unpublish every key in a module')
197
+ .action(run((project, names, opts) => publishKeysCommand(project, names, opts, false)));
198
+ // ── AI translation ──────────────────────────────────────────────
199
+ withGlobals(program.command('translate [project]')).description('fill missing translations with AI (DeepL) from the default language')
200
+ .option('-t, --to <codes>', 'comma-separated target languages (default: every non-default language)')
201
+ .option('-k, --keys <names>', 'only these keys (comma-separated)')
202
+ .option('-m, --module <name>', 'only keys in a module')
203
+ .option('--overwrite', 'retranslate keys that already have a translation')
204
+ .option('--dry-run', 'show what would be translated without using any AI translations')
205
+ .action(run((project, opts) => translateCommand(project, opts)));
206
+ withGlobals(program.command('review [project]')).description('list AI translations awaiting review, or approve them')
207
+ .option('--approve', 'approve the listed translations (asks to confirm; --yes in CI)')
208
+ .option('-k, --keys <names>', 'only these keys (comma-separated)')
209
+ .option('-l, --languages <codes>', 'only these languages (comma-separated)')
210
+ .action(run((project, opts) => reviewCommand(project, opts)));
211
+ // ── Team & org ──────────────────────────────────────────────────
212
+ const team = program.command('team').description('manage team members (needs an API key with the org:admin scope)');
213
+ withGlobals(team.command('list')).alias('ls').description('list members').action(run(() => listTeamCommand()));
214
+ withGlobals(team.command('get <email>')).description('show a member').action(run((email) => getTeamMemberCommand(email)));
215
+ withGlobals(team.command('invite <email>')).description('invite someone')
216
+ .option('-r, --role <role>', 'viewer, member or admin', 'member')
217
+ .action(run((email, opts) => inviteTeamMemberCommand(email, opts)));
218
+ withGlobals(team.command('remove <email>')).description('remove a member (asks to confirm; --yes in CI)')
219
+ .action(run((email) => removeTeamMemberCommand(email)));
220
+ withGlobals(team.command('update-role <email> <role>')).description('change a member\'s role')
221
+ .action(run((email, role) => updateTeamRoleCommand(email, role)));
222
+ withGlobals(team.command('invitations')).description('list invitations')
223
+ .option('-p, --pending', 'only pending invitations')
224
+ .option('--status <status>', 'pending, accepted, expired or revoked')
225
+ .action(run(opts => listInvitationsCommand(opts)));
226
+ withGlobals(team.command('revoke-invitation <email>')).description('revoke a pending invitation')
227
+ .action(run((email) => revokeInvitationCommand(email)));
228
+ const org = program.command('org').description('organization info');
229
+ withGlobals(org.command('info')).description('organization details').action(run(() => orgInfoCommand()));
230
+ withGlobals(org.command('stats')).description('member, project and key counts').action(run(() => orgStatsCommand()));
231
+ withGlobals(org.command('plan')).description('plan limits and usage').action(run(() => orgPlanCommand()));
232
+ program.addHelpText('after', `
233
+ Getting started:
234
+ $ langctl init # authenticate and create langctl.json
235
+ $ langctl pull # download translations
236
+ $ langctl push # upload new source strings
237
+
238
+ In CI (no config file needed):
239
+ env LANGCTL_API_KEY=lc_… (store it as a secret)
240
+ $ langctl pull --check # fail if committed files are out of date
241
+ $ langctl push --dry-run --json # preview what would be uploaded
242
+
243
+ Exit codes:
244
+ 0 ok · 1 error · 2 invalid usage · 3 auth/permission · 4 not found
245
+ 5 network/API unavailable · 6 plan limit reached · 7 files out of date (--check)
246
+
247
+ Docs: https://langctl.com/docs`);
248
+ // ── Run ─────────────────────────────────────────────────────────
249
+ process.on('SIGINT', () => {
250
+ process.stderr.write('\n');
251
+ process.exit(130);
252
+ });
253
+ async function main() {
254
+ if (process.argv.length <= 2) {
255
+ program.outputHelp();
256
+ return;
257
+ }
258
+ try {
259
+ await program.parseAsync(process.argv);
260
+ }
261
+ catch (err) {
262
+ process.exitCode = handleError(err);
316
263
  }
317
- });
318
- team
319
- .command('remove <email>')
320
- .description('Remove team member')
321
- .action(async (email) => {
322
- try {
323
- await removeTeamMemberCommand(email);
324
- }
325
- catch (error) {
326
- console.error(chalk.red(`Error: ${error.message}\n`));
327
- process.exit(1);
328
- }
329
- });
330
- team
331
- .command('update-role <email> <role>')
332
- .description('Update member role (viewer, member, admin)')
333
- .action(async (email, role) => {
334
- try {
335
- await updateTeamRoleCommand(email, role);
336
- }
337
- catch (error) {
338
- console.error(chalk.red(`Error: ${error.message}\n`));
339
- process.exit(1);
340
- }
341
- });
342
- team
343
- .command('invitations')
344
- .description('List invitations')
345
- .option('-p, --pending', 'Show only pending invitations')
346
- .action(async (options) => {
347
- try {
348
- await listInvitationsCommand(options);
349
- }
350
- catch (error) {
351
- console.error(chalk.red(`Error: ${error.message}\n`));
352
- process.exit(1);
353
- }
354
- });
355
- team
356
- .command('revoke-invitation <email>')
357
- .description('Revoke pending invitation')
358
- .action(async (email) => {
359
- try {
360
- await revokeInvitationCommand(email);
361
- }
362
- catch (error) {
363
- console.error(chalk.red(`Error: ${error.message}\n`));
364
- process.exit(1);
365
- }
366
- });
367
- // Organization command
368
- const org = program
369
- .command('org')
370
- .description('Manage organization');
371
- org
372
- .command('info')
373
- .description('Get organization information')
374
- .action(async () => {
375
- try {
376
- await orgInfoCommand();
377
- }
378
- catch (error) {
379
- console.error(chalk.red(`Error: ${error.message}\n`));
380
- process.exit(1);
381
- }
382
- });
383
- org
384
- .command('stats')
385
- .description('Get organization statistics')
386
- .action(async () => {
387
- try {
388
- await orgStatsCommand();
389
- }
390
- catch (error) {
391
- console.error(chalk.red(`Error: ${error.message}\n`));
392
- process.exit(1);
393
- }
394
- });
395
- org
396
- .command('plan')
397
- .description('Get subscription plan and limits')
398
- .action(async () => {
399
- try {
400
- await orgPlanCommand();
401
- }
402
- catch (error) {
403
- console.error(chalk.red(`Error: ${error.message}\n`));
404
- process.exit(1);
405
- }
406
- });
407
- // Pull command (deprecated, use export)
408
- program
409
- .command('pull <project>')
410
- .description('Pull translations from a project (deprecated, use export)')
411
- .option('-l, --language <code>', 'Pull specific language only (default: all)')
412
- .option('-f, --format <type>', 'Export format: json, json-nested, ios, android, flutter (default: json)')
413
- .option('-d, --dir <path>', 'Output directory (default: ./translations)')
414
- .option('-o, --output <path>', 'Specific output file path (optional)')
415
- .option('--no-published-only', 'Include unpublished translations')
416
- .action(async (project, options) => {
417
- try {
418
- await pullCommand(project, options);
419
- }
420
- catch (error) {
421
- console.error(chalk.red(`Error: ${error.message}\n`));
422
- process.exit(1);
423
- }
424
- });
425
- // Export command
426
- program
427
- .command('export <project>')
428
- .description('Export translations from a project')
429
- .option('-l, --language <code>', 'Export specific language (default: all)')
430
- .option('-f, --format <type>', 'Export format: flat-json, nested-json, i18n-json, android-xml, ios-strings, flutter-arb (default: flat-json)')
431
- .option('-o, --output <path>', 'Output file path')
432
- .option('-m, --module <name>', 'Export only keys from specific module')
433
- .option('--include-unpublished', 'Include unpublished translations')
434
- .action(async (project, options) => {
435
- try {
436
- await exportCommand(project, options);
437
- }
438
- catch (error) {
439
- console.error(chalk.red(`Error: ${error.message}\n`));
440
- process.exit(1);
441
- }
442
- });
443
- // Import command
444
- program
445
- .command('import <project> <file>')
446
- .description('Import translations from a file')
447
- .requiredOption('-l, --language <code>', 'Target language code')
448
- .option('--overwrite', 'Overwrite existing translations')
449
- .option('--publish', 'Auto-publish after import')
450
- .action(async (project, file, options) => {
451
- try {
452
- await importCommand(project, file, options);
453
- }
454
- catch (error) {
455
- console.error(chalk.red(`Error: ${error.message}\n`));
456
- process.exit(1);
457
- }
458
- });
459
- // Debug command
460
- const debug = program
461
- .command('debug')
462
- .description('Debug helpers');
463
- debug
464
- .command('list-api-keys')
465
- .description('Test API connection and show debug info')
466
- .action(async () => {
467
- try {
468
- await debugListApiKeys();
469
- }
470
- catch (error) {
471
- console.error(chalk.red(`Error: ${error.message}\n`));
472
- process.exit(1);
473
- }
474
- });
475
- debug
476
- .command('list-organizations')
477
- .description('Show current organization details')
478
- .action(async () => {
479
- try {
480
- await debugListOrganizations();
481
- }
482
- catch (error) {
483
- console.error(chalk.red(`Error: ${error.message}\n`));
484
- process.exit(1);
485
- }
486
- });
487
- // Help command
488
- program.on('--help', () => {
489
- console.log('');
490
- console.log('Examples:');
491
- console.log(' $ langctl init');
492
- console.log(' $ langctl auth lc_abc123...');
493
- console.log(' $ langctl projects list');
494
- console.log('');
495
- console.log(' # Pull all languages as JSON to ./translations/i18n/');
496
- console.log(' $ langctl pull litcode');
497
- console.log('');
498
- console.log(' # Pull to custom directory (e.g., ./src/i18n/)');
499
- console.log(' $ langctl pull litcode --dir ./src/i18n');
500
- console.log('');
501
- console.log(' # Pull specific language');
502
- console.log(' $ langctl pull litcode --language en');
503
- console.log('');
504
- console.log(' # Pull all languages in iOS format to ./ios/localization/');
505
- console.log(' $ langctl pull litcode --format ios --dir ./ios/localization');
506
- console.log('');
507
- console.log('For more information, visit: https://langctl.com/docs');
508
- console.log('');
509
- });
510
- // Parse arguments
511
- program.parse(process.argv);
512
- // Show help if no command provided
513
- if (!process.argv.slice(2).length) {
514
- showLogo();
515
- program.outputHelp();
516
264
  }
517
- //# sourceMappingURL=index.js.map
265
+ function handleError(err) {
266
+ if (err instanceof CommanderError) {
267
+ // help/version requests exit cleanly; everything else commander rejects is a usage error
268
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version' || err.code === 'commander.help')
269
+ return 0;
270
+ return ExitCode.Usage;
271
+ }
272
+ if (err instanceof CliError) {
273
+ if (runtime.json)
274
+ printJson({ error: { message: err.message, exitCode: err.exitCode, hint: err.hint ?? null, status: err.status ?? null } });
275
+ log.error(err.message, err.hint);
276
+ return err.exitCode;
277
+ }
278
+ const e = err;
279
+ if (runtime.json)
280
+ printJson({ error: { message: e?.message ?? String(err), exitCode: ExitCode.Error } });
281
+ log.error(e?.message ?? String(err), 'This looks like a bug — please report it at https://github.com/litcode-pvt-ltd/langctl-cli/issues (run with --verbose for details).');
282
+ if (runtime.verbose && e?.stack)
283
+ process.stderr.write(e.stack + '\n');
284
+ return ExitCode.Error;
285
+ }
286
+ void main();