@tolgee/cli 1.4.0 → 2.0.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 (39) hide show
  1. package/dist/{index.js → cli.js} +54 -63
  2. package/dist/client/ApiClient.js +72 -0
  3. package/dist/client/ExportClient.js +19 -0
  4. package/dist/client/ImportClient.js +22 -0
  5. package/dist/client/TolgeeClient.js +18 -0
  6. package/dist/client/errorFromLoadable.js +35 -0
  7. package/dist/client/getApiKeyInformation.js +39 -0
  8. package/dist/client/internal/requester.js +2 -5
  9. package/dist/commands/extract/check.js +10 -8
  10. package/dist/commands/extract/print.js +10 -8
  11. package/dist/commands/extract.js +3 -5
  12. package/dist/commands/login.js +6 -14
  13. package/dist/commands/pull.js +38 -32
  14. package/dist/commands/push.js +89 -71
  15. package/dist/commands/sync/compare.js +14 -11
  16. package/dist/commands/sync/sync.js +41 -24
  17. package/dist/commands/tag.js +49 -0
  18. package/dist/config/tolgeerc.js +59 -36
  19. package/dist/constants.js +0 -1
  20. package/dist/extractor/machines/vue/decoder.js +1 -1
  21. package/dist/options.js +31 -7
  22. package/dist/utils/checkPathNotAFile.js +16 -0
  23. package/dist/utils/getSingleOption.js +10 -0
  24. package/dist/utils/getStackTrace.js +7 -0
  25. package/dist/utils/logger.js +8 -0
  26. package/dist/utils/mapExportFormat.js +62 -0
  27. package/dist/utils/mapImportFormat.js +18 -0
  28. package/dist/utils/prepareDir.js +12 -0
  29. package/dist/utils/zip.js +2 -7
  30. package/package.json +17 -13
  31. package/schema.json +175 -0
  32. package/dist/arguments.js +0 -2
  33. package/dist/client/errors.js +0 -37
  34. package/dist/client/export.js +0 -20
  35. package/dist/client/import.js +0 -55
  36. package/dist/client/index.js +0 -73
  37. package/dist/client/languages.js +0 -13
  38. package/dist/client/project.js +0 -41
  39. package/dist/utils/overwriteDir.js +0 -34
@@ -3,17 +3,19 @@ import { Command } from 'commander';
3
3
  import ansi from 'ansi-colors';
4
4
  import { getApiKey, savePak, savePat } from './config/credentials.js';
5
5
  import loadTolgeeRc from './config/tolgeerc.js';
6
- import RestClient from './client/index.js';
7
- import { HttpError } from './client/errors.js';
8
- import { setDebug, isDebugEnabled, debug, info, error, } from './utils/logger.js';
9
- import { API_KEY_OPT, API_URL_OPT, PROJECT_ID_OPT } from './options.js';
10
- import { API_KEY_PAK_PREFIX, API_KEY_PAT_PREFIX, VERSION, } from './constants.js';
6
+ import { setDebug, info, error, exitWithError } from './utils/logger.js';
7
+ import { API_KEY_OPT, API_URL_OPT, CONFIG_OPT, EXTRACTOR, FILE_PATTERNS, FORMAT_OPT, PROJECT_ID_OPT, } from './options.js';
8
+ import { API_KEY_PAK_PREFIX, API_KEY_PAT_PREFIX, DEFAULT_API_URL, VERSION, } from './constants.js';
11
9
  import { Login, Logout } from './commands/login.js';
12
10
  import PushCommand from './commands/push.js';
13
11
  import PullCommand from './commands/pull.js';
14
12
  import ExtractCommand from './commands/extract.js';
15
13
  import CompareCommand from './commands/sync/compare.js';
16
14
  import SyncCommand from './commands/sync/sync.js';
15
+ import TagCommand from './commands/tag.js';
16
+ import { getSingleOption } from './utils/getSingleOption.js';
17
+ import { createTolgeeClient } from './client/TolgeeClient.js';
18
+ import { projectIdFromKey } from './client/ApiClient.js';
17
19
  const NO_KEY_COMMANDS = ['login', 'logout', 'extract'];
