@vocoder/cli 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -2,759 +2,36 @@
2
2
  import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  StringExtractor,
5
+ VocoderAPI,
6
+ VocoderAPIError,
5
7
  buildInstallCommand,
8
+ clearAuthData,
6
9
  detectLocalEcosystem,
7
10
  getPackagesToInstall,
8
11
  getSetupSnippets,
9
- loadVocoderConfig
10
- } from "./chunk-73U4VZYP.mjs";
12
+ loadVocoderConfig,
13
+ readAuthData,
14
+ writeAuthData
15
+ } from "./chunk-XF3KGGYQ.mjs";
11
16
 
12
17
  // src/bin.ts
13
18
  import { Command } from "commander";
14
19
 
15
20
  // src/commands/init.ts
16
- import { execSync as execSync3, spawn as spawn2 } from "child_process";
17
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
18
- import { join as join3 } from "path";
19
21
  import * as p5 from "@clack/prompts";
20
- import chalk6 from "chalk";
21
- import { config as loadEnv } from "dotenv";
22
-
23
- // src/utils/api.ts
24
- function isLimitErrorResponse(value) {
25
- if (!value || typeof value !== "object") {
26
- return false;
27
- }
28
- const candidate = value;
29
- return typeof candidate.errorCode === "string" && typeof candidate.limitType === "string" && typeof candidate.planId === "string" && typeof candidate.current === "number" && typeof candidate.required === "number" && typeof candidate.upgradeUrl === "string" && typeof candidate.message === "string";
30
- }
31
- function isSyncPolicyErrorResponse(value) {
32
- if (!value || typeof value !== "object") {
33
- return false;
34
- }
35
- const candidate = value;
36
- return (candidate.errorCode === "BRANCH_NOT_ALLOWED" || candidate.errorCode === "PROJECT_REPOSITORY_MISMATCH") && typeof candidate.message === "string";
37
- }
38
- function extractErrorMessage(payload, fallback) {
39
- if (!payload || typeof payload !== "object") {
40
- return fallback;
41
- }
42
- const candidate = payload;
43
- if (typeof candidate.message === "string") {
44
- return candidate.message;
45
- }
46
- if (typeof candidate.error === "string") {
47
- return candidate.error;
48
- }
49
- return fallback;
50
- }
51
- function parsePayload(raw) {
52
- if (raw.length === 0) {
53
- return null;
54
- }
55
- const trimmed = raw.trimStart();
56
- if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
57
- return {
58
- message: "Unexpected response from server (received HTML). Check your network connection or try again."
59
- };
60
- }
61
- try {
62
- return JSON.parse(raw);
63
- } catch {
64
- return { message: raw };
65
- }
66
- }
67
- async function readPayload(response) {
68
- if (typeof response.text === "function") {
69
- const raw = await response.text();
70
- return parsePayload(raw);
71
- }
72
- if (typeof response.json === "function") {
73
- return response.json();
74
- }
75
- return null;
76
- }
77
- var VocoderAPIError = class extends Error {
78
- constructor(params) {
79
- super(params.message);
80
- this.name = "VocoderAPIError";
81
- this.status = params.status;
82
- this.payload = params.payload;
83
- this.limitError = params.limitError ?? null;
84
- this.syncPolicyError = params.syncPolicyError ?? null;
85
- }
86
- };
87
- var VocoderAPI = class {
88
- constructor(config) {
89
- this.apiUrl = config.apiUrl;
90
- this.apiKey = config.apiKey;
91
- }
92
- async request(path, init2 = {}, errorPrefix) {
93
- const response = await fetch(`${this.apiUrl}${path}`, {
94
- ...init2,
95
- headers: {
96
- Authorization: `Bearer ${this.apiKey}`,
97
- ...init2.headers ?? {}
98
- }
99
- });
100
- const payload = await readPayload(response);
101
- if (!response.ok) {
102
- const limitError = isLimitErrorResponse(payload) ? payload : null;
103
- const syncPolicyError = isSyncPolicyErrorResponse(payload) ? payload : null;
104
- const baseMessage = extractErrorMessage(
105
- payload,
106
- `Request failed with status ${response.status}`
107
- );
108
- throw new VocoderAPIError({
109
- message: errorPrefix ? `${errorPrefix}: ${baseMessage}` : baseMessage,
110
- status: response.status,
111
- payload,
112
- limitError,
113
- syncPolicyError
114
- });
115
- }
116
- return payload;
117
- }
118
- /**
119
- * Fetch project configuration from API
120
- * Project is determined from the API key
121
- */
122
- async getProjectConfig() {
123
- const data = await this.request("/api/cli/config", {}, "Failed to fetch project config");
124
- return {
125
- projectName: data.projectName,
126
- organizationName: data.organizationName,
127
- shortCode: data.shortCode,
128
- sourceLocale: data.sourceLocale,
129
- targetLocales: data.targetLocales,
130
- targetBranches: data.targetBranches ?? ["main"],
131
- primaryBranch: data.primaryBranch,
132
- syncPolicy: {
133
- blockingBranches: data.syncPolicy?.blockingBranches ?? [
134
- "main",
135
- "master"
136
- ],
137
- blockingMode: data.syncPolicy?.blockingMode ?? "required",
138
- nonBlockingMode: data.syncPolicy?.nonBlockingMode ?? "best-effort",
139
- defaultMaxWaitMs: data.syncPolicy?.defaultMaxWaitMs ?? 6e4
140
- }
141
- };
142
- }
143
- /**
144
- * Submit strings for translation
145
- * Project is determined from the API key
146
- */
147
- stableTextKey(text2) {
148
- let hash = 2166136261;
149
- for (let i = 0; i < text2.length; i++) {
150
- hash ^= text2.charCodeAt(i);
151
- hash = Math.imul(hash, 16777619);
152
- }
153
- return `SK_TEXT_${(hash >>> 0).toString(16).toUpperCase().padStart(8, "0")}`;
154
- }
155
- normalizeStringEntries(entries) {
156
- if (entries.length === 0) {
157
- return [];
158
- }
159
- const first = entries[0];
160
- if (typeof first === "string") {
161
- return entries.map((text2) => ({
162
- key: this.stableTextKey(text2),
163
- text: text2
164
- }));
165
- }
166
- return entries.map((entry, index) => ({
167
- key: entry.key || this.stableTextKey(`${entry.text}:${index}`),
168
- text: entry.text,
169
- ...entry.context ? { context: entry.context } : {},
170
- ...entry.formality ? { formality: entry.formality } : {},
171
- ...entry.uiRole ? { uiRole: entry.uiRole } : {},
172
- ...entry.featureArea ? { featureArea: entry.featureArea } : {}
173
- }));
174
- }
175
- async submitTranslation(branch, entries, targetLocales, options, repoIdentity) {
176
- const stringEntries = this.normalizeStringEntries(entries);
177
- const strings = stringEntries.map((entry) => entry.text);
178
- const crypto = await import("crypto");
179
- const sortedStrings = [...strings].sort();
180
- const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
181
- return this.request(
182
- "/api/cli/sync",
183
- {
184
- method: "POST",
185
- headers: {
186
- "Content-Type": "application/json"
187
- },
188
- body: JSON.stringify({
189
- branch,
190
- stringEntries,
191
- targetLocales,
192
- ...options?.force ? {} : { stringsHash },
193
- ...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
194
- ...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
195
- ...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
196
- ...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
197
- ...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
198
- ...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {},
199
- ...options?.appIndustry ? { appIndustry: options.appIndustry } : {}
200
- })
201
- },
202
- "Translation submission failed"
203
- );
204
- }
205
- /**
206
- * Check translation status
207
- */
208
- async getTranslationStatus(batchId) {
209
- return this.request(
210
- `/api/cli/sync/status/${batchId}`,
211
- {},
212
- "Failed to check translation status"
213
- );
214
- }
215
- async getTranslationSnapshot(params) {
216
- const search = new URLSearchParams();
217
- search.set("branch", params.branch);
218
- for (const locale of params.targetLocales) {
219
- search.append("targetLocale", locale);
220
- }
221
- return this.request(
222
- `/api/cli/sync/snapshot?${search.toString()}`,
223
- {},
224
- "Failed to fetch translation snapshot"
225
- );
226
- }
227
- /**
228
- * Wait for translation to complete with polling
229
- */
230
- async waitForCompletion(batchId, timeout = 6e4, onProgress) {
231
- const startTime = Date.now();
232
- const pollInterval = 1e3;
233
- while (Date.now() - startTime < timeout) {
234
- const status = await this.getTranslationStatus(batchId);
235
- if (onProgress) {
236
- onProgress(status.progress);
237
- }
238
- if (status.status === "COMPLETED") {
239
- if (!status.translations) {
240
- throw new Error("Translation completed but no translations returned");
241
- }
242
- return {
243
- translations: status.translations,
244
- localeMetadata: status.localeMetadata
245
- };
246
- }
247
- if (status.status === "FAILED") {
248
- throw new Error(
249
- `Translation failed: ${status.errorMessage || "Unknown error"}`
250
- );
251
- }
252
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
253
- }
254
- throw new Error(`Translation timeout after ${timeout}ms`);
255
- }
256
- async startInitSession(input) {
257
- const response = await fetch(`${this.apiUrl}/api/cli/init/start`, {
258
- method: "POST",
259
- headers: {
260
- "Content-Type": "application/json"
261
- },
262
- body: JSON.stringify(input)
263
- });
264
- const payload = await readPayload(response);
265
- if (!response.ok) {
266
- throw new VocoderAPIError({
267
- message: extractErrorMessage(
268
- payload,
269
- `Failed to start init session (${response.status})`
270
- ),
271
- status: response.status,
272
- payload
273
- });
274
- }
275
- return payload;
276
- }
277
- async getInitSessionStatus(params) {
278
- const response = await fetch(
279
- `${this.apiUrl}/api/cli/init/status/${params.sessionId}`,
280
- {
281
- headers: {
282
- Authorization: `Bearer ${params.pollToken}`
283
- }
284
- }
285
- );
286
- const payload = await readPayload(response);
287
- if (!response.ok) {
288
- throw new VocoderAPIError({
289
- message: extractErrorMessage(
290
- payload,
291
- `Failed to get init status (${response.status})`
292
- ),
293
- status: response.status,
294
- payload
295
- });
296
- }
297
- return payload;
298
- }
299
- // ── CLI Auth endpoints (no project API key needed) ──────────────────────────
300
- /**
301
- * Start a CLI auth session. Returns `{ sessionId, verificationUrl, expiresAt }`.
302
- * `sessionId` is the raw poll token — keep it secret, used for polling.
303
- */
304
- async startCliAuthSession(callbackPort, repoCanonical) {
305
- const response = await fetch(`${this.apiUrl}/api/cli/auth/start`, {
306
- method: "POST",
307
- headers: { "Content-Type": "application/json" },
308
- body: JSON.stringify({
309
- ...callbackPort != null ? { callbackPort } : {},
310
- ...repoCanonical ? { repoCanonical } : {}
311
- })
312
- });
313
- const payload = await readPayload(response);
314
- if (!response.ok) {
315
- throw new VocoderAPIError({
316
- message: extractErrorMessage(
317
- payload,
318
- `Failed to start auth session (${response.status})`
319
- ),
320
- status: response.status,
321
- payload
322
- });
323
- }
324
- return payload;
325
- }
326
- /**
327
- * Poll for CLI auth session completion.
328
- * Returns `{ token }` on success, throws on failure/expiry.
329
- * The server returns HTTP 202 while still pending.
330
- */
331
- async pollCliAuthSession(pollToken) {
332
- const response = await fetch(
333
- `${this.apiUrl}/api/cli/auth/session?session=${encodeURIComponent(pollToken)}`
334
- );
335
- const payload = await readPayload(response);
336
- if (response.status === 202) {
337
- return { status: "pending" };
338
- }
339
- if (response.status === 410) {
340
- return {
341
- status: "failed",
342
- reason: extractErrorMessage(payload, "Auth session expired or failed")
343
- };
344
- }
345
- if (!response.ok) {
346
- return {
347
- status: "failed",
348
- reason: extractErrorMessage(
349
- payload,
350
- `Auth session error (${response.status})`
351
- )
352
- };
353
- }
354
- const result = payload;
355
- if (!result.token) {
356
- return { status: "failed", reason: "No token in response" };
357
- }
358
- return {
359
- status: "complete",
360
- token: result.token,
361
- ...result.organizationId ? { organizationId: result.organizationId } : {}
362
- };
363
- }
364
- /**
365
- * Validate a CLI user token and return the authenticated user's info.
366
- * Used by the CLI to verify stored credentials on startup.
367
- */
368
- async getCliUserInfo(userToken) {
369
- const response = await fetch(`${this.apiUrl}/api/cli/auth/me`, {
370
- headers: { Authorization: `Bearer ${userToken}` }
371
- });
372
- const payload = await readPayload(response);
373
- if (!response.ok) {
374
- throw new VocoderAPIError({
375
- message: extractErrorMessage(
376
- payload,
377
- `Token validation failed (${response.status})`
378
- ),
379
- status: response.status,
380
- payload
381
- });
382
- }
383
- return payload;
384
- }
385
- /**
386
- * Revoke the given CLI user token server-side.
387
- */
388
- async revokeCliToken(userToken) {
389
- const response = await fetch(`${this.apiUrl}/api/cli/auth/token`, {
390
- method: "DELETE",
391
- headers: { Authorization: `Bearer ${userToken}` }
392
- });
393
- if (!response.ok) {
394
- const payload = await readPayload(response);
395
- throw new VocoderAPIError({
396
- message: extractErrorMessage(
397
- payload,
398
- `Token revocation failed (${response.status})`
399
- ),
400
- status: response.status,
401
- payload
402
- });
403
- }
404
- }
405
- // ── Workspaces ────────────────────────────────────────────────────────────────
406
- async listWorkspaces(userToken, params) {
407
- const url = new URL(`${this.apiUrl}/api/cli/workspaces`);
408
- if (params?.repo) url.searchParams.set("repo", params.repo);
409
- const response = await fetch(url.toString(), {
410
- headers: { Authorization: `Bearer ${userToken}` }
411
- });
412
- const payload = await readPayload(response);
413
- if (!response.ok) {
414
- throw new VocoderAPIError({
415
- message: extractErrorMessage(
416
- payload,
417
- `Failed to list workspaces (${response.status})`
418
- ),
419
- status: response.status,
420
- payload
421
- });
422
- }
423
- return payload;
424
- }
425
- async listProjects(userToken, organizationId) {
426
- const url = new URL(`${this.apiUrl}/api/cli/projects`);
427
- url.searchParams.set("organizationId", organizationId);
428
- const response = await fetch(url.toString(), {
429
- headers: { Authorization: `Bearer ${userToken}` }
430
- });
431
- const payload = await readPayload(response);
432
- if (!response.ok) {
433
- throw new VocoderAPIError({
434
- message: extractErrorMessage(
435
- payload,
436
- `Failed to list projects (${response.status})`
437
- ),
438
- status: response.status,
439
- payload
440
- });
441
- }
442
- const result = payload;
443
- return result.projects;
444
- }
445
- async regenerateProjectApiKey(userToken, projectId) {
446
- const response = await fetch(
447
- `${this.apiUrl}/api/cli/project/regenerate-key`,
448
- {
449
- method: "POST",
450
- headers: {
451
- "Content-Type": "application/json",
452
- Authorization: `Bearer ${userToken}`
453
- },
454
- body: JSON.stringify({ projectId })
455
- }
456
- );
457
- const payload = await readPayload(response);
458
- if (!response.ok) {
459
- throw new VocoderAPIError({
460
- message: extractErrorMessage(
461
- payload,
462
- `Failed to regenerate API key (${response.status})`
463
- ),
464
- status: response.status,
465
- payload
466
- });
467
- }
468
- return payload;
469
- }
470
- // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
471
- async startCliGitHubInstall(userToken, params) {
472
- const response = await fetch(
473
- `${this.apiUrl}/api/cli/github/install/start`,
474
- {
475
- method: "POST",
476
- headers: {
477
- Authorization: `Bearer ${userToken}`,
478
- "Content-Type": "application/json"
479
- },
480
- body: JSON.stringify(params)
481
- }
482
- );
483
- const payload = await readPayload(response);
484
- if (!response.ok) {
485
- throw new VocoderAPIError({
486
- message: extractErrorMessage(
487
- payload,
488
- `Failed to start GitHub install (${response.status})`
489
- ),
490
- status: response.status,
491
- payload
492
- });
493
- }
494
- return payload;
495
- }
496
- /**
497
- * Start the "link existing installation" discovery flow.
498
- * Unlike startCliGitHubOAuth, this requires no bearer token — the Vocoder
499
- * account is created from the OAuth code in the callback.
500
- */
501
- async startCliGitHubLinkSession(sessionId, callbackPort) {
502
- const response = await fetch(
503
- `${this.apiUrl}/api/cli/github/oauth/link-start`,
504
- {
505
- method: "POST",
506
- headers: { "Content-Type": "application/json" },
507
- body: JSON.stringify({
508
- sessionId,
509
- ...callbackPort != null ? { callbackPort } : {}
510
- })
511
- }
512
- );
513
- const payload = await readPayload(response);
514
- if (!response.ok) {
515
- throw new VocoderAPIError({
516
- message: extractErrorMessage(
517
- payload,
518
- `Failed to start GitHub link session (${response.status})`
519
- ),
520
- status: response.status,
521
- payload
522
- });
523
- }
524
- return payload;
525
- }
526
- async startCliGitHubOAuth(userToken, params) {
527
- const response = await fetch(`${this.apiUrl}/api/cli/github/oauth/start`, {
528
- method: "POST",
529
- headers: {
530
- Authorization: `Bearer ${userToken}`,
531
- "Content-Type": "application/json"
532
- },
533
- body: JSON.stringify(params)
534
- });
535
- const payload = await readPayload(response);
536
- if (!response.ok) {
537
- throw new VocoderAPIError({
538
- message: extractErrorMessage(
539
- payload,
540
- `Failed to start GitHub OAuth (${response.status})`
541
- ),
542
- status: response.status,
543
- payload
544
- });
545
- }
546
- return payload;
547
- }
548
- async getCliGitHubDiscovery(userToken) {
549
- const response = await fetch(`${this.apiUrl}/api/cli/github/discovery`, {
550
- headers: { Authorization: `Bearer ${userToken}` }
551
- });
552
- const payload = await readPayload(response);
553
- if (!response.ok) {
554
- throw new VocoderAPIError({
555
- message: extractErrorMessage(
556
- payload,
557
- `Failed to fetch GitHub discovery (${response.status})`
558
- ),
559
- status: response.status,
560
- payload
561
- });
562
- }
563
- return payload;
564
- }
565
- async claimCliGitHubInstallation(userToken, params) {
566
- const response = await fetch(`${this.apiUrl}/api/cli/github/claim`, {
567
- method: "POST",
568
- headers: {
569
- Authorization: `Bearer ${userToken}`,
570
- "Content-Type": "application/json"
571
- },
572
- body: JSON.stringify(params)
573
- });
574
- const payload = await readPayload(response);
575
- if (!response.ok) {
576
- throw new VocoderAPIError({
577
- message: extractErrorMessage(
578
- payload,
579
- `Failed to claim GitHub installation (${response.status})`
580
- ),
581
- status: response.status,
582
- payload
583
- });
584
- }
585
- return payload;
586
- }
587
- // ── Locales ───────────────────────────────────────────────────────────────────
588
- async listLocales(userToken) {
589
- const response = await fetch(`${this.apiUrl}/api/cli/locales`, {
590
- headers: { Authorization: `Bearer ${userToken}` }
591
- });
592
- const payload = await readPayload(response);
593
- if (!response.ok) {
594
- throw new VocoderAPIError({
595
- message: extractErrorMessage(
596
- payload,
597
- `Failed to list locales (${response.status})`
598
- ),
599
- status: response.status,
600
- payload
601
- });
602
- }
603
- const result = payload;
604
- return result;
605
- }
606
- async listCompatibleLocales(userToken, sourceLocale) {
607
- const url = `${this.apiUrl}/api/cli/locales/compatible?source=${encodeURIComponent(sourceLocale)}`;
608
- const response = await fetch(url, {
609
- headers: { Authorization: `Bearer ${userToken}` }
610
- });
611
- const payload = await readPayload(response);
612
- if (!response.ok) {
613
- throw new VocoderAPIError({
614
- message: extractErrorMessage(
615
- payload,
616
- `Failed to list compatible locales (${response.status})`
617
- ),
618
- status: response.status,
619
- payload
620
- });
621
- }
622
- const result = payload;
623
- return result.locales;
624
- }
625
- // ── Project creation ──────────────────────────────────────────────────────────
626
- async createProject(userToken, params) {
627
- const response = await fetch(`${this.apiUrl}/api/cli/projects`, {
628
- method: "POST",
629
- headers: {
630
- "Content-Type": "application/json",
631
- Authorization: `Bearer ${userToken}`
632
- },
633
- body: JSON.stringify(params)
634
- });
635
- const payload = await readPayload(response);
636
- if (!response.ok) {
637
- throw new VocoderAPIError({
638
- message: extractErrorMessage(
639
- payload,
640
- `Failed to create project (${response.status})`
641
- ),
642
- status: response.status,
643
- payload
644
- });
645
- }
646
- return payload;
647
- }
648
- // ── Project lookup ────────────────────────────────────────────────────────────
649
- /**
650
- * Look up all project apps for a given repo. Returns info about exact matches,
651
- * existing apps in other scopes, and whether a whole-repo app exists.
652
- * No auth required.
653
- */
654
- async lookupProjectByRepo(params) {
655
- try {
656
- const response = await fetch(`${this.apiUrl}/api/cli/init/lookup`, {
657
- method: "POST",
658
- headers: { "Content-Type": "application/json" },
659
- body: JSON.stringify({
660
- repo: params.repoCanonical,
661
- appDir: params.appDir
662
- })
663
- });
664
- if (!response.ok) {
665
- return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
666
- }
667
- const data = await response.json();
668
- return {
669
- exactMatch: data.exactMatch ?? null,
670
- existingApps: data.existingApps ?? [],
671
- hasWholeRepoApp: data.hasWholeRepoApp ?? false
672
- };
673
- } catch {
674
- return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
675
- }
676
- }
677
- /**
678
- * Add a new ProjectApp to an existing project (monorepo: new app directory).
679
- * Does not check plan limits — no new project is created.
680
- */
681
- async createProjectApp(userToken, params) {
682
- const response = await fetch(`${this.apiUrl}/api/cli/project/apps`, {
683
- method: "POST",
684
- headers: {
685
- "Content-Type": "application/json",
686
- Authorization: `Bearer ${userToken}`
687
- },
688
- body: JSON.stringify(params)
689
- });
690
- const payload = await readPayload(response);
691
- if (!response.ok) {
692
- throw new VocoderAPIError({
693
- message: extractErrorMessage(
694
- payload,
695
- `Failed to create project app (${response.status})`
696
- ),
697
- status: response.status,
698
- payload
699
- });
700
- }
701
- return payload;
702
- }
703
- };
704
-
705
- // src/utils/auth-store.ts
706
- import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
707
- import { homedir } from "os";
708
- import { dirname, join } from "path";
709
- function getAuthFilePath() {
710
- return join(homedir(), ".config", "vocoder", "auth.json");
711
- }
712
- function readAuthData() {
713
- const filePath = getAuthFilePath();
714
- try {
715
- const raw = readFileSync(filePath, "utf8");
716
- const parsed = JSON.parse(raw);
717
- if (!parsed || typeof parsed !== "object") return null;
718
- const data = parsed;
719
- if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
720
- return null;
721
- }
722
- return {
723
- token: data.token,
724
- apiUrl: data.apiUrl,
725
- userId: data.userId,
726
- email: data.email,
727
- name: typeof data.name === "string" ? data.name : null,
728
- createdAt: data.createdAt
729
- };
730
- } catch {
731
- return null;
732
- }
733
- }
734
- function writeAuthData(data) {
735
- const filePath = getAuthFilePath();
736
- const dir = dirname(filePath);
737
- mkdirSync(dir, { recursive: true, mode: 448 });
738
- writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
739
- }
740
- function clearAuthData() {
741
- const filePath = getAuthFilePath();
742
- try {
743
- unlinkSync(filePath);
744
- } catch {
745
- }
746
- }
22
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
23
+ import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
747
24
 
