gtx-cli 2.5.0-alpha.0 → 2.5.0-alpha.1

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 (93) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/api/collectUserEditDiffs.d.ts +2 -7
  3. package/dist/api/collectUserEditDiffs.js +33 -78
  4. package/dist/api/downloadFileBatch.d.ts +11 -10
  5. package/dist/api/downloadFileBatch.js +120 -127
  6. package/dist/api/saveLocalEdits.js +18 -15
  7. package/dist/cli/base.js +1 -1
  8. package/dist/cli/commands/stage.d.ts +8 -2
  9. package/dist/cli/commands/stage.js +25 -7
  10. package/dist/cli/commands/translate.d.ts +4 -2
  11. package/dist/cli/commands/translate.js +5 -6
  12. package/dist/cli/flags.js +4 -1
  13. package/dist/config/generateSettings.js +10 -0
  14. package/dist/console/colors.d.ts +0 -1
  15. package/dist/console/colors.js +0 -3
  16. package/dist/console/index.d.ts +0 -6
  17. package/dist/console/index.js +2 -13
  18. package/dist/console/logging.d.ts +1 -1
  19. package/dist/console/logging.js +3 -4
  20. package/dist/formats/files/translate.d.ts +2 -2
  21. package/dist/formats/files/translate.js +31 -5
  22. package/dist/fs/config/downloadedVersions.d.ts +10 -3
  23. package/dist/fs/config/downloadedVersions.js +8 -0
  24. package/dist/fs/config/updateVersions.d.ts +2 -1
  25. package/dist/git/branches.d.ts +7 -0
  26. package/dist/git/branches.js +88 -0
  27. package/dist/react/{jsx/utils/jsxParsing → data-_gt}/addGTIdentifierToSyntaxTree.d.ts +1 -2
  28. package/dist/react/{jsx/utils/jsxParsing → data-_gt}/addGTIdentifierToSyntaxTree.js +6 -30
  29. package/dist/react/jsx/evaluateJsx.d.ts +6 -5
  30. package/dist/react/jsx/evaluateJsx.js +4 -32
  31. package/dist/react/jsx/trimJsxStringChildren.d.ts +7 -0
  32. package/dist/react/jsx/trimJsxStringChildren.js +122 -0
  33. package/dist/react/jsx/utils/constants.d.ts +0 -2
  34. package/dist/react/jsx/utils/constants.js +2 -11
  35. package/dist/react/jsx/utils/parseJsx.d.ts +21 -0
  36. package/dist/react/jsx/utils/parseJsx.js +259 -0
  37. package/dist/react/jsx/utils/parseStringFunction.js +141 -4
  38. package/dist/react/parse/createInlineUpdates.js +70 -19
  39. package/dist/types/branch.d.ts +14 -0
  40. package/dist/types/branch.js +1 -0
  41. package/dist/types/data.d.ts +1 -1
  42. package/dist/types/files.d.ts +7 -0
  43. package/dist/types/index.d.ts +7 -0
  44. package/dist/utils/SpinnerManager.d.ts +30 -0
  45. package/dist/utils/SpinnerManager.js +73 -0
  46. package/dist/utils/gitDiff.js +18 -16
  47. package/dist/workflow/BranchStep.d.ts +13 -0
  48. package/dist/workflow/BranchStep.js +131 -0
  49. package/dist/workflow/DownloadStep.d.ts +19 -0
  50. package/dist/workflow/DownloadStep.js +127 -0
  51. package/dist/workflow/EnqueueStep.d.ts +15 -0
  52. package/dist/workflow/EnqueueStep.js +33 -0
  53. package/dist/workflow/PollJobsStep.d.ts +31 -0
  54. package/dist/workflow/PollJobsStep.js +284 -0
  55. package/dist/workflow/SetupStep.d.ts +16 -0
  56. package/dist/workflow/SetupStep.js +71 -0
  57. package/dist/workflow/UploadStep.d.ts +21 -0
  58. package/dist/workflow/UploadStep.js +72 -0
  59. package/dist/workflow/UserEditDiffsStep.d.ts +11 -0
  60. package/dist/workflow/UserEditDiffsStep.js +30 -0
  61. package/dist/workflow/Workflow.d.ts +4 -0
  62. package/dist/workflow/Workflow.js +2 -0
  63. package/dist/workflow/download.d.ts +22 -0
  64. package/dist/workflow/download.js +104 -0
  65. package/dist/workflow/stage.d.ts +14 -0
  66. package/dist/workflow/stage.js +76 -0
  67. package/package.json +4 -5
  68. package/dist/api/checkFileTranslations.d.ts +0 -23
  69. package/dist/api/checkFileTranslations.js +0 -281
  70. package/dist/api/sendFiles.d.ts +0 -17
  71. package/dist/api/sendFiles.js +0 -127
  72. package/dist/api/sendUserEdits.d.ts +0 -19
  73. package/dist/api/sendUserEdits.js +0 -15
  74. package/dist/cli/commands/edits.d.ts +0 -8
  75. package/dist/cli/commands/edits.js +0 -32
  76. package/dist/react/jsx/utils/buildImportMap.d.ts +0 -9
  77. package/dist/react/jsx/utils/buildImportMap.js +0 -30
  78. package/dist/react/jsx/utils/getPathsAndAliases.d.ts +0 -17
  79. package/dist/react/jsx/utils/getPathsAndAliases.js +0 -89
  80. package/dist/react/jsx/utils/jsxParsing/handleChildrenWhitespace.d.ts +0 -6
  81. package/dist/react/jsx/utils/jsxParsing/handleChildrenWhitespace.js +0 -199
  82. package/dist/react/jsx/utils/jsxParsing/multiplication/findMultiplicationNode.d.ts +0 -13
  83. package/dist/react/jsx/utils/jsxParsing/multiplication/findMultiplicationNode.js +0 -42
  84. package/dist/react/jsx/utils/jsxParsing/multiplication/multiplyJsxTree.d.ts +0 -5
  85. package/dist/react/jsx/utils/jsxParsing/multiplication/multiplyJsxTree.js +0 -69
  86. package/dist/react/jsx/utils/jsxParsing/parseJsx.d.ts +0 -60
  87. package/dist/react/jsx/utils/jsxParsing/parseJsx.js +0 -949
  88. package/dist/react/jsx/utils/jsxParsing/parseTProps.d.ts +0 -8
  89. package/dist/react/jsx/utils/jsxParsing/parseTProps.js +0 -47
  90. package/dist/react/jsx/utils/jsxParsing/types.d.ts +0 -48
  91. package/dist/react/jsx/utils/jsxParsing/types.js +0 -34
  92. package/dist/react/jsx/utils/resolveImportPath.d.ts +0 -11
  93. package/dist/react/jsx/utils/resolveImportPath.js +0 -111
