@babelize/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +14 -11
  2. package/dist/client.d.ts +9 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +73 -31
  5. package/dist/client.js.map +1 -1
  6. package/dist/commands/account.js +1 -1
  7. package/dist/commands/account.js.map +1 -1
  8. package/dist/commands/admin.js +5 -5
  9. package/dist/commands/admin.js.map +1 -1
  10. package/dist/commands/analytics.js +1 -1
  11. package/dist/commands/analytics.js.map +1 -1
  12. package/dist/commands/api-keys.d.ts +6 -0
  13. package/dist/commands/api-keys.d.ts.map +1 -1
  14. package/dist/commands/api-keys.js +42 -12
  15. package/dist/commands/api-keys.js.map +1 -1
  16. package/dist/commands/auth.d.ts.map +1 -1
  17. package/dist/commands/auth.js +23 -12
  18. package/dist/commands/auth.js.map +1 -1
  19. package/dist/commands/babels.d.ts.map +1 -1
  20. package/dist/commands/babels.js +16 -20
  21. package/dist/commands/babels.js.map +1 -1
  22. package/dist/commands/billing.js +1 -1
  23. package/dist/commands/billing.js.map +1 -1
  24. package/dist/commands/checkout.js +2 -2
  25. package/dist/commands/checkout.js.map +1 -1
  26. package/dist/commands/comments.js +5 -5
  27. package/dist/commands/comments.js.map +1 -1
  28. package/dist/commands/glossary.d.ts +15 -0
  29. package/dist/commands/glossary.d.ts.map +1 -1
  30. package/dist/commands/glossary.js +104 -24
  31. package/dist/commands/glossary.js.map +1 -1
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +35 -22
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/invitations.d.ts.map +1 -1
  36. package/dist/commands/invitations.js +3 -1
  37. package/dist/commands/invitations.js.map +1 -1
  38. package/dist/commands/languages.d.ts.map +1 -1
  39. package/dist/commands/languages.js +38 -17
  40. package/dist/commands/languages.js.map +1 -1
  41. package/dist/commands/members.js +7 -7
  42. package/dist/commands/members.js.map +1 -1
  43. package/dist/commands/notifications.js +1 -1
  44. package/dist/commands/notifications.js.map +1 -1
  45. package/dist/commands/pr.d.ts.map +1 -1
  46. package/dist/commands/pr.js +6 -4
  47. package/dist/commands/pr.js.map +1 -1
  48. package/dist/commands/privacy.d.ts.map +1 -1
  49. package/dist/commands/privacy.js +7 -5
  50. package/dist/commands/privacy.js.map +1 -1
  51. package/dist/commands/projects.d.ts.map +1 -1
  52. package/dist/commands/projects.js +60 -35
  53. package/dist/commands/projects.js.map +1 -1
  54. package/dist/commands/roles.js +1 -1
  55. package/dist/commands/roles.js.map +1 -1
  56. package/dist/commands/run.d.ts +48 -0
  57. package/dist/commands/run.d.ts.map +1 -1
  58. package/dist/commands/run.js +264 -132
  59. package/dist/commands/run.js.map +1 -1
  60. package/dist/commands/scan.js +1 -1
  61. package/dist/commands/scan.js.map +1 -1
  62. package/dist/commands/status.js +1 -1
  63. package/dist/commands/status.js.map +1 -1
  64. package/dist/commands/subscriptions.js +1 -1
  65. package/dist/commands/subscriptions.js.map +1 -1
  66. package/dist/commands/tm.js +2 -2
  67. package/dist/commands/tm.js.map +1 -1
  68. package/dist/commands/translate.d.ts.map +1 -1
  69. package/dist/commands/translate.js +9 -1
  70. package/dist/commands/translate.js.map +1 -1
  71. package/dist/commands/users.js +1 -1
  72. package/dist/commands/users.js.map +1 -1
  73. package/dist/config.d.ts +1 -0
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/config.js +12 -4
  76. package/dist/config.js.map +1 -1
  77. package/dist/global-options.d.ts +6 -2
  78. package/dist/global-options.d.ts.map +1 -1
  79. package/dist/global-options.js +9 -3
  80. package/dist/global-options.js.map +1 -1
  81. package/dist/index.js +0 -0
  82. package/dist/oauth.d.ts.map +1 -1
  83. package/dist/oauth.js +11 -3
  84. package/dist/oauth.js.map +1 -1
  85. package/dist/program.d.ts +22 -0
  86. package/dist/program.d.ts.map +1 -1
  87. package/dist/program.js +26 -10
  88. package/dist/program.js.map +1 -1
  89. package/dist/tui/clack.d.ts +60 -0
  90. package/dist/tui/clack.d.ts.map +1 -0
  91. package/dist/tui/clack.js +138 -0
  92. package/dist/tui/clack.js.map +1 -0
  93. package/dist/tui/help.d.ts.map +1 -1
  94. package/dist/tui/help.js +14 -5
  95. package/dist/tui/help.js.map +1 -1
  96. package/dist/tui/prompt.d.ts.map +1 -1
  97. package/dist/tui/prompt.js +4 -2
  98. package/dist/tui/prompt.js.map +1 -1
  99. package/dist/tui/table.d.ts.map +1 -1
  100. package/dist/tui/table.js +6 -2
  101. package/dist/tui/table.js.map +1 -1
  102. package/dist/utils/validate.d.ts +7 -0
  103. package/dist/utils/validate.d.ts.map +1 -0
  104. package/dist/utils/validate.js +16 -0
  105. package/dist/utils/validate.js.map +1 -0
  106. package/dist/utils/zip-upload.d.ts +2 -0
  107. package/dist/utils/zip-upload.d.ts.map +1 -1
  108. package/dist/utils/zip-upload.js +8 -0
  109. package/dist/utils/zip-upload.js.map +1 -1
  110. package/package.json +3 -7
