@vocoder/cli 0.1.6 → 0.1.8

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
@@ -1,11 +1,320 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ StringExtractor,
4
+ buildInstallCommand,
5
+ detectLocalEcosystem,
6
+ getPackagesToInstall,
7
+ getSetupSnippets
8
+ } from "./chunk-OC5N5C5X.mjs";
2
9
 
3
10
  // src/bin.ts
4
11
  import { Command } from "commander";
5
12
 
6
13
  // src/commands/init.ts
14
+ 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
7
60
  import * as p from "@clack/prompts";
8
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({ value: "install_new", label: "Install on a new account" });
309
+ }
310
+ const selected = await p.select({
311
+ message: "Select a GitHub installation",
312
+ options
313
+ });
314
+ if (p.isCancel(selected)) return null;
315
+ if (selected === "install_new") return "install_new";
316
+ return Number(selected);
317
+ }
9
318
 
10
319
  // src/utils/api.ts
11
320
  function isLimitErrorResponse(value) {
@@ -117,10 +426,10 @@ var VocoderAPI = class {
117
426
  * Submit strings for translation
118
427
  * Project is determined from the API key
119
428
  */
120
- stableTextKey(text) {
429
+ stableTextKey(text2) {
121
430
  let hash = 2166136261;
122
- for (let i = 0; i < text.length; i++) {
123
- hash ^= text.charCodeAt(i);
431
+ for (let i = 0; i < text2.length; i++) {
432
+ hash ^= text2.charCodeAt(i);
124
433
  hash = Math.imul(hash, 16777619);
125
434
  }
126
435
  return `SK_TEXT_${(hash >>> 0).toString(16).toUpperCase().padStart(8, "0")}`;
@@ -131,9 +440,9 @@ var VocoderAPI = class {
131
440
  }
132
441
  const first = entries[0];
133
442
  if (typeof first === "string") {
134
- return entries.map((text) => ({
135
- key: this.stableTextKey(text),
136
- text
443
+ return entries.map((text2) => ({
444
+ key: this.stableTextKey(text2),
445
+ text: text2
137
446
  }));
138
447
  }
139
448
  return entries.map((entry, index) => ({
@@ -255,6 +564,245 @@ var VocoderAPI = class {
255
564
  }
256
565
  return payload;
257
566
  }
567
+ // ── CLI Auth endpoints (no project API key needed) ──────────────────────────
568
+ /**
569
+ * Start a CLI auth session. Returns `{ sessionId, verificationUrl, expiresAt }`.
570
+ * `sessionId` is the raw poll token — keep it secret, used for polling.
571
+ */
572
+ async startCliAuthSession(callbackPort, repoCanonical) {
573
+ const response = await fetch(`${this.apiUrl}/api/cli/auth/start`, {
574
+ method: "POST",
575
+ headers: { "Content-Type": "application/json" },
576
+ body: JSON.stringify({
577
+ ...callbackPort != null ? { callbackPort } : {},
578
+ ...repoCanonical ? { repoCanonical } : {}
579
+ })
580
+ });
581
+ const payload = await readPayload(response);
582
+ if (!response.ok) {
583
+ throw new VocoderAPIError({
584
+ message: extractErrorMessage(payload, `Failed to start auth session (${response.status})`),
585
+ status: response.status,
586
+ payload
587
+ });
588
+ }
589
+ return payload;
590
+ }
591
+ /**
592
+ * Poll for CLI auth session completion.
593
+ * Returns `{ token }` on success, throws on failure/expiry.
594
+ * The server returns HTTP 202 while still pending.
595
+ */
596
+ async pollCliAuthSession(pollToken) {
597
+ const response = await fetch(
598
+ `${this.apiUrl}/api/cli/auth/session?session=${encodeURIComponent(pollToken)}`
599
+ );
600
+ const payload = await readPayload(response);
601
+ if (response.status === 202) {
602
+ return { status: "pending" };
603
+ }
604
+ if (response.status === 410) {
605
+ return {
606
+ status: "failed",
607
+ reason: extractErrorMessage(payload, "Auth session expired or failed")
608
+ };
609
+ }
610
+ if (!response.ok) {
611
+ return {
612
+ status: "failed",
613
+ reason: extractErrorMessage(payload, `Auth session error (${response.status})`)
614
+ };
615
+ }
616
+ const result = payload;
617
+ if (!result.token) {
618
+ return { status: "failed", reason: "No token in response" };
619
+ }
620
+ return {
621
+ status: "complete",
622
+ token: result.token,
623
+ ...result.organizationId ? { organizationId: result.organizationId } : {}
624
+ };
625
+ }
626
+ /**
627
+ * Validate a CLI user token and return the authenticated user's info.
628
+ * Used by the CLI to verify stored credentials on startup.
629
+ */
630
+ async getCliUserInfo(userToken) {
631
+ const response = await fetch(`${this.apiUrl}/api/cli/auth/me`, {
632
+ headers: { Authorization: `Bearer ${userToken}` }
633
+ });
634
+ const payload = await readPayload(response);
635
+ if (!response.ok) {
636
+ throw new VocoderAPIError({
637
+ message: extractErrorMessage(payload, `Token validation failed (${response.status})`),
638
+ status: response.status,
639
+ payload
640
+ });
641
+ }
642
+ return payload;
643
+ }
644
+ /**
645
+ * Revoke the given CLI user token server-side.
646
+ */
647
+ async revokeCliToken(userToken) {
648
+ const response = await fetch(`${this.apiUrl}/api/cli/auth/token`, {
649
+ method: "DELETE",
650
+ headers: { Authorization: `Bearer ${userToken}` }
651
+ });
652
+ if (!response.ok) {
653
+ const payload = await readPayload(response);
654
+ throw new VocoderAPIError({
655
+ message: extractErrorMessage(payload, `Token revocation failed (${response.status})`),
656
+ status: response.status,
657
+ payload
658
+ });
659
+ }
660
+ }
661
+ // ── Workspaces ────────────────────────────────────────────────────────────────
662
+ async listWorkspaces(userToken) {
663
+ const response = await fetch(`${this.apiUrl}/api/cli/workspaces`, {
664
+ headers: { Authorization: `Bearer ${userToken}` }
665
+ });
666
+ const payload = await readPayload(response);
667
+ if (!response.ok) {
668
+ throw new VocoderAPIError({
669
+ message: extractErrorMessage(payload, `Failed to list workspaces (${response.status})`),
670
+ status: response.status,
671
+ payload
672
+ });
673
+ }
674
+ return payload;
675
+ }
676
+ // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
677
+ async startCliGitHubInstall(userToken, params) {
678
+ const response = await fetch(`${this.apiUrl}/api/cli/github/install/start`, {
679
+ method: "POST",
680
+ headers: {
681
+ Authorization: `Bearer ${userToken}`,
682
+ "Content-Type": "application/json"
683
+ },
684
+ body: JSON.stringify(params)
685
+ });
686
+ const payload = await readPayload(response);
687
+ if (!response.ok) {
688
+ throw new VocoderAPIError({
689
+ message: extractErrorMessage(payload, `Failed to start GitHub install (${response.status})`),
690
+ status: response.status,
691
+ payload
692
+ });
693
+ }
694
+ return payload;
695
+ }
696
+ /**
697
+ * Start the "link existing installation" discovery flow.
698
+ * Unlike startCliGitHubOAuth, this requires no bearer token — the Vocoder
699
+ * account is created from the OAuth code in the callback.
700
+ */
701
+ async startCliGitHubLinkSession(sessionId, callbackPort) {
702
+ const response = await fetch(`${this.apiUrl}/api/cli/github/oauth/link-start`, {
703
+ method: "POST",
704
+ headers: { "Content-Type": "application/json" },
705
+ body: JSON.stringify({ sessionId, ...callbackPort != null ? { callbackPort } : {} })
706
+ });
707
+ const payload = await readPayload(response);
708
+ if (!response.ok) {
709
+ throw new VocoderAPIError({
710
+ message: extractErrorMessage(payload, `Failed to start GitHub link session (${response.status})`),
711
+ status: response.status,
712
+ payload
713
+ });
714
+ }
715
+ return payload;
716
+ }
717
+ async startCliGitHubOAuth(userToken, params) {
718
+ const response = await fetch(`${this.apiUrl}/api/cli/github/oauth/start`, {
719
+ method: "POST",
720
+ headers: {
721
+ Authorization: `Bearer ${userToken}`,
722
+ "Content-Type": "application/json"
723
+ },
724
+ body: JSON.stringify(params)
725
+ });
726
+ const payload = await readPayload(response);
727
+ if (!response.ok) {
728
+ throw new VocoderAPIError({
729
+ message: extractErrorMessage(payload, `Failed to start GitHub OAuth (${response.status})`),
730
+ status: response.status,
731
+ payload
732
+ });
733
+ }
734
+ return payload;
735
+ }
736
+ async getCliGitHubDiscovery(userToken) {
737
+ const response = await fetch(`${this.apiUrl}/api/cli/github/discovery`, {
738
+ headers: { Authorization: `Bearer ${userToken}` }
739
+ });
740
+ const payload = await readPayload(response);
741
+ if (!response.ok) {
742
+ throw new VocoderAPIError({
743
+ message: extractErrorMessage(payload, `Failed to fetch GitHub discovery (${response.status})`),
744
+ status: response.status,
745
+ payload
746
+ });
747
+ }
748
+ return payload;
749
+ }
750
+ async claimCliGitHubInstallation(userToken, params) {
751
+ const response = await fetch(`${this.apiUrl}/api/cli/github/claim`, {
752
+ method: "POST",
753
+ headers: {
754
+ Authorization: `Bearer ${userToken}`,
755
+ "Content-Type": "application/json"
756
+ },
757
+ body: JSON.stringify(params)
758
+ });
759
+ const payload = await readPayload(response);
760
+ if (!response.ok) {
761
+ throw new VocoderAPIError({
762
+ message: extractErrorMessage(payload, `Failed to claim GitHub installation (${response.status})`),
763
+ status: response.status,
764
+ payload
765
+ });
766
+ }
767
+ return payload;
768
+ }
769
+ // ── Locales ───────────────────────────────────────────────────────────────────
770
+ async listLocales(userToken) {
771
+ const response = await fetch(`${this.apiUrl}/api/cli/locales`, {
772
+ headers: { Authorization: `Bearer ${userToken}` }
773
+ });
774
+ const payload = await readPayload(response);
775
+ if (!response.ok) {
776
+ throw new VocoderAPIError({
777
+ message: extractErrorMessage(payload, `Failed to list locales (${response.status})`),
778
+ status: response.status,
779
+ payload
780
+ });
781
+ }
782
+ const result = payload;
783
+ return result.locales;
784
+ }
785
+ // ── Project creation ──────────────────────────────────────────────────────────
786
+ async createProject(userToken, params) {
787
+ const response = await fetch(`${this.apiUrl}/api/cli/projects`, {
788
+ method: "POST",
789
+ headers: {
790
+ "Content-Type": "application/json",
791
+ Authorization: `Bearer ${userToken}`
792
+ },
793
+ body: JSON.stringify(params)
794
+ });
795
+ const payload = await readPayload(response);
796
+ if (!response.ok) {
797
+ throw new VocoderAPIError({
798
+ message: extractErrorMessage(payload, `Failed to create project (${response.status})`),
799
+ status: response.status,
800
+ payload
801
+ });
802
+ }
803
+ return payload;
804
+ }
805
+ // ── Project lookup ────────────────────────────────────────────────────────────
258
806
  /**
259
807
  * Look up whether a project already exists for a given repo + scope.
260
808
  * Returns { projectId, projectName, organizationName } or null if not found.
@@ -278,271 +826,10 @@ var VocoderAPI = class {
278
826
  }
279
827
  };
280
828
 
281
- // src/utils/detect-local.ts
282
- import { existsSync, readFileSync } from "fs";
283
- import { join } from "path";
284
- function detectLocalEcosystem(cwd = process.cwd()) {
285
- const packageManager = detectPackageManager(cwd);
286
- const pkg = readPackageJson(cwd);
287
- if (!pkg) {
288
- return {
289
- ecosystem: null,
290
- framework: null,
291
- packageManager,
292
- uiPackage: null,
293
- hasUnplugin: false,
294
- hasUiPackage: false,
295
- sourceLocale: null
296
- };
297
- }
298
- const allDeps = {
299
- ...pkg.dependencies ?? {},
300
- ...pkg.devDependencies ?? {}
301
- };
302
- const hasUnplugin = "@vocoder/unplugin" in allDeps;
303
- const { ecosystem, framework, uiPackage } = detectFromDeps(allDeps, cwd);
304
- const hasUiPackage = uiPackage !== null && uiPackage in allDeps;
305
- return {
306
- ecosystem,
307
- framework,
308
- packageManager,
309
- uiPackage,
310
- hasUnplugin,
311
- hasUiPackage,
312
- sourceLocale: null
313
- };
314
- }
315
- function detectPackageManager(cwd) {
316
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
317
- if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
318
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
319
- return "npm";
320
- }
321
- function readPackageJson(cwd) {
322
- const pkgPath = join(cwd, "package.json");
323
- if (!existsSync(pkgPath)) return null;
324
- try {
325
- return JSON.parse(readFileSync(pkgPath, "utf-8"));
326
- } catch {
327
- return null;
328
- }
329
- }
330
- function detectFromDeps(allDeps, cwd) {
331
- if ("vue" in allDeps) {
332
- const framework = "nuxt" in allDeps ? "nuxt" : null;
333
- return { ecosystem: "vue", framework, uiPackage: "@vocoder/vue" };
334
- }
335
- if ("svelte" in allDeps) {
336
- const framework = "@sveltejs/kit" in allDeps ? "sveltekit" : null;
337
- return { ecosystem: "svelte", framework, uiPackage: "@vocoder/svelte" };
338
- }
339
- if ("@angular/core" in allDeps || existsSync(join(cwd, "angular.json"))) {
340
- return { ecosystem: "angular", framework: "angular", uiPackage: "@vocoder/angular" };
341
- }
342
- if ("react" in allDeps) {
343
- let framework = null;
344
- if ("next" in allDeps) framework = "nextjs";
345
- else if ("@remix-run/react" in allDeps) framework = "remix";
346
- else if ("gatsby" in allDeps) framework = "gatsby";
347
- else if ("vite" in allDeps) framework = "vite";
348
- return { ecosystem: "react", framework, uiPackage: "@vocoder/react" };
349
- }
350
- return { ecosystem: null, framework: null, uiPackage: null };
351
- }
352
- function buildInstallCommand(packageManager, packages) {
353
- if (packages.length === 0) return "";
354
- const pkgList = packages.join(" ");
355
- switch (packageManager) {
356
- case "pnpm":
357
- return `pnpm add ${pkgList}`;
358
- case "yarn":
359
- return `yarn add ${pkgList}`;
360
- case "bun":
361
- return `bun add ${pkgList}`;
362
- default:
363
- return `npm install ${pkgList}`;
364
- }
365
- }
366
- function getPackagesToInstall(detection) {
367
- const packages = [];
368
- if (!detection.hasUnplugin) packages.push("@vocoder/unplugin");
369
- if (detection.uiPackage && !detection.hasUiPackage) packages.push(detection.uiPackage);
370
- return packages;
371
- }
372
-
373
- // src/utils/setup-snippets.ts
374
- function getSetupSnippets(params) {
375
- const { framework, ecosystem, sourceLocale, translationTriggers } = params;
376
- return {
377
- pluginStep: getPluginSnippet(framework, ecosystem),
378
- providerStep: getProviderSnippet(ecosystem, sourceLocale),
379
- wrapStep: getWrapSnippet(ecosystem),
380
- whatsNext: getWhatsNextMessage(translationTriggers)
381
- };
382
- }
383
- function getPluginSnippet(framework, ecosystem) {
384
- switch (framework) {
385
- case "nextjs":
386
- return {
387
- file: "next.config.ts",
388
- code: `import { withVocoder } from '@vocoder/unplugin/next';
389
-
390
- export default withVocoder({
391
- // your existing Next.js config
392
- });`
393
- };
394
- case "vite":
395
- case "remix":
396
- return {
397
- file: "vite.config.ts",
398
- code: `import vocoder from '@vocoder/unplugin/vite';
399
-
400
- export default defineConfig({
401
- plugins: [
402
- vocoder(),
403
- // your other plugins
404
- ],
405
- });`
406
- };
407
- case "nuxt":
408
- return {
409
- file: "nuxt.config.ts",
410
- code: `import vocoder from '@vocoder/unplugin/vite';
411
-
412
- export default defineNuxtConfig({
413
- vite: {
414
- plugins: [vocoder()],
415
- },
416
- });`
417
- };
418
- case "sveltekit":
419
- return {
420
- file: "vite.config.ts",
421
- code: `import vocoder from '@vocoder/unplugin/vite';
422
- import { sveltekit } from '@sveltejs/kit/vite';
423
-
424
- export default defineConfig({
425
- plugins: [
426
- sveltekit(),
427
- vocoder(),
428
- ],
429
- });`
430
- };
431
- case "gatsby":
432
- return {
433
- file: "gatsby-node.js",
434
- code: `const vocoder = require('@vocoder/unplugin/webpack');
435
-
436
- exports.onCreateWebpackConfig = ({ actions }) => {
437
- actions.setWebpackConfig({
438
- plugins: [vocoder()],
439
- });
440
- };`
441
- };
442
- case "angular":
443
- return null;
444
- // Angular CLI doesn't expose plugin config easily
445
- default:
446
- if (ecosystem) {
447
- return {
448
- file: "your bundler config",
449
- code: `// Vite
450
- import vocoder from '@vocoder/unplugin/vite';
451
- // Webpack
452
- const vocoder = require('@vocoder/unplugin/webpack');
453
-
454
- // Add vocoder() to your plugins array`
455
- };
456
- }
457
- return null;
458
- }
459
- }
460
- function getProviderSnippet(ecosystem, sourceLocale) {
461
- switch (ecosystem) {
462
- case "react":
463
- return {
464
- file: "your root layout or App component",
465
- code: `import { VocoderProvider } from '@vocoder/react';
466
-
467
- <VocoderProvider defaultLocale="${sourceLocale}">
468
- {children}
469
- </VocoderProvider>`
470
- };
471
- case "vue":
472
- return {
473
- file: "your app entry",
474
- code: `import { createVocoder } from '@vocoder/vue';
475
-
476
- const vocoder = createVocoder({
477
- defaultLocale: '${sourceLocale}',
478
- });
479
-
480
- app.use(vocoder);`
481
- };
482
- case "svelte":
483
- return {
484
- file: "your root layout",
485
- code: `<script>
486
- import { VocoderProvider } from '@vocoder/svelte';
487
- </script>
488
-
489
- <VocoderProvider defaultLocale="${sourceLocale}">
490
- <slot />
491
- </VocoderProvider>`
492
- };
493
- default:
494
- return null;
495
- }
496
- }
497
- function getWrapSnippet(ecosystem) {
498
- switch (ecosystem) {
499
- case "react":
500
- return {
501
- code: `import { T } from '@vocoder/react';
502
-
503
- <T>Hello, world!</T>`
504
- };
505
- case "vue":
506
- return {
507
- code: `<template>
508
- <T>Hello, world!</T>
509
- </template>
510
-
511
- <script setup>
512
- import { T } from '@vocoder/vue';
513
- </script>`
514
- };
515
- case "svelte":
516
- return {
517
- code: `<script>
518
- import { T } from '@vocoder/svelte';
519
- </script>
520
-
521
- <T>Hello, world!</T>`
522
- };
523
- default:
524
- return {
525
- code: `// Wrap translatable strings with <T>
526
- <T>Hello, world!</T>`
527
- };
528
- }
529
- }
530
- function getWhatsNextMessage(triggers) {
531
- const parts = [];
532
- if (triggers.includes("push")) {
533
- parts.push("Push to a target branch to trigger translations.");
534
- }
535
- if (triggers.includes("pull_request")) {
536
- parts.push("Open a pull request to trigger translations.");
537
- }
538
- if (triggers.includes("manual")) {
539
- parts.push("Run `vocoder sync` to extract and translate.");
540
- }
541
- if (parts.length === 0) {
542
- parts.push("Push to a target branch to trigger translations.");
543
- }
544
- return parts.join("\n");
545
- }
829
+ // src/commands/init.ts
830
+ import chalk6 from "chalk";
831
+ import { execSync as execSync3 } from "child_process";
832
+ import { config as loadEnv } from "dotenv";
546
833
 
547
834
  // src/utils/git-identity.ts
548
835
  import { execSync } from "child_process";
@@ -580,81 +867,636 @@ function parseRemoteUrl(remoteUrl) {
580
867
  }
581
868
  return { host, ownerRepoPath };
582
869
  }
583
- return null;
870
+ return null;
871
+ }
872
+ try {
873
+ const parsed = new URL(trimmed);
874
+ const host = parsed.hostname.toLowerCase();
875
+ const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
876
+ if (!host || !ownerRepoPath) {
877
+ return null;
878
+ }
879
+ return { host, ownerRepoPath };
880
+ } catch {
881
+ return null;
882
+ }
883
+ }
884
+ function toCanonical(host, ownerRepoPath) {
885
+ if (host.includes("github.com")) {
886
+ return `github:${ownerRepoPath.toLowerCase()}`;
887
+ }
888
+ if (host.includes("gitlab.com")) {
889
+ return `gitlab:${ownerRepoPath.toLowerCase()}`;
890
+ }
891
+ if (host.includes("bitbucket.org")) {
892
+ return `bitbucket:${ownerRepoPath.toLowerCase()}`;
893
+ }
894
+ return `git:${host}/${ownerRepoPath.toLowerCase()}`;
895
+ }
896
+ function resolveGitRepositoryIdentity() {
897
+ const remoteUrl = safeExec("git config --get remote.origin.url");
898
+ if (!remoteUrl) {
899
+ return null;
900
+ }
901
+ const parsed = parseRemoteUrl(remoteUrl);
902
+ if (!parsed) {
903
+ return null;
904
+ }
905
+ const repositoryRoot = safeExec("git rev-parse --show-toplevel");
906
+ const currentDirectory = process.cwd();
907
+ let repoScopePath = "";
908
+ if (repositoryRoot) {
909
+ const relativePath = relative(resolve(repositoryRoot), resolve(currentDirectory)).replace(/\\/g, "/").trim();
910
+ if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
911
+ repoScopePath = relativePath;
912
+ }
913
+ }
914
+ return {
915
+ repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
916
+ repoScopePath
917
+ };
918
+ }
919
+ function resolveGitContext() {
920
+ const warnings = [];
921
+ const identity = resolveGitRepositoryIdentity();
922
+ if (!identity) {
923
+ warnings.push(
924
+ "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
925
+ );
926
+ }
927
+ return { identity, warnings };
928
+ }
929
+
930
+ // src/utils/project-create.ts
931
+ import * as p3 from "@clack/prompts";
932
+ import chalk4 from "chalk";
933
+
934
+ // src/utils/locale-search.ts
935
+ import { Prompt, isCancel as isCancel2 } from "@clack/core";
936
+ import * as p2 from "@clack/prompts";
937
+ import chalk2 from "chalk";
938
+ var S_BAR = "\u2502";
939
+ var S_BAR_END = "\u2514";
940
+ var S_ACTIVE = "\u25C6";
941
+ var S_SUBMIT = "\u25C6";
942
+ var S_CANCEL = "\u25A0";
943
+ var S_ERROR = "\u25B2";
944
+ var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
945
+ var dim = (s) => noColor ? s : chalk2.gray(s);
946
+ var cyan = (s) => noColor ? s : chalk2.cyan(s);
947
+ var grn = (s) => noColor ? s : chalk2.green(s);
948
+ var ylw = (s) => noColor ? s : chalk2.yellow(s);
949
+ var red = (s) => noColor ? s : chalk2.red(s);
950
+ var bld = (s) => noColor ? s : chalk2.bold(s);
951
+ function symbol(state) {
952
+ switch (state) {
953
+ case "submit":
954
+ return grn(S_SUBMIT);
955
+ case "cancel":
956
+ return red(S_CANCEL);
957
+ case "error":
958
+ return ylw(S_ERROR);
959
+ default:
960
+ return cyan(S_ACTIVE);
961
+ }
962
+ }
963
+ var MAX_VISIBLE = 12;
964
+ function filterLocales(options, query) {
965
+ if (!query.trim()) return options;
966
+ const lower = query.toLowerCase();
967
+ return options.filter(
968
+ (o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
969
+ );
970
+ }
971
+ function buildList(filtered, cursor, scrollOffset, selected) {
972
+ const isMulti = selected !== null;
973
+ const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
974
+ const visibleLines = [];
975
+ for (let i = scrollOffset; i < end; i++) {
976
+ const opt = filtered[i];
977
+ const isCursor = i === cursor;
978
+ const isChecked = isMulti && selected.has(opt.bcp47);
979
+ const icon = isMulti ? isChecked ? isCursor ? grn("\u25FC") : "\u25FC" : isCursor ? grn("\u25FB") : dim("\u25FB") : isCursor ? grn("\u25CF") : dim("\u25CB");
980
+ visibleLines.push(`${cyan(S_BAR)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`);
981
+ }
982
+ const hidden = filtered.length - (end - scrollOffset);
983
+ if (hidden > 0) visibleLines.push(dim(`${S_BAR} ${hidden} more \u2014 keep typing to narrow`));
984
+ if (filtered.length === 0) visibleLines.push(dim(`${S_BAR} No matches`));
985
+ if (isMulti && selected.size > 0) {
986
+ visibleLines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
987
+ }
988
+ return visibleLines.join("\n");
989
+ }
990
+ async function runFilterablePrompt(opts) {
991
+ const { message, options, multi } = opts;
992
+ let filter = "";
993
+ let cursor = 0;
994
+ let scrollOffset = 0;
995
+ const selected = new Set(multi ? opts.initialValues ?? [] : []);
996
+ if (!multi && opts.initialValue) {
997
+ const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
998
+ if (idx >= 0) cursor = idx;
999
+ }
1000
+ const getFiltered = () => filterLocales(options, filter);
1001
+ const clampCursor = (filtered) => {
1002
+ if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
1003
+ if (cursor < scrollOffset) scrollOffset = cursor;
1004
+ if (cursor >= scrollOffset + MAX_VISIBLE) scrollOffset = cursor - MAX_VISIBLE + 1;
1005
+ if (scrollOffset < 0) scrollOffset = 0;
1006
+ };
1007
+ const prompt = new Prompt(
1008
+ {
1009
+ initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
1010
+ validate() {
1011
+ const f = getFiltered();
1012
+ if (multi && selected.size === 0) return "At least one target language is required.";
1013
+ if (!multi && !f[cursor]) return "Please select a language.";
1014
+ return void 0;
1015
+ },
1016
+ render() {
1017
+ const filtered = getFiltered();
1018
+ clampCursor(filtered);
1019
+ const hdr = `${dim(S_BAR)}
1020
+ ${symbol(this.state)} ${message}
1021
+ `;
1022
+ const hint = filter.length > 0 ? filter : dim("type to filter, \u2191\u2193 navigate" + (multi ? ", space select" : ""));
1023
+ switch (this.state) {
1024
+ case "submit": {
1025
+ 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 ?? "";
1026
+ return `${hdr}${dim(S_BAR)} ${bld(val || dim("none"))}`;
1027
+ }
1028
+ case "cancel":
1029
+ return `${hdr}${dim(S_BAR)}`;
1030
+ case "error":
1031
+ return [
1032
+ hdr.trimEnd(),
1033
+ `${ylw(S_BAR)} ${dim("/")} ${hint}`,
1034
+ buildList(filtered, cursor, scrollOffset, multi ? selected : null),
1035
+ `${ylw(S_BAR_END)} ${ylw(this.error)}`,
1036
+ ""
1037
+ ].join("\n");
1038
+ default:
1039
+ return [
1040
+ hdr.trimEnd(),
1041
+ `${cyan(S_BAR)} ${dim("/")} ${hint}`,
1042
+ buildList(filtered, cursor, scrollOffset, multi ? selected : null),
1043
+ `${cyan(S_BAR_END)}`,
1044
+ ""
1045
+ ].join("\n");
1046
+ }
1047
+ }
1048
+ },
1049
+ false
1050
+ // trackValue=false — we manage value manually
1051
+ );
1052
+ prompt.on("key", (key) => {
1053
+ if (!key || key === " ") return;
1054
+ const cp = key.codePointAt(0) ?? 0;
1055
+ if (cp === 127 || cp === 8) {
1056
+ filter = filter.slice(0, -1);
1057
+ cursor = 0;
1058
+ scrollOffset = 0;
1059
+ } else if (cp >= 32 && cp !== 127) {
1060
+ filter += key;
1061
+ cursor = 0;
1062
+ scrollOffset = 0;
1063
+ }
1064
+ });
1065
+ prompt.on("cursor", (action) => {
1066
+ const filtered = getFiltered();
1067
+ switch (action) {
1068
+ case "up":
1069
+ cursor = Math.max(0, cursor - 1);
1070
+ break;
1071
+ case "down":
1072
+ cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
1073
+ break;
1074
+ case "space":
1075
+ if (multi) {
1076
+ const opt = filtered[cursor];
1077
+ if (opt) {
1078
+ if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
1079
+ else selected.add(opt.bcp47);
1080
+ }
1081
+ }
1082
+ break;
1083
+ }
1084
+ if (!multi) {
1085
+ const opt = getFiltered()[cursor];
1086
+ prompt.value = opt?.bcp47 ?? null;
1087
+ }
1088
+ });
1089
+ prompt.on("finalize", () => {
1090
+ if (prompt.state === "submit") {
1091
+ if (multi) {
1092
+ prompt.value = Array.from(selected);
1093
+ } else {
1094
+ const f = getFiltered();
1095
+ prompt.value = f[cursor]?.bcp47 ?? null;
1096
+ }
1097
+ }
1098
+ });
1099
+ const result = await prompt.prompt();
1100
+ if (isCancel2(result)) return null;
1101
+ return result;
1102
+ }
1103
+ async function searchSelectLocale(options, message, initialValue) {
1104
+ const result = await runFilterablePrompt({ message, options, multi: false, initialValue });
1105
+ return typeof result === "string" ? result : null;
1106
+ }
1107
+ async function searchMultiSelectLocales(options, message, initialValues) {
1108
+ const result = await runFilterablePrompt({ message, options, multi: true, initialValues });
1109
+ if (result === null) return null;
1110
+ const picks = result;
1111
+ if (picks.length === 0) {
1112
+ p2.log.warn("At least one target language is required. Please select at least one.");
1113
+ return searchMultiSelectLocales(options, message, initialValues);
1114
+ }
1115
+ return picks;
1116
+ }
1117
+
1118
+ // src/utils/branch-select.ts
1119
+ import { Prompt as Prompt2, isCancel as isCancel3 } from "@clack/core";
1120
+ import chalk3 from "chalk";
1121
+ import { execSync as execSync2 } from "child_process";
1122
+ var S_BAR2 = "\u2502";
1123
+ var S_BAR_END2 = "\u2514";
1124
+ var S_ACTIVE2 = "\u25C6";
1125
+ var S_SUBMIT2 = "\u25C6";
1126
+ var S_CANCEL2 = "\u25A0";
1127
+ var S_ERROR2 = "\u25B2";
1128
+ var noColor2 = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
1129
+ var dim2 = (s) => noColor2 ? s : chalk3.gray(s);
1130
+ var cyan2 = (s) => noColor2 ? s : chalk3.cyan(s);
1131
+ var grn2 = (s) => noColor2 ? s : chalk3.green(s);
1132
+ var ylw2 = (s) => noColor2 ? s : chalk3.yellow(s);
1133
+ var red2 = (s) => noColor2 ? s : chalk3.red(s);
1134
+ var bld2 = (s) => noColor2 ? s : chalk3.bold(s);
1135
+ function symbol2(state) {
1136
+ switch (state) {
1137
+ case "submit":
1138
+ return grn2(S_SUBMIT2);
1139
+ case "cancel":
1140
+ return red2(S_CANCEL2);
1141
+ case "error":
1142
+ return ylw2(S_ERROR2);
1143
+ default:
1144
+ return cyan2(S_ACTIVE2);
1145
+ }
1146
+ }
1147
+ function detectGitBranches(cwd) {
1148
+ const workDir = cwd ?? process.cwd();
1149
+ try {
1150
+ const localOut = execSync2("git branch", { cwd: workDir, stdio: "pipe" }).toString();
1151
+ const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
1152
+ let remoteBranches = [];
1153
+ try {
1154
+ const remoteOut = execSync2("git branch -r", { cwd: workDir, stdio: "pipe" }).toString();
1155
+ remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
1156
+ } catch {
1157
+ }
1158
+ const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
1159
+ let defaultBranch = "main";
1160
+ try {
1161
+ const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", { cwd: workDir, stdio: "pipe" }).toString().trim();
1162
+ defaultBranch = ref.split("/").pop() ?? "main";
1163
+ } catch {
1164
+ }
1165
+ return {
1166
+ branches: branches.length > 0 ? branches : [defaultBranch],
1167
+ defaultBranch
1168
+ };
1169
+ } catch {
1170
+ return { branches: ["main"], defaultBranch: "main" };
1171
+ }
1172
+ }
1173
+ var INVALID_CHARS = /[\s?^~:[\]\\]/;
1174
+ function validateBranchPattern(pattern) {
1175
+ const t = pattern.trim();
1176
+ if (!t) return "Pattern cannot be empty";
1177
+ if (INVALID_CHARS.test(t)) 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_VISIBLE2 = 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;
1197
+ const lower = query.toLowerCase();
1198
+ return items.filter((i) => i.value.toLowerCase().includes(lower));
1199
+ }
1200
+ function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor) {
1201
+ const lines = [];
1202
+ const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
1203
+ for (let i = scrollOffset; i < end; i++) {
1204
+ const item = filtered[i];
1205
+ const isCursor = i === cursor && !addCursor;
1206
+ const isChecked = selected.has(item.value);
1207
+ const icon = isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB");
1208
+ let label = item.isCustom ? `${item.label} ${dim2("(custom)")}` : item.label;
1209
+ if (isCursor) label = bld2(label);
1210
+ lines.push(`${cyan2(S_BAR2)} ${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);
1217
+ const icon = addCursor ? grn2("\u25FB") : dim2("\u25FB");
1218
+ const label = err ? `${ylw2("+")} ${dim2(`"${trimmed}" \u2014 ${err}`)}` : `${grn2("+")} Add "${trimmed}" as branch pattern`;
1219
+ lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
1220
+ } else if (filtered.length === 0 && trimmed.length === 0) {
1221
+ lines.push(dim2(`${S_BAR2} No branches detected`));
1222
+ }
1223
+ const hidden = filtered.length - (end - scrollOffset);
1224
+ if (hidden > 0) lines.push(dim2(`${S_BAR2} ${hidden} more`));
1225
+ if (selected.size > 0) lines.push(dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`));
1226
+ return lines.join("\n");
1227
+ }
1228
+ async function filterableBranchSelect(params) {
1229
+ const { message, branches, defaultBranch } = params;
1230
+ let filter = "";
1231
+ let cursor = 0;
1232
+ let scrollOffset = 0;
1233
+ let addCursor = false;
1234
+ const customPatterns = [];
1235
+ const selected = new Set(params.initialValues ?? [defaultBranch]);
1236
+ const getItems = () => buildItems(branches, defaultBranch, customPatterns);
1237
+ const getFiltered = () => filterItems(getItems(), filter);
1238
+ const isNewPattern = () => {
1239
+ const t = filter.trim();
1240
+ if (!t) return false;
1241
+ return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
1242
+ };
1243
+ const clampCursor = (filtered) => {
1244
+ const hasAdd = isNewPattern();
1245
+ const max = filtered.length - 1 + (hasAdd ? 1 : 0);
1246
+ if (cursor > max && !addCursor) cursor = Math.max(0, max);
1247
+ if (!addCursor) {
1248
+ if (cursor < scrollOffset) scrollOffset = cursor;
1249
+ if (cursor >= scrollOffset + MAX_VISIBLE2) scrollOffset = cursor - MAX_VISIBLE2 + 1;
1250
+ if (scrollOffset < 0) scrollOffset = 0;
1251
+ }
1252
+ };
1253
+ const prompt = new Prompt2(
1254
+ {
1255
+ validate() {
1256
+ if (selected.size === 0) return "At least one branch is required.";
1257
+ return void 0;
1258
+ },
1259
+ render() {
1260
+ const filtered = getFiltered();
1261
+ clampCursor(filtered);
1262
+ const hdr = `${dim2(S_BAR2)}
1263
+ ${symbol2(this.state)} ${message}
1264
+ `;
1265
+ const hint = filter.length > 0 ? filter : dim2("type to filter or add pattern, \u2191\u2193 navigate, space select");
1266
+ switch (this.state) {
1267
+ case "submit": {
1268
+ const summary = selected.size > 0 ? bld2(Array.from(selected).join(", ")) : dim2("none");
1269
+ return `${hdr}${dim2(S_BAR2)} ${summary}`;
1270
+ }
1271
+ case "cancel":
1272
+ return `${hdr}${dim2(S_BAR2)}`;
1273
+ case "error":
1274
+ return [
1275
+ hdr.trimEnd(),
1276
+ `${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
1277
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor),
1278
+ `${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
1279
+ ""
1280
+ ].join("\n");
1281
+ default:
1282
+ return [
1283
+ hdr.trimEnd(),
1284
+ `${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
1285
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor),
1286
+ `${cyan2(S_BAR_END2)}`,
1287
+ ""
1288
+ ].join("\n");
1289
+ }
1290
+ }
1291
+ },
1292
+ false
1293
+ );
1294
+ prompt.on("key", (key) => {
1295
+ if (!key || key === " ") return;
1296
+ const cp = key.codePointAt(0) ?? 0;
1297
+ if (cp === 127 || cp === 8) {
1298
+ filter = filter.slice(0, -1);
1299
+ cursor = 0;
1300
+ scrollOffset = 0;
1301
+ addCursor = false;
1302
+ } else if (cp >= 32 && cp !== 127) {
1303
+ filter += key;
1304
+ cursor = 0;
1305
+ scrollOffset = 0;
1306
+ addCursor = false;
1307
+ }
1308
+ });
1309
+ prompt.on("cursor", (action) => {
1310
+ const filtered = getFiltered();
1311
+ const hasAdd = isNewPattern();
1312
+ switch (action) {
1313
+ case "up":
1314
+ if (addCursor) {
1315
+ addCursor = false;
1316
+ cursor = Math.max(0, filtered.length - 1);
1317
+ } else cursor = Math.max(0, cursor - 1);
1318
+ break;
1319
+ case "down":
1320
+ if (!addCursor && cursor >= filtered.length - 1 && hasAdd) addCursor = true;
1321
+ else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
1322
+ break;
1323
+ case "space":
1324
+ if (addCursor) {
1325
+ const t = filter.trim();
1326
+ const err = validateBranchPattern(t);
1327
+ if (!err) {
1328
+ customPatterns.push(t);
1329
+ selected.add(t);
1330
+ filter = "";
1331
+ cursor = 0;
1332
+ scrollOffset = 0;
1333
+ addCursor = false;
1334
+ }
1335
+ } else {
1336
+ const item = filtered[cursor];
1337
+ if (item) {
1338
+ if (selected.has(item.value)) selected.delete(item.value);
1339
+ else selected.add(item.value);
1340
+ }
1341
+ }
1342
+ break;
1343
+ }
1344
+ });
1345
+ prompt.on("finalize", () => {
1346
+ if (prompt.state === "submit") {
1347
+ prompt.value = Array.from(selected);
1348
+ }
1349
+ });
1350
+ const result = await prompt.prompt();
1351
+ if (isCancel3(result)) return null;
1352
+ return result;
1353
+ }
1354
+
1355
+ // src/utils/project-create.ts
1356
+ function buildLocaleOptions(locales) {
1357
+ return locales.map((l) => ({
1358
+ bcp47: l.code,
1359
+ label: `${l.name} \u2014 ${l.code}`
1360
+ }));
1361
+ }
1362
+ function buildLanguageOptions(locales) {
1363
+ const byFamily = /* @__PURE__ */ new Map();
1364
+ for (const l of locales) {
1365
+ const family = l.code.split("-")[0].toLowerCase();
1366
+ const opt = { bcp47: l.code, label: `${l.name} \u2014 ${l.code}` };
1367
+ const existing = byFamily.get(family);
1368
+ if (!existing || l.code.length < existing.bcp47.length) {
1369
+ byFamily.set(family, opt);
1370
+ }
584
1371
  }
1372
+ return Array.from(byFamily.values());
1373
+ }
1374
+ async function runProjectCreate(params) {
1375
+ const { api, userToken, organizationId, repoCanonical } = params;
1376
+ const projectName = (params.defaultName ?? "my-project").trim();
1377
+ p3.log.success(`Project: ${chalk4.bold(projectName)}`);
1378
+ let rawLocales;
585
1379
  try {
586
- const parsed = new URL(trimmed);
587
- const host = parsed.hostname.toLowerCase();
588
- const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
589
- if (!host || !ownerRepoPath) {
590
- return null;
591
- }
592
- return { host, ownerRepoPath };
1380
+ rawLocales = await api.listLocales(userToken);
593
1381
  } catch {
1382
+ p3.log.error("Failed to fetch supported locales. Check your connection and try again.");
594
1383
  return null;
595
1384
  }
596
- }
597
- function toCanonical(host, ownerRepoPath) {
598
- if (host.includes("github.com")) {
599
- return `github:${ownerRepoPath.toLowerCase()}`;
600
- }
601
- if (host.includes("gitlab.com")) {
602
- return `gitlab:${ownerRepoPath.toLowerCase()}`;
603
- }
604
- if (host.includes("bitbucket.org")) {
605
- return `bitbucket:${ownerRepoPath.toLowerCase()}`;
1385
+ const languageOptions = buildLanguageOptions(rawLocales);
1386
+ const localeOptions = buildLocaleOptions(rawLocales);
1387
+ const rawScope = await p3.text({
1388
+ message: "App directory (leave blank for the entire repo)",
1389
+ placeholder: "e.g. apps/web",
1390
+ initialValue: params.defaultScopePath ?? "",
1391
+ validate(value) {
1392
+ const v = value.trim();
1393
+ if (!v) return;
1394
+ if (v.startsWith("/")) return "Use a relative path, not an absolute path";
1395
+ if (v.includes("..")) return 'Path must not contain ".."';
1396
+ }
1397
+ });
1398
+ if (p3.isCancel(rawScope)) return null;
1399
+ const scopePath = rawScope.trim();
1400
+ const sourceLocale = await searchSelectLocale(
1401
+ languageOptions,
1402
+ "Source language (the language your code is written in)",
1403
+ params.defaultSourceLocale ?? "en"
1404
+ );
1405
+ if (sourceLocale === null) return null;
1406
+ const targetOptions = localeOptions.filter((opt) => opt.bcp47 !== sourceLocale);
1407
+ const targetLocales = await searchMultiSelectLocales(
1408
+ targetOptions,
1409
+ "Target languages (languages to translate into)"
1410
+ );
1411
+ if (targetLocales === null) return null;
1412
+ if (targetLocales.length === 0) {
1413
+ p3.log.warn("No target languages selected \u2014 you can add them later from the dashboard.");
1414
+ }
1415
+ const detected = detectGitBranches();
1416
+ const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
1417
+ let targetBranches = [];
1418
+ {
1419
+ let initial = initialBranches;
1420
+ while (targetBranches.length === 0) {
1421
+ const result = await filterableBranchSelect({
1422
+ message: "Target branches (translations will run when you push to these)",
1423
+ branches: detected.branches,
1424
+ defaultBranch: detected.defaultBranch,
1425
+ initialValues: initial
1426
+ });
1427
+ if (result === null) return null;
1428
+ if (result.length === 0) {
1429
+ p3.log.warn("At least one branch is required. Please select at least one.");
1430
+ initial = [detected.defaultBranch];
1431
+ } else {
1432
+ targetBranches = result;
1433
+ }
1434
+ }
606
1435
  }
607
- return `git:${host}/${ownerRepoPath.toLowerCase()}`;
608
- }
609
- function resolveGitRepositoryIdentity() {
610
- const remoteUrl = safeExec("git config --get remote.origin.url");
611
- if (!remoteUrl) {
1436
+ try {
1437
+ const result = await api.createProject(userToken, {
1438
+ organizationId,
1439
+ name: projectName,
1440
+ sourceLocale,
1441
+ targetLocales,
1442
+ targetBranches,
1443
+ translationTriggers: ["push"],
1444
+ scopePaths: scopePath ? [scopePath] : [],
1445
+ repoCanonical
1446
+ });
1447
+ p3.log.success(`Project ${chalk4.bold(result.projectName)} created!`);
1448
+ return result;
1449
+ } catch (error) {
1450
+ const message = error instanceof Error ? error.message : "Unknown error";
1451
+ p3.log.error(`Failed to create project: ${message}`);
612
1452
  return null;
613
1453
  }
614
- const parsed = parseRemoteUrl(remoteUrl);
615
- if (!parsed) {
616
- return null;
1454
+ }
1455
+
1456
+ // src/utils/workspace.ts
1457
+ import * as p4 from "@clack/prompts";
1458
+ import chalk5 from "chalk";
1459
+ async function selectWorkspace(result) {
1460
+ const { workspaces, canCreateWorkspace } = result;
1461
+ if (workspaces.length === 0) {
1462
+ return { action: "create" };
1463
+ }
1464
+ const options = workspaces.map((ws) => ({
1465
+ value: ws.id,
1466
+ label: ws.name,
1467
+ hint: [
1468
+ ws.projectCount > 0 ? `${ws.projectCount} project${ws.projectCount !== 1 ? "s" : ""}` : "",
1469
+ ws.connectionLabel ? `GitHub: ${ws.connectionLabel}` : ""
1470
+ ].filter(Boolean).join(" \xB7 ") || void 0
1471
+ }));
1472
+ if (canCreateWorkspace) {
1473
+ options.push({ value: "create", label: "Create new workspace" });
1474
+ }
1475
+ const selected = await p4.select({
1476
+ message: "Select workspace",
1477
+ options
1478
+ });
1479
+ if (p4.isCancel(selected)) {
1480
+ return { action: "cancelled" };
617
1481
  }
618
- const repositoryRoot = safeExec("git rev-parse --show-toplevel");
619
- const currentDirectory = process.cwd();
620
- let repoScopePath = "";
621
- if (repositoryRoot) {
622
- const relativePath = relative(resolve(repositoryRoot), resolve(currentDirectory)).replace(/\\/g, "/").trim();
623
- if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
624
- repoScopePath = relativePath;
625
- }
1482
+ if (selected === "create") {
1483
+ return { action: "create" };
626
1484
  }
627
- return {
628
- repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
629
- repoScopePath
630
- };
631
- }
632
- function resolveGitContext() {
633
- const warnings = [];
634
- const identity = resolveGitRepositoryIdentity();
635
- if (!identity) {
636
- warnings.push(
637
- "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
638
- );
1485
+ const workspace = workspaces.find((ws) => ws.id === selected);
1486
+ if (!workspace) {
1487
+ return { action: "cancelled" };
639
1488
  }
640
- return { identity, warnings };
1489
+ return { action: "use", workspace };
641
1490
  }
642
1491
 
643
1492
  // src/commands/init.ts
644
- import { config as loadEnv } from "dotenv";
645
- import { execSync as execSync2 } from "child_process";
646
- import { spawn } from "child_process";
1493
+ import { spawn as spawn2 } from "child_process";
647
1494
  loadEnv();
648
1495
  var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
649
- function parseTargetLocales(value) {
650
- if (!value) return void 0;
651
- const locales = value.split(",").map((locale) => locale.trim()).filter(Boolean);
652
- return locales.length > 0 ? locales : void 0;
653
- }
654
1496
  async function sleep(ms) {
655
1497
  await new Promise((resolve2) => setTimeout(resolve2, ms));
656
1498
  }
657
- async function tryOpenBrowser(url) {
1499
+ async function tryOpenBrowser2(url) {
658
1500
  if (!process.stdout.isTTY || process.env.CI === "true") {
659
1501
  return false;
660
1502
  }
@@ -672,7 +1514,7 @@ async function tryOpenBrowser(url) {
672
1514
  }
673
1515
  return await new Promise((resolve2) => {
674
1516
  try {
675
- const child = spawn(command, args, {
1517
+ const child = spawn2(command, args, {
676
1518
  detached: true,
677
1519
  stdio: "ignore",
678
1520
  windowsHide: true
@@ -707,35 +1549,35 @@ function getSubscriptionSettingsUrl(apiUrl) {
707
1549
  return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
708
1550
  }
709
1551
  function printPlanLimitMessage(apiUrl, message) {
710
- p.log.error(`You are over your plan limits.
1552
+ p5.log.error(`You are over your plan limits.
711
1553
  ${message}`);
712
- p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
1554
+ p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
713
1555
  }
714
1556
  function runScaffold(params) {
715
1557
  const { projectName, organizationName, sourceLocale, translationTriggers } = params;
716
- p.log.info(`Project: ${chalk.bold(projectName)}`);
717
- p.log.info(`Workspace: ${chalk.bold(organizationName)}`);
1558
+ p5.log.info(`Project: ${chalk6.bold(projectName)}`);
1559
+ p5.log.info(`Workspace: ${chalk6.bold(organizationName)}`);
718
1560
  const detection = detectLocalEcosystem();
719
1561
  if (detection.ecosystem) {
720
1562
  const frameworkLabel = detection.framework ?? detection.ecosystem;
721
1563
  const pmLabel = detection.packageManager;
722
- p.log.info(`Detected: ${chalk.bold(frameworkLabel)} (${pmLabel})`);
1564
+ p5.log.info(`Detected: ${chalk6.bold(frameworkLabel)} (${pmLabel})`);
723
1565
  }
724
1566
  const packagesToInstall = getPackagesToInstall(detection);
725
1567
  if (packagesToInstall.length > 0) {
726
1568
  const installCmd = buildInstallCommand(detection.packageManager, packagesToInstall);
727
- p.log.info("");
728
- const installSpinner = p.spinner();
1569
+ p5.log.info("");
1570
+ const installSpinner = p5.spinner();
729
1571
  installSpinner.start(`Installing ${packagesToInstall.join(", ")}...`);
730
1572
  try {
731
- execSync2(installCmd, { stdio: "pipe", cwd: process.cwd() });
1573
+ execSync3(installCmd, { stdio: "pipe", cwd: process.cwd() });
732
1574
  installSpinner.stop(`Installed ${packagesToInstall.join(", ")}`);
733
1575
  } catch {
734
1576
  installSpinner.stop("Package installation failed");
735
- p.log.warn(`Run manually: ${chalk.cyan(installCmd)}`);
1577
+ p5.log.warn(`Run manually: ${chalk6.cyan(installCmd)}`);
736
1578
  }
737
1579
  } else if (detection.ecosystem) {
738
- p.log.info(`Packages: ${chalk.green("already installed")}`);
1580
+ p5.log.info(`Packages: ${chalk6.green("already installed")}`);
739
1581
  }
740
1582
  const snippets = getSetupSnippets({
741
1583
  framework: detection.framework,
@@ -745,162 +1587,518 @@ function runScaffold(params) {
745
1587
  });
746
1588
  let stepNum = 1;
747
1589
  if (snippets.pluginStep) {
748
- p.log.message("");
749
- p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk.cyan(snippets.pluginStep.file)}`);
1590
+ p5.log.message("");
1591
+ p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`);
750
1592
  printCodeBlock(snippets.pluginStep.code);
751
1593
  stepNum++;
752
1594
  }
753
1595
  if (snippets.providerStep) {
754
- p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Add the provider to ${chalk.cyan(snippets.providerStep.file)}`);
1596
+ p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`);
755
1597
  printCodeBlock(snippets.providerStep.code);
756
1598
  stepNum++;
757
1599
  }
758
- p.log.step(`${chalk.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
1600
+ p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
759
1601
  printCodeBlock(snippets.wrapStep.code);
760
- p.log.message("");
1602
+ p5.log.message("");
761
1603
  for (const line of snippets.whatsNext.split("\n")) {
762
- p.log.success(line);
1604
+ p5.log.success(line);
763
1605
  }
764
1606
  }
1607
+ function printMcpSetup(apiKey) {
1608
+ const addCommand = `claude mcp add --scope project --transport stdio \\
1609
+ --env VOCODER_API_KEY=${apiKey} \\
1610
+ vocoder -- npx -y @vocoder/mcp`;
1611
+ const teamConfig = JSON.stringify(
1612
+ {
1613
+ mcpServers: {
1614
+ vocoder: {
1615
+ type: "stdio",
1616
+ command: "npx",
1617
+ args: ["-y", "@vocoder/mcp"],
1618
+ env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
1619
+ }
1620
+ }
1621
+ },
1622
+ null,
1623
+ 2
1624
+ );
1625
+ p5.log.message("");
1626
+ p5.log.message(chalk6.bold("Use Vocoder with Claude Code"));
1627
+ p5.log.message("Run this to add the MCP server to your project:");
1628
+ p5.log.message("");
1629
+ printCodeBlock(addCommand);
1630
+ p5.log.message("");
1631
+ p5.log.message("To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference");
1632
+ p5.log.message("so each developer supplies their own key:");
1633
+ p5.log.message("");
1634
+ printCodeBlock(teamConfig);
1635
+ p5.log.message("");
1636
+ p5.log.message(chalk6.gray("Setup instructions: https://vocoder.app/docs/mcp"));
1637
+ }
765
1638
  function printCodeBlock(code) {
766
1639
  const lines = code.split("\n");
767
1640
  const maxLen = lines.reduce((max, line) => Math.max(max, line.length), 0);
768
- const bar = chalk.gray("\u2502");
1641
+ const bar = chalk6.gray("\u2502");
769
1642
  const pad = (s) => s + " ".repeat(maxLen - s.length);
770
- process.stdout.write(`${chalk.gray("\u2502")}
1643
+ process.stdout.write(`${chalk6.gray("\u2502")}
771
1644
  `);
772
- process.stdout.write(`${chalk.gray("\u2502")} ${chalk.gray("\u250C" + "\u2500".repeat(maxLen + 2) + "\u2510")}
1645
+ process.stdout.write(`${chalk6.gray("\u2502")} ${chalk6.gray("\u250C" + "\u2500".repeat(maxLen + 2) + "\u2510")}
773
1646
  `);
774
1647
  for (const line of lines) {
775
- process.stdout.write(`${chalk.gray("\u2502")} ${bar} ${pad(line)} ${bar}
1648
+ process.stdout.write(`${chalk6.gray("\u2502")} ${bar} ${pad(line)} ${bar}
1649
+ `);
1650
+ }
1651
+ process.stdout.write(`${chalk6.gray("\u2502")} ${chalk6.gray("\u2514" + "\u2500".repeat(maxLen + 2) + "\u2518")}
776
1652
  `);
1653
+ }
1654
+ async function verifyStoredToken(api, token) {
1655
+ try {
1656
+ return await api.getCliUserInfo(token);
1657
+ } catch {
1658
+ clearAuthData();
1659
+ return null;
1660
+ }
1661
+ }
1662
+ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1663
+ let server = null;
1664
+ if (!options.ci) {
1665
+ try {
1666
+ server = await startCallbackServer();
1667
+ } catch {
1668
+ }
777
1669
  }
778
- process.stdout.write(`${chalk.gray("\u2502")} ${chalk.gray("\u2514" + "\u2500".repeat(maxLen + 2) + "\u2518")}
1670
+ const session = await api.startCliAuthSession(server?.port, repoCanonical);
1671
+ const browserUrl = reauth ? session.verificationUrl : session.installUrl ?? session.verificationUrl;
1672
+ const expiresAt = new Date(session.expiresAt).getTime();
1673
+ if (options.ci) {
1674
+ process.stdout.write(`VOCODER_AUTH_URL: ${browserUrl}
779
1675
  `);
1676
+ process.stdout.write(`VOCODER_SESSION_ID: ${session.sessionId}
1677
+ `);
1678
+ } else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
1679
+ if (reauth) {
1680
+ if (!options.yes) {
1681
+ const shouldOpen = await p5.confirm({ message: "Open your browser to sign in again?" });
1682
+ if (p5.isCancel(shouldOpen)) {
1683
+ server?.close();
1684
+ p5.cancel("Setup cancelled.");
1685
+ return null;
1686
+ }
1687
+ if (!shouldOpen) {
1688
+ p5.log.info("Open the URL above manually in your browser to continue.");
1689
+ } else {
1690
+ const opened = await tryOpenBrowser2(browserUrl);
1691
+ if (!opened) {
1692
+ p5.note(browserUrl, "Sign In");
1693
+ p5.log.info("Open the URL above manually to continue.");
1694
+ }
1695
+ }
1696
+ } else {
1697
+ await tryOpenBrowser2(browserUrl);
1698
+ }
1699
+ } else {
1700
+ let isLinkFlow = false;
1701
+ if (!options.yes) {
1702
+ const connectChoice = await p5.select({
1703
+ message: "Vocoder needs to be installed on your GitHub account to get started",
1704
+ options: [
1705
+ { value: "install", label: "Install GitHub App", hint: "recommended" },
1706
+ { value: "link", label: "Already installed? Link your account" }
1707
+ ]
1708
+ });
1709
+ if (p5.isCancel(connectChoice)) {
1710
+ server?.close();
1711
+ p5.cancel("Setup cancelled.");
1712
+ return null;
1713
+ }
1714
+ isLinkFlow = connectChoice === "link";
1715
+ }
1716
+ let urlToOpen = browserUrl;
1717
+ if (isLinkFlow) {
1718
+ try {
1719
+ const linkSession = await api.startCliGitHubLinkSession(
1720
+ session.sessionId,
1721
+ server?.port
1722
+ );
1723
+ urlToOpen = linkSession.oauthUrl;
1724
+ } catch {
1725
+ urlToOpen = browserUrl;
1726
+ }
1727
+ }
1728
+ const opened = await tryOpenBrowser2(urlToOpen);
1729
+ if (!opened) {
1730
+ p5.log.warn("Could not open your browser automatically.");
1731
+ p5.note(urlToOpen, "GitHub");
1732
+ p5.log.info("Open the URL above to continue.");
1733
+ }
1734
+ }
1735
+ }
1736
+ const authSpinner = p5.spinner();
1737
+ authSpinner.start("Waiting for GitHub authorization...");
1738
+ let rawToken = null;
1739
+ let callbackOrganizationId;
1740
+ let callbackDiscoveryReady = false;
1741
+ if (server) {
1742
+ try {
1743
+ const deadline = Math.min(expiresAt, Date.now() + 10 * 60 * 1e3);
1744
+ const timeoutMs = deadline - Date.now();
1745
+ const params = await Promise.race([
1746
+ server.waitForCallback(),
1747
+ new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
1748
+ ]);
1749
+ if (params && typeof params.token === "string") {
1750
+ rawToken = params.token;
1751
+ if (typeof params.organizationId === "string" && params.organizationId) {
1752
+ callbackOrganizationId = params.organizationId;
1753
+ }
1754
+ if (params.discovery_ready === "1") {
1755
+ callbackDiscoveryReady = true;
1756
+ }
1757
+ }
1758
+ } catch {
1759
+ } finally {
1760
+ server.close();
1761
+ }
1762
+ }
1763
+ if (!rawToken) {
1764
+ while (Date.now() < expiresAt) {
1765
+ const result = await api.pollCliAuthSession(session.sessionId);
1766
+ if (result.status === "complete") {
1767
+ rawToken = result.token;
1768
+ if (result.organizationId) {
1769
+ callbackOrganizationId = result.organizationId;
1770
+ }
1771
+ break;
1772
+ }
1773
+ if (result.status === "failed") {
1774
+ authSpinner.stop();
1775
+ p5.log.error(result.reason);
1776
+ return null;
1777
+ }
1778
+ await sleep(2e3);
1779
+ }
1780
+ }
1781
+ if (!rawToken) {
1782
+ authSpinner.stop();
1783
+ p5.log.error("The authentication link expired. Run `vocoder init` again.");
1784
+ return null;
1785
+ }
1786
+ const userInfo = await api.getCliUserInfo(rawToken);
1787
+ authSpinner.stop();
1788
+ p5.log.success(`Authenticated as ${chalk6.bold(userInfo.email)}`);
1789
+ return { token: rawToken, ...userInfo, organizationId: callbackOrganizationId, discoveryReady: callbackDiscoveryReady };
780
1790
  }
781
1791
  async function init(options = {}) {
782
1792
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
783
- p.intro("Vocoder Setup");
784
- const spinner3 = p.spinner();
1793
+ p5.intro("Vocoder Setup");
785
1794
  try {
786
1795
  const gitContext = resolveGitContext();
787
1796
  const identity = gitContext.identity;
788
1797
  if (gitContext.warnings.length > 0) {
789
1798
  for (const warning of gitContext.warnings) {
790
- p.log.warn(warning);
1799
+ p5.log.warn(warning);
791
1800
  }
792
1801
  }
793
1802
  if (identity) {
794
- spinner3.start("Checking for existing project...");
795
- const api2 = new VocoderAPI({ apiUrl, apiKey: "" });
796
- const existing = await api2.lookupProjectByRepo({
1803
+ const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
1804
+ const existing = await anonApi.lookupProjectByRepo({
797
1805
  repoCanonical: identity.repoCanonical,
798
1806
  scopePath: identity.repoScopePath
799
1807
  });
800
1808
  if (existing) {
801
- spinner3.stop("Found existing project!");
802
- p.outro("Vocoder is already set up for this repository.");
803
1809
  runScaffold({
804
1810
  projectName: existing.projectName,
805
1811
  organizationName: existing.organizationName,
806
1812
  sourceLocale: existing.sourceLocale ?? "en",
807
1813
  translationTriggers: existing.translationTriggers ?? ["push"]
808
1814
  });
1815
+ p5.outro("Vocoder is already set up for this repository.");
809
1816
  return 0;
810
1817
  }
811
- spinner3.stop("No existing project found for this repo.");
812
1818
  }
813
- spinner3.start("Creating setup session");
814
1819
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
815
- const start = await api.startInitSession({
816
- projectName: options.projectName,
817
- sourceLocale: options.sourceLocale,
818
- targetLocales: parseTargetLocales(options.targetLocales),
819
- ...identity?.repoCanonical ? { repoCanonical: identity.repoCanonical } : {},
820
- ...identity ? { repoScopePath: identity.repoScopePath } : {}
821
- });
822
- spinner3.stop("Setup session created");
823
- const verificationUrlString = start.verificationUrl;
824
- p.log.info("Create a project in your browser to continue.");
825
- p.note(verificationUrlString, "Setup URL");
826
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
827
- const shouldOpen = options.yes ? true : await p.confirm({ message: "Open this URL in your browser?" });
828
- if (p.isCancel(shouldOpen)) {
829
- p.cancel("Setup cancelled.");
830
- return 1;
831
- }
832
- if (shouldOpen) {
833
- const opened = await tryOpenBrowser(verificationUrlString);
834
- if (opened) {
835
- p.log.info("Opened your browser.");
836
- } else {
837
- p.log.info("Could not open a browser automatically. Use the URL above.");
838
- }
1820
+ let userToken;
1821
+ let userEmail;
1822
+ let userName;
1823
+ let authOrganizationId;
1824
+ let authDiscoveryReady = false;
1825
+ const stored = readAuthData();
1826
+ if (stored && stored.apiUrl === apiUrl) {
1827
+ const verified = await verifyStoredToken(api, stored.token);
1828
+ if (verified) {
1829
+ p5.log.success(`Authenticated as ${chalk6.bold(verified.email)}`);
1830
+ userToken = stored.token;
1831
+ userEmail = verified.email;
1832
+ userName = verified.name;
1833
+ } else {
1834
+ p5.log.warn("Stored credentials expired \u2014 signing in again");
1835
+ const authResult = await runAuthFlow(
1836
+ api,
1837
+ options,
1838
+ /* reauth */
1839
+ true
1840
+ );
1841
+ if (!authResult) return 1;
1842
+ userToken = authResult.token;
1843
+ userEmail = authResult.email;
1844
+ userName = authResult.name;
1845
+ authOrganizationId = authResult.organizationId;
1846
+ authDiscoveryReady = authResult.discoveryReady ?? false;
1847
+ writeAuthData({
1848
+ token: userToken,
1849
+ apiUrl,
1850
+ userId: authResult.userId,
1851
+ email: userEmail,
1852
+ name: userName,
1853
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1854
+ });
839
1855
  }
840
- }
841
- const expiresAt = new Date(start.expiresAt).getTime();
842
- spinner3.start("Waiting for setup to complete...");
843
- while (Date.now() < expiresAt) {
844
- const status = await api.getInitSessionStatus({
845
- sessionId: start.sessionId,
846
- pollToken: start.poll.token
1856
+ } else {
1857
+ const authResult = await runAuthFlow(api, options, false, identity?.repoCanonical);
1858
+ if (!authResult) return 1;
1859
+ userToken = authResult.token;
1860
+ userEmail = authResult.email;
1861
+ userName = authResult.name;
1862
+ authOrganizationId = authResult.organizationId;
1863
+ writeAuthData({
1864
+ token: userToken,
1865
+ apiUrl,
1866
+ userId: authResult.userId,
1867
+ email: userEmail,
1868
+ name: userName,
1869
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
847
1870
  });
848
- if (status.status === "pending") {
849
- const pendingMessage = status.message?.trim();
850
- if (pendingMessage) {
851
- spinner3.message(`Waiting for setup to complete... (${pendingMessage})`);
1871
+ }
1872
+ let selectedWorkspaceId;
1873
+ let selectedWorkspaceName;
1874
+ if (authOrganizationId) {
1875
+ const workspaceData = await api.listWorkspaces(userToken);
1876
+ const ws = workspaceData.workspaces.find((w) => w.id === authOrganizationId);
1877
+ selectedWorkspaceId = authOrganizationId;
1878
+ selectedWorkspaceName = ws?.name ?? userEmail;
1879
+ p5.log.success(`Connected as ${chalk6.bold(userEmail)} \u2014 workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1880
+ } else {
1881
+ const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
1882
+ const cachedInstallations = discoveryResult?.installations ?? [];
1883
+ if (cachedInstallations.length > 0) {
1884
+ if (identity?.repoCanonical) {
1885
+ const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
1886
+ if (repoOwner) {
1887
+ const hasMatchingAccount = cachedInstallations.some(
1888
+ (i) => i.accountLogin.toLowerCase() === repoOwner
1889
+ );
1890
+ if (!hasMatchingAccount) {
1891
+ p5.log.warn(
1892
+ `None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
1893
+ The project will be created but translations won't trigger automatically.
1894
+ To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
1895
+ );
1896
+ }
1897
+ }
852
1898
  }
853
- await sleep((status.pollIntervalSeconds || start.poll.intervalSeconds) * 1e3);
854
- continue;
855
- }
856
- if (status.status === "failed") {
857
- spinner3.stop("Setup failed");
858
- if (isPlanLimitFailure(status.message)) {
859
- printPlanLimitMessage(apiUrl, status.message);
1899
+ const validInstallations = cachedInstallations.filter(
1900
+ (i) => !i.isSuspended && !i.conflictLabel
1901
+ );
1902
+ let selectedInstallationId = null;
1903
+ if (validInstallations.length === 1 && cachedInstallations.length === 1) {
1904
+ selectedInstallationId = validInstallations[0].installationId;
860
1905
  } else {
861
- p.log.error(status.message);
1906
+ selectedInstallationId = await selectGitHubInstallation(
1907
+ cachedInstallations.map((inst) => ({
1908
+ installationId: inst.installationId,
1909
+ accountLogin: inst.accountLogin,
1910
+ accountType: inst.accountType,
1911
+ isSuspended: inst.isSuspended,
1912
+ conflictLabel: inst.conflictLabel
1913
+ })),
1914
+ false
1915
+ );
862
1916
  }
863
- p.cancel("Setup could not be completed.");
864
- return 1;
865
- }
866
- if (status.status === "completed") {
867
- spinner3.stop("Setup complete!");
868
- const { credentials } = status;
869
- p.outro("Vocoder initialized successfully!");
870
- runScaffold({
871
- projectName: credentials.projectName,
872
- organizationName: credentials.organizationName,
873
- sourceLocale: credentials.sourceLocale,
874
- translationTriggers: credentials.translationTriggers ?? ["push"]
1917
+ if (selectedInstallationId === null || selectedInstallationId === "install_new") {
1918
+ p5.cancel("Setup cancelled. Re-run `vocoder init` and choose Install GitHub App.");
1919
+ return 1;
1920
+ }
1921
+ const claimResult = await api.claimCliGitHubInstallation(userToken, {
1922
+ installationId: String(selectedInstallationId),
1923
+ organizationId: null
875
1924
  });
876
- return 0;
1925
+ selectedWorkspaceId = claimResult.organizationId;
1926
+ selectedWorkspaceName = claimResult.organizationName;
1927
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1928
+ } else {
1929
+ const workspaceData = await api.listWorkspaces(userToken);
1930
+ if (workspaceData.workspaces.length === 1 && !workspaceData.canCreateWorkspace) {
1931
+ const ws = workspaceData.workspaces[0];
1932
+ selectedWorkspaceId = ws.id;
1933
+ selectedWorkspaceName = ws.name;
1934
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1935
+ } else {
1936
+ const workspaceResult = await selectWorkspace(workspaceData);
1937
+ if (workspaceResult.action === "cancelled") {
1938
+ p5.cancel("Setup cancelled.");
1939
+ return 1;
1940
+ }
1941
+ if (workspaceResult.action === "use") {
1942
+ selectedWorkspaceId = workspaceResult.workspace.id;
1943
+ selectedWorkspaceName = workspaceResult.workspace.name;
1944
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1945
+ } else {
1946
+ const connectChoice = await p5.select({
1947
+ message: "Connect your new workspace to GitHub",
1948
+ options: [
1949
+ { value: "install", label: "Install the Vocoder GitHub App" },
1950
+ { value: "link", label: "Link an existing installation" }
1951
+ ]
1952
+ });
1953
+ if (p5.isCancel(connectChoice)) {
1954
+ p5.cancel("Setup cancelled.");
1955
+ return 1;
1956
+ }
1957
+ if (connectChoice === "install") {
1958
+ const connectResult = await runGitHubInstallFlow({
1959
+ api,
1960
+ userToken,
1961
+ yes: options.yes
1962
+ });
1963
+ if (!connectResult) {
1964
+ p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
1965
+ return 1;
1966
+ }
1967
+ selectedWorkspaceId = connectResult.organizationId;
1968
+ selectedWorkspaceName = connectResult.organizationName;
1969
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1970
+ } else {
1971
+ const installations = await runGitHubDiscoveryFlow({
1972
+ api,
1973
+ userToken,
1974
+ yes: options.yes
1975
+ });
1976
+ if (!installations) return 1;
1977
+ if (installations.length === 0) {
1978
+ p5.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
1979
+ const installNow = await p5.confirm({ message: "Open GitHub to install the App?" });
1980
+ if (p5.isCancel(installNow) || !installNow) return 1;
1981
+ const connectResult = await runGitHubInstallFlow({
1982
+ api,
1983
+ userToken,
1984
+ yes: options.yes
1985
+ });
1986
+ if (!connectResult) return 1;
1987
+ selectedWorkspaceId = connectResult.organizationId;
1988
+ selectedWorkspaceName = connectResult.organizationName;
1989
+ } else {
1990
+ const selectedInstallationId = await selectGitHubInstallation(
1991
+ installations.map((inst) => ({
1992
+ installationId: inst.installationId,
1993
+ accountLogin: inst.accountLogin,
1994
+ accountType: inst.accountType,
1995
+ isSuspended: inst.isSuspended,
1996
+ conflictLabel: inst.conflictLabel
1997
+ })),
1998
+ true
1999
+ );
2000
+ if (selectedInstallationId === null) {
2001
+ p5.cancel("Setup cancelled.");
2002
+ return 1;
2003
+ }
2004
+ if (selectedInstallationId === "install_new") {
2005
+ const connectResult = await runGitHubInstallFlow({
2006
+ api,
2007
+ userToken,
2008
+ yes: options.yes
2009
+ });
2010
+ if (!connectResult) return 1;
2011
+ selectedWorkspaceId = connectResult.organizationId;
2012
+ selectedWorkspaceName = connectResult.organizationName;
2013
+ } else {
2014
+ const claimResult = await api.claimCliGitHubInstallation(userToken, {
2015
+ installationId: String(selectedInstallationId),
2016
+ organizationId: null
2017
+ });
2018
+ selectedWorkspaceId = claimResult.organizationId;
2019
+ selectedWorkspaceName = claimResult.organizationName;
2020
+ }
2021
+ }
2022
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2023
+ }
2024
+ }
2025
+ }
877
2026
  }
878
2027
  }
879
- spinner3.stop("Setup timed out");
880
- p.log.error("Setup timed out. Run `vocoder init` again.");
881
- p.cancel("Setup could not be completed.");
882
- return 1;
2028
+ const projectResult = await runProjectCreate({
2029
+ api,
2030
+ userToken,
2031
+ organizationId: selectedWorkspaceId,
2032
+ defaultName: identity?.repoCanonical ? identity.repoCanonical.split("/").pop() : void 0,
2033
+ defaultSourceLocale: "en",
2034
+ repoCanonical: identity?.repoCanonical,
2035
+ defaultBranches: ["main"],
2036
+ defaultScopePath: identity?.repoScopePath
2037
+ });
2038
+ if (!projectResult) {
2039
+ p5.log.error("Project creation failed. Run `vocoder init` again.");
2040
+ return 1;
2041
+ }
2042
+ if (!projectResult.repositoryBound && identity?.repoCanonical) {
2043
+ p5.log.warn(
2044
+ `This repository isn't accessible to your GitHub App installation.
2045
+ Translations won't run automatically until you grant access.
2046
+
2047
+ To fix: go to your GitHub App installation settings and add this
2048
+ repository to the allowed list, or switch to "All repositories".
2049
+ ` + (projectResult.configureUrl ? `
2050
+ ${chalk6.dim(projectResult.configureUrl)}
2051
+ ` : "")
2052
+ );
2053
+ }
2054
+ runScaffold({
2055
+ projectName: projectResult.projectName,
2056
+ organizationName: selectedWorkspaceName,
2057
+ sourceLocale: projectResult.sourceLocale,
2058
+ translationTriggers: projectResult.translationTriggers
2059
+ });
2060
+ printMcpSetup(projectResult.apiKey);
2061
+ p5.outro("You're all set.");
2062
+ return 0;
883
2063
  } catch (error) {
884
- spinner3.stop();
885
2064
  if (error instanceof Error) {
886
2065
  if (isPlanLimitFailure(error.message)) {
887
2066
  printPlanLimitMessage(apiUrl, error.message);
888
2067
  return 1;
889
2068
  }
890
- p.log.error(`Error: ${error.message}`);
2069
+ p5.log.error(`Error: ${error.message}`);
891
2070
  } else {
892
- p.log.error("Unknown setup error");
2071
+ p5.log.error("Unknown setup error");
893
2072
  }
894
2073
  return 1;
895
2074
  }
896
2075
  }
897
2076
 
2077
+ // src/commands/logout.ts
2078
+ import * as p6 from "@clack/prompts";
2079
+ async function logout(options = {}) {
2080
+ const stored = readAuthData();
2081
+ if (!stored) {
2082
+ p6.log.info("Not currently authenticated.");
2083
+ return 0;
2084
+ }
2085
+ const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
2086
+ const api = new VocoderAPI({ apiUrl, apiKey: "" });
2087
+ try {
2088
+ await api.revokeCliToken(stored.token);
2089
+ } catch {
2090
+ }
2091
+ clearAuthData();
2092
+ p6.log.success(`Logged out (was ${stored.email})`);
2093
+ return 0;
2094
+ }
2095
+
898
2096
  // src/commands/sync.ts
899
- import * as p2 from "@clack/prompts";
900
- import { createHash as createHash2, randomUUID } from "crypto";
2097
+ import * as p7 from "@clack/prompts";
2098
+ import { createHash, randomUUID } from "crypto";
901
2099
 
902
2100
  // src/utils/branch.ts
903
- import { execSync as execSync3 } from "child_process";
2101
+ import { execSync as execSync4 } from "child_process";
904
2102
  var REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
905
2103
  function escapeRegexChar(value) {
906
2104
  return value.replace(REGEX_SPECIAL_CHARS, "\\$&");
@@ -922,7 +2120,7 @@ function detectBranch(override) {
922
2120
  return envBranch;
923
2121
  }
924
2122
  try {
925
- const branch = execSync3("git rev-parse --abbrev-ref HEAD", {
2123
+ const branch = execSync4("git rev-parse --abbrev-ref HEAD", {
926
2124
  encoding: "utf-8",
927
2125
  stdio: ["pipe", "pipe", "ignore"]
928
2126
  }).trim();
@@ -966,10 +2164,10 @@ function matchBranchPattern(branch, pattern) {
966
2164
  }
967
2165
 
968
2166
  // src/commands/sync.ts
969
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
2167
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
970
2168
 
971
2169
  // src/utils/config.ts
972
- import chalk2 from "chalk";
2170
+ import chalk7 from "chalk";
973
2171
  import { config as loadEnv2 } from "dotenv";
974
2172
  loadEnv2();
975
2173
  function validateLocalConfig(config) {
@@ -1061,19 +2259,19 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
1061
2259
  configSources.noFallback = "environment";
1062
2260
  }
1063
2261
  if (verbose) {
1064
- console.log(chalk2.dim("\n Configuration sources:"));
1065
- console.log(chalk2.dim(` Include patterns: ${configSources.extractionPattern}`));
2262
+ console.log(chalk7.dim("\n Configuration sources:"));
2263
+ console.log(chalk7.dim(` Include patterns: ${configSources.extractionPattern}`));
1066
2264
  if (excludePattern.length > 0) {
1067
- console.log(chalk2.dim(` Exclude patterns: ${configSources.excludePattern}`));
2265
+ console.log(chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`));
1068
2266
  }
1069
- console.log(chalk2.dim(` API key: ${configSources.apiKey}`));
1070
- console.log(chalk2.dim(` API URL: ${configSources.apiUrl}
2267
+ console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
2268
+ console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
1071
2269
  `));
1072
- console.log(chalk2.dim(` Sync mode: ${configSources.mode}`));
2270
+ console.log(chalk7.dim(` Sync mode: ${configSources.mode}`));
1073
2271
  if (maxWaitMs) {
1074
- console.log(chalk2.dim(` Max wait: ${configSources.maxWaitMs}`));
2272
+ console.log(chalk7.dim(` Max wait: ${configSources.maxWaitMs}`));
1075
2273
  }
1076
- console.log(chalk2.dim(` No fallback: ${configSources.noFallback}
2274
+ console.log(chalk7.dim(` No fallback: ${configSources.noFallback}
1077
2275
  `));
1078
2276
  }
1079
2277
  return {
@@ -1088,280 +2286,8 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
1088
2286
  };
1089
2287
  }
1090
2288
 
1091
- // src/utils/extract.ts
1092
- import { createHash } from "crypto";
1093
- import { readFileSync as readFileSync2 } from "fs";
1094
- import { parse } from "@babel/parser";
1095
- import babelTraverse from "@babel/traverse";
1096
- import { glob } from "glob";
1097
- import { relative as pathRelative } from "path";
1098
- var traverse = babelTraverse.default || babelTraverse;
1099
- var StringExtractor = class {
1100
- /**
1101
- * Extract strings from all files matching the pattern(s)
1102
- *
1103
- * @param pattern - Glob pattern(s) to include
1104
- * @param projectRoot - Project root directory
1105
- * @param excludePattern - Glob pattern(s) to exclude (optional)
1106
- */
1107
- async extractFromProject(pattern, projectRoot = process.cwd(), excludePattern) {
1108
- const includePatterns = Array.isArray(pattern) ? pattern : [pattern];
1109
- const defaultIgnore = ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"];
1110
- const ignorePatterns = excludePattern ? [...defaultIgnore, ...Array.isArray(excludePattern) ? excludePattern : [excludePattern]] : defaultIgnore;
1111
- const allFiles = /* @__PURE__ */ new Set();
1112
- for (const includePattern of includePatterns) {
1113
- const files = await glob(includePattern, {
1114
- cwd: projectRoot,
1115
- absolute: true,
1116
- ignore: ignorePatterns
1117
- });
1118
- files.forEach((file) => allFiles.add(file));
1119
- }
1120
- const allStrings = [];
1121
- const sortedFiles = Array.from(allFiles).sort();
1122
- for (const file of sortedFiles) {
1123
- try {
1124
- const strings = await this.extractFromFile(file, projectRoot);
1125
- allStrings.push(...strings);
1126
- } catch (error) {
1127
- console.warn(`Warning: Failed to extract from ${file}:`, error);
1128
- }
1129
- }
1130
- const unique = this.deduplicateStrings(allStrings);
1131
- return unique;
1132
- }
1133
- /**
1134
- * Extract strings from a single file
1135
- */
1136
- async extractFromFile(filePath, projectRoot) {
1137
- const code = readFileSync2(filePath, "utf-8");
1138
- const strings = [];
1139
- const relativeFilePath = pathRelative(projectRoot, filePath).split("\\").join("/");
1140
- try {
1141
- const ast = parse(code, {
1142
- sourceType: "module",
1143
- plugins: ["jsx", "typescript"]
1144
- });
1145
- const vocoderImports = /* @__PURE__ */ new Map();
1146
- const tFunctionNames = /* @__PURE__ */ new Set();
1147
- traverse(ast, {
1148
- // Track imports of <T> component and t function
1149
- ImportDeclaration: (path) => {
1150
- const source = path.node.source.value;
1151
- if (source === "@vocoder/react") {
1152
- path.node.specifiers.forEach((spec) => {
1153
- if (spec.type === "ImportSpecifier") {
1154
- const imported = spec.imported.type === "Identifier" ? spec.imported.name : null;
1155
- const local = spec.local.name;
1156
- if (imported === "T") {
1157
- vocoderImports.set(local, "T");
1158
- }
1159
- if (imported === "t") {
1160
- tFunctionNames.add(local);
1161
- }
1162
- if (imported === "useVocoder") {
1163
- }
1164
- }
1165
- });
1166
- }
1167
- },
1168
- // Track destructured 't' from useVocoder hook
1169
- VariableDeclarator: (path) => {
1170
- const init2 = path.node.init;
1171
- if (init2 && init2.type === "CallExpression" && init2.callee.type === "Identifier" && init2.callee.name === "useVocoder" && path.node.id.type === "ObjectPattern") {
1172
- path.node.id.properties.forEach((prop) => {
1173
- if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "t") {
1174
- const localName = prop.value.type === "Identifier" ? prop.value.name : "t";
1175
- tFunctionNames.add(localName);
1176
- }
1177
- });
1178
- }
1179
- },
1180
- // Extract from t() function calls
1181
- CallExpression: (path) => {
1182
- const callee = path.node.callee;
1183
- const isTFunction = callee.type === "Identifier" && tFunctionNames.has(callee.name);
1184
- if (!isTFunction) return;
1185
- const firstArg = path.node.arguments[0];
1186
- if (!firstArg) return;
1187
- let text = null;
1188
- if (firstArg.type === "StringLiteral") {
1189
- text = firstArg.value;
1190
- } else if (firstArg.type === "TemplateLiteral") {
1191
- text = this.extractTemplateText(firstArg);
1192
- }
1193
- if (!text || text.trim().length === 0) return;
1194
- const secondArg = path.node.arguments[1];
1195
- let context;
1196
- let formality;
1197
- let explicitKey;
1198
- if (secondArg && secondArg.type === "ObjectExpression") {
1199
- secondArg.properties.forEach((prop) => {
1200
- if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
1201
- if (prop.key.name === "context" && prop.value.type === "StringLiteral") {
1202
- context = prop.value.value;
1203
- }
1204
- if (prop.key.name === "formality" && prop.value.type === "StringLiteral") {
1205
- formality = prop.value.value;
1206
- }
1207
- if (prop.key.name === "id" && prop.value.type === "StringLiteral") {
1208
- explicitKey = prop.value.value.trim();
1209
- }
1210
- }
1211
- });
1212
- }
1213
- const line = path.node.loc?.start.line || 0;
1214
- const column = path.node.loc?.start.column || 0;
1215
- const key = explicitKey && explicitKey.length > 0 ? explicitKey : this.generateStableKey({
1216
- filePath: relativeFilePath,
1217
- kind: "t-call",
1218
- line,
1219
- column
1220
- });
1221
- strings.push({
1222
- key,
1223
- text: text.trim(),
1224
- file: filePath,
1225
- line,
1226
- context,
1227
- formality
1228
- });
1229
- },
1230
- // Extract from JSX elements
1231
- JSXElement: (path) => {
1232
- const opening = path.node.openingElement;
1233
- const tagName = opening.name.type === "JSXIdentifier" ? opening.name.name : null;
1234
- if (!tagName) return;
1235
- const isTranslationComponent = vocoderImports.has(tagName);
1236
- if (!isTranslationComponent) return;
1237
- const msgAttribute = this.getStringAttribute(opening.attributes, "msg");
1238
- const text = msgAttribute || this.extractTextContent(path.node.children);
1239
- if (!text || text.trim().length === 0) return;
1240
- const id = this.getStringAttribute(opening.attributes, "id");
1241
- const context = this.getStringAttribute(opening.attributes, "context");
1242
- const formality = this.getStringAttribute(
1243
- opening.attributes,
1244
- "formality"
1245
- );
1246
- const line = path.node.loc?.start.line || 0;
1247
- const column = path.node.loc?.start.column || 0;
1248
- const key = id && id.trim().length > 0 ? id.trim() : this.generateStableKey({
1249
- filePath: relativeFilePath,
1250
- kind: "jsx",
1251
- line,
1252
- column
1253
- });
1254
- strings.push({
1255
- key,
1256
- text: text.trim(),
1257
- file: filePath,
1258
- line,
1259
- context,
1260
- formality
1261
- });
1262
- }
1263
- });
1264
- } catch (error) {
1265
- throw new Error(
1266
- `Failed to parse ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`
1267
- );
1268
- }
1269
- return strings;
1270
- }
1271
- /**
1272
- * Extract text from template literal
1273
- * Converts template literals like `Hello ${name}` to `Hello {name}`
1274
- */
1275
- extractTemplateText(node) {
1276
- let text = "";
1277
- for (let i = 0; i < node.quasis.length; i++) {
1278
- const quasi = node.quasis[i];
1279
- text += quasi.value.raw;
1280
- if (i < node.expressions.length) {
1281
- const expr = node.expressions[i];
1282
- if (expr.type === "Identifier") {
1283
- text += `{${expr.name}}`;
1284
- } else {
1285
- text += "{value}";
1286
- }
1287
- }
1288
- }
1289
- return text;
1290
- }
1291
- /**
1292
- * Extract text content from JSX children
1293
- */
1294
- extractTextContent(children) {
1295
- let text = "";
1296
- for (const child of children) {
1297
- if (child.type === "JSXText") {
1298
- text += child.value;
1299
- } else if (child.type === "JSXExpressionContainer") {
1300
- const expr = child.expression;
1301
- if (expr.type === "Identifier") {
1302
- text += `{${expr.name}}`;
1303
- } else if (expr.type === "StringLiteral") {
1304
- text += expr.value;
1305
- } else if (expr.type === "TemplateLiteral") {
1306
- text += this.extractTemplateText(expr);
1307
- }
1308
- }
1309
- }
1310
- return text;
1311
- }
1312
- /**
1313
- * Get string value from JSX attribute
1314
- * Handles both string literals and template literals
1315
- */
1316
- getStringAttribute(attributes, name) {
1317
- const attr = attributes.find(
1318
- (a) => a.type === "JSXAttribute" && a.name.name === name
1319
- );
1320
- if (!attr || !attr.value) return void 0;
1321
- if (attr.value.type === "StringLiteral") {
1322
- return attr.value.value;
1323
- }
1324
- if (attr.value.type === "JSXExpressionContainer") {
1325
- const expr = attr.value.expression;
1326
- if (expr.type === "TemplateLiteral") {
1327
- return this.extractTemplateText(expr);
1328
- }
1329
- if (expr.type === "StringLiteral") {
1330
- return expr.value;
1331
- }
1332
- }
1333
- return void 0;
1334
- }
1335
- /**
1336
- * Deduplicate strings (keep first occurrence)
1337
- */
1338
- deduplicateStrings(strings) {
1339
- const seen = /* @__PURE__ */ new Map();
1340
- const unique = [];
1341
- for (const str of strings) {
1342
- const dedupeKey = `${str.text}|${str.context || ""}|${str.formality || ""}`;
1343
- const existingIndex = seen.get(dedupeKey);
1344
- if (existingIndex === void 0) {
1345
- seen.set(dedupeKey, unique.length);
1346
- unique.push(str);
1347
- continue;
1348
- }
1349
- const existing = unique[existingIndex];
1350
- if (existing && str.key < existing.key) {
1351
- existing.key = str.key;
1352
- }
1353
- }
1354
- return unique;
1355
- }
1356
- generateStableKey(params) {
1357
- const payload = `${params.filePath}|${params.kind}|${params.line}:${params.column}`;
1358
- const digest = createHash("sha1").update(payload).digest("hex");
1359
- return `SK_${digest.slice(0, 24).toUpperCase()}`;
1360
- }
1361
- };
1362
-
1363
2289
  // src/commands/sync.ts
1364
- import chalk3 from "chalk";
2290
+ import chalk8 from "chalk";
1365
2291
  import { join as join2 } from "path";
1366
2292
  function isRecord(value) {
1367
2293
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1408,7 +2334,7 @@ function parseTranslations(value) {
1408
2334
  }
1409
2335
  function getCacheFilePath(projectRoot, branch) {
1410
2336
  const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
1411
- const branchHash = createHash2("sha1").update(branch).digest("hex").slice(0, 12);
2337
+ const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
1412
2338
  const filename = `${slug || "branch"}-${branchHash}.json`;
1413
2339
  return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", filename);
1414
2340
  }
@@ -1416,11 +2342,11 @@ function readLocalSnapshotCache(params) {
1416
2342
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
1417
2343
  for (const candidateBranch of candidateBranches) {
1418
2344
  const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
1419
- if (!existsSync2(cacheFilePath)) {
2345
+ if (!existsSync(cacheFilePath)) {
1420
2346
  continue;
1421
2347
  }
1422
2348
  try {
1423
- const raw = readFileSync3(cacheFilePath, "utf-8");
2349
+ const raw = readFileSync2(cacheFilePath, "utf-8");
1424
2350
  const parsed = JSON.parse(raw);
1425
2351
  if (!isRecord(parsed)) {
1426
2352
  continue;
@@ -1446,7 +2372,7 @@ function readLocalSnapshotCache(params) {
1446
2372
  }
1447
2373
  function writeLocalSnapshotCache(params) {
1448
2374
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
1449
- mkdirSync(join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"), {
2375
+ mkdirSync2(join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"), {
1450
2376
  recursive: true
1451
2377
  });
1452
2378
  const payload = {
@@ -1460,7 +2386,7 @@ function writeLocalSnapshotCache(params) {
1460
2386
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
1461
2387
  translations: params.translations
1462
2388
  };
1463
- writeFileSync(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
2389
+ writeFileSync2(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
1464
2390
  return cacheFilePath;
1465
2391
  }
1466
2392
  function resolveEffectiveModeFromPolicy(params) {
@@ -1615,13 +2541,13 @@ async function fetchApiSnapshot(api, params) {
1615
2541
  async function sync(options = {}) {
1616
2542
  const startTime = Date.now();
1617
2543
  const projectRoot = process.cwd();
1618
- p2.intro("Vocoder Sync");
1619
- const spinner3 = p2.spinner();
2544
+ p7.intro("Vocoder Sync");
2545
+ const spinner4 = p7.spinner();
1620
2546
  try {
1621
- spinner3.start("Detecting branch");
2547
+ spinner4.start("Detecting branch");
1622
2548
  const branch = detectBranch(options.branch);
1623
- spinner3.stop(`Branch: ${chalk3.cyan(branch)}`);
1624
- spinner3.start("Loading project configuration");
2549
+ spinner4.stop(`Branch: ${chalk8.cyan(branch)}`);
2550
+ spinner4.start("Loading project configuration");
1625
2551
  const mergedConfig = await getMergedConfig(options, options.verbose);
1626
2552
  const localConfig = {
1627
2553
  apiKey: mergedConfig.apiKey || "",
@@ -1643,18 +2569,18 @@ async function sync(options = {}) {
1643
2569
  excludePattern: mergedConfig.excludePattern,
1644
2570
  timeout: waitTimeoutMs
1645
2571
  };
1646
- spinner3.stop("Project configuration loaded");
2572
+ spinner4.stop("Project configuration loaded");
1647
2573
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
1648
- p2.log.warn(
1649
- `Skipping translations (${chalk3.cyan(branch)} is not a target branch)`
2574
+ p7.log.warn(
2575
+ `Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
1650
2576
  );
1651
- p2.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
1652
- p2.log.info("Use --force to translate anyway");
1653
- p2.outro("");
2577
+ p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
2578
+ p7.log.info("Use --force to translate anyway");
2579
+ p7.outro("");
1654
2580
  return 0;
1655
2581
  }
1656
2582
  const patternsDisplay = Array.isArray(config.extractionPattern) ? config.extractionPattern.join(", ") : config.extractionPattern;
1657
- spinner3.start(`Extracting strings from ${patternsDisplay}`);
2583
+ spinner4.start(`Extracting strings from ${patternsDisplay}`);
1658
2584
  const extractor = new StringExtractor();
1659
2585
  const extractedStrings = await extractor.extractFromProject(
1660
2586
  config.extractionPattern,
@@ -1662,23 +2588,23 @@ async function sync(options = {}) {
1662
2588
  config.excludePattern
1663
2589
  );
1664
2590
  if (extractedStrings.length === 0) {
1665
- spinner3.stop("No translatable strings found");
1666
- p2.log.warn("Make sure you are wrapping translatable strings with Vocoder");
1667
- p2.outro("");
2591
+ spinner4.stop("No translatable strings found");
2592
+ p7.log.warn("Make sure you are wrapping translatable strings with Vocoder");
2593
+ p7.outro("");
1668
2594
  return 0;
1669
2595
  }
1670
- spinner3.stop(
1671
- `Extracted ${chalk3.cyan(extractedStrings.length)} strings from ${chalk3.cyan(patternsDisplay)}`
2596
+ spinner4.stop(
2597
+ `Extracted ${chalk8.cyan(extractedStrings.length)} strings from ${chalk8.cyan(patternsDisplay)}`
1672
2598
  );
1673
2599
  if (options.verbose) {
1674
2600
  const sampleLines = extractedStrings.slice(0, 5).map((s) => ` "${s.text}" (${s.file}:${s.line})`);
1675
2601
  if (extractedStrings.length > 5) {
1676
2602
  sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
1677
2603
  }
1678
- p2.note(sampleLines.join("\n"), "Sample strings");
2604
+ p7.note(sampleLines.join("\n"), "Sample strings");
1679
2605
  }
1680
2606
  if (options.dryRun) {
1681
- p2.note(
2607
+ p7.note(
1682
2608
  [
1683
2609
  `Strings: ${extractedStrings.length}`,
1684
2610
  `Branch: ${branch}`,
@@ -1689,23 +2615,23 @@ async function sync(options = {}) {
1689
2615
  ].join("\n"),
1690
2616
  "Dry run - would translate"
1691
2617
  );
1692
- p2.outro("No API calls made.");
2618
+ p7.outro("No API calls made.");
1693
2619
  return 0;
1694
2620
  }
1695
2621
  const repoIdentity = resolveGitRepositoryIdentity();
1696
2622
  if (!repoIdentity && options.verbose) {
1697
- p2.log.warn(
2623
+ p7.log.warn(
1698
2624
  "Could not detect git remote origin. Sync will continue without repo metadata."
1699
2625
  );
1700
2626
  }
1701
2627
  const stringEntries = buildStringEntries(extractedStrings);
1702
2628
  const sourceStrings = stringEntries.map((entry) => entry.text);
1703
2629
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
1704
- p2.log.info(
2630
+ p7.log.info(
1705
2631
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
1706
2632
  );
1707
2633
  }
1708
- spinner3.start("Submitting strings to Vocoder API");
2634
+ spinner4.start("Submitting strings to Vocoder API");
1709
2635
  const batchResponse = await api.submitTranslation(
1710
2636
  branch,
1711
2637
  stringEntries,
@@ -1717,38 +2643,38 @@ async function sync(options = {}) {
1717
2643
  },
1718
2644
  repoIdentity ?? void 0
1719
2645
  );
1720
- spinner3.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
2646
+ spinner4.stop(`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`);
1721
2647
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
1722
2648
  branch,
1723
2649
  requestedMode,
1724
2650
  policy: config.syncPolicy
1725
2651
  });
1726
2652
  if (options.verbose) {
1727
- p2.log.info(`Requested mode: ${requestedMode}`);
1728
- p2.log.info(`Effective mode: ${effectiveMode}`);
1729
- p2.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
2653
+ p7.log.info(`Requested mode: ${requestedMode}`);
2654
+ p7.log.info(`Effective mode: ${effectiveMode}`);
2655
+ p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
1730
2656
  if (batchResponse.queueStatus) {
1731
- p2.log.info(`Queue status: ${batchResponse.queueStatus}`);
2657
+ p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
1732
2658
  }
1733
2659
  }
1734
2660
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
1735
- p2.log.success("No changes detected - strings are up to date");
2661
+ p7.log.success("No changes detected - strings are up to date");
1736
2662
  }
1737
- p2.log.info(`New strings: ${chalk3.cyan(batchResponse.newStrings)}`);
2663
+ p7.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
1738
2664
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
1739
- p2.log.info(
1740
- `Deleted strings: ${chalk3.yellow(batchResponse.deletedStrings)} (archived)`
2665
+ p7.log.info(
2666
+ `Deleted strings: ${chalk8.yellow(batchResponse.deletedStrings)} (archived)`
1741
2667
  );
1742
2668
  }
1743
- p2.log.info(`Total strings: ${chalk3.cyan(batchResponse.totalStrings)}`);
2669
+ p7.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
1744
2670
  if (batchResponse.newStrings === 0) {
1745
- p2.log.success("No new strings - using existing translations");
2671
+ p7.log.success("No new strings - using existing translations");
1746
2672
  } else {
1747
- p2.log.info(
2673
+ p7.log.info(
1748
2674
  `Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
1749
2675
  );
1750
2676
  if (batchResponse.estimatedTime) {
1751
- p2.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
2677
+ p7.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
1752
2678
  }
1753
2679
  }
1754
2680
  let artifacts = null;
@@ -1760,7 +2686,7 @@ async function sync(options = {}) {
1760
2686
  }
1761
2687
  let waitError = null;
1762
2688
  if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
1763
- spinner3.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
2689
+ spinner4.start(`Waiting for translations (max ${waitTimeoutMs}ms)`);
1764
2690
  let lastProgress = 0;
1765
2691
  try {
1766
2692
  const completion = await api.waitForCompletion(
@@ -1769,7 +2695,7 @@ async function sync(options = {}) {
1769
2695
  (progress) => {
1770
2696
  const percent = Math.round(progress * 100);
1771
2697
  if (percent > lastProgress) {
1772
- spinner3.message(`Translating... ${percent}%`);
2698
+ spinner4.message(`Translating... ${percent}%`);
1773
2699
  lastProgress = percent;
1774
2700
  }
1775
2701
  }
@@ -1779,14 +2705,14 @@ async function sync(options = {}) {
1779
2705
  translations: completion.translations,
1780
2706
  localeMetadata: completion.localeMetadata
1781
2707
  };
1782
- spinner3.stop("Translations complete");
2708
+ spinner4.stop("Translations complete");
1783
2709
  } catch (error) {
1784
- spinner3.stop("Translation wait incomplete");
2710
+ spinner4.stop("Translation wait incomplete");
1785
2711
  waitError = error instanceof Error ? error : new Error(String(error));
1786
2712
  if (effectiveMode === "required") {
1787
2713
  throw waitError;
1788
2714
  }
1789
- p2.log.warn(`Best-effort wait ended early: ${waitError.message}`);
2715
+ p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
1790
2716
  }
1791
2717
  }
1792
2718
  if (!artifacts) {
@@ -1795,7 +2721,7 @@ async function sync(options = {}) {
1795
2721
  "Fresh translations are not available and fallback is disabled (--no-fallback)."
1796
2722
  );
1797
2723
  }
1798
- spinner3.start("Loading fallback translations");
2724
+ spinner4.start("Loading fallback translations");
1799
2725
  const localFallback = readLocalSnapshotCache({
1800
2726
  projectRoot,
1801
2727
  branch
@@ -1803,7 +2729,7 @@ async function sync(options = {}) {
1803
2729
  if (localFallback) {
1804
2730
  artifacts = localFallback;
1805
2731
  const cacheBranchLabel = localFallback.cacheBranch && localFallback.cacheBranch !== branch ? `${localFallback.cacheBranch} fallback` : localFallback.cacheBranch || branch;
1806
- spinner3.stop(`Using local cached snapshot (${cacheBranchLabel})`);
2732
+ spinner4.stop(`Using local cached snapshot (${cacheBranchLabel})`);
1807
2733
  } else {
1808
2734
  try {
1809
2735
  const apiSnapshot = await fetchApiSnapshot(api, {
@@ -1812,15 +2738,15 @@ async function sync(options = {}) {
1812
2738
  });
1813
2739
  if (apiSnapshot) {
1814
2740
  artifacts = apiSnapshot;
1815
- spinner3.stop("Using latest completed API snapshot");
2741
+ spinner4.stop("Using latest completed API snapshot");
1816
2742
  } else {
1817
- spinner3.stop("No completed API snapshot available");
2743
+ spinner4.stop("No completed API snapshot available");
1818
2744
  }
1819
2745
  } catch (error) {
1820
- spinner3.stop("Failed to fetch API snapshot");
2746
+ spinner4.stop("Failed to fetch API snapshot");
1821
2747
  if (options.verbose) {
1822
2748
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
1823
- p2.log.warn(`Snapshot fetch error: ${message}`);
2749
+ p7.log.warn(`Snapshot fetch error: ${message}`);
1824
2750
  }
1825
2751
  }
1826
2752
  }
@@ -1853,81 +2779,108 @@ async function sync(options = {}) {
1853
2779
  completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
1854
2780
  });
1855
2781
  if (options.verbose) {
1856
- p2.log.info(`Cached snapshot: ${cachePath}`);
2782
+ p7.log.info(`Cached snapshot: ${cachePath}`);
1857
2783
  }
1858
2784
  } catch (error) {
1859
2785
  if (options.verbose) {
1860
2786
  const message = error instanceof Error ? error.message : "Unknown cache write error";
1861
- p2.log.warn(`Failed to write local snapshot cache: ${message}`);
2787
+ p7.log.warn(`Failed to write local snapshot cache: ${message}`);
1862
2788
  }
1863
2789
  }
1864
2790
  if (artifacts.source !== "fresh") {
1865
2791
  const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
1866
- p2.log.warn(
2792
+ p7.log.warn(
1867
2793
  `Using ${sourceLabel}. New strings may appear after the background sync completes.`
1868
2794
  );
1869
2795
  }
1870
2796
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
1871
- p2.outro(`Sync complete! (${duration}s)`);
1872
- p2.log.info("Translations will be injected at build time by @vocoder/unplugin.");
1873
- p2.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
2797
+ p7.outro(`Sync complete! (${duration}s)`);
2798
+ p7.log.info("Translations will be injected at build time by @vocoder/unplugin.");
2799
+ p7.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
1874
2800
  return 0;
1875
2801
  } catch (error) {
1876
- spinner3.stop();
2802
+ spinner4.stop();
1877
2803
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
1878
- p2.log.error(error.syncPolicyError.message);
2804
+ p7.log.error(error.syncPolicyError.message);
1879
2805
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
1880
2806
  for (const line of guidance) {
1881
- p2.log.info(line);
2807
+ p7.log.info(line);
1882
2808
  }
1883
2809
  return 1;
1884
2810
  }
1885
2811
  if (error instanceof VocoderAPIError && error.limitError) {
1886
2812
  const { limitError } = error;
1887
- p2.log.error(limitError.message);
2813
+ p7.log.error(limitError.message);
1888
2814
  const guidance = getLimitErrorGuidance(limitError);
1889
2815
  for (const line of guidance) {
1890
- p2.log.info(line);
2816
+ p7.log.info(line);
1891
2817
  }
1892
2818
  return 1;
1893
2819
  }
1894
2820
  if (error instanceof Error) {
1895
- p2.log.error(error.message);
2821
+ p7.log.error(error.message);
1896
2822
  if (error.message.includes("VOCODER_API_KEY")) {
1897
- p2.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
1898
- p2.log.info(" Create one at: https://vocoder.app/dashboard");
1899
- p2.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
1900
- p2.log.info("");
1901
- p2.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
1902
- p2.log.info(" Translations are fetched automatically at build time.");
2823
+ p7.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
2824
+ p7.log.info(" Create one at: https://vocoder.app/dashboard");
2825
+ p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
2826
+ p7.log.info("");
2827
+ p7.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
2828
+ p7.log.info(" Translations are fetched automatically at build time.");
1903
2829
  } else if (error.message.includes("git branch")) {
1904
- p2.log.warn("Run from a git repository, or use:");
1905
- p2.log.info(" vocoder sync --branch main");
2830
+ p7.log.warn("Run from a git repository, or use:");
2831
+ p7.log.info(" vocoder sync --branch main");
1906
2832
  }
1907
2833
  if (options.verbose) {
1908
- p2.log.info(`Full error: ${error.stack ?? error}`);
2834
+ p7.log.info(`Full error: ${error.stack ?? error}`);
1909
2835
  }
1910
2836
  }
1911
2837
  return 1;
1912
2838
  }
1913
2839
  }
1914
2840
 
2841
+ // src/commands/whoami.ts
2842
+ import * as p8 from "@clack/prompts";
2843
+ import chalk9 from "chalk";
2844
+ async function whoami(options = {}) {
2845
+ const stored = readAuthData();
2846
+ if (!stored) {
2847
+ p8.log.info("Not logged in. Run `vocoder init` to authenticate.");
2848
+ return 1;
2849
+ }
2850
+ const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
2851
+ const api = new VocoderAPI({ apiUrl, apiKey: "" });
2852
+ try {
2853
+ const info = await api.getCliUserInfo(stored.token);
2854
+ p8.log.info(`Logged in as ${chalk9.bold(info.email)}`);
2855
+ if (info.name) {
2856
+ p8.log.info(`Name: ${info.name}`);
2857
+ }
2858
+ p8.log.info(`API: ${apiUrl}`);
2859
+ return 0;
2860
+ } catch {
2861
+ p8.log.error("Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate.");
2862
+ return 1;
2863
+ }
2864
+ }
2865
+
1915
2866
  // src/bin.ts
1916
2867
  function collect(value, previous = []) {
1917
2868
  return previous.concat([value]);
1918
2869
  }
1919
2870
  async function runCommand(command, options) {
1920
2871
  const exitCode = await command(options);
1921
- process.exitCode = exitCode;
2872
+ process.exit(exitCode);
1922
2873
  }
1923
2874
  var program = new Command();
1924
2875
  program.name("vocoder").description("Vocoder CLI - Project setup and string extraction").version("0.1.5");
1925
- 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("--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));
2876
+ 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));
1926
2877
  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) => {
1927
2878
  const translated = { ...options };
1928
2879
  if (options.maxWait) translated.maxWaitMs = Number(options.maxWait);
1929
2880
  if (options.fallback === false) translated.noFallback = true;
1930
2881
  return runCommand(sync, translated);
1931
2882
  });
2883
+ program.command("logout").description("Log out and remove stored credentials").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(logout, options));
2884
+ program.command("whoami").description("Show the currently authenticated user").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(whoami, options));
1932
2885
  program.parse(process.argv);
1933
2886
  //# sourceMappingURL=bin.mjs.map