@@ -8,9 +8,12 @@ import traverseModule from '@babel/traverse';
8
8
  const generate = generateModule.default || generateModule;
9
9
  const traverse = traverseModule.default || traverseModule;
10
10
  import fs from 'node:fs';
11
+ import path from 'node:path';
11
12
  import { parse } from '@babel/parser';
12
- import { resolveImportPath } from './resolveImportPath.js';
13
- import { buildImportMap } from './buildImportMap.js';
13
+ import { createMatchPath, loadConfig } from 'tsconfig-paths';
14
+ import resolve from 'resolve';
15
+ import enhancedResolve from 'enhanced-resolve';
16
+ const { ResolverFactory } = enhancedResolve;
14
17
  /**
15
18
  * Cache for resolved import paths to avoid redundant I/O operations.
16
19
  * Key: `${currentFile}::${importPath}`
@@ -109,6 +112,35 @@ function extractParameterName(param) {
109
112
  }
110
113
  return null;
111
114
  }
115
+ /**
116
+ * Builds a map of imported function names to their import paths from a given program path.
117
+ * Handles both named imports and default imports.
118
+ *
119
+ * Example: import { getInfo } from './constants' -> Map { 'getInfo' => './constants' }
120
+ * Example: import utils from './utils' -> Map { 'utils' => './utils' }
121
+ */
122
+ function buildImportMap(programPath) {
123
+ const importMap = new Map();
124
+ programPath.traverse({
125
+ ImportDeclaration(importPath) {
126
+ if (t.isStringLiteral(importPath.node.source)) {
127
+ const importSource = importPath.node.source.value;
128
+ importPath.node.specifiers.forEach((spec) => {
129
+ if (t.isImportSpecifier(spec) &&
130
+ t.isIdentifier(spec.imported) &&
131
+ t.isIdentifier(spec.local)) {
132
+ importMap.set(spec.local.name, importSource);
133
+ }
134
+ else if (t.isImportDefaultSpecifier(spec) &&
135
+ t.isIdentifier(spec.local)) {
136
+ importMap.set(spec.local.name, importSource);
137
+ }
138
+ });
139
+ }
140
+ },
141
+ });
142
+ return importMap;
143
+ }
112
144
  /**
113
145
  * Recursively resolves variable assignments to find all aliases of a translation callback parameter.
114
146
  * Handles cases like: const t = translate; const a = translate; const b = a; const c = b;
@@ -180,7 +212,7 @@ function handleFunctionCall(tPath, updates, errors, warnings, file, importMap, i
180
212
  // If not found locally, check if it's an imported function
181
213
  else if (importMap.has(callee.name)) {
182
214
  const importPath = importMap.get(callee.name);
183
- const resolvedPath = resolveImportPath(file, importPath, parsingOptions, resolveImportPathCache);
215
+ const resolvedPath = resolveImportPath(file, importPath, parsingOptions);
184
216
  if (resolvedPath) {
185
217
  processFunctionInFile(resolvedPath, callee.name, argIndex, updates, errors, warnings, ignoreAdditionalData, ignoreDynamicContent, ignoreInvalidIcu, parsingOptions);
186
218
  }
@@ -231,6 +263,111 @@ function findFunctionParameterUsage(functionPath, parameterName, updates, errors
231
263
  });
232
264
  }
233
265
  }
266
+ /**
267
+ * Resolves import paths to absolute file paths using battle-tested libraries.
268
+ * Handles relative paths, TypeScript paths, and node module resolution.
269
+ *
270
+ * Examples:
271
+ * - './constants' -> '/full/path/to/constants.ts'
272
+ * - '@/components/ui/button' -> '/full/path/to/src/components/ui/button.tsx'
273
+ * - '@shared/utils' -> '/full/path/to/packages/utils/index.ts'
274
+ */
275
+ function resolveImportPath(currentFile, importPath, parsingOptions) {
276
+ // Check cache first
277
+ const cacheKey = `${currentFile}::${importPath}`;
278
+ if (resolveImportPathCache.has(cacheKey)) {
279
+ return resolveImportPathCache.get(cacheKey);
280
+ }
281
+ const basedir = path.dirname(currentFile);
282
+ const extensions = ['.tsx', '.ts', '.jsx', '.js'];
283
+ const mainFields = ['module', 'main'];
284
+ let result = null;
285
+ // 1. Try tsconfig-paths resolution first (handles TypeScript path mapping)
286
+ const tsConfigResult = loadConfig(basedir);
287
+ if (tsConfigResult.resultType === 'success') {
288
+ const matchPath = createMatchPath(tsConfigResult.absoluteBaseUrl, tsConfigResult.paths, mainFields);
289
+ // First try without any extension
290
+ let tsResolved = matchPath(importPath);
291
+ if (tsResolved && fs.existsSync(tsResolved)) {
292
+ result = tsResolved;
293
+ resolveImportPathCache.set(cacheKey, result);
294
+ return result;
295
+ }
296
+ // Then try with each extension
297
+ for (const ext of extensions) {
298
+ tsResolved = matchPath(importPath + ext);
299
+ if (tsResolved && fs.existsSync(tsResolved)) {
300
+ result = tsResolved;
301
+ resolveImportPathCache.set(cacheKey, result);
302
+ return result;
303
+ }
304
+ // Also try the resolved path with extension
305
+ tsResolved = matchPath(importPath);
306
+ if (tsResolved) {
307
+ const resolvedWithExt = tsResolved + ext;
308
+ if (fs.existsSync(resolvedWithExt)) {
309
+ result = resolvedWithExt;
310
+ resolveImportPathCache.set(cacheKey, result);
311
+ return result;
312
+ }
313
+ }
314
+ }
315
+ }
316
+ // 2. Try enhanced-resolve (handles package.json exports field and modern resolution)
317
+ try {
318
+ const resolver = ResolverFactory.createResolver({
319
+ useSyncFileSystemCalls: true,
320
+ fileSystem: fs,
321
+ extensions,
322
+ // Include 'development' condition to resolve to source files in monorepos
323
+ conditionNames: parsingOptions.conditionNames, // defaults to ['browser', 'module', 'import', 'require', 'default']. See generateSettings.ts for more details
324
+ exportsFields: ['exports'],
325
+ mainFields,
326
+ });
327
+ const resolved = resolver.resolveSync({}, basedir, importPath);
328
+ if (resolved) {
329
+ result = resolved;
330
+ resolveImportPathCache.set(cacheKey, result);
331
+ return result;
332
+ }
333
+ }
334
+ catch {
335
+ // Fall through to next resolution strategy
336
+ }
337
+ // 3. Fallback to Node.js resolution (handles relative paths and node_modules)
338
+ try {
339
+ result = resolve.sync(importPath, { basedir, extensions });
340
+ resolveImportPathCache.set(cacheKey, result);
341
+ return result;
342
+ }
343
+ catch {
344
+ // If resolution fails, try to manually replace .js/.jsx with .ts/.tsx for source files
345
+ if (importPath.endsWith('.js')) {
346
+ const tsPath = importPath.replace(/\.js$/, '.ts');
347
+ try {
348
+ result = resolve.sync(tsPath, { basedir, extensions });
349
+ resolveImportPathCache.set(cacheKey, result);
350
+ return result;
351
+ }
352
+ catch {
353
+ // Continue to return null
354
+ }
355
+ }
356
+ else if (importPath.endsWith('.jsx')) {
357
+ const tsxPath = importPath.replace(/\.jsx$/, '.tsx');
358
+ try {
359
+ result = resolve.sync(tsxPath, { basedir, extensions });
360
+ resolveImportPathCache.set(cacheKey, result);
361
+ return result;
362
+ }
363
+ catch {
364
+ // Continue to return null
365
+ }
366
+ }
367
+ resolveImportPathCache.set(cacheKey, null);
368
+ return null;
369
+ }
370
+ }
234
371
  /**
235
372
  * Searches for a specific user-defined function in a file and analyzes how a translation callback
236
373
  * parameter (at argIndex position) is used within that function.
@@ -309,7 +446,7 @@ function processFunctionInFile(filePath, functionName, argIndex, updates, errors
309
446
  // If function not found, follow re-exports
310
447
  if (!found && reExports.length > 0) {
311
448
  for (const reExportPath of reExports) {
312
- const resolvedPath = resolveImportPath(filePath, reExportPath, parsingOptions, resolveImportPathCache);
449
+ const resolvedPath = resolveImportPath(filePath, reExportPath, parsingOptions);
313
450
  if (resolvedPath) {
314
451
  processFunctionInFile(resolvedPath, functionName, argIndex, updates, errors, warnings, ignoreAdditionalData, ignoreDynamicContent, ignoreInvalidIcu, parsingOptions, visited);
315
452
  }
@@ -1,12 +1,16 @@
1
1
  import fs from 'node:fs';
2
2
  import { parse } from '@babel/parser';
3
+ import traverseModule from '@babel/traverse';
4
+ // Handle CommonJS/ESM interop
5
+ const traverse = traverseModule.default || traverseModule;
3
6
  import { hashSource } from 'generaltranslation/id';
4
- import { parseTranslationComponent } from '../jsx/utils/jsxParsing/parseJsx.js';
7
+ import { parseJSXElement } from '../jsx/utils/parseJsx.js';
5
8
  import { parseStrings } from '../jsx/utils/parseStringFunction.js';
9
+ import { extractImportName } from '../jsx/utils/parseAst.js';
6
10
  import { logError } from '../../console/logging.js';
11
+ import { GT_TRANSLATION_FUNCS, INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, INLINE_MESSAGE_HOOK, INLINE_MESSAGE_HOOK_ASYNC, MSG_TRANSLATION_HOOK, } from '../jsx/utils/constants.js';
7
12
  import { matchFiles } from '../../fs/matchFiles.js';
8
13
  import { DEFAULT_SRC_PATTERNS } from '../../config/generateSettings.js';
9
- import { getPathsAndAliases } from '../jsx/utils/getPathsAndAliases.js';
10
14
  export async function createInlineUpdates(pkg, validate, filePatterns, parsingOptions) {
11
15
  const updates = [];
12
16
  const errors = [];
@@ -26,28 +30,75 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingOp
26
30
  logError(`Error parsing file ${file}: ${error}`);
27
31
  continue;
28
32
  }
33
+ const importAliases = {};
29
34
  // First pass: collect imports and process translation functions
30
- const { importAliases, inlineTranslationPaths, translationComponentPaths } = getPathsAndAliases(ast, pkg);
35
+ const translationPaths = [];
36
+ traverse(ast, {
37
+ ImportDeclaration(path) {
38
+ if (path.node.source.value.startsWith(pkg)) {
39
+ const importName = extractImportName(path.node, pkg, GT_TRANSLATION_FUNCS);
40
+ for (const name of importName) {
41
+ if (name.original === INLINE_TRANSLATION_HOOK ||
42
+ name.original === INLINE_TRANSLATION_HOOK_ASYNC ||
43
+ name.original === INLINE_MESSAGE_HOOK ||
44
+ name.original === INLINE_MESSAGE_HOOK_ASYNC ||
45
+ name.original === MSG_TRANSLATION_HOOK) {
46
+ translationPaths.push({
47
+ localName: name.local,
48
+ path,
49
+ originalName: name.original,
50
+ });
51
+ }
52
+ else {
53
+ importAliases[name.local] = name.original;
54
+ }
55
+ }
56
+ }
57
+ },
58
+ VariableDeclarator(path) {
59
+ // Check if the init is a require call
60
+ if (path.node.init?.type === 'CallExpression' &&
61
+ path.node.init.callee.type === 'Identifier' &&
62
+ path.node.init.callee.name === 'require') {
63
+ // Check if it's requiring our package
64
+ const args = path.node.init.arguments;
65
+ if (args.length === 1 &&
66
+ args[0].type === 'StringLiteral' &&
67
+ args[0].value.startsWith(pkg)) {
68
+ const parentPath = path.parentPath;
69
+ if (parentPath.isVariableDeclaration()) {
70
+ const importName = extractImportName(parentPath.node, pkg, GT_TRANSLATION_FUNCS);
71
+ for (const name of importName) {
72
+ if (name.original === INLINE_TRANSLATION_HOOK ||
73
+ name.original === INLINE_TRANSLATION_HOOK_ASYNC ||
74
+ name.original === INLINE_MESSAGE_HOOK ||
75
+ name.original === INLINE_MESSAGE_HOOK_ASYNC ||
76
+ name.original === MSG_TRANSLATION_HOOK) {
77
+ translationPaths.push({
78
+ localName: name.local,
79
+ path: parentPath,
80
+ originalName: name.original,
81
+ });
82
+ }
83
+ else {
84
+ importAliases[name.local] = name.original;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ },
91
+ });
31
92
  // Process translation functions asynchronously
32
- for (const { localName: name, originalName, path, } of inlineTranslationPaths) {
93
+ for (const { localName: name, originalName, path } of translationPaths) {
33
94
  parseStrings(name, originalName, path, updates, errors, warnings, file, parsingOptions);
34
95
  }
35
96
  // Parse <T> components
36
- for (const { localName, path } of translationComponentPaths) {
37
- parseTranslationComponent({
38
- importAliases: importAliases,
39
- originalName: localName,
40
- localName,
41
- ast,
42
- pkg,
43
- path,
44
- updates,
45
- errors,
46
- warnings,
47
- file,
48
- parsingOptions,
49
- });
50
- }
97
+ traverse(ast, {
98
+ JSXElement(path) {
99
+ parseJSXElement(importAliases, path.node, updates, errors, warnings, file);
100
+ },
101
+ });
51
102
  // Extra validation (for Locadex)
52
103
  // Done in parseStrings() atm
53
104
  // if (validate) {
@@ -0,0 +1,14 @@
1
+ export type BranchData = {
2
+ currentBranch: {
3
+ id: string;
4
+ name: string;
5
+ };
6
+ incomingBranch: {
7
+ id: string;
8
+ name: string;
9
+ } | null;
10
+ checkedOutBranch: {
11
+ id: string;
12
+ name: string;
13
+ } | null;
14
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -17,7 +17,7 @@ export type JSONDictionary = {
17
17
  export type FlattenedJSONDictionary = {
18
18
  [key: string]: string;
19
19
  };
20
- export type { FileFormat, DataFormat, FileToTranslate, } from 'generaltranslation/types';
20
+ export type { FileFormat, DataFormat, FileToUpload, } from 'generaltranslation/types';
21
21
  export type JsxChildren = string | string[] | any;
22
22
  export type Translations = {
23
23
  [key: string]: JsxChildren;
@@ -1 +1,8 @@
1
1
  export type FileMapping = Record<string, Record<string, string>>;
2
+ export type FileProperties = {
3
+ versionId: string;
4
+ fileName: string;
5
+ fileId: string;
6
+ locale: string;
7
+ branchId: string;
8
+ };
@@ -133,6 +133,13 @@ export type Settings = {
133
133
  options?: AdditionalOptions;
134
134
  modelProvider?: string;
135
135
  parsingOptions: ParsingConfigOptions;
136
+ branchOptions: BranchOptions;
137
+ };
138
+ export type BranchOptions = {
139
+ currentBranch?: string;
140
+ autoDetectBranches?: boolean;
141
+ remoteName: string;
142
+ enabled: boolean;
136
143
  };
137
144
  export type AdditionalOptions = {
138
145
  jsonSchema?: {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Centralized spinner management for tracking multiple async operations
3
+ */
4
+ export declare class SpinnerManager {
5
+ private spinners;
6
+ /**
7
+ * Run an async operation with a spinner
8
+ */
9
+ run<T>(id: string, message: string, fn: () => Promise<T>): Promise<T>;
10
+ /**
11
+ * Mark a spinner as successful
12
+ */
13
+ succeed(id: string, message: string): void;
14
+ /**
15
+ * Mark a spinner as warning
16
+ */
17
+ warn(id: string, message: string): void;
18
+ /**
19
+ * Start a new spinner
20
+ */
21
+ start(id: string, message: string): void;
22
+ /**
23
+ * Stop a specific spinner
24
+ */
25
+ stop(id: string, message?: string): void;
26
+ /**
27
+ * Stop all running spinners
28
+ */
29
+ stopAll(): void;
30
+ }
@@ -0,0 +1,73 @@
1
+ import chalk from 'chalk';
2
+ import { createSpinner } from '../console/logging.js';
3
+ /**
4
+ * Centralized spinner management for tracking multiple async operations
5
+ */
6
+ export class SpinnerManager {
7
+ spinners = new Map();
8
+ /**
9
+ * Run an async operation with a spinner
10
+ */
11
+ async run(id, message, fn) {
12
+ const spinner = createSpinner('dots');
13
+ this.spinners.set(id, spinner);
14
+ spinner.start(message);
15
+ try {
16
+ const result = await fn();
17
+ spinner.stop(chalk.green('✓'));
18
+ return result;
19
+ }
20
+ catch (error) {
21
+ spinner.stop(chalk.red('✗'));
22
+ throw error;
23
+ }
24
+ finally {
25
+ this.spinners.delete(id);
26
+ }
27
+ }
28
+ /**
29
+ * Mark a spinner as successful
30
+ */
31
+ succeed(id, message) {
32
+ const spinner = this.spinners.get(id);
33
+ if (spinner) {
34
+ spinner.stop(chalk.green(message));
35
+ this.spinners.delete(id);
36
+ }
37
+ }
38
+ /**
39
+ * Mark a spinner as warning
40
+ */
41
+ warn(id, message) {
42
+ const spinner = this.spinners.get(id);
43
+ if (spinner) {
44
+ spinner.stop(chalk.yellow(message));
45
+ this.spinners.delete(id);
46
+ }
47
+ }
48
+ /**
49
+ * Start a new spinner
50
+ */
51
+ start(id, message) {
52
+ const spinner = createSpinner('dots');
53
+ this.spinners.set(id, spinner);
54
+ spinner.start(message);
55
+ }
56
+ /**
57
+ * Stop a specific spinner
58
+ */
59
+ stop(id, message) {
60
+ const spinner = this.spinners.get(id);
61
+ if (spinner) {
62
+ spinner.stop(message);
63
+ this.spinners.delete(id);
64
+ }
65
+ }
66
+ /**
67
+ * Stop all running spinners
68
+ */
69
+ stopAll() {
70
+ this.spinners.forEach((s) => s.stop());
71
+ this.spinners.clear();
72
+ }
73
+ }
@@ -9,24 +9,26 @@ const execFileAsync = promisify(execFile);
9
9
  * Throws if git is unavailable or another error occurs.