18
20
  ansi.enabled = process.stdout.isTTY;
19
21
  function topLevelName(command) {
@@ -46,7 +48,7 @@ function loadProjectId(cmd) {
46
48
  const opts = cmd.optsWithGlobals();
47
49
  if (opts.apiKey?.startsWith(API_KEY_PAK_PREFIX)) {
48
50
  // Parse the key and ensure we can access the specified Project ID
49
- const projectId = RestClient.projectIdFromKey(opts.apiKey);
51
+ const projectId = projectIdFromKey(opts.apiKey);
50
52
  program.setOptionValue('projectId', projectId);
51
53
  if (opts.projectId !== -1 && opts.projectId !== projectId) {
52
54
  error('The specified API key cannot be used to perform operations on the specified project.');
@@ -64,93 +66,82 @@ function validateOptions(cmd) {
64
66
  process.exit(1);
65
67
  }
66
68
  if (!opts.apiKey) {
67
- error('No API key has been provided. You must either provide one via --api-key, or login via `tolgee login`.');
68
- process.exit(1);
69
+ exitWithError('No API key has been provided. You must either provide one via --api-key, or login via `tolgee login`.');
69
70
  }
70
71
  }
71
- async function preHandler(prog, cmd) {
72
+ const preHandler = (config) => async function (prog, cmd) {
72
73
  if (!NO_KEY_COMMANDS.includes(topLevelName(cmd))) {
73
74
  await loadApiKey(cmd);
74
75
  loadProjectId(cmd);
75
76
  validateOptions(cmd);
76
77
  const opts = cmd.optsWithGlobals();
77
- const client = new RestClient({
78
- apiUrl: opts.apiUrl,
78
+ const client = createTolgeeClient({
79
+ baseUrl: opts.apiUrl?.toString() ?? config.apiUrl?.toString(),
79
80
  apiKey: opts.apiKey,
80
- projectId: opts.projectId,
81
+ projectId: opts.projectId !== undefined
82
+ ? Number(opts.projectId)
83
+ : config.projectId !== undefined
84
+ ? Number(config.projectId)
85
+ : undefined,
81
86
  });
82
87
  cmd.setOptionValue('client', client);
83
88
  }
84
89
  // Apply verbosity
85
90
  setDebug(prog.opts().verbose);
86
- }
91
+ };
87
92
  const program = new Command('tolgee')
88
93
  .version(VERSION)
89
94
  .configureOutput({ writeErr: error })
90
95
  .description('Command Line Interface to interact with the Tolgee Platform')
91
- .option('-v, --verbose', 'Enable verbose logging.')
92
- .hook('preAction', preHandler);
93
- // Global options
94
- program.addOption(API_URL_OPT);
95
- program.addOption(API_KEY_OPT);
96
- program.addOption(PROJECT_ID_OPT);
97
- // Register commands
98
- program.addCommand(Login);
99
- program.addCommand(Logout);
100
- program.addCommand(PushCommand);
101
- program.addCommand(PullCommand);
102
- program.addCommand(ExtractCommand);
103
- program.addCommand(CompareCommand);
104
- program.addCommand(SyncCommand);
105
- async function loadConfig() {
106
- const tgConfig = await loadTolgeeRc();
96
+ .option('-v, --verbose', 'Enable verbose logging.');
97
+ // get config path to update defaults
98
+ const configPath = getSingleOption(CONFIG_OPT, process.argv);
99
+ async function loadConfig(program) {
100
+ const tgConfig = await loadTolgeeRc(configPath);
107
101
  if (tgConfig) {
108
- for (const [key, value] of Object.entries(tgConfig)) {
109
- program.setOptionValue(key, value);
110
- }
111
- }
112
- }
113
- async function handleHttpError(e) {
114
- error('An error occurred while requesting the API.');
115
- error(`${e.request.method} ${e.request.path}`);
116
- error(e.getErrorText());
117
- // Remove token from store if necessary
118
- if (e.response.statusCode === 401) {
119
- const removeFn = program.getOptionValue('_removeApiKeyFromStore');
120
- if (removeFn) {
121
- info('Removing the API key from the authentication store.');
122
- removeFn();
123
- }
124
- }
125
- // Print server output for server errors
126
- if (isDebugEnabled()) {
127
- // We cannot parse the response as JSON and pull error codes here as we may be here due to a 5xx error:
128
- // by nature 5xx class errors can happen for a lot of reasons (e.g. upstream issues, server issues,
129
- // catastrophic failure) which means the output is completely unpredictable. While some errors are
130
- // formatted by the Tolgee server, reality is there's a huge chance the 5xx error hasn't been raised
131
- // by Tolgee's error handler.
132
- const res = await e.response.body.text();
133
- debug(`Server response:\n\n---\n${res}\n---`);
102
+ [program, ...program.commands].forEach((cmd) => cmd.options.forEach((opt) => {
103
+ const key = opt.attributeName();
104
+ const value = tgConfig[key];
105
+ if (value) {
106
+ const parsedValue = opt.parseArg
107
+ ? opt.parseArg(value, undefined)
108
+ : value;
109
+ cmd.setOptionValueWithSource(key, parsedValue, 'config');
110
+ }
111
+ }));
134
112
  }
113
+ return tgConfig ?? {};
135
114
  }
136
115
  async function run() {
137
116
  try {
138
- await loadConfig();
117
+ // Global options
118
+ program.addOption(CONFIG_OPT);
119
+ program.addOption(API_URL_OPT.default(DEFAULT_API_URL));
120
+ program.addOption(API_KEY_OPT);
121
+ program.addOption(PROJECT_ID_OPT.default(-1));
122
+ program.addOption(FORMAT_OPT.default('JSON_TOLGEE'));
123
+ program.addOption(EXTRACTOR);
124
+ program.addOption(FILE_PATTERNS);
125
+ const config = await loadConfig(program);
126
+ program.hook('preAction', preHandler(config));
127
+ // Register commands
128
+ program.addCommand(Login);
129
+ program.addCommand(Logout);
130
+ program.addCommand(PushCommand(config).configureHelp({ showGlobalOptions: true }));
131
+ program.addCommand(PullCommand(config).configureHelp({ showGlobalOptions: true }));
132
+ program.addCommand(ExtractCommand(config).configureHelp({ showGlobalOptions: true }));
133
+ program.addCommand(CompareCommand(config).configureHelp({ showGlobalOptions: true }));
134
+ program.addCommand(SyncCommand(config).configureHelp({ showGlobalOptions: true }));
135
+ program.addCommand(TagCommand(config).configureHelp({ showGlobalOptions: true }));
139
136
  await program.parseAsync();
140
137
  }
141
138
  catch (e) {
142
- if (e instanceof HttpError) {
143
- await handleHttpError(e);
144
- process.exit(1);
145
- }
146
139
  // If the error is uncaught, huge chance that either:
147
140
  // - The error should be handled here but isn't
148
141
  // - The error should be handled in the command but isn't
149
142
  // - Something went wrong with the code
150
143
  error('An unexpected error occurred while running the command.');
151
- error('Please report this to our issue tracker: https://github.com/tolgee/tolgee-cli/issues');
152
- console.log(e.stack);
153
- process.exit(1);
144
+ exitWithError('Please report this to our issue tracker: https://github.com/tolgee/tolgee-cli/issues');
154
145
  }
155
146
  }
156
147
  run();
@@ -0,0 +1,72 @@
1
+ import createClient from 'openapi-fetch';
2
+ import base32Decode from 'base32-decode';
3
+ import { API_KEY_PAK_PREFIX, USER_AGENT } from '../constants.js';
4
+ import { getApiKeyInformation } from './getApiKeyInformation.js';
5
+ import { debug } from '../utils/logger.js';
6
+ import { errorFromLoadable } from './errorFromLoadable.js';
7
+ async function parseResponse(response, parseAs) {
8
+ // handle empty content
9
+ // note: we return `{}` because we want user truthy checks for `.data` or `.error` to succeed
10
+ if (response.status === 204 ||
11
+ response.headers.get('Content-Length') === '0') {
12
+ return response.ok ? { data: {}, response } : { error: {}, response };
13
+ }
14
+ // parse response (falling back to .text() when necessary)
15
+ if (response.ok) {
16
+ // if "stream", skip parsing entirely
17
+ if (parseAs === 'stream') {
18
+ return { data: response.body, response };
19
+ }
20
+ return { data: await response[parseAs](), response };
21
+ }
22
+ // handle errors
23
+ let error = await response.text();
24
+ try {
25
+ error = JSON.parse(error); // attempt to parse as JSON
26
+ }
27
+ catch {
28
+ // noop
29
+ }
30
+ return { error, response };
31
+ }
32
+ export function projectIdFromKey(key) {
33
+ if (!key.startsWith(API_KEY_PAK_PREFIX)) {
34
+ return undefined;
35
+ }
36
+ const keyBuffer = base32Decode(key.slice(API_KEY_PAK_PREFIX.length).toUpperCase(), 'RFC4648');
37
+ const decoded = Buffer.from(keyBuffer).toString('utf8');
38
+ return Number(decoded.split('_')[0]);
39
+ }
40
+ export function createApiClient({ baseUrl, apiKey, projectId, autoThrow = false, }) {
41
+ const computedProjectId = projectId ?? (apiKey ? projectIdFromKey(apiKey) : undefined);
42
+ const apiClient = createClient({
43
+ baseUrl,
44
+ headers: {
45
+ 'user-agent': USER_AGENT,
46
+ 'x-api-key': apiKey,
47
+ },
48
+ });
49
+ apiClient.use({
50
+ onRequest: (req, options) => {
51
+ debug(`[HTTP] Requesting: ${req.method} ${req.url}`);
52
+ return undefined;
53
+ },
54
+ onResponse: async (res, options) => {
55
+ debug(`[HTTP] Response: ${res.url} [${res.status}]`);
56
+ if (autoThrow && !res.ok) {
57
+ const loadable = await parseResponse(res, options.parseAs);
58
+ throw new Error(`Tolgee request error ${res.url} ${errorFromLoadable(loadable)}`);
59
+ }
60
+ return undefined;
61
+ },
62
+ });
63
+ return {
64
+ ...apiClient,
65
+ getProjectId() {
66
+ return computedProjectId;
67
+ },
68
+ getApiKeyInfo() {
69
+ return getApiKeyInformation(apiClient, apiKey);
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,19 @@
1
+ export const createExportClient = ({ apiClient }) => {
2
+ return {
3
+ async export(req) {
4
+ const body = { ...req, zip: true };
5
+ const loadable = await apiClient.POST('/v2/projects/{projectId}/export', {
6
+ params: { path: { projectId: apiClient.getProjectId() } },
7
+ body: body,
8
+ parseAs: 'blob',
9
+ });
10
+ return { ...loadable, data: loadable.data };
11
+ },
12
+ async exportSingle(req) {
13
+ return apiClient.POST('/v2/projects/{projectId}/export', {
14
+ params: { path: { projectId: apiClient.getProjectId() } },
15
+ body: { ...req, zip: false },
16
+ });
17
+ },
18
+ };
19
+ };
@@ -0,0 +1,22 @@
1
+ import FormData from 'form-data';
2
+ export const createImportClient = ({ apiClient }) => {
3
+ return {
4
+ async import(data) {
5
+ const body = new FormData();
6
+ for (const file of data.files) {
7
+ body.append('files', file.data, { filepath: file.name });
8
+ }
9
+ body.append('params', JSON.stringify(data.params));
10
+ return apiClient.POST('/v2/projects/{projectId}/single-step-import', {
11
+ params: { path: { projectId: apiClient.getProjectId() } },
12
+ body: body,
13
+ bodySerializer: (r) => {
14
+ return r.getBuffer();
15
+ },
16
+ headers: {
17
+ 'content-type': `multipart/form-data; boundary=${body.getBoundary()}`,
18
+ },
19
+ });
20
+ },
21
+ };
22
+ };
@@ -0,0 +1,18 @@
1
+ import { exitWithError } from './../utils/logger.js';
2
+ import { createApiClient } from './ApiClient.js';
3
+ import { createExportClient } from './ExportClient.js';
4
+ import { createImportClient } from './ImportClient.js';
5
+ import { errorFromLoadable } from './errorFromLoadable.js';
6
+ export function createTolgeeClient(props) {
7
+ const apiClient = createApiClient(props);
8
+ return {
9
+ ...apiClient,
10
+ import: createImportClient({ apiClient }),
11
+ export: createExportClient({ apiClient }),
12
+ };
13
+ }
14
+ export const handleLoadableError = (loadable) => {
15
+ if (loadable.error) {
16
+ exitWithError(errorFromLoadable(loadable));
17
+ }
18
+ };
@@ -0,0 +1,35 @@
1
+ export const addErrorDetails = (loadable, showBeError = true) => {
2
+ const items = [];
3
+ items.push(`status: ${loadable.response.status}`);
4
+ if (showBeError && loadable.error?.code) {
5
+ items.push(`code: ${loadable.error.code}`);
6
+ }
7
+ if (loadable.response.status === 403 && loadable.error?.params?.[0]) {
8
+ items.push(`missing scope: ${loadable.error.params[0]}`);
9
+ }
10
+ return `[${items.join(', ')}]`;
11
+ };
12
+ export const errorFromLoadable = (loadable) => {
13
+ switch (loadable.response.status) {
14
+ // Unauthorized
15
+ case 400:
16
+ return `Invalid request data ${addErrorDetails(loadable)}`;
17
+ // Unauthorized
18
+ case 401:
19
+ return `Missing or invalid authentication token ${addErrorDetails(loadable)}`;
20
+ // Forbidden
21
+ case 403:
22
+ return `You are not allowed to perform this operation ${addErrorDetails(loadable)}`;
23
+ // Rate limited
24
+ case 429:
25
+ return `You've been rate limited. Please try again later ${addErrorDetails(loadable)}`;
26
+ // Service Unavailable
27
+ case 503:
28
+ return `API is temporarily unavailable. Please try again later ${addErrorDetails(loadable)}`;
29
+ // Server error
30
+ case 500:
31
+ return `API reported a server error. Please try again later ${addErrorDetails(loadable)}`;
32
+ default:
33
+ return `Unknown error ${addErrorDetails(loadable)}`;
34
+ }
35
+ };
@@ -0,0 +1,39 @@
1
+ import { API_KEY_PAK_PREFIX } from './../constants.js';
2
+ import { handleLoadableError } from './TolgeeClient.js';
3
+ import { exitWithError } from './../utils/logger.js';
4
+ export const getApiKeyInformation = async (client, key) => {
5
+ if (key.startsWith(API_KEY_PAK_PREFIX)) {
6
+ const loadable = await client.GET('/v2/api-keys/current');
7
+ if (loadable.response.status === 401) {
8
+ exitWithError("Couldn't log in: the API key you provided is invalid.");
9
+ }
10
+ handleLoadableError(loadable);
11
+ const info = loadable.data;
12
+ const username = info.userFullName || info.username || '<unknown user>';
13
+ return {
14
+ type: 'PAK',
15
+ key: key,
16
+ username: username,
17
+ project: {
18
+ id: info.projectId,
19
+ name: info.projectName,
20
+ },
21
+ expires: info.expiresAt ?? 0,
22
+ };
23
+ }
24
+ else {
25
+ const loadable = await client.GET('/v2/pats/current');
26
+ if (loadable.response.status === 401) {
27
+ exitWithError("Couldn't log in: the API key you provided is invalid.");
28
+ }
29
+ handleLoadableError(loadable);
30
+ const info = loadable.data;
31
+ const username = info.user.name || info.user.username;
32
+ return {
33
+ type: 'PAT',
34
+ key: key,
35
+ username: username,
36
+ expires: info.expiresAt ?? 0,
37
+ };
38
+ }
39
+ };
@@ -1,7 +1,6 @@
1
1
  import { STATUS_CODES } from 'http';
2
2
  import { request } from 'undici';
3
3
  import FormData from 'form-data';
4
- import { HttpError } from '../errors.js';
5
4
  import { debug } from '../../utils/logger.js';
6
5
  import { USER_AGENT } from '../../constants.js';
7
6
  export default class Requester {
@@ -59,12 +58,10 @@ export default class Requester {
59
58
  method: req.method,
60
59
  headers: headers,
61
60
  body: body,
62
- headersTimeout: req.headersTimeout ?? 300000,
63
- bodyTimeout: req.bodyTimeout ?? 300000,
61
+ headersTimeout: req.headersTimeout ?? 300_000,
62
+ bodyTimeout: req.bodyTimeout ?? 300_000,
64
63
  });
65
64
  debug(`[HTTP] ${req.method} ${url} -> ${response.statusCode} ${STATUS_CODES[response.statusCode]}`);
66
- if (response.statusCode >= 400)
67
- throw new HttpError(req, response);
68
65
  return response;
69
66
  }
70
67
  /**
@@ -2,11 +2,14 @@ import { relative } from 'path';
2
2
  import { Command } from 'commander';
3
3
  import { extractKeysOfFiles } from '../../extractor/runner.js';
4
4
  import { WarningMessages, emitGitHubWarning, } from '../../extractor/warnings.js';
5
- import { loading } from '../../utils/logger.js';
6
- import { FILE_PATTERNS } from '../../arguments.js';
7
- async function lintHandler(filesPatterns) {
5
+ import { exitWithError, loading } from '../../utils/logger.js';
6
+ const lintHandler = (config) => async function () {
8
7
  const opts = this.optsWithGlobals();
9
- const extracted = await loading('Analyzing code...', extractKeysOfFiles(filesPatterns, opts.extractor));
8
+ const patterns = opts.patterns;
9
+ if (!patterns?.length) {
10
+ exitWithError('Missing option --patterns or config.patterns option');
11
+ }
12
+ const extracted = await loading('Analyzing code...', extractKeysOfFiles(patterns, opts.extractor));
10
13
  let warningCount = 0;
11
14
  let filesCount = 0;
12
15
  for (const [file, { warnings }] of extracted) {
@@ -33,8 +36,7 @@ async function lintHandler(filesPatterns) {
33
36
  process.exit(1);
34
37
  }
35
38
  console.log('No issues found.');
36
- }
37
- export default new Command('check')
39
+ };
40
+ export default (config) => new Command('check')
38
41
  .description('Checks if the keys can be extracted automatically, and reports problems if any')
39
- .addArgument(FILE_PATTERNS)
40
- .action(lintHandler);
42
+ .action(lintHandler(config));
@@ -2,11 +2,14 @@ import { relative } from 'path';
2
2
  import { Command } from 'commander';
3
3
  import { extractKeysOfFiles } from '../../extractor/runner.js';
4
4
  import { WarningMessages } from '../../extractor/warnings.js';
5
- import { loading } from '../../utils/logger.js';
6
- import { FILE_PATTERNS } from '../../arguments.js';
7
- async function printHandler(filesPatterns) {
5
+ import { exitWithError, loading } from '../../utils/logger.js';
6
+ const printHandler = (config) => async function () {
8
7
  const opts = this.optsWithGlobals();
9
- const extracted = await loading('Analyzing code...', extractKeysOfFiles(filesPatterns, opts.extractor));
8
+ const patterns = opts.patterns;
9
+ if (!patterns?.length) {
10
+ exitWithError('Missing option --patterns or config.patterns option');
11
+ }
12
+ const extracted = await loading('Analyzing code...', extractKeysOfFiles(patterns, opts.extractor));
10
13
  let warningCount = 0;
11
14
  const keySet = new Set();
12
15
  for (const [file, { keys, warnings }] of extracted) {
@@ -43,8 +46,7 @@ async function printHandler(filesPatterns) {
43
46
  }
44
47
  console.log('Total unique keys found: %d', keySet.size);
45
48
  console.log('Total warnings: %d', warningCount);
46
- }
47
- export default new Command('print')
49
+ };
50
+ export default (config) => new Command('print')
48
51
  .description('Prints extracted data to the console')
49
- .addArgument(FILE_PATTERNS)
50
- .action(printHandler);
52
+ .action(printHandler(config));
@@ -1,9 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import extractPrint from './extract/print.js';
3
3
  import extractCheck from './extract/check.js';
4
- import { EXTRACTOR } from '../options.js';
5
- export default new Command('extract')
4
+ export default (config) => new Command('extract')
6
5
  .description('Extracts strings from your projects')
7
- .addOption(EXTRACTOR)
8
- .addCommand(extractPrint)
9
- .addCommand(extractCheck);
6
+ .addCommand(extractPrint(config))
7
+ .addCommand(extractCheck(config));
@@ -1,21 +1,13 @@
1
1
  import { Command } from 'commander';
2
- import RestClient from '../client/index.js';
3
- import { HttpError } from '../client/errors.js';
4
2
  import { saveApiKey, removeApiKeys, clearAuthStore, } from '../config/credentials.js';
5
- import { success, error } from '../utils/logger.js';
3
+ import { success } from '../utils/logger.js';
4
+ import { createTolgeeClient } from '../client/TolgeeClient.js';
6
5
  async function loginHandler(key) {
7
6
  const opts = this.optsWithGlobals();
8
- let keyInfo;
9
- try {
10
- keyInfo = await RestClient.getApiKeyInformation(opts.apiUrl, key);
11
- }
12
- catch (e) {
13
- if (e instanceof HttpError && e.response.statusCode === 401) {
14
- error("Couldn't log in: the API key you provided is invalid.");
15
- process.exit(1);
16
- }
17
- throw e;
18
- }
7
+ const keyInfo = await createTolgeeClient({
8
+ baseUrl: opts.apiUrl.toString(),
9
+ apiKey: key,
10
+ }).getApiKeyInfo();
19
11
  await saveApiKey(opts.apiUrl, keyInfo);
20
12
  success(keyInfo.type === 'PAK'
21
13
  ? `Logged in as ${keyInfo.username} on ${opts.apiUrl.hostname} for project ${keyInfo.project.name} (#${keyInfo.project.id}). Welcome back!`
@@ -1,49 +1,55 @@
1
1
  import { Command, Option } from 'commander';
2
2
  import { unzipBuffer } from '../utils/zip.js';
3
- import { overwriteDir } from '../utils/overwriteDir.js';
4
- import { error, loading, success } from '../utils/logger.js';
5
- import { HttpError } from '../client/errors.js';
3
+ import { prepareDir } from '../utils/prepareDir.js';
4
+ import { loading, success } from '../utils/logger.js';
5
+ import { checkPathNotAFile } from '../utils/checkPathNotAFile.js';
6
+ import { mapExportFormat } from '../utils/mapExportFormat.js';
7
+ import { handleLoadableError } from '../client/TolgeeClient.js';
6
8
  async function fetchZipBlob(opts) {
7
- return opts.client.export.export({
8
- format: opts.format,
9
+ const exportFormat = mapExportFormat(opts.format);
10
+ const { format, messageFormat } = exportFormat;
11
+ const loadable = await opts.client.export.export({
12
+ format,
13
+ messageFormat,
14
+ supportArrays: opts.supportArrays,
9
15
  languages: opts.languages,
10
16
  filterState: opts.states,
11
17
  structureDelimiter: opts.delimiter,
12
18
  filterNamespace: opts.namespaces,
19
+ filterTagIn: opts.tags,
20
+ filterTagNotIn: opts.excludeTags,
21
+ fileStructureTemplate: opts.fileStructureTemplate,
13
22
  });
23
+ handleLoadableError(loadable);
24
+ return loadable.data;
14
25
  }
15
- async function pullHandler(path) {
26
+ const pullHandler = () => async function () {
16
27
  const opts = this.optsWithGlobals();
17
- await overwriteDir(path, opts.overwrite);
18
- try {
19
- const zipBlob = await loading('Fetching strings from Tolgee...', fetchZipBlob(opts));
20
- await loading('Extracting strings...', unzipBuffer(zipBlob, path));
21
- success('Done!');
28
+ if (!opts.path) {
29
+ throw new Error('Missing or option --path <path>');
22
30
  }
23
- catch (e) {
24
- if (e instanceof HttpError && e.response.statusCode === 400) {
25
- const res = await e.response.body.json();
26
- error(`Please check if your parameters, including namespaces, are configured correctly. Tolgee responded with: ${res.code}`);
27
- return;
28
- }
29
- throw e;
30
- }
31
- }
32
- export default new Command()
31
+ await checkPathNotAFile(opts.path);
32
+ const zipBlob = await loading('Fetching strings from Tolgee...', fetchZipBlob(opts));
33
+ await prepareDir(opts.path, opts.emptyDir);
34
+ await loading('Extracting strings...', unzipBuffer(zipBlob, opts.path));
35
+ success('Done!');
36
+ };
37
+ export default (config) => new Command()
33
38
  .name('pull')
34
- .description('Pulls translations to Tolgee')
35
- .argument('<path>', 'Destination path where translation files will be stored in')
36
- .addOption(new Option('-f, --format <format>', 'Format of the exported files')
37
- .choices(['JSON', 'XLIFF'])
38
- .default('JSON')
39
- .argParser((v) => v.toUpperCase()))
40
- .option('-l, --languages <languages...>', 'List of languages to pull. Leave unspecified to export them all')
39
+ .description('Pulls translations from Tolgee')
40
+ .addOption(new Option('-p, --path <path>', 'Destination of a folder where translation files will be stored in').default(config.pull?.path))
41
+ .addOption(new Option('-l, --languages <languages...>', 'List of languages to pull. Leave unspecified to export them all').default(config.pull?.languagess))
41
42
  .addOption(new Option('-s, --states <states...>', 'List of translation states to include. Defaults all except untranslated')
42
43
  .choices(['UNTRANSLATED', 'TRANSLATED', 'REVIEWED'])
44
+ .default(config.pull?.states)
43
45
  .argParser((v, a) => [v.toUpperCase(), ...(a || [])]))
44
46
  .addOption(new Option('-d, --delimiter', 'Structure delimiter to use. By default, Tolgee interprets `.` as a nested structure. You can change the delimiter, or disable structure formatting by not specifying any value to the option')
45
- .default('.')
47
+ .default(config.pull?.delimiter ?? '.')
46
48
  .argParser((v) => v || ''))
47
- .addOption(new Option('-n, --namespaces <namespaces...>', 'List of namespaces to pull. Defaults to all namespaces'))
48
- .option('-o, --overwrite', 'Whether to automatically overwrite existing files. BE CAREFUL, THIS WILL WIPE *ALL* THE CONTENTS OF THE TARGET FOLDER. If unspecified, the user will be prompted interactively, or the command will fail when in non-interactive')
49
- .action(pullHandler);
49
+ .addOption(new Option('-n, --namespaces <namespaces...>', 'List of namespaces to pull. Defaults to all namespaces').default(config.pull?.namespaces))
50
+ .addOption(new Option('-t, --tags <tags...>', 'List of tags which to include. Keys tagged by at least one of these tags will be included.').default(config.pull?.tags))
51
+ .addOption(new Option('--exclude-tags <tags...>', 'List of tags which to exclude. Keys tagged by at least one of these tags will be excluded.').default(config.pull?.excludeTags))
52
+ .addOption(new Option('--support-arrays', 'Export keys with array syntax (e.g. item[0]) as arrays.').default(config.pull?.supportArrays ?? false))
53
+ .addOption(new Option('--empty-dir', 'Empty target directory before inserting pulled files.').default(config.pull?.emptyDir))
54
+ .addOption(new Option('--file-structure-template <template>', 'Defines exported file structure: https://tolgee.io/tolgee-cli/push-pull-strings#file-structure-template-format').default(config.pull?.fileStructureTemplate))
55
+ .action(pullHandler());