748
25
  // src/utils/write-config.ts
749
- import { existsSync, writeFileSync as writeFileSync2 } from "fs";
750
- import { join as join2 } from "path";
26
+ import { existsSync, writeFileSync } from "fs";
27
+ import { join } from "path";
751
28
  function findExistingConfig(cwd = process.cwd()) {
752
29
  for (const name of [
753
30
  "vocoder.config.ts",
754
31
  "vocoder.config.js",
755
32
  "vocoder.config.json"
756
33
  ]) {
757
- const candidate = join2(cwd, name);
34
+ const candidate = join(cwd, name);
758
35
  if (existsSync(candidate)) return candidate;
759
36
  }
760
37
  return null;
@@ -767,7 +44,7 @@ function writeVocoderConfig(options) {
767
44
  } = options;
768
45
  if (findExistingConfig(cwd)) return null;
769
46
  const ext = useTypeScript ? "ts" : "js";
770
- const configPath = join2(cwd, `vocoder.config.${ext}`);
47
+ const configPath = join(cwd, `vocoder.config.${ext}`);
771
48
  const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
772
49
  const content = `import { defineConfig } from '@vocoder/config'
773
50
 
@@ -785,121 +62,13 @@ export default defineConfig({
785
62
  })
786
63
  `;
787
64
  try {
788
- writeFileSync2(configPath, content, "utf-8");
65
+ writeFileSync(configPath, content, "utf-8");
789
66
  return `vocoder.config.${ext}`;
790
67
  } catch {
791
68
  return null;
792
69
  }
793
70
  }
