@vocoder/cli 0.13.4 → 0.14.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.
package/dist/bin.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  readAuthData,
14
14
  verifyStoredAuth,
15
15
  writeAuthData
16
- } from "./chunk-ZHRKJ2KZ.mjs";
16
+ } from "./chunk-LLEMSC3X.mjs";
17
17
 
18
18
  // src/bin.ts
19
19
  import { Command } from "commander";
@@ -64,311 +64,29 @@ export default defineConfig({
64
64
  }
65
65
  }
66
66
 
67
- // src/utils/github-connect.ts
68
- import { spawn } from "child_process";
69
- import * as p from "@clack/prompts";
70
- import chalk from "chalk";
71
-
72
- // src/utils/local-server.ts
73
- import { createServer } from "http";
74
- import { URL as URL2 } from "url";
75
- function startCallbackServer() {
76
- return new Promise((resolve2, reject) => {
77
- let settled = false;
78
- let callbackResolve = null;
79
- let callbackReject = null;
80
- const callbackPromise = new Promise((res, rej) => {
81
- callbackResolve = res;
82
- callbackReject = rej;
83
- });
84
- const server = createServer((req, res) => {
85
- if (!req.url) {
86
- res.writeHead(400);
87
- res.end();
88
- return;
89
- }
90
- let pathname;
91
- let params;
92
- try {
93
- const parsed = new URL2(req.url, "http://localhost");
94
- pathname = parsed.pathname;
95
- params = Object.fromEntries(parsed.searchParams.entries());
96
- } catch {
97
- res.writeHead(400);
98
- res.end("Bad request");
99
- return;
100
- }
101
- if (pathname !== "/callback") {
102
- res.writeHead(404);
103
- res.end("Not found");
104
- return;
105
- }
106
- res.writeHead(200, { "Content-Type": "text/html" });
107
- res.end(
108
- '<!DOCTYPE html><html><head><title>Authenticated</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;"><h2>Authenticated</h2><p>Return to your terminal to continue. You can close this tab.</p></body></html>'
109
- );
110
- if (callbackResolve) {
111
- callbackResolve(params);
112
- callbackResolve = null;
113
- }
114
- setImmediate(() => server.close());
115
- });
116
- server.on("error", (err) => {
117
- if (!settled) {
118
- settled = true;
119
- if (callbackReject) callbackReject(err);
120
- reject(err);
121
- }
122
- });
123
- server.listen(0, "127.0.0.1", () => {
124
- if (settled) return;
125
- settled = true;
126
- const port = server.address().port;
127
- resolve2({
128
- port,
129
- waitForCallback: () => callbackPromise,
130
- close: () => server.close()
131
- });
132
- });
133
- });
134
- }
135
-
136
- // src/utils/github-connect.ts
137
- async function tryOpenBrowser(url) {
138
- if (!process.stdout.isTTY || process.env.CI === "true") {
139
- return false;
140
- }
141
- const platform = process.platform;
142
- let command;
143
- let args;
144
- if (platform === "darwin") {
145
- command = "open";
146
- args = [url];
147
- } else if (platform === "win32") {
148
- command = "rundll32";
149
- args = ["url.dll,FileProtocolHandler", url];
150
- } else {
151
- command = "xdg-open";
152
- args = [url];
153
- }
154
- return new Promise((resolve2) => {
155
- try {
156
- const child = spawn(command, args, {
157
- detached: true,
158
- stdio: "ignore",
159
- windowsHide: true
160
- });
161
- let settled = false;
162
- child.once("spawn", () => {
163
- if (settled) return;
164
- settled = true;
165
- child.unref();
166
- resolve2(true);
167
- });
168
- child.once("error", () => {
169
- if (settled) return;
170
- settled = true;
171
- resolve2(false);
172
- });
173
- setTimeout(() => {
174
- if (settled) return;
175
- settled = true;
176
- resolve2(false);
177
- }, 300);
178
- } catch {
179
- resolve2(false);
180
- }
181
- });
182
- }
183
- async function runGitHubInstallFlow(params) {
184
- let server = null;
185
- try {
186
- server = await startCallbackServer();
187
- } catch {
188
- }
189
- const { installUrl } = await params.api.startCliGitHubInstall(
190
- params.userToken,
191
- {
192
- organizationId: params.organizationId,
193
- callbackPort: server?.port
194
- }
195
- );
196
- p.log.info("Opening GitHub to install the Vocoder App...");
197
- p.note(installUrl, "Install URL");
198
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
199
- const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
200
- if (p.isCancel(shouldOpen)) {
201
- server?.close();
202
- return null;
203
- }
204
- if (shouldOpen) {
205
- const opened = await tryOpenBrowser(installUrl);
206
- if (!opened) {
207
- p.log.info(
208
- "Could not open a browser automatically. Use the URL above."
209
- );
210
- }
211
- }
212
- }
213
- const connectSpinner = p.spinner();
214
- connectSpinner.start("Waiting for GitHub App installation...");
215
- if (server) {
216
- try {
217
- const params_timeout = 15 * 60 * 1e3;
218
- const callbackParams = await Promise.race([
219
- server.waitForCallback(),
220
- new Promise(
221
- (resolve2) => setTimeout(() => resolve2(null), params_timeout)
222
- )
223
- ]);
224
- server.close();
225
- if (!callbackParams) {
226
- connectSpinner.stop("GitHub App installation timed out");
227
- p.log.error(
228
- "The installation flow timed out. Run `vocoder init` again."
229
- );
230
- return null;
231
- }
232
- if (callbackParams.error) {
233
- connectSpinner.stop("GitHub App installation failed");
234
- p.log.error(callbackParams.error);
235
- return null;
236
- }
237
- const { organizationId, connectionLabel, workspace_created } = callbackParams;
238
- if (!organizationId || !connectionLabel) {
239
- connectSpinner.stop("GitHub App installation incomplete");
240
- p.log.error("Missing organization or connection data from callback.");
241
- return null;
242
- }
243
- connectSpinner.stop(
244
- `Connected to GitHub as ${chalk.bold(connectionLabel)}`
245
- );
246
- const orgName = workspace_created ? connectionLabel : organizationId;
247
- return {
248
- organizationId,
249
- organizationName: orgName,
250
- connectionLabel
251
- };
252
- } catch {
253
- server.close();
254
- connectSpinner.stop("GitHub App installation failed");
255
- return null;
256
- }
257
- }
258
- connectSpinner.stop("Could not detect GitHub App installation automatically");
259
- p.log.warn(
260
- "Complete the installation in your browser, then run `vocoder init` again."
261
- );
262
- return null;
263
- }
264
- async function runGitHubDiscoveryFlow(params) {
265
- let server = null;
266
- try {
267
- server = await startCallbackServer();
268
- } catch {
269
- }
270
- const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
271
- organizationId: params.organizationId,
272
- callbackPort: server?.port
273
- });
274
- p.log.info("Opening GitHub to authorize your account...");
275
- p.note("Complete authorization in your browser.");
276
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
277
- const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
278
- if (p.isCancel(shouldOpen)) {
279
- server?.close();
280
- return null;
281
- }
282
- if (shouldOpen) {
283
- const opened = await tryOpenBrowser(oauthUrl);
284
- if (!opened) {
285
- p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
286
- }
287
- }
288
- }
289
- const oauthSpinner = p.spinner();
290
- oauthSpinner.start("Waiting for GitHub authorization...");
291
- if (server) {
292
- try {
293
- const timeoutMs = 10 * 60 * 1e3;
294
- const callbackParams = await Promise.race([
295
- server.waitForCallback(),
296
- new Promise(
297
- (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
298
- )
299
- ]);
300
- server.close();
301
- if (!callbackParams) {
302
- oauthSpinner.stop("GitHub authorization timed out");
303
- return null;
304
- }
305
- if (callbackParams.error) {
306
- oauthSpinner.stop("GitHub authorization failed");
307
- p.log.error(callbackParams.error);
308
- return null;
309
- }
310
- } catch {
311
- server.close();
312
- oauthSpinner.stop("GitHub authorization failed");
313
- return null;
314
- }
315
- }
316
- oauthSpinner.stop("GitHub account authorized");
317
- const discoveryResult = await params.api.getCliGitHubDiscovery(
318
- params.userToken
319
- );
320
- return discoveryResult.installations;
321
- }
322
- async function selectGitHubInstallation(installations, canInstallNew) {
323
- const options = installations.map((inst) => ({
324
- value: String(inst.installationId),
325
- label: inst.accountLogin,
326
- hint: [
327
- inst.accountType === "Organization" ? "organization" : "personal",
328
- inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
329
- inst.isSuspended ? "suspended" : ""
330
- ].filter(Boolean).join(" \xB7 ") || void 0
331
- }));
332
- if (canInstallNew) {
333
- options.push({
334
- value: "install_new",
335
- label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
336
- });
337
- }
338
- const selected = await p.select({
339
- message: "Select a GitHub installation",
340
- options
341
- });
342
- if (p.isCancel(selected)) return null;
343
- if (selected === "install_new") return "install_new";
344
- return Number(selected);
345
- }
346
-
347
- // src/utils/project-create.ts
348
- import * as p3 from "@clack/prompts";
349
- import chalk3 from "chalk";
350
-
351
- // src/utils/branch-select.ts
352
- import { execSync } from "child_process";
353
- import { isCancel as isCancel2, Prompt } from "@clack/core";
354
-
355
67
  // src/utils/theme.ts
