@vocoder/cli 0.1.17 → 0.1.18

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
@@ -5,319 +5,16 @@ import {
5
5
  detectLocalEcosystem,
6
6
  getPackagesToInstall,
7
7
  getSetupSnippets
8
- } from "./chunk-3QBORM6T.mjs";
8
+ } from "./chunk-OFQLREXF.mjs";
9
9
 
10
10
  // src/bin.ts
11
11
  import { Command } from "commander";
12
12
 
13
13
  // src/commands/init.ts
14
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
14
15
  import * as p5 from "@clack/prompts";
15
-
16
- // src/utils/auth-store.ts
17
- import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
18
- import { homedir } from "os";
19
- import { dirname, join } from "path";
20
- function getAuthFilePath() {
21
- return join(homedir(), ".config", "vocoder", "auth.json");
22
- }
23
- function readAuthData() {
24
- const filePath = getAuthFilePath();
25
- try {
26
- const raw = readFileSync(filePath, "utf8");
27
- const parsed = JSON.parse(raw);
28
- if (!parsed || typeof parsed !== "object") return null;
29
- const data = parsed;
30
- if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
31
- return null;
32
- }
33
- return {
34
- token: data.token,
35
- apiUrl: data.apiUrl,
36
- userId: data.userId,
37
- email: data.email,
38
- name: typeof data.name === "string" ? data.name : null,
39
- createdAt: data.createdAt
40
- };
41
- } catch {
42
- return null;
43
- }
44
- }
45
- function writeAuthData(data) {
46
- const filePath = getAuthFilePath();
47
- const dir = dirname(filePath);
48
- mkdirSync(dir, { recursive: true, mode: 448 });
49
- writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
50
- }
51
- function clearAuthData() {
52
- const filePath = getAuthFilePath();
53
- try {
54
- unlinkSync(filePath);
55
- } catch {
56
- }
57
- }
58
-
59
- // src/utils/github-connect.ts
60
- import * as p from "@clack/prompts";
61
- import chalk from "chalk";
62
- import { spawn } from "child_process";
63
-
64
- // src/utils/local-server.ts
65
- import { createServer } from "http";
66
- import { URL as URL2 } from "url";
67
- function startCallbackServer() {
68
- return new Promise((resolve2, reject) => {
69
- let settled = false;
70
- let callbackResolve = null;
71
- let callbackReject = null;
72
- const callbackPromise = new Promise((res, rej) => {
73
- callbackResolve = res;
74
- callbackReject = rej;
75
- });
76
- const server = createServer((req, res) => {
77
- if (!req.url) {
78
- res.writeHead(400);
79
- res.end();
80
- return;
81
- }
82
- let pathname;
83
- let params;
84
- try {
85
- const parsed = new URL2(req.url, "http://localhost");
86
- pathname = parsed.pathname;
87
- params = Object.fromEntries(parsed.searchParams.entries());
88
- } catch {
89
- res.writeHead(400);
90
- res.end("Bad request");
91
- return;
92
- }
93
- if (pathname !== "/callback") {
94
- res.writeHead(404);
95
- res.end("Not found");
96
- return;
97
- }
98
- res.writeHead(200, { "Content-Type": "text/html" });
99
- res.end(
100
- '<!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>'
101
- );
102
- if (callbackResolve) {
103
- callbackResolve(params);
104
- callbackResolve = null;
105
- }
106
- setImmediate(() => server.close());
107
- });
108
- server.on("error", (err) => {
109
- if (!settled) {
110
- settled = true;
111
- if (callbackReject) callbackReject(err);
112
- reject(err);
113
- }
114
- });
115
- server.listen(0, "127.0.0.1", () => {
116
- if (settled) return;
117
- settled = true;
118
- const port = server.address().port;
119
- resolve2({
120
- port,
121
- waitForCallback: () => callbackPromise,
122
- close: () => server.close()
123
- });
124
- });
125
- });
126
- }
127
-
128
- // src/utils/github-connect.ts
129
- async function tryOpenBrowser(url) {
130
- if (!process.stdout.isTTY || process.env.CI === "true") {
131
- return false;
132
- }
133
- const platform = process.platform;
134
- let command;
135
- let args;
136
- if (platform === "darwin") {
137
- command = "open";
138
- args = [url];
139
- } else if (platform === "win32") {
140
- command = "rundll32";
141
- args = ["url.dll,FileProtocolHandler", url];
142
- } else {
143
- command = "xdg-open";
144
- args = [url];
145
- }
146
- return new Promise((resolve2) => {
147
- try {
148
- const child = spawn(command, args, {
149
- detached: true,
150
- stdio: "ignore",
151
- windowsHide: true
152
- });
153
- let settled = false;
154
- child.once("spawn", () => {
155
- if (settled) return;
156
- settled = true;
157
- child.unref();
158
- resolve2(true);
159
- });
160
- child.once("error", () => {
161
- if (settled) return;
162
- settled = true;
163
- resolve2(false);
164
- });
165
- setTimeout(() => {
166
- if (settled) return;
167
- settled = true;
168
- resolve2(false);
169
- }, 300);
170
- } catch {
171
- resolve2(false);
172
- }
173
- });
174
- }
175
- async function runGitHubInstallFlow(params) {
176
- let server = null;
177
- try {
178
- server = await startCallbackServer();
179
- } catch {
180
- }
181
- const { installUrl } = await params.api.startCliGitHubInstall(params.userToken, {
182
- organizationId: params.organizationId,
183
- callbackPort: server?.port
184
- });
185
- p.log.info("Opening GitHub to install the Vocoder App...");
186
- p.note(installUrl, "Install URL");
187
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
188
- const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
189
- if (p.isCancel(shouldOpen)) {
190
- server?.close();
191
- return null;
192
- }
193
- if (shouldOpen) {
194
- const opened = await tryOpenBrowser(installUrl);
195
- if (!opened) {
196
- p.log.info("Could not open a browser automatically. Use the URL above.");
197
- }
198
- }
199
- }
200
- const connectSpinner = p.spinner();
201
- connectSpinner.start("Waiting for GitHub App installation...");
202
- if (server) {
203
- try {
204
- const params_timeout = 15 * 60 * 1e3;
205
- const callbackParams = await Promise.race([
206
- server.waitForCallback(),
207
- new Promise((resolve2) => setTimeout(() => resolve2(null), params_timeout))
208
- ]);
209
- server.close();
210
- if (!callbackParams) {
211
- connectSpinner.stop("GitHub App installation timed out");
212
- p.log.error("The installation flow timed out. Run `vocoder init` again.");
213
- return null;
214
- }
215
- if (callbackParams.error) {
216
- connectSpinner.stop("GitHub App installation failed");
217
- p.log.error(callbackParams.error);
218
- return null;
219
- }
220
- const { organizationId, connectionLabel, workspace_created } = callbackParams;
221
- if (!organizationId || !connectionLabel) {
222
- connectSpinner.stop("GitHub App installation incomplete");
223
- p.log.error("Missing organization or connection data from callback.");
224
- return null;
225
- }
226
- connectSpinner.stop(`Connected to GitHub as ${chalk.bold(connectionLabel)}`);
227
- const orgName = workspace_created ? connectionLabel : organizationId;
228
- return {
229
- organizationId,
230
- organizationName: orgName,
231
- connectionLabel
232
- };
233
- } catch {
234
- server.close();
235
- connectSpinner.stop("GitHub App installation failed");
236
- return null;
237
- }
238
- }
239
- connectSpinner.stop("Could not detect GitHub App installation automatically");
240
- p.log.warn("Complete the installation in your browser, then run `vocoder init` again.");
241
- return null;
242
- }
243
- async function runGitHubDiscoveryFlow(params) {
244
- let server = null;
245
- try {
246
- server = await startCallbackServer();
247
- } catch {
248
- }
249
- const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
250
- organizationId: params.organizationId,
251
- callbackPort: server?.port
252
- });
253
- p.log.info("Opening GitHub to authorize your account...");
254
- p.note("Complete authorization in your browser.");
255
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
256
- const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
257
- if (p.isCancel(shouldOpen)) {
258
- server?.close();
259
- return null;
260
- }
261
- if (shouldOpen) {
262
- const opened = await tryOpenBrowser(oauthUrl);
263
- if (!opened) {
264
- p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
265
- }
266
- }
267
- }
268
- const oauthSpinner = p.spinner();
269
- oauthSpinner.start("Waiting for GitHub authorization...");
270
- if (server) {
271
- try {
272
- const timeoutMs = 10 * 60 * 1e3;
273
- const callbackParams = await Promise.race([
274
- server.waitForCallback(),
275
- new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
276
- ]);
277
- server.close();
278
- if (!callbackParams) {
279
- oauthSpinner.stop("GitHub authorization timed out");
280
- return null;
281
- }
282
- if (callbackParams.error) {
283
- oauthSpinner.stop("GitHub authorization failed");
284
- p.log.error(callbackParams.error);
285
- return null;
286
- }
287
- } catch {
288
- server.close();
289
- oauthSpinner.stop("GitHub authorization failed");
290
- return null;
291
- }
292
- }
293
- oauthSpinner.stop("GitHub account authorized");
294
- const discoveryResult = await params.api.getCliGitHubDiscovery(params.userToken);
295
- return discoveryResult.installations;
296
- }
297
- async function selectGitHubInstallation(installations, canInstallNew) {
298
- const options = installations.map((inst) => ({
299
- value: String(inst.installationId),
300
- label: inst.accountLogin,
301
- hint: [
302
- inst.accountType === "Organization" ? "organization" : "personal",
303
- inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
304
- inst.isSuspended ? "suspended" : ""
305
- ].filter(Boolean).join(" \xB7 ") || void 0
306
- }));
307
- if (canInstallNew) {
308
- options.push({
309
- value: "install_new",
310
- label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
311
- });
312
- }
313
- const selected = await p.select({
314
- message: "Select a GitHub installation",
315
- options
316
- });
317
- if (p.isCancel(selected)) return null;
318
- if (selected === "install_new") return "install_new";
319
- return Number(selected);
320
- }
16
+ import chalk6 from "chalk";
17
+ import { config as loadEnv } from "dotenv";
321
18
 
322
19
  // src/utils/api.ts
323
20
  function isLimitErrorResponse(value) {
@@ -353,7 +50,9 @@ function parsePayload(raw) {
353
50
  }
354
51
  const trimmed = raw.trimStart();
355
52
  if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
356
- return { message: "Unexpected response from server (received HTML). Check your network connection or try again." };
53
+ return {
54
+ message: "Unexpected response from server (received HTML). Check your network connection or try again."
55
+ };
357
56
  }