794
71
 
795
- // src/utils/git-identity.ts
796
- import { execSync } from "child_process";
797
- import { relative, resolve } from "path";
798
- var SHA_REGEX = /^[0-9a-f]{40}$/i;
799
- function detectCommitSha() {
800
- if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
801
- return process.env.VOCODER_COMMIT_SHA;
802
- }
803
- const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
804
- if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
805
- return safeExec("git rev-parse HEAD");
806
- }
807
- function safeExec(command) {
808
- try {
809
- const output = execSync(command, {
810
- encoding: "utf-8",
811
- stdio: ["pipe", "pipe", "ignore"]
812
- }).trim();
813
- return output.length > 0 ? output : null;
814
- } catch {
815
- return null;
816
- }
817
- }
818
- function normalizePath(pathname) {
819
- const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
820
- if (!cleaned || !cleaned.includes("/")) {
821
- return null;
822
- }
823
- return cleaned;
824
- }
825
- function parseRemoteUrl(remoteUrl) {
826
- const trimmed = remoteUrl.trim();
827
- if (!trimmed) {
828
- return null;
829
- }
830
- if (!trimmed.includes("://")) {
831
- const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
832
- if (scpMatch) {
833
- const host = (scpMatch[1] || "").toLowerCase();
834
- const ownerRepoPath = normalizePath(scpMatch[2] || "");
835
- if (!host || !ownerRepoPath) {
836
- return null;
837
- }
838
- return { host, ownerRepoPath };
839
- }
840
- return null;
841
- }
842
- try {
843
- const parsed = new URL(trimmed);
844
- const host = parsed.hostname.toLowerCase();
845
- const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
846
- if (!host || !ownerRepoPath) {
847
- return null;
848
- }
849
- return { host, ownerRepoPath };
850
- } catch {
851
- return null;
852
- }
853
- }
854
- function toCanonical(host, ownerRepoPath) {
855
- if (host.includes("github.com")) {
856
- return `github:${ownerRepoPath.toLowerCase()}`;
857
- }
858
- if (host.includes("gitlab.com")) {
859
- return `gitlab:${ownerRepoPath.toLowerCase()}`;
860
- }
861
- if (host.includes("bitbucket.org")) {
862
- return `bitbucket:${ownerRepoPath.toLowerCase()}`;
863
- }
864
- return `git:${host}/${ownerRepoPath.toLowerCase()}`;
865
- }
866
- function resolveGitRepositoryIdentity() {
867
- const remoteUrl = safeExec("git config --get remote.origin.url");
868
- if (!remoteUrl) {
869
- return null;
870
- }
871
- const parsed = parseRemoteUrl(remoteUrl);
872
- if (!parsed) {
873
- return null;
874
- }
875
- const repositoryRoot = safeExec("git rev-parse --show-toplevel");
876
- const currentDirectory = process.cwd();
877
- let repoAppDir = "";
878
- if (repositoryRoot) {
879
- const relativePath = relative(
880
- resolve(repositoryRoot),
881
- resolve(currentDirectory)
882
- ).replace(/\\/g, "/").trim();
883
- if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
884
- repoAppDir = relativePath;
885
- }
886
- }
887
- return {
888
- repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
889
- repoAppDir
890
- };
891
- }
892
- function resolveGitContext() {
893
- const warnings = [];
894
- const identity = resolveGitRepositoryIdentity();
895
- if (!identity) {
896
- warnings.push(
897
- "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
898
- );
899
- }
900
- return { identity, warnings };
901
- }
902
-
903
72
  // src/utils/github-connect.ts