10
10
  */
11
11
  export async function getGitUnifiedDiff(oldPath, newPath) {
12
- const res = await execFileAsync('git', [
13
- 'diff',
14
- '--no-index',
15
- '--text',
16
- '--unified=3',
17
- '--no-color',
18
- '--',
19
- oldPath,
20
- newPath,
21
- ], {
22
- windowsHide: true,
23
- }).catch((error) => {
12
+ try {
13
+ const res = await execFileAsync('git', [
14
+ 'diff',
15
+ '--no-index',
16
+ '--text',
17
+ '--unified=3',
18
+ '--no-color',
19
+ '--',
20
+ oldPath,
21
+ newPath,
22
+ ], {
23
+ windowsHide: true,
24
+ });
25
+ return res.stdout || '';
26
+ }
27
+ catch (error) {
24
28
  // Exit code 1 means differences found; stdout contains the diff
25
29
  if (error && error.code === 1 && typeof error.stdout === 'string') {
26
- return { stdout: error.stdout };
30
+ return error.stdout;
27
31
  }
28
32
  throw error;
29
- });
30
- // When there are no changes, stdout is empty string and exit code 0
31
- return res.stdout || '';
33
+ }
32
34
  }
@@ -0,0 +1,13 @@
1
+ import { WorkflowStep } from './Workflow.js';
2
+ import { GT } from 'generaltranslation';
3
+ import { Settings } from '../types/index.js';
4
+ import { BranchData } from '../types/branch.js';
5
+ export declare class BranchStep extends WorkflowStep<null, BranchData | null> {
6
+ private spinner;
7
+ private branchData;
8
+ private settings;
9
+ private gt;
10
+ constructor(gt: GT, settings: Settings);
11
+ run(): Promise<BranchData | null>;
12
+ wait(): Promise<void>;
13
+ }
@@ -0,0 +1,131 @@
1
+ import { WorkflowStep } from './Workflow.js';
2
+ import { createSpinner, logError, logErrorAndExit, } from '../console/logging.js';
3
+ import chalk from 'chalk';
4
+ import { getCurrentBranch, getIncomingBranches, getCheckedOutBranches, } from '../git/branches.js';
5
+ import { ApiError } from 'generaltranslation/errors';
6
+ // Step 1: Resolve the current branch id & update API with branch information
7
+ export class BranchStep extends WorkflowStep {
8
+ spinner = createSpinner('dots');
9
+ branchData;
10
+ settings;
11
+ gt;
12
+ constructor(gt, settings) {
13
+ super();
14
+ this.gt = gt;
15
+ this.settings = settings;
16
+ this.branchData = {
17
+ currentBranch: {
18
+ id: '',
19
+ name: '',
20
+ },
21
+ incomingBranch: null,
22
+ checkedOutBranch: null,
23
+ };
24
+ }
25
+ async run() {
26
+ this.spinner.start(`Resolving branch information...`);
27
+ // First get some info about the branches we're working with
28
+ let current = null;
29
+ let incoming = [];
30
+ let checkedOut = [];
31
+ let useDefaultBranch = true;
32
+ if (this.settings.branchOptions.enabled &&
33
+ this.settings.branchOptions.autoDetectBranches) {
34
+ const [currentResult, incomingResult, checkedOutResult] = await Promise.all([
35
+ getCurrentBranch(this.settings.branchOptions.remoteName),
36
+ getIncomingBranches(this.settings.branchOptions.remoteName),
37
+ getCheckedOutBranches(this.settings.branchOptions.remoteName),
38
+ ]);
39
+ current = currentResult;
40
+ incoming = incomingResult;
41
+ checkedOut = checkedOutResult;
42
+ useDefaultBranch = false;
43
+ }
44
+ if (this.settings.branchOptions.enabled &&
45
+ this.settings.branchOptions.currentBranch) {
46
+ current = {
47
+ currentBranchName: this.settings.branchOptions.currentBranch,
48
+ defaultBranch: current?.defaultBranch ?? false, // we have no way of knowing if this is the default branch without using the auto-detection logic
49
+ };
50
+ useDefaultBranch = false;
51
+ }
52
+ const branchData = await this.gt.queryBranchData({
53
+ branchNames: [
54
+ ...(current ? [current.currentBranchName] : []),
55
+ ...incoming,
56
+ ...checkedOut,
57
+ ],
58
+ });
59
+ if (useDefaultBranch) {
60
+ if (!branchData.defaultBranch) {
61
+ const createBranchResult = await this.gt.createBranch({
62
+ branchName: 'main', // name doesn't matter for default branch
63
+ defaultBranch: true,
64
+ });
65
+ this.branchData.currentBranch = createBranchResult.branch;
66
+ }
67
+ else {
68
+ this.branchData.currentBranch = branchData.defaultBranch;
69
+ }
70
+ }
71
+ else {
72
+ if (!current) {
73
+ logErrorAndExit('Failed to determine the current branch. Please specify a custom branch or enable automatic branch detection.');
74
+ }
75
+ const currentBranch = branchData.branches.find((b) => b.name === current.currentBranchName);
76
+ if (!currentBranch) {
77
+ try {
78
+ const createBranchResult = await this.gt.createBranch({
79
+ branchName: current.currentBranchName,
80
+ defaultBranch: current.defaultBranch,
81
+ });
82
+ this.branchData.currentBranch = createBranchResult.branch;
83
+ }
84
+ catch (error) {
85
+ if (error instanceof ApiError && error.code === 403) {
86
+ logError('Failed to create branch. To enable branching, please upgrade your plan.');
87
+ // retry with default branch
88
+ const createBranchResult = await this.gt.createBranch({
89
+ branchName: 'main', // name doesn't matter for default branch
90
+ defaultBranch: true,
91
+ });
92
+ this.branchData.currentBranch = createBranchResult.branch;
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ this.branchData.currentBranch = currentBranch;
98
+ }
99
+ }
100
+ // Now set the incoming and checked out branches (first one that exists)
101
+ this.branchData.incomingBranch =
102
+ incoming
103
+ .map((b) => {
104
+ const branch = branchData.branches.find((bb) => bb.name === b);
105
+ if (branch) {
106
+ return branch;
107
+ }
108
+ else {
109
+ return null;
110
+ }
111
+ })
112
+ .filter((b) => b !== null)[0] ?? null;
113
+ this.branchData.checkedOutBranch =
114
+ checkedOut
115
+ .map((b) => {
116
+ const branch = branchData.branches.find((bb) => bb.name === b);
117
+ if (branch) {
118
+ return branch;
119
+ }
120
+ else {
121
+ return null;
122
+ }
123
+ })
124
+ .filter((b) => b !== null)[0] ?? null;
125
+ this.spinner.stop(chalk.green('Branch information resolved successfully'));
126
+ return this.branchData;
127
+ }
128
+ async wait() {
129
+ return;
130
+ }
131
+ }
@@ -0,0 +1,19 @@
1
+ import { WorkflowStep } from './Workflow.js';
2
+ import { GT } from 'generaltranslation';
3
+ import { Settings } from '../types/index.js';
4
+ import { FileStatusTracker } from './PollJobsStep.js';
5
+ export type DownloadTranslationsInput = {
6
+ fileTracker: FileStatusTracker;
7
+ resolveOutputPath: (sourcePath: string, locale: string) => string | null;
8
+ forceDownload?: boolean;
9
+ };
10
+ export declare class DownloadTranslationsStep extends WorkflowStep<DownloadTranslationsInput, boolean> {
11
+ private gt;
12
+ private settings;
13
+ private spinner;
14
+ constructor(gt: GT, settings: Settings);
15
+ run({ fileTracker, resolveOutputPath, forceDownload, }: DownloadTranslationsInput): Promise<boolean>;
16
+ private downloadFiles;
17
+ private downloadFilesWithRetry;
18
+ wait(): Promise<void>;
19
+ }