356
- import chalk2 from "chalk";
68
+ import chalk from "chalk";
357
69
  var ORANGE = "#FC5206";
358
70
  var PINK = "#D51977";
359
71
  var BLUE = "#2450A9";
360
72
  var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
361
- var hex = (color) => (s) => noColor ? s : chalk2.hex(color)(s);
362
- var dim = (s) => noColor ? s : chalk2.dim(s);
363
- var bld = (s) => noColor ? s : chalk2.bold(s);
364
- var grn = (s) => noColor ? s : chalk2.green(s);
365
- var ylw = (s) => noColor ? s : chalk2.yellow(s);
366
- var red = (s) => noColor ? s : chalk2.red(s);
73
+ var hex = (color) => (s) => noColor ? s : chalk.hex(color)(s);
74
+ var dim = (s) => noColor ? s : chalk.dim(s);
75
+ var bld = (s) => noColor ? s : chalk.bold(s);
76
+ var grn = (s) => noColor ? s : chalk.green(s);
77
+ var ylw = (s) => noColor ? s : chalk.yellow(s);
78
+ var red = (s) => noColor ? s : chalk.red(s);
367
79
  var highlight = hex(PINK);
368
80
  var info = hex(BLUE);
369
81
  var active = hex(ORANGE);
370
82
 