904
73
  import { spawn } from "child_process";
905
74
  import * as p from "@clack/prompts";
@@ -1185,7 +354,7 @@ import * as p3 from "@clack/prompts";
1185
354
  import chalk4 from "chalk";
1186
355
 
1187
356
  // src/utils/branch-select.ts
1188
- import { execSync as execSync2 } from "child_process";
357
+ import { execSync } from "child_process";
1189
358
  import { isCancel as isCancel2, Prompt } from "@clack/core";
1190
359
  import chalk2 from "chalk";
1191
360
  var S_BAR = "\u2502";
@@ -1216,14 +385,14 @@ function symbol(state) {
1216
385
  function detectGitBranches(cwd) {
1217
386
  const workDir = cwd ?? process.cwd();
1218
387
  try {
1219
- const localOut = execSync2("git branch", {
388
+ const localOut = execSync("git branch", {
1220
389
  cwd: workDir,
1221
390
  stdio: "pipe"
1222
391
  }).toString();
1223
392
  const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
1224
393
  let remoteBranches = [];
1225
394
  try {
1226
- const remoteOut = execSync2("git branch -r", {
395
+ const remoteOut = execSync("git branch -r", {
1227
396
  cwd: workDir,
1228
397
  stdio: "pipe"
1229
398
  }).toString();
@@ -1233,7 +402,7 @@ function detectGitBranches(cwd) {
1233
402
  const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
1234
403
  let defaultBranch = "main";
1235
404
  try {
1236
- const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", {
405
+ const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
1237
406
  cwd: workDir,
1238
407
  stdio: "pipe"
1239
408
  }).toString().trim();
@@ -1919,6 +1088,119 @@ async function runProjectAppCreate(params) {
1919
1088
  }
1920
1089
  }
1921
1090
 
1091
+ // src/commands/init.ts
1092
+ import chalk6 from "chalk";
1093
+ import { join as join2 } from "path";
1094
+ import { config as loadEnv } from "dotenv";
1095
+
1096
+ // src/utils/git-identity.ts
1097
+ import { execSync as execSync2 } from "child_process";
1098
+ import { relative, resolve } from "path";
1099
+ var SHA_REGEX = /^[0-9a-f]{40}$/i;
1100
+ function detectCommitSha() {
1101
+ if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
1102
+ return process.env.VOCODER_COMMIT_SHA;
1103
+ }
1104
+ const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
1105
+ if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
1106
+ return safeExec("git rev-parse HEAD");
1107
+ }
1108
+ function safeExec(command) {
1109
+ try {
1110
+ const output = execSync2(command, {
1111
+ encoding: "utf-8",
1112
+ stdio: ["pipe", "pipe", "ignore"]
1113
+ }).trim();
1114
+ return output.length > 0 ? output : null;
1115
+ } catch {
1116
+ return null;
1117
+ }
1118
+ }
1119
+ function normalizePath(pathname) {
1120
+ const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
1121
+ if (!cleaned || !cleaned.includes("/")) {
1122
+ return null;
1123
+ }
1124
+ return cleaned;
1125
+ }
1126
+ function parseRemoteUrl(remoteUrl) {
1127
+ const trimmed = remoteUrl.trim();
1128
+ if (!trimmed) {
1129
+ return null;
1130
+ }
1131
+ if (!trimmed.includes("://")) {
1132
+ const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
1133
+ if (scpMatch) {
1134
+ const host = (scpMatch[1] || "").toLowerCase();
1135
+ const ownerRepoPath = normalizePath(scpMatch[2] || "");
1136
+ if (!host || !ownerRepoPath) {
1137
+ return null;
1138
+ }
1139
+ return { host, ownerRepoPath };
1140
+ }
1141
+ return null;
1142
+ }
1143
+ try {
1144
+ const parsed = new URL(trimmed);
1145
+ const host = parsed.hostname.toLowerCase();
1146
+ const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
1147
+ if (!host || !ownerRepoPath) {
1148
+ return null;
1149
+ }
1150
+ return { host, ownerRepoPath };
1151
+ } catch {
1152
+ return null;
1153
+ }
1154
+ }
1155
+ function toCanonical(host, ownerRepoPath) {
1156
+ if (host.includes("github.com")) {
1157
+ return `github:${ownerRepoPath.toLowerCase()}`;
1158
+ }
1159
+ if (host.includes("gitlab.com")) {
1160
+ return `gitlab:${ownerRepoPath.toLowerCase()}`;
1161
+ }
1162
+ if (host.includes("bitbucket.org")) {
1163
+ return `bitbucket:${ownerRepoPath.toLowerCase()}`;
1164
+ }
1165
+ return `git:${host}/${ownerRepoPath.toLowerCase()}`;
1166
+ }
1167
+ function resolveGitRepositoryIdentity() {
1168
+ const remoteUrl = safeExec("git config --get remote.origin.url");
1169
+ if (!remoteUrl) {
1170
+ return null;
1171
+ }
1172
+ const parsed = parseRemoteUrl(remoteUrl);
1173
+ if (!parsed) {
1174
+ return null;
1175
+ }
1176
+ const repositoryRoot = safeExec("git rev-parse --show-toplevel");
1177
+ const currentDirectory = process.cwd();
1178
+ let repoAppDir = "";
1179
+ if (repositoryRoot) {
1180
+ const relativePath = relative(
1181
+ resolve(repositoryRoot),
1182
+ resolve(currentDirectory)
1183
+ ).replace(/\\/g, "/").trim();
1184
+ if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
1185
+ repoAppDir = relativePath;
1186
+ }
1187
+ }
1188
+ return {
1189
+ repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
1190
+ repoAppDir
1191
+ };
1192
+ }
1193
+ function resolveGitContext() {
1194
+ const warnings = [];
1195
+ const identity = resolveGitRepositoryIdentity();
1196
+ if (!identity) {
1197
+ warnings.push(
1198
+ "Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
1199
+ );
1200
+ }
1201
+ return { identity, warnings };
1202
+ }
1203
+
1922
1204
  // src/utils/workspace.ts
1923
1205
  import * as p4 from "@clack/prompts";
1924
1206
  import chalk5 from "chalk";
@@ -2112,10 +1394,10 @@ function runScaffold(params) {
2112
1394
  p5.log.message(chalk6.gray(" Docs: https://vocoder.app/docs/getting-started"));
2113
1395
  }
2114
1396
  function writeApiKeyToEnv(apiKey) {
2115
- const envPath = join3(process.cwd(), ".env");
1397
+ const envPath = join2(process.cwd(), ".env");
2116
1398
  if (!existsSync2(envPath)) return false;
2117
1399
  try {
2118
- const content = readFileSync2(envPath, "utf-8");
1400
+ const content = readFileSync(envPath, "utf-8");
2119
1401
  const keyLine = `VOCODER_API_KEY=${apiKey}`;
2120
1402
  let updated;
2121
1403
  if (/^VOCODER_API_KEY=/m.test(content)) {
@@ -2125,7 +1407,7 @@ function writeApiKeyToEnv(apiKey) {
2125
1407
  updated = `${content}${sep}${keyLine}
2126
1408
  `;
2127
1409
  }
2128
- writeFileSync3(envPath, updated);
1410
+ writeFileSync2(envPath, updated);
2129
1411
  return true;
2130
1412
  } catch {
2131
1413
  return false;
@@ -2402,7 +1684,7 @@ async function init(options = {}) {
2402
1684
  let userName;
2403
1685
  let authOrganizationId;
2404
1686
  const stored = readAuthData();
2405
- if (stored && stored.apiUrl === apiUrl) {
1687
+ if (stored) {
2406
1688
  const verified = await verifyStoredToken(api, stored.token);
2407
1689
  if (verified && !("userGone" in verified)) {
2408
1690
  p5.log.success(`Authenticated as ${chalk6.bold(verified.email)}`);
@@ -2430,7 +1712,6 @@ async function init(options = {}) {
2430
1712
  authOrganizationId = authResult.organizationId;
2431
1713
  writeAuthData({
2432
1714
  token: userToken,
2433
- apiUrl,
2434
1715
  userId: authResult.userId,
2435
1716
  email: userEmail,
2436
1717
  name: userName,
@@ -2451,7 +1732,6 @@ async function init(options = {}) {
2451
1732
  authOrganizationId = authResult.organizationId;
2452
1733
  writeAuthData({
2453
1734
  token: userToken,
2454
- apiUrl,
2455
1735
  userId: authResult.userId,
2456
1736
  email: userEmail,
2457
1737
  name: userName,
@@ -2883,8 +2163,8 @@ async function logout(options = {}) {
2883
2163
 
2884
2164
  // src/commands/sync.ts
2885
2165
  import { createHash, randomUUID } from "crypto";
2886
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
2887
- import { join as join4 } from "path";
2166
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2167
+ import { join as join3 } from "path";
2888
2168
  import * as p8 from "@clack/prompts";
2889
2169
  import chalk8 from "chalk";
2890
2170
 
@@ -3165,7 +2445,7 @@ function parseTranslations(value) {
3165
2445
  return Object.keys(translations).length > 0 ? translations : null;
3166
2446
  }
3167
2447
  function getCacheFilePath(projectRoot, fingerprint) {
3168
- return join4(projectRoot, "node_modules", ".vocoder", "cache", `${fingerprint}.json`);
2448
+ return join3(projectRoot, "node_modules", ".vocoder", "cache", `${fingerprint}.json`);
3169
2449
  }
3170
2450
  function buildTranslationData(params) {
3171
2451
  const textToHash = new Map(params.stringEntries.map((e) => [e.text, e.key]));
@@ -3192,7 +2472,7 @@ function readLocalCache(params) {
3192
2472
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
3193
2473
  if (!existsSync3(cacheFilePath)) return null;
3194
2474
  try {
3195
- const raw = readFileSync3(cacheFilePath, "utf-8");
2475
+ const raw = readFileSync2(cacheFilePath, "utf-8");
3196
2476
  const parsed = JSON.parse(raw);
3197
2477
  if (!isRecord(parsed)) return null;
3198
2478
  const inner = isRecord(parsed.config) ? parsed : null;
@@ -3206,10 +2486,10 @@ function readLocalCache(params) {
3206
2486
  }
3207
2487
  }
3208
2488
  function writeCache(params) {
3209
- const cacheDir = join4(params.projectRoot, "node_modules", ".vocoder", "cache");
3210
- mkdirSync2(cacheDir, { recursive: true });
2489
+ const cacheDir = join3(params.projectRoot, "node_modules", ".vocoder", "cache");
2490
+ mkdirSync(cacheDir, { recursive: true });
3211
2491
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
3212
- writeFileSync4(cacheFilePath, JSON.stringify(params.data), "utf-8");
2492
+ writeFileSync3(cacheFilePath, JSON.stringify(params.data), "utf-8");
3213
2493
  return cacheFilePath;
3214
2494
  }
3215
2495
  function resolveEffectiveModeFromPolicy(params) {
@@ -3329,8 +2609,7 @@ function buildStringEntries(extractedStrings) {
3329
2609
  text: str.text,
3330
2610
  ...str.context ? { context: str.context } : {},
3331
2611
  ...str.formality ? { formality: str.formality } : {},
3332
- ...str.uiRole ? { uiRole: str.uiRole } : {},
3333
- ...str.featureArea ? { featureArea: str.featureArea } : {}
2612
+ ...str.uiRole ? { uiRole: str.uiRole } : {}
3334
2613
  });
3335
2614
  continue;
3336
2615
  }