@@ -1,3 +1,51 @@
1
1
  import { Command } from 'commander';
2
+ import { AxiosInstance } from 'axios';
3
+ /**
4
+ * Pipeline phases as the worker reports them: each `babel_logs.step` the
5
+ * engine emits through onProgress (packages/engine/src/orchestrator.ts),
6
+ * grouped in run order. The lockfile step runs around the locale step, and
7
+ * format-injection reports under injection.
8
+ */
9
+ export declare const RUN_PHASES: {
10
+ id: string;
11
+ title: string;
12
+ steps: string[];
13
+ }[];
14
+ export type PhaseState = 'pending' | 'running' | 'done' | 'error';
15
+ export interface PhaseProgress {
16
+ states: PhaseState[];
17
+ /** Index of the phase in progress, or -1 before the first log. */
18
+ current: number;
19
+ /** Latest message logged for the current phase. */
20
+ message?: string;
21
+ }
22
+ /**
23
+ * Where a run is, from its logs. A phase is done once it logs `success` or a
24
+ * later phase has started; the final `complete` step (or a `completed` run)
25
+ * marks every phase done. A phase that logged `error` is the failed one.
26
+ */
27
+ export declare function computePhaseProgress(logs: Record<string, unknown>[], runStatus?: string): PhaseProgress;
2
28
  export declare function registerRunCommands(program: Command): void;
29
+ /**
30
+ * Poll a run until it ends, showing each phase as it finishes.
31
+ *
32
+ * Returns the final status payload of a completed run. A run that ends any
33
+ * other way exits the process:
34
+ * - failed → ExitCode.API_ERROR (6), with the run's error
35
+ * - cancelled → ExitCode.CANCEL (128), the code the CLI already uses for
36
+ * anything the user cancelled, so it never looks like success
37
+ * With --json, the final payload is printed before exiting on failed or
38
+ * cancelled; a completed run's payload is left to the caller.
39
+ */
40
+ export declare function pollRunStatus(client: AxiosInstance, babelId: string, options?: {
41
+ intervalMs?: number;
42
+ }): Promise<Record<string, unknown>>;
43
+ export interface AutoPrOutcome {
44
+ status: 'created' | 'exists' | 'failed';
45
+ url?: string;
46
+ message?: string;
47
+ }
48
+ export declare function renderRunStatus(data: Record<string, unknown>, options?: {
49
+ source?: string;
50
+ }): void;
3
51
  //# sourceMappingURL=run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0J1D"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAa7C;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,EAStE,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAKD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAkCvG;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+K1D;AASD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACpC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA8ElC;AAqBD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuDD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAgFtG"}