83
+ // src/utils/project-create.ts
84
+ import * as p2 from "@clack/prompts";
85
+ import chalk2 from "chalk";
86
+
371
87
  // src/utils/branch-select.ts
88
+ import { execSync } from "child_process";
89
+ import { isCancel, Prompt } from "@clack/core";
372
90
  var S_BAR = "\u2502";
373
91
  var S_BAR_END = "\u2514";
374
92
  var S_ACTIVE = "\u25C6";
@@ -630,13 +348,13 @@ ${symbol(this.state)} ${message}
630
348
  }
631
349
  });
632
350
  const result = await prompt.prompt();
633
- if (isCancel2(result)) return null;
351
+ if (isCancel(result)) return null;
634
352
  return result;
635
353
  }
636
354
 
637
355
  // src/utils/locale-search.ts
638
- import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
639
- import * as p2 from "@clack/prompts";
356
+ import { isCancel as isCancel2, Prompt as Prompt2 } from "@clack/core";
357
+ import * as p from "@clack/prompts";
640
358
  var S_BAR2 = "\u2502";
641
359
  var S_BAR_END2 = "\u2514";
642
360
  var S_ACTIVE2 = "\u25C6";
@@ -811,7 +529,7 @@ ${symbol2(this.state)} ${message}
811
529
  }
812
530
  });
813
531
  const result = await prompt.prompt();
814
- if (isCancel3(result)) return null;
532
+ if (isCancel2(result)) return null;
815
533
  return result;
816
534
  }