358
57
  try {
359
58
  return JSON.parse(raw);
@@ -398,7 +97,10 @@ var VocoderAPI = class {
398
97
  if (!response.ok) {
399
98
  const limitError = isLimitErrorResponse(payload) ? payload : null;
400
99
  const syncPolicyError = isSyncPolicyErrorResponse(payload) ? payload : null;
401
- const baseMessage = extractErrorMessage(payload, `Request failed with status ${response.status}`);
100
+ const baseMessage = extractErrorMessage(
101
+ payload,
102
+ `Request failed with status ${response.status}`
103
+ );
402
104
  throw new VocoderAPIError({
403
105
  message: errorPrefix ? `${errorPrefix}: ${baseMessage}` : baseMessage,
404
106
  status: response.status,
@@ -423,7 +125,10 @@ var VocoderAPI = class {
423
125
  targetBranches: data.targetBranches ?? ["main"],
424
126
  primaryBranch: data.primaryBranch,
425
127
  syncPolicy: {
426
- blockingBranches: data.syncPolicy?.blockingBranches ?? ["main", "master"],
128
+ blockingBranches: data.syncPolicy?.blockingBranches ?? [
129
+ "main",
130
+ "master"
131
+ ],
427
132
  blockingMode: data.syncPolicy?.blockingMode ?? "required",
428
133
  nonBlockingMode: data.syncPolicy?.nonBlockingMode ?? "best-effort",
429
134
  defaultMaxWaitMs: data.syncPolicy?.defaultMaxWaitMs ?? 6e4
@@ -466,24 +171,28 @@ var VocoderAPI = class {
466
171
  const crypto = await import("crypto");
467
172
  const sortedStrings = [...strings].sort();
468
173
  const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
469
- return this.request("/api/cli/sync", {
470
- method: "POST",
471
- headers: {
472
- "Content-Type": "application/json"
174
+ return this.request(
175
+ "/api/cli/sync",
176
+ {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json"
180
+ },
181
+ body: JSON.stringify({
182
+ branch,
183
+ stringEntries,
184
+ targetLocales,
185
+ stringsHash,
186
+ ...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
187
+ ...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
188
+ ...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
189
+ ...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
190
+ ...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
191
+ ...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {}
192
+ })
473
193
  },
474
- body: JSON.stringify({
475
- branch,
476
- stringEntries,
477
- targetLocales,
478
- stringsHash,
479
- ...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
480
- ...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
481
- ...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
482
- ...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
483
- ...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
484
- ...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {}
485
- })
486
- }, "Translation submission failed");
194
+ "Translation submission failed"
195
+ );
487
196
  }
488
197
  /**
489
198
  * Check translation status
@@ -547,7 +256,10 @@ var VocoderAPI = class {
547
256
  const payload = await readPayload(response);
548
257
  if (!response.ok) {
549
258
  throw new VocoderAPIError({
550
- message: extractErrorMessage(payload, `Failed to start init session (${response.status})`),
259
+ message: extractErrorMessage(
260
+ payload,
261
+ `Failed to start init session (${response.status})`
262
+ ),
551
263
  status: response.status,
552
264
  payload
553
265
  });
@@ -566,7 +278,10 @@ var VocoderAPI = class {
566
278
  const payload = await readPayload(response);
567
279
  if (!response.ok) {
568
280
  throw new VocoderAPIError({
569
- message: extractErrorMessage(payload, `Failed to get init status (${response.status})`),
281
+ message: extractErrorMessage(
282
+ payload,
283
+ `Failed to get init status (${response.status})`
284
+ ),
570
285
  status: response.status,
571
286
  payload
572
287
  });
@@ -590,7 +305,10 @@ var VocoderAPI = class {
590
305
  const payload = await readPayload(response);
591
306
  if (!response.ok) {
592
307
  throw new VocoderAPIError({
593
- message: extractErrorMessage(payload, `Failed to start auth session (${response.status})`),
308
+ message: extractErrorMessage(
309
+ payload,
310
+ `Failed to start auth session (${response.status})`
311
+ ),
594
312
  status: response.status,
595
313
  payload
596
314
  });
@@ -619,7 +337,10 @@ var VocoderAPI = class {
619
337
  if (!response.ok) {
620
338
  return {
621
339
  status: "failed",
622
- reason: extractErrorMessage(payload, `Auth session error (${response.status})`)
340
+ reason: extractErrorMessage(
341
+ payload,
342
+ `Auth session error (${response.status})`
343
+ )
623
344
  };
624
345
  }
625
346
  const result = payload;
@@ -643,7 +364,10 @@ var VocoderAPI = class {
643
364
  const payload = await readPayload(response);
644
365
  if (!response.ok) {
645
366
  throw new VocoderAPIError({
646
- message: extractErrorMessage(payload, `Token validation failed (${response.status})`),
367
+ message: extractErrorMessage(
368
+ payload,
369
+ `Token validation failed (${response.status})`
370
+ ),
647
371
  status: response.status,
648
372
  payload
649
373
  });
@@ -661,7 +385,10 @@ var VocoderAPI = class {
661
385
  if (!response.ok) {
662
386
  const payload = await readPayload(response);
663
387
  throw new VocoderAPIError({
664
- message: extractErrorMessage(payload, `Token revocation failed (${response.status})`),
388
+ message: extractErrorMessage(
389
+ payload,
390
+ `Token revocation failed (${response.status})`
391
+ ),
665
392
  status: response.status,
666
393
  payload
667
394
  });
@@ -677,7 +404,10 @@ var VocoderAPI = class {
677
404
  const payload = await readPayload(response);
678
405
  if (!response.ok) {
679
406
  throw new VocoderAPIError({
680
- message: extractErrorMessage(payload, `Failed to list workspaces (${response.status})`),
407
+ message: extractErrorMessage(
408
+ payload,
409
+ `Failed to list workspaces (${response.status})`
410
+ ),
681
411
  status: response.status,
682
412
  payload
683
413
  });
@@ -693,7 +423,10 @@ var VocoderAPI = class {
693
423
  const payload = await readPayload(response);
694
424
  if (!response.ok) {
695
425
  throw new VocoderAPIError({
696
- message: extractErrorMessage(payload, `Failed to list projects (${response.status})`),
426
+ message: extractErrorMessage(
427
+ payload,
428
+ `Failed to list projects (${response.status})`
429
+ ),
697
430
  status: response.status,
698
431
  payload
699
432
  });
@@ -702,18 +435,24 @@ var VocoderAPI = class {
702
435
  return result.projects;
703
436
  }
704
437
  async regenerateProjectApiKey(userToken, projectId) {
705
- const response = await fetch(`${this.apiUrl}/api/cli/project/regenerate-key`, {
706
- method: "POST",
707
- headers: {
708
- "Content-Type": "application/json",
709
- Authorization: `Bearer ${userToken}`
710
- },
711
- body: JSON.stringify({ projectId })
712
- });
438
+ const response = await fetch(
439
+ `${this.apiUrl}/api/cli/project/regenerate-key`,
440
+ {
441
+ method: "POST",
442
+ headers: {
443
+ "Content-Type": "application/json",
444
+ Authorization: `Bearer ${userToken}`
445
+ },
446
+ body: JSON.stringify({ projectId })
447
+ }
448
+ );
713
449
  const payload = await readPayload(response);
714
450
  if (!response.ok) {
715
451
  throw new VocoderAPIError({
716
- message: extractErrorMessage(payload, `Failed to regenerate API key (${response.status})`),
452
+ message: extractErrorMessage(
453
+ payload,
454
+ `Failed to regenerate API key (${response.status})`
455
+ ),
717
456
  status: response.status,
718
457
  payload
719
458
  });
@@ -722,18 +461,24 @@ var VocoderAPI = class {
722
461
  }
723
462
  // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
724
463
  async startCliGitHubInstall(userToken, params) {
725
- const response = await fetch(`${this.apiUrl}/api/cli/github/install/start`, {
726
- method: "POST",
727
- headers: {
728
- Authorization: `Bearer ${userToken}`,
729
- "Content-Type": "application/json"
730
- },
731
- body: JSON.stringify(params)
732
- });
464
+ const response = await fetch(
465
+ `${this.apiUrl}/api/cli/github/install/start`,
466
+ {
467
+ method: "POST",
468
+ headers: {
469
+ Authorization: `Bearer ${userToken}`,
470
+ "Content-Type": "application/json"
471
+ },
472
+ body: JSON.stringify(params)
473
+ }
474
+ );
733
475
  const payload = await readPayload(response);
734
476
  if (!response.ok) {
735
477
  throw new VocoderAPIError({
736
- message: extractErrorMessage(payload, `Failed to start GitHub install (${response.status})`),
478
+ message: extractErrorMessage(
479
+ payload,
480
+ `Failed to start GitHub install (${response.status})`
481
+ ),
737
482
  status: response.status,
738
483
  payload
739
484
  });
@@ -746,15 +491,24 @@ var VocoderAPI = class {
746
491
  * account is created from the OAuth code in the callback.
747
492
  */
748
493
  async startCliGitHubLinkSession(sessionId, callbackPort) {
749
- const response = await fetch(`${this.apiUrl}/api/cli/github/oauth/link-start`, {
750
- method: "POST",
751
- headers: { "Content-Type": "application/json" },
752
- body: JSON.stringify({ sessionId, ...callbackPort != null ? { callbackPort } : {} })
753
- });
494
+ const response = await fetch(
495
+ `${this.apiUrl}/api/cli/github/oauth/link-start`,
496
+ {
497
+ method: "POST",
498
+ headers: { "Content-Type": "application/json" },
499
+ body: JSON.stringify({
500
+ sessionId,
501
+ ...callbackPort != null ? { callbackPort } : {}
502
+ })
503
+ }
504
+ );
754
505
  const payload = await readPayload(response);
755
506
  if (!response.ok) {
756
507
  throw new VocoderAPIError({
757
- message: extractErrorMessage(payload, `Failed to start GitHub link session (${response.status})`),
508
+ message: extractErrorMessage(
509
+ payload,
510
+ `Failed to start GitHub link session (${response.status})`
511
+ ),
758
512
  status: response.status,
759
513
  payload
760
514
  });
@@ -773,7 +527,10 @@ var VocoderAPI = class {
773
527
  const payload = await readPayload(response);
774
528
  if (!response.ok) {
775
529
  throw new VocoderAPIError({
776
- message: extractErrorMessage(payload, `Failed to start GitHub OAuth (${response.status})`),
530
+ message: extractErrorMessage(
531
+ payload,
532
+ `Failed to start GitHub OAuth (${response.status})`
533
+ ),
777
534
  status: response.status,
778
535
  payload
779
536
  });
@@ -787,7 +544,10 @@ var VocoderAPI = class {
787
544
  const payload = await readPayload(response);
788
545
  if (!response.ok) {
789
546
  throw new VocoderAPIError({
790
- message: extractErrorMessage(payload, `Failed to fetch GitHub discovery (${response.status})`),
547
+ message: extractErrorMessage(
548
+ payload,
549
+ `Failed to fetch GitHub discovery (${response.status})`
550
+ ),
791
551
  status: response.status,
792
552
  payload
793
553
  });
@@ -806,7 +566,10 @@ var VocoderAPI = class {
806
566
  const payload = await readPayload(response);
807
567
  if (!response.ok) {
808
568
  throw new VocoderAPIError({
809
- message: extractErrorMessage(payload, `Failed to claim GitHub installation (${response.status})`),
569
+ message: extractErrorMessage(
570
+ payload,
571
+ `Failed to claim GitHub installation (${response.status})`
572
+ ),
810
573
  status: response.status,
811
574
  payload
812
575
  });
@@ -821,7 +584,10 @@ var VocoderAPI = class {
821
584
  const payload = await readPayload(response);
822
585
  if (!response.ok) {
823
586
  throw new VocoderAPIError({
824
- message: extractErrorMessage(payload, `Failed to list locales (${response.status})`),
587
+ message: extractErrorMessage(
588
+ payload,
589
+ `Failed to list locales (${response.status})`
590
+ ),
825
591
  status: response.status,
826
592
  payload
827
593
  });
@@ -842,7 +608,10 @@ var VocoderAPI = class {
842
608
  const payload = await readPayload(response);
843
609
  if (!response.ok) {
844
610
  throw new VocoderAPIError({
845
- message: extractErrorMessage(payload, `Failed to create project (${response.status})`),
611
+ message: extractErrorMessage(
612
+ payload,
613
+ `Failed to create project (${response.status})`
614
+ ),
846
615
  status: response.status,
847
616
  payload
848
617
  });
@@ -889,19 +658,60 @@ var VocoderAPI = class {
889
658
  const payload = await readPayload(response);
890
659
  if (!response.ok) {
891
660
  throw new VocoderAPIError({
892
- message: extractErrorMessage(payload, `Failed to create project app (${response.status})`),
661
+ message: extractErrorMessage(
662
+ payload,
663
+ `Failed to create project app (${response.status})`
664
+ ),
893
665
  status: response.status,
894
666
  payload
895
667
  });
896
668
  }
897
- return payload;
669
+ return payload;
670
+ }
671
+ };
672
+
673
+ // src/utils/auth-store.ts
674
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
675
+ import { homedir } from "os";
676
+ import { dirname, join } from "path";
677
+ function getAuthFilePath() {
678
+ return join(homedir(), ".config", "vocoder", "auth.json");
679
+ }
680
+ function readAuthData() {
681
+ const filePath = getAuthFilePath();
682
+ try {
683
+ const raw = readFileSync(filePath, "utf8");
684
+ const parsed = JSON.parse(raw);
685
+ if (!parsed || typeof parsed !== "object") return null;
686
+ const data = parsed;
687
+ if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
688
+ return null;
689
+ }
690
+ return {
691
+ token: data.token,
692
+ apiUrl: data.apiUrl,
693
+ userId: data.userId,
694
+ email: data.email,
695
+ name: typeof data.name === "string" ? data.name : null,
696
+ createdAt: data.createdAt
697
+ };
698
+ } catch {
699
+ return null;
898
700
  }
899
- };
900
-
901
- // src/commands/init.ts
902
- import chalk6 from "chalk";
903
- import { execSync as execSync3 } from "child_process";
904
- import { config as loadEnv } from "dotenv";
701
+ }
702
+ function writeAuthData(data) {
703
+ const filePath = getAuthFilePath();
704
+ const dir = dirname(filePath);
705
+ mkdirSync(dir, { recursive: true, mode: 448 });
706
+ writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
707
+ }
708
+ function clearAuthData() {
709
+ const filePath = getAuthFilePath();
710
+ try {
711
+ unlinkSync(filePath);
712
+ } catch {
713
+ }
714
+ }
905
715
 
906
716
  // src/utils/git-identity.ts
907
717
  import { execSync } from "child_process";
@@ -972,49 +782,332 @@ function toCanonical(host, ownerRepoPath) {
972
782
  if (host.includes("bitbucket.org")) {
973
783
  return `bitbucket:${ownerRepoPath.toLowerCase()}`;
974
784
  }
975
- return `git:${host}/${ownerRepoPath.toLowerCase()}`;
785
+ return `git:${host}/${ownerRepoPath.toLowerCase()}`;
786
+ }
787
+ function resolveGitRepositoryIdentity() {
788
+ const remoteUrl = safeExec("git config --get remote.origin.url");
789
+ if (!remoteUrl) {
790
+ return null;
791
+ }
792
+ const parsed = parseRemoteUrl(remoteUrl);
793
+ if (!parsed) {
794
+ return null;
795
+ }
796
+ const repositoryRoot = safeExec("git rev-parse --show-toplevel");
797
+ const currentDirectory = process.cwd();
798
+ let repoAppDir = "";
799
+ if (repositoryRoot) {
800
+ const relativePath = relative(
801
+ resolve(repositoryRoot),
802
+ resolve(currentDirectory)
803
+ ).replace(/\\/g, "/").trim();
804
+ if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
805
+ repoAppDir = relativePath;
806
+ }
807
+ }
808
+ return {
809
+ repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
810
+ repoAppDir
811
+ };
812
+ }
813
+ function resolveGitContext() {
814
+ const warnings = [];
815
+ const identity = resolveGitRepositoryIdentity();
816
+ if (!identity) {
817
+ warnings.push(
818
+ "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
819
+ );
820
+ }
821
+ return { identity, warnings };
822
+ }
823
+
824
+ // src/utils/github-connect.ts
825
+ import { spawn } from "child_process";
826
+ import * as p from "@clack/prompts";
827
+ import chalk from "chalk";
828
+
829
+ // src/utils/local-server.ts
830
+ import { createServer } from "http";
831
+ import { URL as URL2 } from "url";
832
+ function startCallbackServer() {
833
+ return new Promise((resolve2, reject) => {
834
+ let settled = false;
835
+ let callbackResolve = null;
836
+ let callbackReject = null;
837
+ const callbackPromise = new Promise((res, rej) => {
838
+ callbackResolve = res;
839
+ callbackReject = rej;
840
+ });
841
+ const server = createServer((req, res) => {
842
+ if (!req.url) {
843
+ res.writeHead(400);
844
+ res.end();
845
+ return;
846
+ }
847
+ let pathname;
848
+ let params;
849
+ try {
850
+ const parsed = new URL2(req.url, "http://localhost");
851
+ pathname = parsed.pathname;
852
+ params = Object.fromEntries(parsed.searchParams.entries());
853
+ } catch {
854
+ res.writeHead(400);
855
+ res.end("Bad request");
856
+ return;
857
+ }
858
+ if (pathname !== "/callback") {
859
+ res.writeHead(404);
860
+ res.end("Not found");
861
+ return;
862
+ }
863
+ res.writeHead(200, { "Content-Type": "text/html" });
864
+ res.end(
865
+ '<!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>'
866
+ );
867
+ if (callbackResolve) {
868
+ callbackResolve(params);
869
+ callbackResolve = null;
870
+ }
871
+ setImmediate(() => server.close());
872
+ });
873
+ server.on("error", (err) => {
874
+ if (!settled) {
875
+ settled = true;
876
+ if (callbackReject) callbackReject(err);
877
+ reject(err);
878
+ }
879
+ });
880
+ server.listen(0, "127.0.0.1", () => {
881
+ if (settled) return;
882
+ settled = true;
883
+ const port = server.address().port;
884
+ resolve2({
885
+ port,
886
+ waitForCallback: () => callbackPromise,
887
+ close: () => server.close()
888
+ });
889
+ });
890
+ });
891
+ }
892
+
893
+ // src/utils/github-connect.ts
894
+ async function tryOpenBrowser(url) {
895
+ if (!process.stdout.isTTY || process.env.CI === "true") {
896
+ return false;
897
+ }
898
+ const platform = process.platform;
899
+ let command;
900
+ let args;
901
+ if (platform === "darwin") {
902
+ command = "open";
903
+ args = [url];
904
+ } else if (platform === "win32") {
905
+ command = "rundll32";
906
+ args = ["url.dll,FileProtocolHandler", url];
907
+ } else {
908
+ command = "xdg-open";
909
+ args = [url];
910
+ }
911
+ return new Promise((resolve2) => {
912
+ try {
913
+ const child = spawn(command, args, {
914
+ detached: true,
915
+ stdio: "ignore",
916
+ windowsHide: true
917
+ });
918
+ let settled = false;
919
+ child.once("spawn", () => {
920
+ if (settled) return;
921
+ settled = true;
922
+ child.unref();
923
+ resolve2(true);
924
+ });
925
+ child.once("error", () => {
926
+ if (settled) return;
927
+ settled = true;
928
+ resolve2(false);
929
+ });
930
+ setTimeout(() => {
931
+ if (settled) return;
932
+ settled = true;
933
+ resolve2(false);
934
+ }, 300);
935
+ } catch {
936
+ resolve2(false);
937
+ }
938
+ });
939
+ }
940
+ async function runGitHubInstallFlow(params) {
941
+ let server = null;
942
+ try {
943
+ server = await startCallbackServer();
944
+ } catch {
945
+ }
946
+ const { installUrl } = await params.api.startCliGitHubInstall(
947
+ params.userToken,
948
+ {
949
+ organizationId: params.organizationId,
950
+ callbackPort: server?.port
951
+ }
952
+ );
953
+ p.log.info("Opening GitHub to install the Vocoder App...");
954
+ p.note(installUrl, "Install URL");
955
+ if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
956
+ const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
957
+ if (p.isCancel(shouldOpen)) {
958
+ server?.close();
959
+ return null;
960
+ }
961
+ if (shouldOpen) {
962
+ const opened = await tryOpenBrowser(installUrl);
963
+ if (!opened) {
964
+ p.log.info(
965
+ "Could not open a browser automatically. Use the URL above."
966
+ );
967
+ }
968
+ }
969
+ }
970
+ const connectSpinner = p.spinner();
971
+ connectSpinner.start("Waiting for GitHub App installation...");
972
+ if (server) {
973
+ try {
974
+ const params_timeout = 15 * 60 * 1e3;
975
+ const callbackParams = await Promise.race([
976
+ server.waitForCallback(),
977
+ new Promise(
978
+ (resolve2) => setTimeout(() => resolve2(null), params_timeout)
979
+ )
980
+ ]);
981
+ server.close();
982
+ if (!callbackParams) {
983
+ connectSpinner.stop("GitHub App installation timed out");
984
+ p.log.error(
985
+ "The installation flow timed out. Run `vocoder init` again."
986
+ );
987
+ return null;
988
+ }
989
+ if (callbackParams.error) {
990
+ connectSpinner.stop("GitHub App installation failed");
991
+ p.log.error(callbackParams.error);
992
+ return null;
993
+ }
994
+ const { organizationId, connectionLabel, workspace_created } = callbackParams;
995
+ if (!organizationId || !connectionLabel) {
996
+ connectSpinner.stop("GitHub App installation incomplete");
997
+ p.log.error("Missing organization or connection data from callback.");
998
+ return null;
999
+ }
1000
+ connectSpinner.stop(
1001
+ `Connected to GitHub as ${chalk.bold(connectionLabel)}`
1002
+ );
1003
+ const orgName = workspace_created ? connectionLabel : organizationId;
1004
+ return {
1005
+ organizationId,
1006
+ organizationName: orgName,
1007
+ connectionLabel
1008
+ };
1009
+ } catch {
1010
+ server.close();
1011
+ connectSpinner.stop("GitHub App installation failed");
1012
+ return null;
1013
+ }
1014
+ }
1015
+ connectSpinner.stop("Could not detect GitHub App installation automatically");
1016
+ p.log.warn(
1017
+ "Complete the installation in your browser, then run `vocoder init` again."
1018
+ );
1019
+ return null;
976
1020
  }
977
- function resolveGitRepositoryIdentity() {
978
- const remoteUrl = safeExec("git config --get remote.origin.url");
979
- if (!remoteUrl) {
980
- return null;
1021
+ async function runGitHubDiscoveryFlow(params) {
1022
+ let server = null;
1023
+ try {
1024
+ server = await startCallbackServer();
1025
+ } catch {
981
1026
  }
982
- const parsed = parseRemoteUrl(remoteUrl);
983
- if (!parsed) {
984
- return null;
1027
+ const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
1028
+ organizationId: params.organizationId,
1029
+ callbackPort: server?.port
1030
+ });
1031
+ p.log.info("Opening GitHub to authorize your account...");
1032
+ p.note("Complete authorization in your browser.");
1033
+ if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
1034
+ const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
1035
+ if (p.isCancel(shouldOpen)) {
1036
+ server?.close();
1037
+ return null;
1038
+ }
1039
+ if (shouldOpen) {
1040
+ const opened = await tryOpenBrowser(oauthUrl);
1041
+ if (!opened) {
1042
+ p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
1043
+ }
1044
+ }
985
1045
  }
986
- const repositoryRoot = safeExec("git rev-parse --show-toplevel");
987
- const currentDirectory = process.cwd();
988
- let repoAppDir = "";
989
- if (repositoryRoot) {
990
- const relativePath = relative(resolve(repositoryRoot), resolve(currentDirectory)).replace(/\\/g, "/").trim();
991
- if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
992
- repoAppDir = relativePath;
1046
+ const oauthSpinner = p.spinner();
1047
+ oauthSpinner.start("Waiting for GitHub authorization...");
1048
+ if (server) {
1049
+ try {
1050
+ const timeoutMs = 10 * 60 * 1e3;
1051
+ const callbackParams = await Promise.race([
1052
+ server.waitForCallback(),
1053
+ new Promise(
1054
+ (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
1055
+ )
1056
+ ]);
1057
+ server.close();
1058
+ if (!callbackParams) {
1059
+ oauthSpinner.stop("GitHub authorization timed out");
1060
+ return null;
1061
+ }
1062
+ if (callbackParams.error) {
1063
+ oauthSpinner.stop("GitHub authorization failed");
1064
+ p.log.error(callbackParams.error);
1065
+ return null;
1066
+ }
1067
+ } catch {
1068
+ server.close();
1069
+ oauthSpinner.stop("GitHub authorization failed");
1070
+ return null;
993
1071
  }
994
1072
  }
995
- return {
996
- repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
997
- repoAppDir
998
- };
1073
+ oauthSpinner.stop("GitHub account authorized");
1074
+ const discoveryResult = await params.api.getCliGitHubDiscovery(
1075
+ params.userToken
1076
+ );
1077
+ return discoveryResult.installations;
999
1078
  }
1000
- function resolveGitContext() {
1001
- const warnings = [];
1002
- const identity = resolveGitRepositoryIdentity();
1003
- if (!identity) {
1004
- warnings.push(
1005
- "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
1006
- );
1079
+ async function selectGitHubInstallation(installations, canInstallNew) {
1080
+ const options = installations.map((inst) => ({
1081
+ value: String(inst.installationId),
1082
+ label: inst.accountLogin,
1083
+ hint: [
1084
+ inst.accountType === "Organization" ? "organization" : "personal",
1085
+ inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
1086
+ inst.isSuspended ? "suspended" : ""
1087
+ ].filter(Boolean).join(" \xB7 ") || void 0
1088
+ }));
1089
+ if (canInstallNew) {
1090
+ options.push({
1091
+ value: "install_new",
1092
+ label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
1093
+ });
1007
1094
  }
1008
- return { identity, warnings };
1095
+ const selected = await p.select({
1096
+ message: "Select a GitHub installation",
1097
+ options
1098
+ });
1099
+ if (p.isCancel(selected)) return null;
1100
+ if (selected === "install_new") return "install_new";
1101
+ return Number(selected);
1009
1102
  }
1010
1103
 
1011
1104
  // src/utils/project-create.ts
1012
1105
  import * as p3 from "@clack/prompts";
1013
1106
  import chalk4 from "chalk";
1014
1107
 
1015
- // src/utils/locale-search.ts
1016
- import { Prompt, isCancel as isCancel2 } from "@clack/core";
1017
- import * as p2 from "@clack/prompts";
1108
+ // src/utils/branch-select.ts
1109
+ import { execSync as execSync2 } from "child_process";
1110
+ import { isCancel as isCancel2, Prompt } from "@clack/core";
1018
1111
  import chalk2 from "chalk";
1019
1112
  var S_BAR = "\u2502";
1020
1113
  var S_BAR_END = "\u2514";
@@ -1041,57 +1134,134 @@ function symbol(state) {
1041
1134
  return cyan(S_ACTIVE);
1042
1135
  }
1043
1136
  }
1044
- var MAX_VISIBLE = 12;
1045
- function filterLocales(options, query) {
1046
- if (!query.trim()) return options;
1137
+ function detectGitBranches(cwd) {
1138
+ const workDir = cwd ?? process.cwd();
1139
+ try {
1140
+ const localOut = execSync2("git branch", {
1141
+ cwd: workDir,
1142
+ stdio: "pipe"
1143
+ }).toString();
1144
+ const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
1145
+ let remoteBranches = [];
1146
+ try {
1147
+ const remoteOut = execSync2("git branch -r", {
1148
+ cwd: workDir,
1149
+ stdio: "pipe"
1150
+ }).toString();
1151
+ remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
1152
+ } catch {
1153
+ }
1154
+ const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
1155
+ let defaultBranch = "main";
1156
+ try {
1157
+ const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", {
1158
+ cwd: workDir,
1159
+ stdio: "pipe"
1160
+ }).toString().trim();
1161
+ defaultBranch = ref.split("/").pop() ?? "main";
1162
+ } catch {
1163
+ }
1164
+ return {
1165
+ branches: branches.length > 0 ? branches : [defaultBranch],
1166
+ defaultBranch
1167
+ };
1168
+ } catch {
1169
+ return { branches: ["main"], defaultBranch: "main" };
1170
+ }
1171
+ }
1172
+ var INVALID_CHARS = /[\s?^~:[\]\\]/;
1173
+ function validateBranchPattern(pattern) {
1174
+ const t = pattern.trim();
1175
+ if (!t) return "Pattern cannot be empty";
1176
+ if (INVALID_CHARS.test(t))
1177
+ return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
1178
+ if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
1179
+ if (t.includes("//")) return "Cannot contain //";
1180
+ return null;
1181
+ }
1182
+ var MAX_VISIBLE = 10;
1183
+ function buildItems(branches, defaultBranch, customPatterns) {
1184
+ const items = branches.map((b) => ({
1185
+ value: b,
1186
+ label: b === defaultBranch ? `${b} (default branch)` : b
1187
+ }));
1188
+ for (const pt of customPatterns) {
1189
+ if (!branches.includes(pt)) {
1190
+ items.push({ value: pt, label: pt, isCustom: true });
1191
+ }
1192
+ }
1193
+ return items;
1194
+ }
1195
+ function filterItems(items, query) {
1196
+ if (!query.trim()) return items;
1047
1197
  const lower = query.toLowerCase();
1048
- return options.filter(
1049
- (o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
1050
- );
1198
+ return items.filter((i) => i.value.toLowerCase().includes(lower));
1051
1199
  }
1052
- function buildList(filtered, cursor, scrollOffset, selected) {
1053
- const isMulti = selected !== null;
1200
+ function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
1201
+ const lines = [];
1054
1202
  const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
1055
- const visibleLines = [];
1056
1203
  for (let i = scrollOffset; i < end; i++) {
1057
- const opt = filtered[i];
1058
- const isCursor = i === cursor;
1059
- const isChecked = isMulti && selected.has(opt.bcp47);
1060
- const icon = isMulti ? isChecked ? isCursor ? grn("\u25FC") : "\u25FC" : isCursor ? grn("\u25FB") : dim("\u25FB") : isCursor ? grn("\u25CF") : dim("\u25CB");
1061
- visibleLines.push(`${cyan(S_BAR)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`);
1204
+ const item = filtered[i];
1205
+ const isCursor = i === cursor && !addCursor;
1206
+ const isChecked = selected.has(item.value);
1207
+ const icon = isChecked ? isCursor ? grn("\u25FC") : "\u25FC" : isCursor ? grn("\u25FB") : dim("\u25FB");
1208
+ let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
1209
+ if (isCursor) label = bld(label);
1210
+ lines.push(`${cyan(S_BAR)} ${icon} ${label}`);
1211
+ }
1212
+ const trimmed = filter.trim();
1213
+ const allItems = [...filtered];
1214
+ const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
1215
+ if (isNewPattern) {
1216
+ const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
1217
+ const icon = addCursor ? grn("\u25FB") : dim("\u25FB");
1218
+ const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
1219
+ lines.push(`${cyan(S_BAR)} ${icon} ${label}`);
1220
+ } else if (filtered.length === 0 && trimmed.length === 0) {
1221
+ lines.push(dim(`${S_BAR} No branches detected`));
1062
1222
  }
1063
1223
  const hidden = filtered.length - (end - scrollOffset);
1064
- if (hidden > 0) visibleLines.push(dim(`${S_BAR} ${hidden} more \u2014 keep typing to narrow`));
1065
- if (filtered.length === 0) visibleLines.push(dim(`${S_BAR} No matches`));
1066
- if (isMulti && selected.size > 0) {
1067
- visibleLines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
1224
+ if (hidden > 0) lines.push(dim(`${S_BAR} ${hidden} more`));
1225
+ if (selected.size > 0) {
1226
+ lines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
1227
+ } else if (optional) {
1228
+ lines.push(dim(`${S_BAR} Enter to skip`));
1068
1229
  }
1069
- return visibleLines.join("\n");
1230
+ return lines.join("\n");
1070
1231
  }
1071
- async function runFilterablePrompt(opts) {
1072
- const { message, options, multi } = opts;
1232
+ async function filterableBranchSelect(params) {
1233
+ const { message, branches, defaultBranch } = params;
1234
+ const optional = params.optional ?? false;
1235
+ const excludedSet = new Set(params.excludedPatterns ?? []);
1073
1236
  let filter = "";
1074
1237
  let cursor = 0;
1075
1238
  let scrollOffset = 0;
1076
- const selected = new Set(multi ? opts.initialValues ?? [] : []);
1077
- if (!multi && opts.initialValue) {
1078
- const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
1079
- if (idx >= 0) cursor = idx;
1080
- }
1081
- const getFiltered = () => filterLocales(options, filter);
1239
+ let addCursor = false;
1240
+ const customPatterns = [];
1241
+ const selected = new Set(params.initialValues ?? [defaultBranch]);
1242
+ const getItems = () => buildItems(branches, defaultBranch, customPatterns);
1243
+ const getFiltered = () => filterItems(getItems(), filter);
1244
+ const isNewPattern = () => {
1245
+ const t = filter.trim();
1246
+ if (!t) return false;
1247
+ return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
1248
+ };
1082
1249
  const clampCursor = (filtered) => {
1083
- if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
1084
- if (cursor < scrollOffset) scrollOffset = cursor;
1085
- if (cursor >= scrollOffset + MAX_VISIBLE) scrollOffset = cursor - MAX_VISIBLE + 1;
1086
- if (scrollOffset < 0) scrollOffset = 0;
1250
+ const hasAdd = isNewPattern();
1251
+ const max = filtered.length - 1 + (hasAdd ? 1 : 0);
1252
+ if (cursor > max && !addCursor) cursor = Math.max(0, max);
1253
+ if (!addCursor) {
1254
+ if (cursor < scrollOffset) scrollOffset = cursor;
1255
+ if (cursor >= scrollOffset + MAX_VISIBLE)
1256
+ scrollOffset = cursor - MAX_VISIBLE + 1;
1257
+ if (scrollOffset < 0) scrollOffset = 0;
1258
+ }
1087
1259
  };
1088
1260
  const prompt = new Prompt(
1089
1261
  {
1090
- initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
1091
1262
  validate() {
1092
- const f = getFiltered();
1093
- if (multi && selected.size === 0) return "At least one target language is required.";
1094
- if (!multi && !f[cursor]) return "Please select a language.";
1263
+ if (!optional && selected.size === 0)
1264
+ return "At least one branch is required.";
1095
1265
  return void 0;
1096
1266
  },
1097
1267
  render() {
@@ -1100,11 +1270,11 @@ async function runFilterablePrompt(opts) {
1100
1270
  const hdr = `${dim(S_BAR)}
1101
1271
  ${symbol(this.state)} ${message}
1102
1272
  `;
1103
- const hint = filter.length > 0 ? filter : dim("type to filter, \u2191\u2193 navigate" + (multi ? ", space select" : ""));
1273
+ const hint = filter.length > 0 ? filter : dim("type to filter or add pattern, \u2191\u2193 navigate, space select");
1104
1274
  switch (this.state) {
1105
1275
  case "submit": {
1106
- const val = multi ? Array.from(selected).map((id) => options.find((o) => o.bcp47 === id)?.label ?? id).join(", ") : options.find((o) => o.bcp47 === this.value)?.label ?? "";
1107
- return `${hdr}${dim(S_BAR)} ${bld(val || dim("none"))}`;
1276
+ const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
1277
+ return `${hdr}${dim(S_BAR)} ${summary}`;
1108
1278
  }
1109
1279
  case "cancel":
1110
1280
  return `${hdr}${dim(S_BAR)}`;
@@ -1112,7 +1282,17 @@ ${symbol(this.state)} ${message}
1112
1282
  return [
1113
1283
  hdr.trimEnd(),
1114
1284
  `${ylw(S_BAR)} ${dim("/")} ${hint}`,
1115
- buildList(filtered, cursor, scrollOffset, multi ? selected : null),
1285
+ buildList(
1286
+ filtered,
1287
+ cursor,
1288
+ scrollOffset,
1289
+ selected,
1290
+ filter,
1291
+ customPatterns,
1292
+ addCursor,
1293
+ optional,
1294
+ excludedSet
1295
+ ),
1116
1296
  `${ylw(S_BAR_END)} ${ylw(this.error)}`,
1117
1297
  ""
1118
1298
  ].join("\n");
@@ -1120,7 +1300,17 @@ ${symbol(this.state)} ${message}
1120
1300
  return [
1121
1301
  hdr.trimEnd(),
1122
1302
  `${cyan(S_BAR)} ${dim("/")} ${hint}`,
1123
- buildList(filtered, cursor, scrollOffset, multi ? selected : null),
1303
+ buildList(
1304
+ filtered,
1305
+ cursor,
1306
+ scrollOffset,
1307
+ selected,
1308
+ filter,
1309
+ customPatterns,
1310
+ addCursor,
1311
+ optional,
1312
+ excludedSet
1313
+ ),
1124
1314
  `${cyan(S_BAR_END)}`,
1125
1315
  ""
1126
1316
  ].join("\n");
@@ -1128,7 +1318,6 @@ ${symbol(this.state)} ${message}
1128
1318
  }
1129
1319
  },
1130
1320
  false
1131
- // trackValue=false — we manage value manually
1132
1321
  );
1133
1322
  prompt.on("key", (key) => {
1134
1323
  if (!key || key === " ") return;
@@ -1137,69 +1326,65 @@ ${symbol(this.state)} ${message}
1137
1326
  filter = filter.slice(0, -1);
1138
1327
  cursor = 0;
1139
1328
  scrollOffset = 0;
1329
+ addCursor = false;
1140
1330
  } else if (cp >= 32 && cp !== 127) {
1141
1331
  filter += key;
1142
1332
  cursor = 0;
1143
1333
  scrollOffset = 0;
1334
+ addCursor = false;
1144
1335
  }
1145
1336
  });
1146
1337
  prompt.on("cursor", (action) => {
1147
1338
  const filtered = getFiltered();
1339
+ const hasAdd = isNewPattern();
1148
1340
  switch (action) {
1149
1341
  case "up":
1150
- cursor = Math.max(0, cursor - 1);
1342
+ if (addCursor) {
1343
+ addCursor = false;
1344
+ cursor = Math.max(0, filtered.length - 1);
1345
+ } else cursor = Math.max(0, cursor - 1);
1151
1346
  break;
1152
1347
  case "down":
1153
- cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
1348
+ if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
1349
+ addCursor = true;
1350
+ else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
1154
1351
  break;
1155
1352
  case "space":
1156
- if (multi) {
1157
- const opt = filtered[cursor];
1158
- if (opt) {
1159
- if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
1160
- else selected.add(opt.bcp47);
1353
+ if (addCursor) {
1354
+ const t = filter.trim();
1355
+ const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
1356
+ if (!err) {
1357
+ customPatterns.push(t);
1358
+ selected.add(t);
1359
+ filter = "";
1360
+ cursor = 0;
1361
+ scrollOffset = 0;
1362
+ addCursor = false;
1363
+ }
1364
+ } else {
1365
+ const item = filtered[cursor];
1366
+ if (item) {
1367
+ if (selected.has(item.value)) selected.delete(item.value);
1368
+ else selected.add(item.value);
1161
1369
  }
1162
1370
  }
1163
1371
  break;
1164
1372
  }
1165
- if (!multi) {
1166
- const opt = getFiltered()[cursor];
1167
- prompt.value = opt?.bcp47 ?? null;
1168
- }
1169
1373
  });
1170
- prompt.on("finalize", () => {
1171
- if (prompt.state === "submit") {
1172
- if (multi) {
1173
- prompt.value = Array.from(selected);
1174
- } else {
1175
- const f = getFiltered();
1176
- prompt.value = f[cursor]?.bcp47 ?? null;
1177
- }
1374
+ prompt.on("finalize", () => {
1375
+ if (prompt.state === "submit") {
1376
+ prompt.value = Array.from(selected);
1178
1377
  }
1179
1378
  });
1180
1379
  const result = await prompt.prompt();
1181
1380
  if (isCancel2(result)) return null;
1182
1381
  return result;
1183
1382
  }
1184
- async function searchSelectLocale(options, message, initialValue) {
1185
- const result = await runFilterablePrompt({ message, options, multi: false, initialValue });
1186
- return typeof result === "string" ? result : null;
1187
- }
1188
- async function searchMultiSelectLocales(options, message, initialValues) {
1189
- const result = await runFilterablePrompt({ message, options, multi: true, initialValues });
1190
- if (result === null) return null;
1191
- const picks = result;
1192
- if (picks.length === 0) {
1193
- p2.log.warn("At least one target language is required. Please select at least one.");
1194
- return searchMultiSelectLocales(options, message, initialValues);
1195
- }
1196
- return picks;
1197
- }
1198
1383
 
1199
- // src/utils/branch-select.ts
1200
- import { Prompt as Prompt2, isCancel as isCancel3 } from "@clack/core";
1384
+ // src/utils/locale-search.ts
1385
+ import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
1386
+ import * as p2 from "@clack/prompts";
1201
1387
  import chalk3 from "chalk";
1202
- import { execSync as execSync2 } from "child_process";
1203
1388
  var S_BAR2 = "\u2502";
1204
1389
  var S_BAR_END2 = "\u2514";
1205
1390
  var S_ACTIVE2 = "\u25C6";
@@ -1225,122 +1410,64 @@ function symbol2(state) {
1225
1410
  return cyan2(S_ACTIVE2);
1226
1411
  }
1227
1412
  }
1228
- function detectGitBranches(cwd) {
1229
- const workDir = cwd ?? process.cwd();
1230
- try {
1231
- const localOut = execSync2("git branch", { cwd: workDir, stdio: "pipe" }).toString();
1232
- const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
1233
- let remoteBranches = [];
1234
- try {
1235
- const remoteOut = execSync2("git branch -r", { cwd: workDir, stdio: "pipe" }).toString();
1236
- remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
1237
- } catch {
1238
- }
1239
- const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
1240
- let defaultBranch = "main";
1241
- try {
1242
- const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", { cwd: workDir, stdio: "pipe" }).toString().trim();
1243
- defaultBranch = ref.split("/").pop() ?? "main";
1244
- } catch {
1245
- }
1246
- return {
1247
- branches: branches.length > 0 ? branches : [defaultBranch],
1248
- defaultBranch
1249
- };
1250
- } catch {
1251
- return { branches: ["main"], defaultBranch: "main" };
1252
- }
1253
- }
1254
- var INVALID_CHARS = /[\s?^~:[\]\\]/;
1255
- function validateBranchPattern(pattern) {
1256
- const t = pattern.trim();
1257
- if (!t) return "Pattern cannot be empty";
1258
- if (INVALID_CHARS.test(t)) return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
1259
- if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
1260
- if (t.includes("//")) return "Cannot contain //";
1261
- return null;
1262
- }
1263
- var MAX_VISIBLE2 = 10;
1264
- function buildItems(branches, defaultBranch, customPatterns) {
1265
- const items = branches.map((b) => ({
1266
- value: b,
1267
- label: b === defaultBranch ? `${b} (default branch)` : b
1268
- }));
1269
- for (const pt of customPatterns) {
1270
- if (!branches.includes(pt)) {
1271
- items.push({ value: pt, label: pt, isCustom: true });
1272
- }
1273
- }
1274
- return items;
1275
- }
1276
- function filterItems(items, query) {
1277
- if (!query.trim()) return items;
1413
+ var MAX_VISIBLE2 = 12;
1414
+ function filterLocales(options, query) {
1415
+ if (!query.trim()) return options;
1278
1416
  const lower = query.toLowerCase();
1279
- return items.filter((i) => i.value.toLowerCase().includes(lower));
1417
+ return options.filter(
1418
+ (o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
1419
+ );
1280
1420
  }
1281
- function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
1282
- const lines = [];
1421
+ function buildList2(filtered, cursor, scrollOffset, selected) {
1422
+ const isMulti = selected !== null;
1283
1423
  const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
1424
+ const visibleLines = [];
1284
1425
  for (let i = scrollOffset; i < end; i++) {
1285
- const item = filtered[i];
1286
- const isCursor = i === cursor && !addCursor;
1287
- const isChecked = selected.has(item.value);
1288
- const icon = isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB");
1289
- let label = item.isCustom ? `${item.label} ${dim2("(custom)")}` : item.label;
1290
- if (isCursor) label = bld2(label);
1291
- lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
1292
- }
1293
- const trimmed = filter.trim();
1294
- const allItems = [...filtered];
1295
- const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
1296
- if (isNewPattern) {
1297
- const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
1298
- const icon = addCursor ? grn2("\u25FB") : dim2("\u25FB");
1299
- const label = err ? `${ylw2("+")} ${dim2(`"${trimmed}" \u2014 ${err}`)}` : `${grn2("+")} Add "${trimmed}" as branch pattern`;
1300
- lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
1301
- } else if (filtered.length === 0 && trimmed.length === 0) {
1302
- lines.push(dim2(`${S_BAR2} No branches detected`));
1426
+ const opt = filtered[i];
1427
+ const isCursor = i === cursor;
1428
+ const isChecked = isMulti && selected.has(opt.bcp47);
1429
+ const icon = isMulti ? isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB") : isCursor ? grn2("\u25CF") : dim2("\u25CB");
1430
+ visibleLines.push(
1431
+ `${cyan2(S_BAR2)} ${icon} ${isCursor ? bld2(opt.label) : opt.label}`
1432
+ );
1303
1433
  }
1304
1434
  const hidden = filtered.length - (end - scrollOffset);
1305
- if (hidden > 0) lines.push(dim2(`${S_BAR2} ${hidden} more`));
1306
- if (selected.size > 0) {
1307
- lines.push(dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`));
1308
- } else if (optional) {
1309
- lines.push(dim2(`${S_BAR2} Enter to skip`));
1435
+ if (hidden > 0)
1436
+ visibleLines.push(dim2(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
1437
+ if (filtered.length === 0) visibleLines.push(dim2(`${S_BAR2} No matches`));
1438
+ if (isMulti && selected.size > 0) {
1439
+ visibleLines.push(
1440
+ dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`)
1441
+ );
1310
1442
  }
1311
- return lines.join("\n");
1443
+ return visibleLines.join("\n");
1312
1444
  }
1313
- async function filterableBranchSelect(params) {
1314
- const { message, branches, defaultBranch } = params;
1315
- const optional = params.optional ?? false;
1316
- const excludedSet = new Set(params.excludedPatterns ?? []);
1445
+ async function runFilterablePrompt(opts) {
1446
+ const { message, options, multi } = opts;
1317
1447
  let filter = "";
1318
1448
  let cursor = 0;
1319
1449
  let scrollOffset = 0;
1320
- let addCursor = false;
1321
- const customPatterns = [];
1322
- const selected = new Set(params.initialValues ?? [defaultBranch]);
1323
- const getItems = () => buildItems(branches, defaultBranch, customPatterns);
1324
- const getFiltered = () => filterItems(getItems(), filter);
1325
- const isNewPattern = () => {
1326
- const t = filter.trim();
1327
- if (!t) return false;
1328
- return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
1329
- };
1450
+ const selected = new Set(multi ? opts.initialValues ?? [] : []);
1451
+ if (!multi && opts.initialValue) {
1452
+ const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
1453
+ if (idx >= 0) cursor = idx;
1454
+ }
1455
+ const getFiltered = () => filterLocales(options, filter);
1330
1456
  const clampCursor = (filtered) => {
1331
- const hasAdd = isNewPattern();
1332
- const max = filtered.length - 1 + (hasAdd ? 1 : 0);
1333
- if (cursor > max && !addCursor) cursor = Math.max(0, max);
1334
- if (!addCursor) {
1335
- if (cursor < scrollOffset) scrollOffset = cursor;
1336
- if (cursor >= scrollOffset + MAX_VISIBLE2) scrollOffset = cursor - MAX_VISIBLE2 + 1;
1337
- if (scrollOffset < 0) scrollOffset = 0;
1338
- }
1457
+ if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
1458
+ if (cursor < scrollOffset) scrollOffset = cursor;
1459
+ if (cursor >= scrollOffset + MAX_VISIBLE2)
1460
+ scrollOffset = cursor - MAX_VISIBLE2 + 1;
1461
+ if (scrollOffset < 0) scrollOffset = 0;
1339
1462
  };
1340
1463
  const prompt = new Prompt2(
1341
1464
  {
1465
+ initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
1342
1466
  validate() {
1343
- if (!optional && selected.size === 0) return "At least one branch is required.";
1467
+ const f = getFiltered();
1468
+ if (multi && selected.size === 0)
1469
+ return "At least one target language is required.";
1470
+ if (!multi && !f[cursor]) return "Please select a language.";
1344
1471
  return void 0;
1345
1472
  },
1346
1473
  render() {
@@ -1349,11 +1476,13 @@ async function filterableBranchSelect(params) {
1349
1476
  const hdr = `${dim2(S_BAR2)}
1350
1477
  ${symbol2(this.state)} ${message}
1351
1478
  `;
1352
- const hint = filter.length > 0 ? filter : dim2("type to filter or add pattern, \u2191\u2193 navigate, space select");
1479
+ const hint = filter.length > 0 ? filter : dim2(
1480
+ `type to filter, \u2191\u2193 navigate${multi ? ", space select" : ""}`
1481
+ );
1353
1482
  switch (this.state) {
1354
1483
  case "submit": {
1355
- const summary = selected.size > 0 ? bld2(Array.from(selected).join(", ")) : dim2("none");
1356
- return `${hdr}${dim2(S_BAR2)} ${summary}`;
1484
+ const val = multi ? Array.from(selected).map((id) => options.find((o) => o.bcp47 === id)?.label ?? id).join(", ") : options.find((o) => o.bcp47 === this.value)?.label ?? "";
1485
+ return `${hdr}${dim2(S_BAR2)} ${bld2(val || dim2("none"))}`;
1357
1486
  }
1358
1487
  case "cancel":
1359
1488
  return `${hdr}${dim2(S_BAR2)}`;
@@ -1361,7 +1490,12 @@ ${symbol2(this.state)} ${message}
1361
1490
  return [
1362
1491
  hdr.trimEnd(),
1363
1492
  `${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
1364
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1493
+ buildList2(
1494
+ filtered,
1495
+ cursor,
1496
+ scrollOffset,
1497
+ multi ? selected : null
1498
+ ),
1365
1499
  `${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
1366
1500
  ""
1367
1501
  ].join("\n");
@@ -1369,7 +1503,12 @@ ${symbol2(this.state)} ${message}
1369
1503
  return [
1370
1504
  hdr.trimEnd(),
1371
1505
  `${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
1372
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1506
+ buildList2(
1507
+ filtered,
1508
+ cursor,
1509
+ scrollOffset,
1510
+ multi ? selected : null
1511
+ ),
1373
1512
  `${cyan2(S_BAR_END2)}`,
1374
1513
  ""
1375
1514
  ].join("\n");
@@ -1377,6 +1516,7 @@ ${symbol2(this.state)} ${message}
1377
1516
  }
1378
1517
  },
1379
1518
  false
1519
+ // trackValue=false — we manage value manually
1380
1520
  );
1381
1521
  prompt.on("key", (key) => {
1382
1522
  if (!key || key === " ") return;
@@ -1385,59 +1525,76 @@ ${symbol2(this.state)} ${message}
1385
1525
  filter = filter.slice(0, -1);
1386
1526
  cursor = 0;
1387
1527
  scrollOffset = 0;
1388
- addCursor = false;
1389
1528
  } else if (cp >= 32 && cp !== 127) {
1390
1529
  filter += key;
1391
1530
  cursor = 0;
1392
1531
  scrollOffset = 0;
1393
- addCursor = false;
1394
1532
  }
1395
1533
  });
1396
1534
  prompt.on("cursor", (action) => {
1397
1535
  const filtered = getFiltered();
1398
- const hasAdd = isNewPattern();
1399
1536
  switch (action) {
1400
1537
  case "up":
1401
- if (addCursor) {
1402
- addCursor = false;
1403
- cursor = Math.max(0, filtered.length - 1);
1404
- } else cursor = Math.max(0, cursor - 1);
1538
+ cursor = Math.max(0, cursor - 1);
1405
1539
  break;
1406
1540
  case "down":
1407
- if (!addCursor && cursor >= filtered.length - 1 && hasAdd) addCursor = true;
1408
- else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
1541
+ cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
1409
1542
  break;
1410
1543
  case "space":
1411
- if (addCursor) {
1412
- const t = filter.trim();
1413
- const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
1414
- if (!err) {
1415
- customPatterns.push(t);
1416
- selected.add(t);
1417
- filter = "";
1418
- cursor = 0;
1419
- scrollOffset = 0;
1420
- addCursor = false;
1421
- }
1422
- } else {
1423
- const item = filtered[cursor];
1424
- if (item) {
1425
- if (selected.has(item.value)) selected.delete(item.value);
1426
- else selected.add(item.value);
1544
+ if (multi) {
1545
+ const opt = filtered[cursor];
1546
+ if (opt) {
1547
+ if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
1548
+ else selected.add(opt.bcp47);
1427
1549
  }
1428
1550
  }
1429
1551
  break;
1430
1552
  }
1553
+ if (!multi) {
1554
+ const opt = getFiltered()[cursor];
1555
+ prompt.value = opt?.bcp47 ?? null;
1556
+ }
1431
1557
  });
1432
1558
  prompt.on("finalize", () => {
1433
1559
  if (prompt.state === "submit") {
1434
- prompt.value = Array.from(selected);
1560
+ if (multi) {
1561
+ prompt.value = Array.from(selected);
1562
+ } else {
1563
+ const f = getFiltered();
1564
+ prompt.value = f[cursor]?.bcp47 ?? null;
1565
+ }
1435
1566
  }
1436
1567
  });
1437
1568
  const result = await prompt.prompt();
1438
1569
  if (isCancel3(result)) return null;
1439
1570
  return result;
1440
1571
  }
1572
+ async function searchSelectLocale(options, message, initialValue) {
1573
+ const result = await runFilterablePrompt({
1574
+ message,
1575
+ options,
1576
+ multi: false,
1577
+ initialValue
1578
+ });
1579
+ return typeof result === "string" ? result : null;
1580
+ }
1581
+ async function searchMultiSelectLocales(options, message, initialValues) {
1582
+ const result = await runFilterablePrompt({
1583
+ message,
1584
+ options,
1585
+ multi: true,
1586
+ initialValues
1587
+ });
1588
+ if (result === null) return null;
1589
+ const picks = result;
1590
+ if (picks.length === 0) {
1591
+ p2.log.warn(
1592
+ "At least one target language is required. Please select at least one."
1593
+ );
1594
+ return searchMultiSelectLocales(options, message, initialValues);
1595
+ }
1596
+ return picks;
1597
+ }
1441
1598
 
1442
1599
  // src/utils/project-create.ts
1443
1600
  function buildLocaleOptions(locales) {
@@ -1466,7 +1623,9 @@ async function runProjectCreate(params) {
1466
1623
  try {
1467
1624
  rawLocales = await api.listLocales(userToken);
1468
1625
  } catch {
1469
- p3.log.error("Failed to fetch supported locales. Check your connection and try again.");
1626
+ p3.log.error(
1627
+ "Failed to fetch supported locales. Check your connection and try again."
1628
+ );
1470
1629
  return null;
1471
1630
  }
1472
1631
  const languageOptions = buildLanguageOptions(rawLocales);
@@ -1483,7 +1642,8 @@ async function runProjectCreate(params) {
1483
1642
  validate(value) {
1484
1643
  const v = value.trim();
1485
1644
  if (!v) return;
1486
- if (v.startsWith("/")) return "Use a relative path, not an absolute path";
1645
+ if (v.startsWith("/"))
1646
+ return "Use a relative path, not an absolute path";
1487
1647
  if (v.includes("..")) return 'Path must not contain ".."';
1488
1648
  }
1489
1649
  });
@@ -1496,14 +1656,18 @@ async function runProjectCreate(params) {
1496
1656
  params.defaultSourceLocale ?? "en"
1497
1657
  );
1498
1658
  if (sourceLocale === null) return null;
1499
- const targetOptions = localeOptions.filter((opt) => opt.bcp47 !== sourceLocale);
1659
+ const targetOptions = localeOptions.filter(
1660
+ (opt) => opt.bcp47 !== sourceLocale
1661
+ );
1500
1662
  const targetLocales = await searchMultiSelectLocales(
1501
1663
  targetOptions,
1502
1664
  "Target languages (languages to translate into)"
1503
1665
  );
1504
1666
  if (targetLocales === null) return null;
1505
1667
  if (targetLocales.length === 0) {
1506
- p3.log.warn("No target languages selected \u2014 you can add them later from the dashboard.");
1668
+ p3.log.warn(
1669
+ "No target languages selected \u2014 you can add them later from the dashboard."
1670
+ );
1507
1671
  }
1508
1672
  const detected = detectGitBranches();
1509
1673
  const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
@@ -1519,7 +1683,9 @@ async function runProjectCreate(params) {
1519
1683
  });
1520
1684
  if (result === null) return null;
1521
1685
  if (result.length === 0) {
1522
- p3.log.warn("At least one branch is required. Please select at least one.");
1686
+ p3.log.warn(
1687
+ "At least one branch is required. Please select at least one."
1688
+ );
1523
1689
  initial = [detected.defaultBranch];
1524
1690
  } else {
1525
1691
  pushBranches = result;
@@ -1552,7 +1718,9 @@ async function runProjectAppCreate(params) {
1552
1718
  try {
1553
1719
  rawLocales = await api.listLocales(userToken);
1554
1720
  } catch {
1555
- p3.log.error("Failed to fetch supported locales. Check your connection and try again.");
1721
+ p3.log.error(
1722
+ "Failed to fetch supported locales. Check your connection and try again."
1723
+ );
1556
1724
  return null;
1557
1725
  }
1558
1726
  const languageOptions = buildLanguageOptions(rawLocales);
@@ -1573,11 +1741,15 @@ async function runProjectAppCreate(params) {
1573
1741
  initialValue: params.defaultAppDir ?? "",
1574
1742
  validate(value) {
1575
1743
  const v = value.trim();
1576
- if (!v && hasWholeRepoApp) return "This project already covers the entire repo.";
1577
- if (!v) return "App directory is required when other apps already exist.";
1578
- if (v.startsWith("/")) return "Use a relative path, not an absolute path.";
1744
+ if (!v && hasWholeRepoApp)
1745
+ return "This project already covers the entire repo.";
1746
+ if (!v)
1747
+ return "App directory is required when other apps already exist.";
1748
+ if (v.startsWith("/"))
1749
+ return "Use a relative path, not an absolute path.";
1579
1750
  if (v.includes("..")) return 'Path must not contain "..".';
1580
- if (existingScopes.has(v)) return `"${v}" is already configured. Choose a different directory.`;
1751
+ if (existingScopes.has(v))
1752
+ return `"${v}" is already configured. Choose a different directory.`;
1581
1753
  }
1582
1754
  });
1583
1755
  if (p3.isCancel(rawScope)) return null;
@@ -1589,14 +1761,18 @@ async function runProjectAppCreate(params) {
1589
1761
  "en"
1590
1762
  );
1591
1763
  if (sourceLocale === null) return null;
1592
- const targetOptions = localeOptions.filter((opt) => opt.bcp47 !== sourceLocale);
1764
+ const targetOptions = localeOptions.filter(
1765
+ (opt) => opt.bcp47 !== sourceLocale
1766
+ );
1593
1767
  const targetLocales = await searchMultiSelectLocales(
1594
1768
  targetOptions,
1595
1769
  "Target languages"
1596
1770
  );
1597
1771
  if (targetLocales === null) return null;
1598
1772
  if (targetLocales.length === 0) {
1599
- p3.log.warn("No target languages selected \u2014 you can add them later from the dashboard.");
1773
+ p3.log.warn(
1774
+ "No target languages selected \u2014 you can add them later from the dashboard."
1775
+ );
1600
1776
  }
1601
1777
  const detectedApp = detectGitBranches();
1602
1778
  let appPushBranches = [];
@@ -1628,7 +1804,9 @@ async function runProjectAppCreate(params) {
1628
1804
  targetBranches,
1629
1805
  repoCanonical: repoCanonical ?? ""
1630
1806
  });
1631
- p3.log.success(`App ${chalk4.bold(appDir)} added to ${chalk4.bold(projectName)}!`);
1807
+ p3.log.success(
1808
+ `App ${chalk4.bold(appDir)} added to ${chalk4.bold(projectName)}!`
1809
+ );
1632
1810
  return {
1633
1811
  projectId: result.projectId,
1634
1812
  projectName: result.projectName,
@@ -1682,7 +1860,6 @@ async function selectWorkspace(result) {
1682
1860
  }
1683
1861
 
1684
1862
  // src/commands/init.ts
1685
- import { spawn as spawn2 } from "child_process";
1686
1863
  loadEnv();
1687
1864
  var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
1688
1865
  async function sleep(ms) {
@@ -1755,7 +1932,10 @@ function runScaffold(params) {
1755
1932
  }
1756
1933
  const packagesToInstall = getPackagesToInstall(detection);
1757
1934
  if (packagesToInstall.length > 0) {
1758
- const installCmd = buildInstallCommand(detection.packageManager, packagesToInstall);
1935
+ const installCmd = buildInstallCommand(
1936
+ detection.packageManager,
1937
+ packagesToInstall
1938
+ );
1759
1939
  p5.log.info("");
1760
1940
  const installSpinner = p5.spinner();
1761
1941
  installSpinner.start(`Installing ${packagesToInstall.join(", ")}...`);
@@ -1778,12 +1958,16 @@ function runScaffold(params) {
1778
1958
  let stepNum = 1;
1779
1959
  if (snippets.pluginStep) {
1780
1960
  p5.log.message("");
1781
- p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`);
1961
+ p5.log.step(
1962
+ `${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`
1963
+ );
1782
1964
  printCodeBlock(snippets.pluginStep.code);
1783
1965
  stepNum++;
1784
1966
  }
1785
1967
  if (snippets.providerStep) {
1786
- p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`);
1968
+ p5.log.step(
1969
+ `${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`
1970
+ );
1787
1971
  printCodeBlock(snippets.providerStep.code);
1788
1972
  stepNum++;
1789
1973
  }
@@ -1805,6 +1989,7 @@ function printMcpSetup(apiKey) {
1805
1989
  type: "stdio",
1806
1990
  command: "npx",
1807
1991
  args: ["-y", "@vocoder/mcp"],
1992
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: MCP config template, not a JS template literal
1808
1993
  env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
1809
1994
  }
1810
1995
  }
@@ -1818,7 +2003,9 @@ function printMcpSetup(apiKey) {
1818
2003
  p5.log.message("");
1819
2004
  printCodeBlock(addCommand);
1820
2005
  p5.log.message("");
1821
- p5.log.message("To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference");
2006
+ p5.log.message(
2007
+ "To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference"
2008
+ );
1822
2009
  p5.log.message("so each developer supplies their own key:");
1823
2010
  p5.log.message("");
1824
2011
  printCodeBlock(teamConfig);
@@ -1827,19 +2014,26 @@ function printMcpSetup(apiKey) {
1827
2014
  }
1828
2015
  function printCodeBlock(code) {
1829
2016
  const lines = code.split("\n");
1830
- const maxLen = lines.reduce((max, line) => Math.max(max, line.length), 0);
2017
+ const maxLen = lines.reduce(
2018
+ (max, line) => Math.max(max, line.length),
2019
+ 0
2020
+ );
1831
2021
  const bar = chalk6.gray("\u2502");
1832
2022
  const pad = (s) => s + " ".repeat(maxLen - s.length);
1833
2023
  process.stdout.write(`${chalk6.gray("\u2502")}
1834
2024
  `);
1835
- process.stdout.write(`${chalk6.gray("\u2502")} ${chalk6.gray("\u250C" + "\u2500".repeat(maxLen + 2) + "\u2510")}
1836
- `);
2025
+ process.stdout.write(
2026
+ `${chalk6.gray("\u2502")} ${chalk6.gray(`\u250C${"\u2500".repeat(maxLen + 2)}\u2510`)}
2027
+ `
2028
+ );
1837
2029
  for (const line of lines) {
1838
2030
  process.stdout.write(`${chalk6.gray("\u2502")} ${bar} ${pad(line)} ${bar}
1839
2031
  `);
1840
2032
  }
1841
- process.stdout.write(`${chalk6.gray("\u2502")} ${chalk6.gray("\u2514" + "\u2500".repeat(maxLen + 2) + "\u2518")}
1842
- `);
2033
+ process.stdout.write(
2034
+ `${chalk6.gray("\u2502")} ${chalk6.gray(`\u2514${"\u2500".repeat(maxLen + 2)}\u2518`)}
2035
+ `
2036
+ );
1843
2037
  }
1844
2038
  async function verifyStoredToken(api, token) {
1845
2039
  try {
@@ -1871,14 +2065,18 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1871
2065
  } else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
1872
2066
  if (reauth) {
1873
2067
  if (!options.yes) {
1874
- const shouldOpen = await p5.confirm({ message: "Open your browser to sign in again?" });
2068
+ const shouldOpen = await p5.confirm({
2069
+ message: "Open your browser to sign in again?"
2070
+ });
1875
2071
  if (p5.isCancel(shouldOpen)) {
1876
2072
  server?.close();
1877
2073
  p5.cancel("Setup cancelled.");
1878
2074
  return null;
1879
2075
  }
1880
2076
  if (!shouldOpen) {
1881
- p5.log.info("Open the URL above manually in your browser to continue.");
2077
+ p5.log.info(
2078
+ "Open the URL above manually in your browser to continue."
2079
+ );
1882
2080
  } else {
1883
2081
  const opened = await tryOpenBrowser2(browserUrl);
1884
2082
  if (!opened) {
@@ -1895,7 +2093,11 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1895
2093
  const connectChoice = await p5.select({
1896
2094
  message: "Vocoder needs to be installed on your GitHub account to get started",
1897
2095
  options: [
1898
- { value: "install", label: "Install GitHub App", hint: "recommended" },
2096
+ {
2097
+ value: "install",
2098
+ label: "Install GitHub App",
2099
+ hint: "recommended"
2100
+ },
1899
2101
  { value: "link", label: "Already installed? Link your account" }
1900
2102
  ]
1901
2103
  });
@@ -1937,7 +2139,9 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1937
2139
  const timeoutMs = deadline - Date.now();
1938
2140
  const params = await Promise.race([
1939
2141
  server.waitForCallback(),
1940
- new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
2142
+ new Promise(
2143
+ (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
2144
+ )
1941
2145
  ]);
1942
2146
  if (params && typeof params.token === "string") {
1943
2147
  rawToken = params.token;
@@ -1978,7 +2182,12 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1978
2182
  }
1979
2183
  const userInfo = await api.getCliUserInfo(rawToken);
1980
2184
  authSpinner.stop(`Authenticated as ${chalk6.bold(userInfo.email)}`);
1981
- return { token: rawToken, ...userInfo, organizationId: callbackOrganizationId, discoveryReady: callbackDiscoveryReady };
2185
+ return {
2186
+ token: rawToken,
2187
+ ...userInfo,
2188
+ organizationId: callbackOrganizationId,
2189
+ discoveryReady: callbackDiscoveryReady
2190
+ };
1982
2191
  }
1983
2192
  async function init(options = {}) {
1984
2193
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
@@ -2003,7 +2212,9 @@ async function init(options = {}) {
2003
2212
  if (lookup.exactMatch) {
2004
2213
  const { exactMatch } = lookup;
2005
2214
  p5.log.success(`Project: ${chalk6.bold(exactMatch.projectName)}`);
2006
- p5.log.info(`Branches: ${chalk6.cyan((exactMatch.targetBranches ?? ["main"]).join(", "))}`);
2215
+ p5.log.info(
2216
+ `Branches: ${chalk6.cyan((exactMatch.targetBranches ?? ["main"]).join(", "))}`
2217
+ );
2007
2218
  const needsKey = await p5.confirm({
2008
2219
  message: "Need to regenerate your API key?"
2009
2220
  });
@@ -2019,12 +2230,17 @@ async function init(options = {}) {
2019
2230
  const spinner4 = p5.spinner();
2020
2231
  spinner4.start("Generating new API key...");
2021
2232
  try {
2022
- const { apiKey } = await anonApi2.regenerateProjectApiKey(authResult.token, exactMatch.projectId);
2233
+ const { apiKey } = await anonApi2.regenerateProjectApiKey(
2234
+ authResult.token,
2235
+ exactMatch.projectId
2236
+ );
2023
2237
  spinner4.stop("New API key generated");
2024
2238
  printMcpSetup(apiKey);
2025
2239
  } catch {
2026
2240
  spinner4.stop("Failed to generate key");
2027
- p5.log.error("Could not generate API key. Try again or generate one from the dashboard.");
2241
+ p5.log.error(
2242
+ "Could not generate API key. Try again or generate one from the dashboard."
2243
+ );
2028
2244
  return 1;
2029
2245
  }
2030
2246
  }
@@ -2087,7 +2303,12 @@ async function init(options = {}) {
2087
2303
  });
2088
2304
  }
2089
2305
  } else {
2090
- const authResult = await runAuthFlow(api, options, false, identity?.repoCanonical);
2306
+ const authResult = await runAuthFlow(
2307
+ api,
2308
+ options,
2309
+ false,
2310
+ identity?.repoCanonical
2311
+ );
2091
2312
  if (!authResult) return 1;
2092
2313
  userToken = authResult.token;
2093
2314
  userEmail = authResult.email;
@@ -2106,10 +2327,14 @@ async function init(options = {}) {
2106
2327
  let selectedWorkspaceName;
2107
2328
  if (authOrganizationId) {
2108
2329
  const workspaceData = await api.listWorkspaces(userToken);
2109
- const ws = workspaceData.workspaces.find((w) => w.id === authOrganizationId);
2330
+ const ws = workspaceData.workspaces.find(
2331
+ (w) => w.id === authOrganizationId
2332
+ );
2110
2333
  selectedWorkspaceId = authOrganizationId;
2111
2334
  selectedWorkspaceName = ws?.name ?? userEmail;
2112
- p5.log.success(`Connected as ${chalk6.bold(userEmail)} \u2014 workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2335
+ p5.log.success(
2336
+ `Connected as ${chalk6.bold(userEmail)} \u2014 workspace: ${chalk6.bold(selectedWorkspaceName)}`
2337
+ );
2113
2338
  } else {
2114
2339
  const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
2115
2340
  const cachedInstallations = discoveryResult?.installations ?? [];
@@ -2148,7 +2373,9 @@ async function init(options = {}) {
2148
2373
  );
2149
2374
  }
2150
2375
  if (selectedInstallationId === null || selectedInstallationId === "install_new") {
2151
- p5.cancel("Setup cancelled. Re-run `vocoder init` and choose Install GitHub App.");
2376
+ p5.cancel(
2377
+ "Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
2378
+ );
2152
2379
  return 1;
2153
2380
  }
2154
2381
  const claimResult = await api.claimCliGitHubInstallation(userToken, {
@@ -2164,7 +2391,9 @@ async function init(options = {}) {
2164
2391
  });
2165
2392
  const repoCanonical = identity?.repoCanonical ?? null;
2166
2393
  const covering = repoCanonical ? workspaceData.workspaces.filter((w) => w.coversRepo === true) : [];
2167
- const connected = workspaceData.workspaces.filter((w) => w.hasGitHubConnection);
2394
+ const connected = workspaceData.workspaces.filter(
2395
+ (w) => w.hasGitHubConnection
2396
+ );
2168
2397
  if (repoCanonical && covering.length === 1) {
2169
2398
  const ws = covering[0];
2170
2399
  selectedWorkspaceId = ws.id;
@@ -2223,9 +2452,15 @@ async function init(options = {}) {
2223
2452
  );
2224
2453
  return 1;
2225
2454
  }
2226
- const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2455
+ const connectResult = await runGitHubInstallFlow({
2456
+ api,
2457
+ userToken,
2458
+ yes: options.yes
2459
+ });
2227
2460
  if (!connectResult) {
2228
- p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
2461
+ p5.log.error(
2462
+ "GitHub App installation did not complete. Run `vocoder init` again."
2463
+ );
2229
2464
  return 1;
2230
2465
  }
2231
2466
  selectedWorkspaceId = connectResult.organizationId;
@@ -2260,22 +2495,42 @@ async function init(options = {}) {
2260
2495
  return 1;
2261
2496
  }
2262
2497
  if (connectChoice === "install") {
2263
- const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2498
+ const connectResult = await runGitHubInstallFlow({
2499
+ api,
2500
+ userToken,
2501
+ yes: options.yes
2502
+ });
2264
2503
  if (!connectResult) {
2265
- p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
2504
+ p5.log.error(
2505
+ "GitHub App installation did not complete. Run `vocoder init` again."
2506
+ );
2266
2507
  return 1;
2267
2508
  }
2268
2509
  selectedWorkspaceId = connectResult.organizationId;
2269
2510
  selectedWorkspaceName = connectResult.organizationName;
2270
- p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2511
+ p5.log.success(
2512
+ `Workspace: ${chalk6.bold(selectedWorkspaceName)}`
2513
+ );
2271
2514
  } else {
2272
- const installations = await runGitHubDiscoveryFlow({ api, userToken, yes: options.yes });
2515
+ const installations = await runGitHubDiscoveryFlow({
2516
+ api,
2517
+ userToken,
2518
+ yes: options.yes
2519
+ });
2273
2520
  if (!installations) return 1;
2274
2521
  if (installations.length === 0) {
2275
- p5.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
2276
- const installNow = await p5.confirm({ message: "Open GitHub to install the App?" });
2522
+ p5.log.warn(
2523
+ "No GitHub installations found. Install the Vocoder GitHub App first."
2524
+ );
2525
+ const installNow = await p5.confirm({
2526
+ message: "Open GitHub to install the App?"
2527
+ });
2277
2528
  if (p5.isCancel(installNow) || !installNow) return 1;
2278
- const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2529
+ const connectResult = await runGitHubInstallFlow({
2530
+ api,
2531
+ userToken,
2532
+ yes: options.yes
2533
+ });
2279
2534
  if (!connectResult) return 1;
2280
2535
  selectedWorkspaceId = connectResult.organizationId;
2281
2536
  selectedWorkspaceName = connectResult.organizationName;
@@ -2295,20 +2550,29 @@ async function init(options = {}) {
2295
2550
  return 1;
2296
2551
  }
2297
2552
  if (selectedInstallationId === "install_new") {
2298
- const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2553
+ const connectResult = await runGitHubInstallFlow({
2554
+ api,
2555
+ userToken,
2556
+ yes: options.yes
2557
+ });
2299
2558
  if (!connectResult) return 1;
2300
2559
  selectedWorkspaceId = connectResult.organizationId;
2301
2560
  selectedWorkspaceName = connectResult.organizationName;
2302
2561
  } else {
2303
- const claimResult = await api.claimCliGitHubInstallation(userToken, {
2304
- installationId: String(selectedInstallationId),
2305
- organizationId: null
2306
- });
2562
+ const claimResult = await api.claimCliGitHubInstallation(
2563
+ userToken,
2564
+ {
2565
+ installationId: String(selectedInstallationId),
2566
+ organizationId: null
2567
+ }
2568
+ );
2307
2569
  selectedWorkspaceId = claimResult.organizationId;
2308
2570
  selectedWorkspaceName = claimResult.organizationName;
2309
2571
  }
2310
2572
  }
2311
- p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2573
+ p5.log.success(
2574
+ `Workspace: ${chalk6.bold(selectedWorkspaceName)}`
2575
+ );
2312
2576
  }
2313
2577
  }
2314
2578
  }
@@ -2368,10 +2632,15 @@ async function init(options = {}) {
2368
2632
  }
2369
2633
  if (limitAction === "upgrade") {
2370
2634
  await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
2371
- p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
2635
+ p5.cancel(
2636
+ "Upgrade your plan in the browser, then re-run `vocoder init`."
2637
+ );
2372
2638
  return 1;
2373
2639
  }
2374
- const existingProjects = await api.listProjects(userToken, selectedWorkspaceId);
2640
+ const existingProjects = await api.listProjects(
2641
+ userToken,
2642
+ selectedWorkspaceId
2643
+ );
2375
2644
  if (existingProjects.length === 0) {
2376
2645
  p5.log.error("No projects found in this workspace.");
2377
2646
  return 1;
@@ -2478,8 +2747,11 @@ async function logout(options = {}) {
2478
2747
  }
2479
2748
 
2480
2749
  // src/commands/sync.ts
2481
- import * as p7 from "@clack/prompts";
2482
2750
  import { createHash, randomUUID } from "crypto";
2751
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2752
+ import { join as join2 } from "path";
2753
+ import * as p7 from "@clack/prompts";
2754
+ import chalk8 from "chalk";
2483
2755
 
2484
2756
  // src/utils/branch.ts
2485
2757
  import { execSync as execSync4 } from "child_process";
@@ -2509,7 +2781,7 @@ function detectBranch(override) {
2509
2781
  stdio: ["pipe", "pipe", "ignore"]
2510
2782
  }).trim();
2511
2783
  return branch;
2512
- } catch (error) {
2784
+ } catch (_error) {
2513
2785
  throw new Error(
2514
2786
  "Failed to detect git branch. Make sure you are in a git repository or set the --branch flag."
2515
2787
  );
@@ -2547,9 +2819,6 @@ function matchBranchPattern(branch, pattern) {
2547
2819
  return new RegExp(regexSource).test(branch);
2548
2820
  }
2549
2821
 
2550
- // src/commands/sync.ts
2551
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2552
-
2553
2822
  // src/utils/config.ts
2554
2823
  import chalk7 from "chalk";
2555
2824
  import { config as loadEnv2 } from "dotenv";
@@ -2560,9 +2829,13 @@ function validateLocalConfig(config) {
2560
2829
  }
2561
2830
  if (!config.apiKey.startsWith("vcp_")) {
2562
2831
  if (config.apiKey.startsWith("vco_") || config.apiKey.startsWith("vcu_")) {
2563
- throw new Error("VOCODER_API_KEY must be a project-scoped key (starts with vcp_). Got an org or user key.");
2832
+ throw new Error(
2833
+ "VOCODER_API_KEY must be a project-scoped key (starts with vcp_). Got an org or user key."
2834
+ );
2564
2835
  }
2565
- throw new Error("Invalid API key format. Expected a project API key starting with vcp_.");
2836
+ throw new Error(
2837
+ "Invalid API key format. Expected a project API key starting with vcp_."
2838
+ );
2566
2839
  }
2567
2840
  if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
2568
2841
  throw new Error("Invalid API URL");
@@ -2666,14 +2939,20 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2666
2939
  noFallback = cliOptions.noFallback;
2667
2940
  configSources.noFallback = "CLI flag";
2668
2941
  } else if (envSyncNoFallback) {
2669
- noFallback = ["1", "true", "yes", "on"].includes(envSyncNoFallback.toLowerCase());
2942
+ noFallback = ["1", "true", "yes", "on"].includes(
2943
+ envSyncNoFallback.toLowerCase()
2944
+ );
2670
2945
  configSources.noFallback = "environment";
2671
2946
  }
2672
2947
  if (verbose) {
2673
2948
  console.log(chalk7.dim("\n Configuration sources:"));
2674
- console.log(chalk7.dim(` Include patterns: ${configSources.includePattern}`));
2949
+ console.log(
2950
+ chalk7.dim(` Include patterns: ${configSources.includePattern}`)
2951
+ );
2675
2952
  if (excludePattern.length > 0) {
2676
- console.log(chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`));
2953
+ console.log(
2954
+ chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`)
2955
+ );
2677
2956
  }
2678
2957
  console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
2679
2958
  console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
@@ -2698,8 +2977,6 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2698
2977
  }
2699
2978
 
2700
2979
  // src/commands/sync.ts
2701
- import chalk8 from "chalk";
2702
- import { join as join2 } from "path";
2703
2980
  function computeStringsHash(texts) {
2704
2981
  const sorted = [...texts].sort();
2705
2982
  return createHash("sha256").update(sorted.join("\0")).digest("hex").slice(0, 16);
@@ -2709,7 +2986,8 @@ function readCachedStringsHash(projectRoot, branch) {
2709
2986
  if (!existsSync(filePath)) return null;
2710
2987
  try {
2711
2988
  const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2712
- if (isRecord(raw) && typeof raw.stringsHash === "string") return raw.stringsHash;
2989
+ if (isRecord(raw) && typeof raw.stringsHash === "string")
2990
+ return raw.stringsHash;
2713
2991
  } catch {
2714
2992
  }
2715
2993
  return null;
@@ -2759,7 +3037,14 @@ function parseTranslations(value) {
2759
3037
  }
2760
3038
  function getCacheFilePath(projectRoot, branch) {
2761
3039
  const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
2762
- return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", `${branchHash}.json`);
3040
+ return join2(
3041
+ projectRoot,
3042
+ "node_modules",
3043
+ ".vocoder",
3044
+ "cache",
3045
+ "sync",
3046
+ `${branchHash}.json`
3047
+ );
2763
3048
  }
2764
3049
  function readLocalSnapshotCache(params) {
2765
3050
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
@@ -2788,16 +3073,18 @@ function readLocalSnapshotCache(params) {
2788
3073
  cacheBranch: candidateBranch
2789
3074
  };
2790
3075
  } catch {
2791
- continue;
2792
3076
  }
2793
3077
  }
2794
3078
  return null;
2795
3079
  }
2796
3080
  function writeLocalSnapshotCache(params) {
2797
3081
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
2798
- mkdirSync2(join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"), {
2799
- recursive: true
2800
- });
3082
+ mkdirSync2(
3083
+ join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
3084
+ {
3085
+ recursive: true
3086
+ }
3087
+ );
2801
3088
  const payload = {
2802
3089
  version: 1,
2803
3090
  branch: params.branch,
@@ -2894,7 +3181,9 @@ function getSyncPolicyErrorGuidance(error) {
2894
3181
  if (error.branch) {
2895
3182
  lines2.push(`Current branch: ${error.branch}`);
2896
3183
  }
2897
- lines2.push("Update your project target branches in the dashboard if needed.");
3184
+ lines2.push(
3185
+ "Update your project target branches in the dashboard if needed."
3186
+ );
2898
3187
  return lines2;
2899
3188
  }
2900
3189
  const lines = ["This project is bound to a different repository."];
@@ -3010,7 +3299,9 @@ async function sync(options = {}) {
3010
3299
  );
3011
3300
  if (extractedStrings.length === 0) {
3012
3301
  spinner4.stop("No translatable strings found");
3013
- p7.log.warn("Make sure you are wrapping translatable strings with Vocoder");
3302
+ p7.log.warn(
3303
+ "Make sure you are wrapping translatable strings with Vocoder"
3304
+ );
3014
3305
  p7.outro("");
3015
3306
  return 0;
3016
3307
  }
@@ -3074,7 +3365,9 @@ async function sync(options = {}) {
3074
3365
  },
3075
3366
  repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
3076
3367
  );
3077
- spinner4.stop(`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`);
3368
+ spinner4.stop(
3369
+ `Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`
3370
+ );
3078
3371
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
3079
3372
  branch,
3080
3373
  requestedMode,
@@ -3250,11 +3543,15 @@ async function sync(options = {}) {
3250
3543
  if (error instanceof Error) {
3251
3544
  p7.log.error(error.message);
3252
3545
  if (error.message.includes("VOCODER_API_KEY")) {
3253
- p7.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
3546
+ p7.log.warn(
3547
+ "VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
3548
+ );
3254
3549
  p7.log.info(" Create one at: https://vocoder.app/dashboard");
3255
3550
  p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
3256
3551
  p7.log.info("");
3257
- p7.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
3552
+ p7.log.info(
3553
+ " Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
3554
+ );
3258
3555
  p7.log.info(" Translations are fetched automatically at build time.");
3259
3556
  } else if (error.message.includes("git branch")) {
3260
3557
  p7.log.warn("Run from a git repository, or use:");
@@ -3288,7 +3585,9 @@ async function whoami(options = {}) {
3288
3585
  p8.log.info(`API: ${apiUrl}`);
3289
3586
  return 0;
3290
3587
  } catch {
3291
- p8.log.error("Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate.");
3588
+ p8.log.error(
3589
+ "Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
3590
+ );
3292
3591
  return 1;
3293
3592
  }
3294
3593
  }
@@ -3303,7 +3602,13 @@ async function runCommand(command, options) {
3303
3602
  }
3304
3603
  var program = new Command();
3305
3604
  program.name("vocoder").description("Vocoder CLI - Project setup and string extraction").version("0.1.5");
3306
- program.command("init").description("Authenticate and provision Vocoder for this project").option("--api-url <url>", "Override Vocoder API URL").option("--yes", "Allow overwriting existing local config values").option("--ci", "Non-interactive mode: print auth URL to stdout, skip browser open").option("--project-name <name>", "Starter project name to create").option("--source-locale <locale>", "Source locale for the starter project").option("--target-locales <list>", "Comma-separated target locales (e.g. es,fr,de)").action((options) => runCommand(init, options));
3605
+ program.command("init").description("Authenticate and provision Vocoder for this project").option("--api-url <url>", "Override Vocoder API URL").option("--yes", "Allow overwriting existing local config values").option(
3606
+ "--ci",
3607
+ "Non-interactive mode: print auth URL to stdout, skip browser open"
3608
+ ).option("--project-name <name>", "Starter project name to create").option("--source-locale <locale>", "Source locale for the starter project").option(
3609
+ "--target-locales <list>",
3610
+ "Comma-separated target locales (e.g. es,fr,de)"
3611
+ ).action((options) => runCommand(init, options));
3307
3612
  program.command("sync").description("Extract strings and sync translations").option("--branch <branch>", "Override detected branch").option("--mode <mode>", "Sync mode: auto, required, best-effort", "auto").option("--max-wait <ms>", "Max wait for translations (ms)").option("--force", "Force re-extraction even if no changes").option("--dry-run", "Preview without syncing").option("--no-fallback", "Disable fallback to cached translations").option("--include <pattern>", "Include glob pattern", collect, []).option("--exclude <pattern>", "Exclude glob pattern", collect, []).option("--verbose", "Detailed output").action((options) => {
3308
3613
  const translated = { ...options };
3309
3614
  if (options.maxWait) translated.maxWaitMs = Number(options.maxWait);