@@ -1,25 +1,81 @@
1
- import * as clack from '@clack/prompts';
1
+ import * as clack from '../tui/clack.js';
2
2
  import chalk from 'chalk';
3
- import { success as successColor, error as errorColor, statusBadge } from '../tui/theme.js';
3
+ import axios from 'axios';
4
+ import { success as successColor, error as errorColor, warning as warningColor, statusBadge, sym } from '../tui/theme.js';
4
5
  import { table, keyValue } from '../tui/table.js';
5
- import { section, dim, divider, success, warning } from '../tui/display.js';
6
- import { createPipeline, delay, renderRunTitle } from '../tui/task-list.js';
6
+ import { section, dim, success, warning } from '../tui/display.js';
7
+ import { delay, renderRunTitle } from '../tui/task-list.js';
7
8
  import { config } from '../config.js';
8
9
  import { getClient, handleApiError } from '../client.js';
9
10
  import { isJsonMode, printJson } from '../formatters.js';
10
11
  import { isNonInteractive } from '../global-options.js';
11
12
  import { renderCommandHelp } from '../tui/help.js';
12
13
  import { ExitCode } from '../exit-codes.js';
13
- const PHASES = [
14
- 'Phase 0: Workspace Validation',
15
- 'Phase 1: String Extraction',
16
- 'Phase 2: Key Generation',
17
- 'Phase 3: Base Locale Handling',
18
- 'Phase 4: Translation',
19
- 'Phase 5: AST Injection',
20
- 'Phase 6: Summary',
21
- 'Phase 7: Finalization',
14
+ import { requireUuid } from '../utils/validate.js';
15
+ /**
16
+ * Pipeline phases as the worker reports them: each `babel_logs.step` the
17
+ * engine emits through onProgress (packages/engine/src/orchestrator.ts),
18
+ * grouped in run order. The lockfile step runs around the locale step, and
19
+ * format-injection reports under injection.
20
+ */
21
+ export const RUN_PHASES = [
22
+ { id: 'validation', title: 'Workspace validation', steps: ['validation'] },
23
+ { id: 'extraction', title: 'String extraction', steps: ['extraction'] },
24
+ { id: 'keygen', title: 'Key generation', steps: ['keygen'] },
25
+ { id: 'locale', title: 'Base locale and lockfile', steps: ['locale', 'lockfile'] },
26
+ { id: 'translation', title: 'Translation', steps: ['translation'] },
27
+ { id: 'injection', title: 'Code injection', steps: ['injection', 'format-injection'] },
28
+ { id: 'finalization', title: 'Finalization', steps: ['finalization'] },
29
+ { id: 'summary', title: 'Summary', steps: ['summary'] },
22
30
  ];
31
+ /** Run statuses after which a run never changes again. */
32
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
33
+ /**
34
+ * Where a run is, from its logs. A phase is done once it logs `success` or a
35
+ * later phase has started; the final `complete` step (or a `completed` run)
36
+ * marks every phase done. A phase that logged `error` is the failed one.
37
+ */
38
+ export function computePhaseProgress(logs, runStatus) {
39
+ const states = RUN_PHASES.map(() => 'pending');
40
+ const succeeded = new Set();
41
+ let reached = -1;
42
+ let errorAt = -1;
43
+ let message;
44
+ let complete = runStatus === 'completed';
45
+ for (const log of logs) {
46
+ const step = String(log.step ?? '').toLowerCase();
47
+ const status = String(log.status ?? '').toLowerCase();
48
+ if (step === 'complete') {
49
+ if (status === 'success')
50
+ complete = true;
51
+ continue;
52
+ }
53
+ const idx = RUN_PHASES.findIndex((p) => p.steps.includes(step));
54
+ if (idx === -1)
55
+ continue;
56
+ if (idx >= reached) {
57
+ reached = idx;
58
+ if (typeof log.message === 'string')
59
+ message = log.message;
60
+ }
61
+ if (status === 'success')
62
+ succeeded.add(idx);
63
+ if (status === 'error')
64
+ errorAt = idx;
65
+ }
66
+ if (complete) {
67
+ return { states: states.map(() => 'done'), current: RUN_PHASES.length - 1, message };
68
+ }
69
+ for (let i = 0; i < states.length; i++) {
70
+ if (i === errorAt)
71
+ states[i] = 'error';
72
+ else if (i < reached || succeeded.has(i))
73
+ states[i] = 'done';
74
+ else if (i === reached)
75
+ states[i] = 'running';
76
+ }
77
+ return { states, current: reached, message };
78
+ }
23
79
  export function registerRunCommands(program) {
24
80
  const run = program
25
81
  .command('run')
@@ -27,13 +83,13 @@ export function registerRunCommands(program) {
27
83
  run.helpInformation = () => renderCommandHelp('run', 'Engine commands — start, preview, and monitor babel runs', [
28
84
  {
29
85
  name: 'start',
30
- desc: 'Start a full Babelize engine run (triggers i18n pipeline)',
86
+ desc: `Start a full Babelize engine run (exits 0 completed, ${ExitCode.API_ERROR} failed, ${ExitCode.CANCEL} cancelled)`,
31
87
  options: [
32
88
  { name: '-p, --project <id>', desc: 'Project ID (defaults to .babelize/config.json)' },
33
89
  { name: '-l, --languages <codes>', desc: 'Comma-separated target language codes (e.g. es,fr,de,ja)' },
34
90
  { name: '--branch <branch>', desc: 'Git branch to run on' },
35
91
  { name: '--no-poll', desc: "Return immediately (don't wait for completion)" },
36
- { name: '--no-pr', desc: 'Skip auto-creating a GitHub PR after completion' },
92
+ { name: '--no-pr', desc: 'Skip auto-creating a GitHub PR after completion (never attempted for ZIP projects)' },
37
93
  ],
38
94
  },
39
95
  {
@@ -64,7 +120,7 @@ export function registerRunCommands(program) {
64
120
  console.error('Error: --project is required in non-interactive mode.');
65
121
  process.exit(ExitCode.USAGE);
66
122
  }
67
- const result = await clack.text({ message: 'Project ID:' });
123
+ const result = await clack.text({ message: 'Project ID:', flag: '--project' });
68
124
  if (clack.isCancel(result))
69
125
  process.exit(ExitCode.CANCEL);
70
126
  projectId = result;
@@ -74,49 +130,69 @@ export function registerRunCommands(program) {
74
130
  console.error('Error: --languages is required in non-interactive mode.');
75
131
  process.exit(ExitCode.USAGE);
76
132
  }
77
- const result = await clack.text({ message: 'Target languages (comma-sep, e.g. es,fr,de):' });
133
+ const result = await clack.text({ message: 'Target languages (comma-sep, e.g. es,fr,de):', flag: '--languages' });
78
134
  if (clack.isCancel(result))
79
135
  process.exit(ExitCode.CANCEL);
80
136
  languages = result;
81
137
  }
82
138
  const langs = languages.split(',').map((l) => l.trim()).filter(Boolean);
139
+ const client = getClient();
83
140
  const s = clack.spinner();
84
141
  s.start('Starting engine run…');
142
+ let babelId;
143
+ let startData;
85
144
  try {
86
- const client = getClient();
87
145
  const res = await client.post('/engine/babel', {
88
146
  project_id: projectId,
89
147
  target_locales: langs,
90
148
  ...(opts.branch ? { branch: opts.branch } : {}),
91
149
  }); // codeql[js/file-access-to-http] — intentional CLI request to own API
92
- const babelId = res.data?.babel_id ?? res.data?.run_id ?? res.data?.id;
150
+ startData = res.data;
151
+ babelId = res.data?.babel_id ?? res.data?.run_id ?? res.data?.id;
93
152
  s.stop(successColor(`Engine run started — Job ID: ${chalk.bold(babelId)}`));
94
- if (!opts.poll) {
95
- dim('Use `babelize run status ' + babelId + '` to check progress.');
96
- if (isJsonMode())
97
- printJson(res.data);
98
- return;
99
- }
100
- const lastData = await pollRunStatus(client, babelId);
101
- printCompletionBanner(lastData);
102
- if (lastData?.status === 'completed' && opts.pr !== false) {
103
- await autoRaisePR(client, projectId, langs, lastData);
104
- }
105
153
  }
106
154
  catch (err) {
107
155
  s.stop('Failed to start engine run.');
108
156
  handleApiError(err);
109
157
  }
158
+ if (!opts.poll) {
159
+ if (isJsonMode())
160
+ printJson(startData);
161
+ else
162
+ dim('Use `babelize run status ' + babelId + '` to check progress.');
163
+ return;
164
+ }
165
+ const lastData = await pollRunStatus(client, babelId);
166
+ // ZIP projects have no repository to open a PR against; their results
167
+ // are downloaded instead. The local config already knows the source of
168
+ // its own project, otherwise ask the API.
169
+ const source = localProject?.projectId === projectId && localProject?.source
170
+ ? localProject.source
171
+ : await fetchProjectSource(client, projectId);
172
+ let autoPr;
173
+ if (isJsonMode()) {
174
+ if (opts.pr !== false && source !== 'zip') {
175
+ autoPr = await autoRaisePR(client, projectId, langs, lastData, true);
176
+ }
177
+ printJson(autoPr ? { ...lastData, auto_pr: autoPr } : lastData);
178
+ return;
179
+ }
180
+ renderRunStatus(lastData, { source });
181
+ printCompletionBanner(lastData);
182
+ if (opts.pr !== false && source !== 'zip') {
183
+ await autoRaisePR(client, projectId, langs, lastData, false);
184
+ }
110
185
  });
111
186
  run
112
187
  .command('status <babelId>')
113
188
  .description('Get status of a running or completed babel job')
114
189
  .option('--watch', 'Keep polling until job completes')
115
190
  .action(async (babelId, opts) => {
191
+ requireUuid(babelId, 'babel ID');
116
192
  const client = getClient();
193
+ let data;
117
194
  if (opts.watch) {
118
- const lastData = await pollRunStatus(client, babelId);
119
- printCompletionBanner(lastData);
195
+ data = await pollRunStatus(client, babelId);
120
196
  }
121
197
  else {
122
198
  const s = clack.spinner();
@@ -124,18 +200,21 @@ export function registerRunCommands(program) {
124
200
  try {
125
201
  const res = await client.get(`/engine/status/${babelId}`);
126
202
  s.stop();
127
- const data = res.data;
128
- if (isJsonMode()) {
129
- printJson(data);
130
- return;
131
- }
132
- renderRunStatus(data);
203
+ data = res.data;
133
204
  }
134
205
  catch (err) {
135
206
  s.stop('Failed to fetch status.');
136
207
  handleApiError(err);
137
208
  }
138
209
  }
210
+ if (isJsonMode()) {
211
+ printJson(data);
212
+ return;
213
+ }
214
+ const source = await fetchProjectSource(client, runOf(data).project_id);
215
+ renderRunStatus(data, { source });
216
+ if (opts.watch)
217
+ printCompletionBanner(data);
139
218
  });
140
219
  run
141
220
  .command('analyze')
@@ -148,7 +227,7 @@ export function registerRunCommands(program) {
148
227
  console.error('Error: --project is required in non-interactive mode.');
149
228
  process.exit(ExitCode.USAGE);
150
229
  }
151
- const result = await clack.text({ message: 'Project ID:' });
230
+ const result = await clack.text({ message: 'Project ID:', flag: '--project' });
152
231
  if (clack.isCancel(result))
153
232
  process.exit(ExitCode.CANCEL);
154
233
  projectId = result;
@@ -171,88 +250,126 @@ export function registerRunCommands(program) {
171
250
  }
172
251
  });
173
252
  }
174
- async function pollRunStatus(client, babelId) {
175
- let lastData = null;
176
- const MAX_RETRIES = 60;
177
- let retryCount = 0;
178
- const pipeline = createPipeline(PHASES.map((phaseTitle) => ({
179
- title: phaseTitle,
180
- task: async (_ctx, task) => {
181
- const phaseShort = phaseTitle.split(':')[1]?.trim()?.toLowerCase() ?? '';
182
- while (true) {
183
- try {
184
- const res = await client.get(`/engine/status/${babelId}`);
185
- lastData = res.data;
186
- const data = res.data;
187
- const status = (data?.status ?? data?.run?.status);
188
- const logs = ((data?.logs ?? data?.run?.logs) ?? []);
189
- if (status === 'completed')
190
- return;
191
- const phaseLog = logs.find((l) => l.step?.toLowerCase().includes(phaseShort));
192
- if (phaseLog?.status === 'completed')
193
- return;
194
- if (phaseLog?.status === 'failed') {
195
- throw new Error(phaseLog.message || `Phase failed: ${phaseTitle}`);
196
- }
197
- if (status === 'failed') {
198
- throw new Error(data?.error ?? data?.run?.error ?? 'Pipeline failed');
199
- }
200
- if (phaseLog?.message) {
201
- task.output = phaseLog.message;
202
- }
203
- retryCount = 0;
204
- }
205
- catch (err) {
206
- const pollErr = err;
207
- if (pollErr.message?.includes('failed') || pollErr.message?.includes('Pipeline')) {
208
- throw err;
209
- }
210
- retryCount++;
211
- if (retryCount >= MAX_RETRIES) {
212
- throw new Error(`Timed out waiting for pipeline to complete after ${MAX_RETRIES} retries`, { cause: err });
213
- }
214
- task.output = `Transient error, retrying (${retryCount}/${MAX_RETRIES})…`;
215
- }
216
- await delay(2500);
217
- }
218
- },
219
- })));
220
- try {
253
+ const POLL_INTERVAL_MS = 2500;
254
+ const MAX_CONSECUTIVE_FAILURES = 60;
255
+ function runOf(data) {
256
+ return (data?.run ?? data ?? {});
257
+ }
258
+ /**
259
+ * Poll a run until it ends, showing each phase as it finishes.
260
+ *
261
+ * Returns the final status payload of a completed run. A run that ends any
262
+ * other way exits the process:
263
+ * - failed → ExitCode.API_ERROR (6), with the run's error
264
+ * - cancelled → ExitCode.CANCEL (128), the code the CLI already uses for
265
+ * anything the user cancelled, so it never looks like success
266
+ * With --json, the final payload is printed before exiting on failed or
267
+ * cancelled; a completed run's payload is left to the caller.
268
+ */
269
+ export async function pollRunStatus(client, babelId, options = {}) {
270
+ const intervalMs = options.intervalMs ?? POLL_INTERVAL_MS;
271
+ const json = isJsonMode();
272
+ const s = clack.spinner();
273
+ let announced = 0;
274
+ let failures = 0;
275
+ let spinning = false;
276
+ const startSpinner = (msg) => { s.start(msg); spinning = true; };
277
+ const stopSpinner = (msg) => { if (spinning)
278
+ s.stop(msg); spinning = false; };
279
+ if (!json)
221
280
  console.log(`\n ${renderRunTitle(babelId)}\n`);
222
- await pipeline.run();
223
- divider();
224
- if (lastData) {
225
- if (isJsonMode()) {
226
- printJson(lastData);
227
- }
228
- else {
229
- renderRunStatus(lastData);
281
+ startSpinner('Waiting for the run to start…');
282
+ while (true) {
283
+ let data;
284
+ try {
285
+ const res = await client.get(`/engine/status/${babelId}`);
286
+ data = (res.data ?? {});
287
+ failures = 0;
288
+ }
289
+ catch (err) {
290
+ const status = axios.isAxiosError(err) ? err.response?.status : undefined;
291
+ failures++;
292
+ // A 4xx (other than rate limiting) will not fix itself: report it now.
293
+ if ((status && status >= 400 && status < 500 && status !== 429) || failures >= MAX_CONSECUTIVE_FAILURES) {
294
+ stopSpinner();
295
+ handleApiError(err);
230
296
  }
297
+ s.message(`Connection problem, retrying (${failures}/${MAX_CONSECUTIVE_FAILURES})…`);
298
+ await delay(intervalMs);
299
+ continue;
231
300
  }
232
- return lastData;
233
- }
234
- catch (err) {
235
- if (isJsonMode()) {
236
- printJson({ error: err.message, babel_id: babelId, status: 'failed' });
301
+ const run = runOf(data);
302
+ const status = String(run.status ?? '');
303
+ const logs = (Array.isArray(run.logs) ? run.logs : []);
304
+ const progress = computePhaseProgress(logs, status);
305
+ // Announce each phase once, in order, as it finishes.
306
+ while (announced < RUN_PHASES.length && progress.states[announced] === 'done') {
307
+ if (!spinning)
308
+ startSpinner(RUN_PHASES[announced].title);
309
+ stopSpinner(successColor(`${sym.check} ${RUN_PHASES[announced].title}`));
310
+ announced++;
311
+ }
312
+ if (status === 'completed') {
313
+ stopSpinner();
314
+ return data;
315
+ }
316
+ if (status === 'failed') {
317
+ const failedLog = [...logs].reverse().find((l) => String(l.status) === 'error');
318
+ const reason = run.error ?? failedLog?.message ?? 'unknown error';
319
+ if (!spinning)
320
+ startSpinner(RUN_PHASES[announced]?.title ?? 'Run');
321
+ stopSpinner(errorColor(`${sym.cross} Run failed: ${reason}`));
322
+ if (json)
323
+ printJson(data);
324
+ process.exit(ExitCode.API_ERROR);
325
+ }
326
+ if (status === 'cancelled') {
327
+ if (!spinning)
328
+ startSpinner(RUN_PHASES[announced]?.title ?? 'Run');
329
+ stopSpinner(warningColor(`${sym.warn} Run cancelled. Nothing from this run was applied.`));
330
+ if (json)
331
+ printJson(data);
332
+ process.exit(ExitCode.CANCEL);
333
+ }
334
+ if (TERMINAL_STATUSES.has(status))
335
+ return data;
336
+ if (!spinning)
337
+ startSpinner(RUN_PHASES[announced]?.title ?? 'Working…');
338
+ if (progress.current >= 0) {
339
+ const title = RUN_PHASES[progress.current].title;
340
+ s.message(progress.message ? `${title}: ${progress.message}` : title);
237
341
  }
238
342
  else {
239
- console.log(errorColor(`Babel run failed: ${err.message}`));
343
+ s.message(status === 'queued' ? 'Queued, waiting for a worker…' : 'Waiting for the run to start…');
240
344
  }
241
- process.exit(ExitCode.API_ERROR);
345
+ await delay(intervalMs);
242
346
  }
243
347
  }
244
348
  function printCompletionBanner(lastData) {
245
- const status = lastData?.status ?? 'unknown';
246
- if (status === 'completed') {
349
+ if (runOf(lastData).status === 'completed') {
247
350
  console.log('');
248
351
  success('All done! Translation run completed successfully.');
249
352
  }
250
353
  }
251
- async function autoRaisePR(client, projectId, targetLocales, lastData) {
252
- const run = (lastData?.run ?? lastData);
253
- if (run?.pr_url) {
254
- success(`PR already created: ${chalk.cyan.underline(run.pr_url)}`);
255
- return;
354
+ /** The project's source (`github` | `zip`), or undefined when it cannot be read. */
355
+ async function fetchProjectSource(client, projectId) {
356
+ if (!projectId)
357
+ return undefined;
358
+ try {
359
+ const res = await client.get(`/projects/${projectId}`);
360
+ const project = (res?.data?.project ?? res?.data);
361
+ return typeof project?.source === 'string' ? project.source : undefined;
362
+ }
363
+ catch {
364
+ return undefined;
365
+ }
366
+ }
367
+ async function autoRaisePR(client, projectId, targetLocales, lastData, quiet) {
368
+ const run = runOf(lastData);
369
+ if (run.pr_url) {
370
+ if (!quiet)
371
+ success(`PR already created: ${chalk.cyan.underline(run.pr_url)}`);
372
+ return { status: 'exists', url: run.pr_url };
256
373
  }
257
374
  const s = clack.spinner();
258
375
  s.start('Auto-creating GitHub PR with translations…');
@@ -262,30 +379,45 @@ async function autoRaisePR(client, projectId, targetLocales, lastData) {
262
379
  });
263
380
  s.stop();
264
381
  const prUrl = res.data?.pr_url ?? res.data?.url;
265
- if (prUrl) {
266
- success(`PR created: ${chalk.cyan.underline(prUrl)}`);
267
- }
268
- else {
269
- success('PR created successfully. Check your GitHub repository.');
382
+ if (!quiet) {
383
+ if (prUrl)
384
+ success(`PR created: ${chalk.cyan.underline(prUrl)}`);
385
+ else
386
+ success('PR created successfully. Check your GitHub repository.');
270
387
  }
388
+ return { status: 'created', url: prUrl };
271
389
  }
272
390
  catch (err) {
273
391
  s.stop();
274
392
  const apiErr = err;
275
- if (apiErr?.response?.status === 409) {
276
- dim('PR already exists for this project.');
393
+ if (apiErr?.response?.status === 409 && apiErr.response.data?.error_code === 'pr_open') {
394
+ const message = `PR #${apiErr.response.data.pr_number ?? '?'} is still open. Run \`babelize pr update ${projectId}\` to add this run to it (or \`babelize pr raise ${projectId} --new\` for a new PR).`;
395
+ if (!quiet)
396
+ dim(message);
397
+ return { status: 'exists', message };
277
398
  }
278
- else if (apiErr?.response?.data?.requires_app_install) {
279
- warning('GitHub App not installed. Install it from the project page to enable auto-PR.');
280
- dim('Run `babelize pr raise ' + projectId + '` after installing.');
399
+ if (apiErr?.response?.status === 409) {
400
+ const message = apiErr.response.data?.error ?? 'PR already exists for this project.';
401
+ if (!quiet)
402
+ dim(message);
403
+ return { status: 'exists', message };
281
404
  }
282
- else {
283
- dim('Could not auto-create PR. Run `babelize pr raise ' + projectId + '` to try manually.');
405
+ if (apiErr?.response?.data?.requires_app_install) {
406
+ const message = 'GitHub App not installed. Install it from the project page to enable auto-PR.';
407
+ if (!quiet) {
408
+ warning(message);
409
+ dim('Run `babelize pr raise ' + projectId + '` after installing.');
410
+ }
411
+ return { status: 'failed', message };
284
412
  }
413
+ const message = 'Could not auto-create PR. Run `babelize pr raise ' + projectId + '` to try manually.';
414
+ if (!quiet)
415
+ dim(message);
416
+ return { status: 'failed', message };
285
417
  }
286
418
  }
287
- function renderRunStatus(data) {
288
- const run = (data.run ?? data);
419
+ export function renderRunStatus(data, options = {}) {
420
+ const run = runOf(data);
289
421
  keyValue({
290
422
  id: run.id,
291
423
  status: statusBadge(run.status),
@@ -320,13 +452,8 @@ function renderRunStatus(data) {
320
452
  processing_time: stats.processing_time_ms ? `${stats.processing_time_ms}ms` : null,
321
453
  }, 'Stats');
322
454
  }
323
- // Local artifacts
324
455
  const projectId = run.project_id;
325
- if (projectId) {
326
- section('Local Artifacts');
327
- dim(`Staging: .babelize/${projectId}/staging/`);
328
- }
329
- // Warnings (capped)
456
+ // Pipeline log
330
457
  const logs = run.logs;
331
458
  if (logs?.length) {
332
459
  section('Pipeline Log');
@@ -336,7 +463,7 @@ function renderRunStatus(data) {
336
463
  { key: 'message', header: 'Message', width: 60 },
337
464
  ]);
338
465
  }
339
- // Warnings from activity payload
466
+ // Warnings from activity payload (capped)
340
467
  const warnings = data.warnings;
341
468
  if (warnings?.length) {
342
469
  const DUPE_CAP = 10;
@@ -347,16 +474,21 @@ function renderRunStatus(data) {
347
474
  if (shown.length > 0) {
348
475
  section('Warnings');
349
476
  for (const w of shown) {
350
- console.log(` ${warning('⚠')} ${w.message ?? ''}`);
477
+ console.log(` ${warningColor(sym.warn)} ${w.message ?? ''}`);
351
478
  }
352
479
  if (remaining > 0) {
353
480
  dim(` … and ${remaining} more duplicate-key warnings. Use --json for full list.`);
354
481
  }
355
482
  }
356
483
  }
357
- // PR hint
484
+ // Next step: ZIP projects download their results; GitHub projects open a PR.
358
485
  if (run.status === 'completed' && !run.pr_url && projectId) {
359
- dim('Run `babelize pr raise ' + projectId + '` to open a GitHub PR with the translations.');
486
+ if (options.source === 'zip') {
487
+ dim('Run `babelize projects download ' + projectId + '` to download the translated files.');
488
+ }
489
+ else {
490
+ dim('Run `babelize pr raise ' + projectId + '` to open a GitHub PR with the translations.');
491
+ }
360
492
  }
361
493
  }
362
494
  //# sourceMappingURL=run.js.map