817
535
  async function searchSelectLocale(options, message, initialValue) {
@@ -833,7 +551,7 @@ async function searchMultiSelectLocales(options, message, initialValues) {
833
551
  if (result === null) return null;
834
552
  const picks = result;
835
553
  if (picks.length === 0) {
836
- p2.log.warn(
554
+ p.log.warn(
837
555
  "At least one target language is required. Please select at least one."
838
556
  );
839
557
  return searchMultiSelectLocales(options, message, initialValues);
@@ -863,12 +581,12 @@ function buildLanguageOptions(locales) {
863
581
  async function runProjectCreate(params) {
864
582
  const { api, userToken, organizationId, repoCanonical } = params;
865
583
  const projectName = (params.defaultName ?? "my-project").trim();
866
- p3.log.success(`Project: ${chalk3.bold(projectName)}`);
584
+ p2.log.success(`Project: ${chalk2.bold(projectName)}`);
867
585
  let sourceLocales;
868
586
  try {
869
587
  ({ sourceLocales } = await api.listLocales(userToken));
870
588
  } catch {
871
- p3.log.error(
589
+ p2.log.error(
872
590
  "Failed to fetch supported locales. Check your connection and try again."
873
591
  );
874
592
  return null;
@@ -876,7 +594,7 @@ async function runProjectCreate(params) {
876
594
  const languageOptions = buildLanguageOptions(sourceLocales);
877
595
  const appDir = params.defaultAppDir ?? "";
878
596
  if (appDir) {
879
- p3.log.success(`App directory: ${chalk3.bold(appDir)}`);
597
+ p2.log.success(`App directory: ${chalk2.bold(appDir)}`);
880
598
  }
881
599
  const sourceLocale = await searchSelectLocale(
882
600
  languageOptions,
@@ -888,7 +606,7 @@ async function runProjectCreate(params) {
888
606
  try {
889
607
  compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
890
608
  } catch {
891
- p3.log.error(
609
+ p2.log.error(
892
610
  "Failed to fetch compatible target locales. Check your connection and try again."
893
611
  );
894
612
  return null;
@@ -903,7 +621,7 @@ async function runProjectCreate(params) {
903
621
  );
904
622
  if (targetLocales === null) return null;
905
623
  if (targetLocales.length === 0) {
906
- p3.log.warn(
624
+ p2.log.warn(
907
625
  "No target languages selected \u2014 you can add them later from the dashboard."
908
626
  );
909
627
  }
@@ -921,7 +639,7 @@ async function runProjectCreate(params) {
921
639
  });
922
640
  if (result === null) return null;
923
641
  if (result.length === 0) {
924
- p3.log.warn(
642
+ p2.log.warn(
925
643
  "At least one branch is required. Please select at least one."
926
644
  );
927
645
  initial = [detected.defaultBranch];
@@ -941,110 +659,390 @@ async function runProjectCreate(params) {
941
659
  appDirs: appDir ? [appDir] : [],
942
660
  repoCanonical
943
661
  });
944
- p3.log.success(`Project ${chalk3.bold(result.projectName)} created!`);
662
+ p2.log.success(`Project ${chalk2.bold(result.projectName)} created!`);
945
663
  return result;
946
664
  } catch (error) {
947
665
  const message = error instanceof Error ? error.message : "Unknown error";
948
- p3.log.error(`Failed to create project: ${message}`);
666
+ p2.log.error(`Failed to create project: ${message}`);
667
+ return null;
668
+ }
669
+ }
670
+ async function runAppCreate(params) {
671
+ const { api, userToken, projectId, projectName, repoCanonical } = params;
672
+ const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
673
+ let sourceLocales;
674
+ try {
675
+ ({ sourceLocales } = await api.listLocales(userToken));
676
+ } catch {
677
+ p2.log.error(
678
+ "Failed to fetch supported locales. Check your connection and try again."
679
+ );
949
680
  return null;
950
681
  }
682
+ const languageOptions = buildLanguageOptions(sourceLocales);
683
+ const appDir = params.defaultAppDir ?? "";
684
+ if (existingScopes.has(appDir)) {
685
+ p2.log.error(`App directory "${appDir}" is already configured for this project.`);
686
+ return null;
687
+ }
688
+ if (appDir) {
689
+ p2.log.success(`App directory: ${chalk2.bold(appDir)}`);
690
+ }
691
+ const sourceLocale = await searchSelectLocale(
692
+ languageOptions,
693
+ "Source language",
694
+ "en"
695
+ );
696
+ if (sourceLocale === null) return null;
697
+ let compatibleTargets;
698
+ try {
699
+ compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
700
+ } catch {
701
+ p2.log.error(
702
+ "Failed to fetch compatible target locales. Check your connection and try again."
703
+ );
704
+ return null;
705
+ }
706
+ const targetOptions = buildLocaleOptions(compatibleTargets).filter(
707
+ (opt) => opt.bcp47 !== sourceLocale
708
+ );
709
+ const targetLocales = await searchMultiSelectLocales(
710
+ targetOptions,
711
+ "Target languages"
712
+ );
713
+ if (targetLocales === null) return null;
714
+ if (targetLocales.length === 0) {
715
+ p2.log.warn(
716
+ "No target languages selected \u2014 you can add them later from the dashboard."
717
+ );
718
+ }
719
+ const detectedApp = detectGitBranches();
720
+ let appPushBranches = [];
721
+ {
722
+ let initial = [detectedApp.defaultBranch];
723
+ while (appPushBranches.length === 0) {
724
+ const result = await filterableBranchSelect({
725
+ message: "Which branches should trigger translations?",
726
+ branches: detectedApp.branches,
727
+ defaultBranch: detectedApp.defaultBranch,
728
+ initialValues: initial
729
+ });
730
+ if (result === null) return null;
731
+ if (result.length === 0) {
732
+ p2.log.warn("At least one branch is required.");
733
+ initial = [detectedApp.defaultBranch];
734
+ } else {
735
+ appPushBranches = result;
736
+ }
737
+ }
738
+ }
739
+ const targetBranches = appPushBranches;
740
+ try {
741
+ const result = await api.createProject(userToken, {
742
+ projectId,
743
+ appDir,
744
+ sourceLocale,
745
+ targetLocales,
746
+ targetBranches,
747
+ repoCanonical: repoCanonical ?? ""
748
+ });
749
+ p2.log.success(
750
+ `App ${chalk2.bold(appDir)} added to ${chalk2.bold(projectName)}!`
751
+ );
752
+ return {
753
+ projectId: result.projectId,
754
+ projectName: result.projectName,
755
+ apiKey: result.apiKey,
756
+ appDir: result.appDir,
757
+ sourceLocale,
758
+ targetLocales,
759
+ targetBranches
760
+ };
761
+ } catch (error) {
762
+ const message = error instanceof Error ? error.message : "Unknown error";
763
+ p2.log.error(`Failed to add app: ${message}`);
764
+ return null;
765
+ }
766
+ }
767
+
768
+ // src/utils/github-connect.ts
769
+ import { spawn } from "child_process";
770
+ import * as p3 from "@clack/prompts";
771
+ import chalk3 from "chalk";
772
+
773
+ // src/utils/local-server.ts
774
+ import { createServer } from "http";
775
+ import { URL as URL2 } from "url";
776
+ function startCallbackServer() {
777
+ return new Promise((resolve2, reject) => {
778
+ let settled = false;
779
+ let callbackResolve = null;
780
+ let callbackReject = null;
781
+ const callbackPromise = new Promise((res, rej) => {
782
+ callbackResolve = res;
783
+ callbackReject = rej;
784
+ });
785
+ const server = createServer((req, res) => {
786
+ if (!req.url) {
787
+ res.writeHead(400);
788
+ res.end();
789
+ return;
790
+ }
791
+ let pathname;
792
+ let params;
793
+ try {
794
+ const parsed = new URL2(req.url, "http://localhost");
795
+ pathname = parsed.pathname;
796
+ params = Object.fromEntries(parsed.searchParams.entries());
797
+ } catch {
798
+ res.writeHead(400);
799
+ res.end("Bad request");
800
+ return;
801
+ }
802
+ if (pathname !== "/callback") {
803
+ res.writeHead(404);
804
+ res.end("Not found");
805
+ return;
806
+ }
807
+ res.writeHead(200, { "Content-Type": "text/html" });
808
+ res.end(
809
+ '<!DOCTYPE html><html><head><title>Authenticated</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;"><h2>Authenticated</h2><p>Return to your terminal to continue. You can close this tab.</p></body></html>'
810
+ );
811
+ if (callbackResolve) {
812
+ callbackResolve(params);
813
+ callbackResolve = null;
814
+ }
815
+ setImmediate(() => server.close());
816
+ });
817
+ server.on("error", (err) => {
818
+ if (!settled) {
819
+ settled = true;
820
+ if (callbackReject) callbackReject(err);
821
+ reject(err);
822
+ }
823
+ });
824
+ server.listen(0, "127.0.0.1", () => {
825
+ if (settled) return;
826
+ settled = true;
827
+ const port = server.address().port;
828
+ resolve2({
829
+ port,
830
+ waitForCallback: () => callbackPromise,
831
+ close: () => server.close()
832
+ });
833
+ });
834
+ });
835
+ }
836
+
837
+ // src/utils/github-connect.ts
838
+ async function tryOpenBrowser(url) {
839
+ if (!process.stdout.isTTY || process.env.CI === "true") {
840
+ return false;
841
+ }
842
+ const platform = process.platform;
843
+ let command;
844
+ let args;
845
+ if (platform === "darwin") {
846
+ command = "open";
847
+ args = [url];
848
+ } else if (platform === "win32") {
849
+ command = "rundll32";
850
+ args = ["url.dll,FileProtocolHandler", url];
851
+ } else {
852
+ command = "xdg-open";
853
+ args = [url];
854
+ }
855
+ return new Promise((resolve2) => {
856
+ try {
857
+ const child = spawn(command, args, {
858
+ detached: true,
859
+ stdio: "ignore",
860
+ windowsHide: true
861
+ });
862
+ let settled = false;
863
+ child.once("spawn", () => {
864
+ if (settled) return;
865
+ settled = true;
866
+ child.unref();
867
+ resolve2(true);
868
+ });
869
+ child.once("error", () => {
870
+ if (settled) return;
871
+ settled = true;
872
+ resolve2(false);
873
+ });
874
+ setTimeout(() => {
875
+ if (settled) return;
876
+ settled = true;
877
+ resolve2(false);
878
+ }, 300);
879
+ } catch {
880
+ resolve2(false);
881
+ }
882
+ });
951
883
  }
952
- async function runAppCreate(params) {
953
- const { api, userToken, projectId, projectName, repoCanonical } = params;
954
- const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
955
- let sourceLocales;
884
+ async function runGitHubInstallFlow(params) {
885
+ let server = null;
956
886
  try {
957
- ({ sourceLocales } = await api.listLocales(userToken));
887
+ server = await startCallbackServer();
958
888
  } catch {
959
- p3.log.error(
960
- "Failed to fetch supported locales. Check your connection and try again."
961
- );
962
- return null;
963
889
  }
964
- const languageOptions = buildLanguageOptions(sourceLocales);
965
- const appDir = params.defaultAppDir ?? "";
966
- if (existingScopes.has(appDir)) {
967
- p3.log.error(`App directory "${appDir}" is already configured for this project.`);
968
- return null;
890
+ const { installUrl } = await params.api.startCliGitHubInstall(
891
+ params.userToken,
892
+ {
893
+ organizationId: params.organizationId,
894
+ callbackPort: server?.port
895
+ }
896
+ );
897
+ p3.log.info("Opening GitHub to install the Vocoder App...");
898
+ p3.note(installUrl, "Install URL");
899
+ if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
900
+ const shouldOpen = params.yes ? true : await p3.confirm({ message: "Open in your browser?" });
901
+ if (p3.isCancel(shouldOpen)) {
902
+ server?.close();
903
+ return null;
904
+ }
905
+ if (shouldOpen) {
906
+ const opened = await tryOpenBrowser(installUrl);
907
+ if (!opened) {
908
+ p3.log.info(
909
+ "Could not open a browser automatically. Use the URL above."
910
+ );
911
+ }
912
+ }
969
913
  }
970
- if (appDir) {
971
- p3.log.success(`App directory: ${chalk3.bold(appDir)}`);
914
+ const connectSpinner = p3.spinner();
915
+ connectSpinner.start("Waiting for GitHub App installation...");
916
+ if (server) {
917
+ try {
918
+ const params_timeout = 15 * 60 * 1e3;
919
+ const callbackParams = await Promise.race([
920
+ server.waitForCallback(),
921
+ new Promise(
922
+ (resolve2) => setTimeout(() => resolve2(null), params_timeout)
923
+ )
924
+ ]);
925
+ server.close();
926
+ if (!callbackParams) {
927
+ connectSpinner.stop("GitHub App installation timed out");
928
+ p3.log.error(
929
+ "The installation flow timed out. Run `vocoder init` again."
930
+ );
931
+ return null;
932
+ }
933
+ if (callbackParams.error) {
934
+ connectSpinner.stop("GitHub App installation failed");
935
+ p3.log.error(callbackParams.error);
936
+ return null;
937
+ }
938
+ const { organizationId, connectionLabel, workspace_created } = callbackParams;
939
+ if (!organizationId || !connectionLabel) {
940
+ connectSpinner.stop("GitHub App installation incomplete");
941
+ p3.log.error("Missing organization or connection data from callback.");
942
+ return null;
943
+ }
944
+ connectSpinner.stop(
945
+ `Connected to GitHub as ${chalk3.bold(connectionLabel)}`
946
+ );
947
+ const orgName = workspace_created ? connectionLabel : organizationId;
948
+ return {
949
+ organizationId,
950
+ organizationName: orgName,
951
+ connectionLabel
952
+ };
953
+ } catch {
954
+ server.close();
955
+ connectSpinner.stop("GitHub App installation failed");
956
+ return null;
957
+ }
972
958
  }
973
- const sourceLocale = await searchSelectLocale(
974
- languageOptions,
975
- "Source language",
976
- "en"
959
+ connectSpinner.stop("Could not detect GitHub App installation automatically");
960
+ p3.log.warn(
961
+ "Complete the installation in your browser, then run `vocoder init` again."
977
962
  );
978
- if (sourceLocale === null) return null;
979
- let compatibleTargets;
963
+ return null;
964
+ }
965
+ async function runGitHubDiscoveryFlow(params) {
966
+ let server = null;
980
967
  try {
981
- compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
968
+ server = await startCallbackServer();
982
969
  } catch {
983
- p3.log.error(
984
- "Failed to fetch compatible target locales. Check your connection and try again."
985
- );
986
- return null;
987
970
  }
988
- const targetOptions = buildLocaleOptions(compatibleTargets).filter(
989
- (opt) => opt.bcp47 !== sourceLocale
990
- );
991
- const targetLocales = await searchMultiSelectLocales(
992
- targetOptions,
993
- "Target languages"
994
- );
995
- if (targetLocales === null) return null;
996
- if (targetLocales.length === 0) {
997
- p3.log.warn(
998
- "No target languages selected \u2014 you can add them later from the dashboard."
999
- );
971
+ const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
972
+ organizationId: params.organizationId,
973
+ callbackPort: server?.port
974
+ });
975
+ p3.log.info("Opening GitHub to authorize your account...");
976
+ p3.note("Complete authorization in your browser.");
977
+ if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
978
+ const shouldOpen = params.yes ? true : await p3.confirm({ message: "Open in your browser?" });
979
+ if (p3.isCancel(shouldOpen)) {
980
+ server?.close();
981
+ return null;
982
+ }
983
+ if (shouldOpen) {
984
+ const opened = await tryOpenBrowser(oauthUrl);
985
+ if (!opened) {
986
+ p3.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
987
+ }
988
+ }
1000
989
  }
1001
- const detectedApp = detectGitBranches();
1002
- let appPushBranches = [];
1003
- {
1004
- let initial = [detectedApp.defaultBranch];
1005
- while (appPushBranches.length === 0) {
1006
- const result = await filterableBranchSelect({
1007
- message: "Which branches should trigger translations?",
1008
- branches: detectedApp.branches,
1009
- defaultBranch: detectedApp.defaultBranch,
1010
- initialValues: initial
1011
- });
1012
- if (result === null) return null;
1013
- if (result.length === 0) {
1014
- p3.log.warn("At least one branch is required.");
1015
- initial = [detectedApp.defaultBranch];
1016
- } else {
1017
- appPushBranches = result;
990
+ const oauthSpinner = p3.spinner();
991
+ oauthSpinner.start("Waiting for GitHub authorization...");
992
+ if (server) {
993
+ try {
994
+ const timeoutMs = 10 * 60 * 1e3;
995
+ const callbackParams = await Promise.race([
996
+ server.waitForCallback(),
997
+ new Promise(
998
+ (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
999
+ )
1000
+ ]);
1001
+ server.close();
1002
+ if (!callbackParams) {
1003
+ oauthSpinner.stop("GitHub authorization timed out");
1004
+ return null;
1005
+ }
1006
+ if (callbackParams.error) {
1007
+ oauthSpinner.stop("GitHub authorization failed");
1008
+ p3.log.error(callbackParams.error);
1009
+ return null;
1018
1010
  }
1011
+ } catch {
1012
+ server.close();
1013
+ oauthSpinner.stop("GitHub authorization failed");
1014
+ return null;
1019
1015
  }
1020
1016
  }
1021
- const targetBranches = appPushBranches;
1022
- try {
1023
- const result = await api.createProject(userToken, {
1024
- projectId,
1025
- appDir,
1026
- sourceLocale,
1027
- targetLocales,
1028
- targetBranches,
1029
- repoCanonical: repoCanonical ?? ""
1017
+ oauthSpinner.stop("GitHub account authorized");
1018
+ const discoveryResult = await params.api.getCliGitHubDiscovery(
1019
+ params.userToken
1020
+ );
1021
+ return discoveryResult.installations;
1022
+ }
1023
+ async function selectGitHubInstallation(installations, canInstallNew) {
1024
+ const options = installations.map((inst) => ({
1025
+ value: String(inst.installationId),
1026
+ label: inst.accountLogin,
1027
+ hint: [
1028
+ inst.accountType === "Organization" ? "organization" : "personal",
1029
+ inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
1030
+ inst.isSuspended ? "suspended" : ""
1031
+ ].filter(Boolean).join(" \xB7 ") || void 0
1032
+ }));
1033
+ if (canInstallNew) {
1034
+ options.push({
1035
+ value: "install_new",
1036
+ label: `Install on a new account ${chalk3.dim("(creates a new personal workspace)")}`
1030
1037
  });
1031
- p3.log.success(
1032
- `App ${chalk3.bold(appDir)} added to ${chalk3.bold(projectName)}!`
1033
- );
1034
- return {
1035
- projectId: result.projectId,
1036
- projectName: result.projectName,
1037
- apiKey: result.apiKey,
1038
- appDir: result.appDir,
1039
- sourceLocale,
1040
- targetLocales,
1041
- targetBranches
1042
- };
1043
- } catch (error) {
1044
- const message = error instanceof Error ? error.message : "Unknown error";
1045
- p3.log.error(`Failed to add app: ${message}`);
1046
- return null;
1047
1038
  }
1039
+ const selected = await p3.select({
1040
+ message: "Select a GitHub installation",
1041
+ options
1042
+ });
1043
+ if (p3.isCancel(selected)) return null;
1044
+ if (selected === "install_new") return "install_new";
1045
+ return Number(selected);
1048
1046
  }
1049
1047
 
1050
1048
  // src/commands/init.ts
@@ -1418,6 +1416,7 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1418
1416
  const session = await api.startCliAuthSession(server?.port, repoCanonical);
1419
1417
  const browserUrl = reauth ? session.verificationUrl : session.installUrl ?? session.verificationUrl;
1420
1418
  const expiresAt = new Date(session.expiresAt).getTime();
1419
+ p5.log.info(browserUrl);
1421
1420
  if (options.ci) {
1422
1421
  process.stdout.write(`VOCODER_AUTH_URL: ${browserUrl}
1423
1422
  `);
@@ -1494,46 +1493,71 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1494
1493
  let rawToken = null;
1495
1494
  let callbackOrganizationId;
1496
1495
  let callbackDiscoveryReady = false;
1497
- if (server) {
1498
- try {
1499
- const deadline = Math.min(expiresAt, Date.now() + 10 * 60 * 1e3);
1500
- const timeoutMs = deadline - Date.now();
1501
- const params = await Promise.race([
1502
- server.waitForCallback(),
1503
- new Promise(
1504
- (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
1505
- )
1506
- ]);
1507
- if (params && typeof params.token === "string") {
1508
- rawToken = params.token;
1509
- if (typeof params.organizationId === "string" && params.organizationId) {
1510
- callbackOrganizationId = params.organizationId;
1511
- }
1512
- if (params.discovery_ready === "1") {
1513
- callbackDiscoveryReady = true;
1496
+ const deadline = Math.min(expiresAt, Date.now() + 10 * 60 * 1e3);
1497
+ let stopPolling = false;
1498
+ const serverCallback = server ? server.waitForCallback().catch(() => null) : Promise.resolve(null);
1499
+ const sessionPoll = (async () => {
1500
+ while (!stopPolling && Date.now() < expiresAt) {
1501
+ try {
1502
+ const result = await api.pollCliAuthSession(session.sessionId);
1503
+ if (result.status === "complete" || result.status === "failed") {
1504
+ return result;
1514
1505
  }
1506
+ } catch {
1515
1507
  }
1516
- } catch {
1517
- } finally {
1518
- server.close();
1508
+ if (!stopPolling) await sleep(2e3);
1519
1509
  }
1520
- }
1521
- if (!rawToken) {
1522
- while (Date.now() < expiresAt) {
1523
- const result = await api.pollCliAuthSession(session.sessionId);
1524
- if (result.status === "complete") {
1525
- rawToken = result.token;
1526
- if (result.organizationId) {
1527
- callbackOrganizationId = result.organizationId;
1510
+ return null;
1511
+ })();
1512
+ const winner = await new Promise((resolve2) => {
1513
+ let done = false;
1514
+ serverCallback.then((params) => {
1515
+ if (done || params === null || typeof params.token !== "string") return;
1516
+ done = true;
1517
+ resolve2({ kind: "server", params });
1518
+ }).catch(() => {
1519
+ });
1520
+ sessionPoll.then((result) => {
1521
+ if (done || result === null) return;
1522
+ if (result.status === "complete" || result.status === "failed") {
1523
+ done = true;
1524
+ resolve2({
1525
+ kind: "poll",
1526
+ result
1527
+ });
1528
+ }
1529
+ }).catch(() => {
1530
+ });
1531
+ setTimeout(
1532
+ () => {
1533
+ if (!done) {
1534
+ done = true;
1535
+ resolve2(null);
1528
1536
  }
1529
- break;
1537
+ },
1538
+ Math.max(0, deadline - Date.now())
1539
+ );
1540
+ });
1541
+ stopPolling = true;
1542
+ server?.close();
1543
+ if (winner !== null) {
1544
+ if (winner.kind === "server") {
1545
+ rawToken = winner.params.token;
1546
+ if (typeof winner.params.organizationId === "string" && winner.params.organizationId) {
1547
+ callbackOrganizationId = winner.params.organizationId;
1530
1548
  }
1531
- if (result.status === "failed") {
1532
- authSpinner.stop();
1533
- p5.log.error(result.reason);
1534
- return null;
1549
+ if (winner.params.discovery_ready === "1") {
1550
+ callbackDiscoveryReady = true;
1535
1551
  }
1536
- await sleep(2e3);
1552
+ } else if (winner.result.status === "complete") {
1553
+ rawToken = winner.result.token;
1554
+ if (winner.result.organizationId) {
1555
+ callbackOrganizationId = winner.result.organizationId;
1556
+ }
1557
+ } else {
1558
+ authSpinner.stop();
1559
+ p5.log.error(winner.result.reason);
1560
+ return null;
1537
1561
  }
1538
1562
  }
1539
1563
  if (!rawToken) {
@@ -2146,6 +2170,9 @@ function matchBranchPattern(branch, pattern) {
2146
2170
  import * as p6 from "@clack/prompts";
2147
2171
  import { config as loadEnv2 } from "dotenv";
2148
2172
  loadEnv2();
2173
+ function extractShortCodeFromApiKey(apiKey) {
2174
+ return apiKey.slice(4, 14);
2175
+ }
2149
2176
  function validateLocalConfig(config) {
2150
2177
  if (!config.apiKey || config.apiKey.length === 0) {
2151
2178
  throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
@@ -2657,7 +2684,7 @@ async function sync(options = {}) {
2657
2684
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
2658
2685
  );
2659
2686
  }
2660
- const fingerprint = computeFingerprint(config.shortCode, sourceStrings);
2687
+ const fingerprint = computeFingerprint(extractShortCodeFromApiKey(localConfig.apiKey), sourceStrings);
2661
2688
  if (!options.force) {
2662
2689
  const cacheFile = getCacheFilePath(projectRoot, fingerprint);
2663
2690
  if (existsSync3(cacheFile)) {