@vocoder/cli 0.14.1 → 0.15.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
@@ -6,6 +6,7 @@ import {
6
6
  VocoderAPIError,
7
7
  buildInstallCommand,
8
8
  clearAuthData,
9
+ computeFingerprint,
9
10
  detectLocalEcosystem,
10
11
  getPackagesToInstall,
11
12
  getSetupSnippets,
@@ -13,15 +14,15 @@ import {
13
14
  readAuthData,
14
15
  verifyStoredAuth,
15
16
  writeAuthData
16
- } from "./chunk-LLEMSC3X.mjs";
17
+ } from "./chunk-62KCB6C6.mjs";
17
18
 
18
19
  // src/bin.ts
19
20
  import { Command } from "commander";
20
21
 
21
22
  // src/commands/init.ts
22
- import * as p5 from "@clack/prompts";
23
+ import * as p6 from "@clack/prompts";
23
24
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
24
- import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
25
+ import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
25
26
 
26
27
  // src/utils/write-config.ts
27
28
  import { existsSync, writeFileSync } from "fs";
@@ -41,18 +42,21 @@ function writeVocoderConfig(options) {
41
42
  const {
42
43
  targetBranches = ["main"],
43
44
  useTypeScript = true,
44
- cwd = process.cwd()
45
+ cwd = process.cwd(),
46
+ appId
45
47
  } = options;
46
48
  if (findExistingConfig(cwd)) return null;
47
49
  const ext = useTypeScript ? "ts" : "js";
48
50
  const configPath = join(cwd, `vocoder.config.${ext}`);
49
51
  const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
50
52
  const includes = ["**/*.{tsx,jsx,ts,js}"];
51
- const includesStr = includes.map((p14) => `'${p14}'`).join(", ");
53
+ const includesStr = includes.map((p15) => `'${p15}'`).join(", ");
54
+ const appIdLine = appId ? ` appId: '${appId}',
55
+ ` : "";
52
56
  const content = `import { defineConfig } from '@vocoder/config'
53
57
 
54
58
  export default defineConfig({
55
- targetBranches: [${branchesStr}],
59
+ ${appIdLine} targetBranches: [${branchesStr}],
56
60
  include: [${includesStr}],
57
61
  })
58
62
  `;
@@ -80,13 +84,18 @@ var highlight = hex(PINK);
80
84
  var info = hex(BLUE);
81
85
  var active = hex(ORANGE);
82
86
 
87
+ // src/commands/init.ts
88
+ import { join as join2, resolve as resolve2 } from "path";
89
+
83
90
  // src/utils/project-create.ts
84
- import * as p2 from "@clack/prompts";
91
+ import * as p3 from "@clack/prompts";
85
92
  import chalk2 from "chalk";
86
93
 
87
- // src/utils/branch-select.ts
88
- import { execSync } from "child_process";
94
+ // src/utils/app-dir-select.ts
95
+ import { existsSync as existsSync2, statSync } from "fs";
96
+ import { resolve } from "path";
89
97
  import { isCancel, Prompt } from "@clack/core";
98
+ import * as p from "@clack/prompts";
90
99
  var S_BAR = "\u2502";
91
100
  var S_BAR_END = "\u2514";
92
101
  var S_ACTIVE = "\u25C6";
@@ -105,6 +114,195 @@ function symbol(state) {
105
114
  return active(S_ACTIVE);
106
115
  }
107
116
  }
117
+ function validateAppDirPath(val, existing, opts = {}) {
118
+ if (val.startsWith("/")) return "Must be a relative path (e.g. apps/web)";
119
+ if (val.includes("..")) return "Path traversal not allowed";
120
+ const hasWholeRepo = existing.includes("");
121
+ const hasScoped = existing.some((d) => d !== "");
122
+ if (val === "" && hasScoped) return "Cannot add whole-repo scope to a monorepo project";
123
+ if (val !== "" && hasWholeRepo) return "Cannot add a scoped directory to a whole-repo project";
124
+ if (existing.includes(val)) return `Already added: ${val}`;
125
+ const nested = existing.find(
126
+ (d) => d !== "" && (val.startsWith(d + "/") || d.startsWith(val + "/"))
127
+ );
128
+ if (nested) return `"${val}" overlaps with already-added "${nested}"`;
129
+ if (val !== "") {
130
+ const abs = resolve(opts.cwd ?? process.cwd(), val);
131
+ if (!existsSync2(abs)) return `Directory not found: ${val}`;
132
+ if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
133
+ }
134
+ return null;
135
+ }
136
+ async function collectAppDirs(opts = {}) {
137
+ const added = [];
138
+ let filter = "";
139
+ let cursor = 0;
140
+ let addCursor = false;
141
+ const isNewDir = () => {
142
+ const t = filter.trim();
143
+ return t.length > 0 && !added.includes(t);
144
+ };
145
+ const clampCursor = () => {
146
+ const max = added.length - 1;
147
+ if (cursor > max) cursor = Math.max(0, max);
148
+ };
149
+ const prompt = new Prompt(
150
+ {
151
+ validate() {
152
+ return void 0;
153
+ },
154
+ render() {
155
+ const trimmed = filter.trim();
156
+ const hdr = `${dim(S_BAR)}
157
+ ${symbol(this.state)} App directories
158
+ `;
159
+ switch (this.state) {
160
+ case "submit": {
161
+ const summary = added.length > 0 ? bld(added.join(", ")) : dim("none (single-app project)");
162
+ return `${hdr}${dim(S_BAR)} ${summary}`;
163
+ }
164
+ case "cancel":
165
+ return `${hdr}${dim(S_BAR)}`;
166
+ default: {
167
+ const inputHint = filter.length > 0 ? filter : added.length === 0 ? dim("e.g. apps/web") : dim("e.g. apps/api");
168
+ const lines = [
169
+ hdr.trimEnd(),
170
+ `${info(S_BAR)} ${dim("/")} ${inputHint}`,
171
+ info(S_BAR)
172
+ ];
173
+ for (let i = 0; i < added.length; i++) {
174
+ const isCursor = i === cursor && !addCursor;
175
+ const icon = isCursor ? active("\u25FC") : info("\u25FC");
176
+ const label = isCursor ? bld(added[i]) : added[i];
177
+ lines.push(`${info(S_BAR)} ${icon} ${label}`);
178
+ }
179
+ const atLimit = opts.maxDirs !== void 0 && added.length >= opts.maxDirs;
180
+ if (atLimit) {
181
+ lines.push(`${info(S_BAR)} ${dim(`App limit reached (${added.length}/${opts.maxDirs} on your plan)`)}`);
182
+ } else if (isNewDir()) {
183
+ const err = validateAppDirPath(trimmed, added, opts);
184
+ const icon = addCursor ? active("\u25FB") : dim("\u25FB");
185
+ const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}"`;
186
+ lines.push(`${info(S_BAR)} ${icon} ${label}`);
187
+ }
188
+ lines.push(info(S_BAR));
189
+ if (atLimit) {
190
+ lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
191
+ } else if (added.length === 0 && !isNewDir()) {
192
+ lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
193
+ lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
194
+ } else if (added.length > 0) {
195
+ lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
196
+ }
197
+ const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
198
+ if (this.state === "error") {
199
+ lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
200
+ } else {
201
+ lines.push(barEnd);
202
+ }
203
+ lines.push("");
204
+ return lines.join("\n");
205
+ }
206
+ }
207
+ }
208
+ },
209
+ false
210
+ );
211
+ prompt.on("key", (key) => {
212
+ if (!key || key === " ") return;
213
+ const cp = key.codePointAt(0) ?? 0;
214
+ if (cp === 127 || cp === 8) {
215
+ filter = filter.slice(0, -1);
216
+ addCursor = false;
217
+ } else if (cp >= 32 && cp !== 127) {
218
+ filter += key;
219
+ cursor = 0;
220
+ addCursor = false;
221
+ }
222
+ });
223
+ prompt.on("cursor", (action) => {
224
+ switch (action) {
225
+ case "up":
226
+ if (addCursor) {
227
+ addCursor = false;
228
+ cursor = Math.max(0, added.length - 1);
229
+ } else {
230
+ cursor = Math.max(0, cursor - 1);
231
+ }
232
+ break;
233
+ case "down":
234
+ if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
235
+ addCursor = true;
236
+ } else if (!addCursor) {
237
+ cursor = Math.min(added.length - 1, cursor + 1);
238
+ }
239
+ break;
240
+ case "space": {
241
+ if (addCursor || filter.trim().length > 0 && isNewDir()) {
242
+ if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
243
+ const trimmed = filter.trim();
244
+ const err = validateAppDirPath(trimmed, added, opts);
245
+ if (!err) {
246
+ added.push(trimmed);
247
+ filter = "";
248
+ addCursor = false;
249
+ cursor = 0;
250
+ }
251
+ } else if (added.length > 0 && !isNewDir()) {
252
+ clampCursor();
253
+ added.splice(cursor, 1);
254
+ if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
255
+ }
256
+ break;
257
+ }
258
+ }
259
+ });
260
+ prompt.on("finalize", () => {
261
+ if (prompt.state === "submit") {
262
+ prompt.value = [...added];
263
+ }
264
+ });
265
+ const result = await prompt.prompt();
266
+ if (isCancel(result)) return null;
267
+ return result;
268
+ }
269
+ async function promptSingleAppDir(params) {
270
+ const { existingDirs, cwd } = params;
271
+ const input = await p.text({
272
+ message: "App directory to add",
273
+ placeholder: "apps/web",
274
+ validate(val) {
275
+ const err = validateAppDirPath(val ?? "", existingDirs, { cwd });
276
+ if (err) return err;
277
+ if (!val) return "Directory is required";
278
+ return void 0;
279
+ }
280
+ });
281
+ if (p.isCancel(input)) return null;
282
+ return input;
283
+ }
284
+
285
+ // src/utils/branch-select.ts
286
+ import { execSync } from "child_process";
287
+ import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
288
+ var S_BAR2 = "\u2502";
289
+ var S_BAR_END2 = "\u2514";
290
+ var S_ACTIVE2 = "\u25C6";
291
+ var S_SUBMIT2 = "\u25C6";
292
+ var S_CANCEL2 = "\u25A0";
293
+ var S_ERROR2 = "\u25B2";
294
+ function symbol2(state) {
295
+ switch (state) {
296
+ case "submit":
297
+ return grn(S_SUBMIT2);
298
+ case "cancel":
299
+ return red(S_CANCEL2);
300
+ case "error":
301
+ return ylw(S_ERROR2);
302
+ default:
303
+ return active(S_ACTIVE2);
304
+ }
305
+ }
108
306
  function detectGitBranches(cwd) {
109
307
  const workDir = cwd ?? process.cwd();
110
308
  try {
@@ -168,8 +366,8 @@ function filterItems(items, query) {
168
366
  const lower = query.toLowerCase();
169
367
  return items.filter((i) => i.value.toLowerCase().includes(lower));
170
368
  }
171
- function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
172
- const lines = [];
369
+ function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
370
+ const lines = [info(S_BAR2)];
173
371
  const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
174
372
  for (let i = scrollOffset; i < end; i++) {
175
373
  const item = filtered[i];
@@ -178,26 +376,20 @@ function buildList(filtered, cursor, scrollOffset, selected, filter, customPatte
178
376
  const icon = isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
179
377
  let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
180
378
  if (isCursor) label = bld(label);
181
- lines.push(`${info(S_BAR)} ${icon} ${label}`);
379
+ lines.push(`${info(S_BAR2)} ${icon} ${label}`);
182
380
  }
183
381
  const trimmed = filter.trim();
184
- const allItems = [...filtered];
185
- const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
382
+ const isNewPattern = trimmed.length > 0 && !filtered.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
186
383
  if (isNewPattern) {
187
384
  const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
188
385
  const icon = addCursor ? active("\u25FB") : dim("\u25FB");
189
386
  const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
190
- lines.push(`${info(S_BAR)} ${icon} ${label}`);
387
+ lines.push(`${info(S_BAR2)} ${icon} ${label}`);
191
388
  } else if (filtered.length === 0 && trimmed.length === 0) {
192
- lines.push(dim(`${S_BAR} No branches detected`));
389
+ lines.push(dim(`${S_BAR2} No branches detected`));
193
390
  }
194
391
  const hidden = filtered.length - (end - scrollOffset);
195
- if (hidden > 0) lines.push(dim(`${S_BAR} ${hidden} more`));
196
- if (selected.size > 0) {
197
- lines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
198
- } else if (optional) {
199
- lines.push(dim(`${S_BAR} Enter to skip`));
200
- }
392
+ if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more`));
201
393
  return lines.join("\n");
202
394
  }
203
395
  async function filterableBranchSelect(params) {
@@ -228,7 +420,7 @@ async function filterableBranchSelect(params) {
228
420
  if (scrollOffset < 0) scrollOffset = 0;
229
421
  }
230
422
  };
231
- const prompt = new Prompt(
423
+ const prompt = new Prompt2(
232
424
  {
233
425
  validate() {
234
426
  if (!optional && selected.size === 0)
@@ -238,51 +430,34 @@ async function filterableBranchSelect(params) {
238
430
  render() {
239
431
  const filtered = getFiltered();
240
432
  clampCursor(filtered);
241
- const hdr = `${dim(S_BAR)}
242
- ${symbol(this.state)} ${message}
433
+ const hdr = `${dim(S_BAR2)}
434
+ ${symbol2(this.state)} ${message}
243
435
  `;
244
- const hint = filter.length > 0 ? filter : dim("type to filter or add pattern, \u2191\u2193 navigate, space select");
436
+ const inputHint = filter.length > 0 ? filter : dim("type to filter \xB7 type a custom pattern to add it");
437
+ const footer = selected.size > 0 ? dim(`${S_BAR2} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : optional ? dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to skip`) : dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`);
245
438
  switch (this.state) {
246
439
  case "submit": {
247
440
  const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
248
- return `${hdr}${dim(S_BAR)} ${summary}`;
441
+ return `${hdr}${dim(S_BAR2)} ${summary}`;
249
442
  }
250
443
  case "cancel":
251
- return `${hdr}${dim(S_BAR)}`;
444
+ return `${hdr}${dim(S_BAR2)}`;
252
445
  case "error":
253
446
  return [
254
447
  hdr.trimEnd(),
255
- `${ylw(S_BAR)} ${dim("/")} ${hint}`,
256
- buildList(
257
- filtered,
258
- cursor,
259
- scrollOffset,
260
- selected,
261
- filter,
262
- customPatterns,
263
- addCursor,
264
- optional,
265
- excludedSet
266
- ),
267
- `${ylw(S_BAR_END)} ${ylw(this.error)}`,
448
+ `${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
449
+ buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
450
+ footer,
451
+ `${ylw(S_BAR_END2)} ${ylw(this.error)}`,
268
452
  ""
269
453
  ].join("\n");
270
454
  default:
271
455
  return [
272
456
  hdr.trimEnd(),
273
- `${info(S_BAR)} ${dim("/")} ${hint}`,
274
- buildList(
275
- filtered,
276
- cursor,
277
- scrollOffset,
278
- selected,
279
- filter,
280
- customPatterns,
281
- addCursor,
282
- optional,
283
- excludedSet
284
- ),
285
- `${info(S_BAR_END)}`,
457
+ `${info(S_BAR2)} ${dim("/")} ${inputHint}`,
458
+ buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
459
+ footer,
460
+ `${info(S_BAR_END2)}`,
286
461
  ""
287
462
  ].join("\n");
288
463
  }
@@ -348,29 +523,29 @@ ${symbol(this.state)} ${message}
348
523
  }
349
524
  });
350
525
  const result = await prompt.prompt();
351
- if (isCancel(result)) return null;
526
+ if (isCancel3(result)) return null;
352
527
  return result;
353
528
  }
354
529
 
355
530
  // src/utils/locale-search.ts
356
- import { isCancel as isCancel2, Prompt as Prompt2 } from "@clack/core";
357
- import * as p from "@clack/prompts";
358
- var S_BAR2 = "\u2502";
359
- var S_BAR_END2 = "\u2514";
360
- var S_ACTIVE2 = "\u25C6";
361
- var S_SUBMIT2 = "\u25C6";
362
- var S_CANCEL2 = "\u25A0";
363
- var S_ERROR2 = "\u25B2";
364
- function symbol2(state) {
531
+ import { isCancel as isCancel4, Prompt as Prompt3 } from "@clack/core";
532
+ import * as p2 from "@clack/prompts";
533
+ var S_BAR3 = "\u2502";
534
+ var S_BAR_END3 = "\u2514";
535
+ var S_ACTIVE3 = "\u25C6";
536
+ var S_SUBMIT3 = "\u25C6";
537
+ var S_CANCEL3 = "\u25A0";
538
+ var S_ERROR3 = "\u25B2";
539
+ function symbol3(state) {
365
540
  switch (state) {
366
541
  case "submit":
367
- return grn(S_SUBMIT2);
542
+ return grn(S_SUBMIT3);
368
543
  case "cancel":
369
- return red(S_CANCEL2);
544
+ return red(S_CANCEL3);
370
545
  case "error":
371
- return ylw(S_ERROR2);
546
+ return ylw(S_ERROR3);
372
547
  default:
373
- return active(S_ACTIVE2);
548
+ return active(S_ACTIVE3);
374
549
  }
375
550
  }
376
551
  var MAX_VISIBLE2 = 12;
@@ -384,25 +559,20 @@ function filterLocales(options, query) {
384
559
  function buildList2(filtered, cursor, scrollOffset, selected) {
385
560
  const isMulti = selected !== null;
386
561
  const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
387
- const visibleLines = [];
562
+ const visibleLines = [info(S_BAR3)];
388
563
  for (let i = scrollOffset; i < end; i++) {
389
564
  const opt = filtered[i];
390
565
  const isCursor = i === cursor;
391
566
  const isChecked = isMulti && selected.has(opt.bcp47);
392
567
  const icon = isMulti ? isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
393
568
  visibleLines.push(
394
- `${info(S_BAR2)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
569
+ `${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
395
570
  );
396
571
  }
397
572
  const hidden = filtered.length - (end - scrollOffset);
398
573
  if (hidden > 0)
399
- visibleLines.push(dim(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
400
- if (filtered.length === 0) visibleLines.push(dim(`${S_BAR2} No matches`));
401
- if (isMulti && selected.size > 0) {
402
- visibleLines.push(
403
- dim(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`)
404
- );
405
- }
574
+ visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
575
+ if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
406
576
  return visibleLines.join("\n");
407
577
  }
408
578
  async function runFilterablePrompt(opts) {
@@ -423,7 +593,7 @@ async function runFilterablePrompt(opts) {
423
593
  scrollOffset = cursor - MAX_VISIBLE2 + 1;
424
594
  if (scrollOffset < 0) scrollOffset = 0;
425
595
  };
426
- const prompt = new Prompt2(
596
+ const prompt = new Prompt3(
427
597
  {
428
598
  initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
429
599
  validate() {
@@ -436,43 +606,34 @@ async function runFilterablePrompt(opts) {
436
606
  render() {
437
607
  const filtered = getFiltered();
438
608
  clampCursor(filtered);
439
- const hdr = `${dim(S_BAR2)}
440
- ${symbol2(this.state)} ${message}
609
+ const hdr = `${dim(S_BAR3)}
610
+ ${symbol3(this.state)} ${message}
441
611
  `;
442
- const hint = filter.length > 0 ? filter : dim(
443
- `type to filter, \u2191\u2193 navigate${multi ? ", space select" : ""}`
444
- );
612
+ const inputHint = filter.length > 0 ? filter : dim("type to filter");
613
+ const footer = multi ? selected.size > 0 ? dim(`${S_BAR3} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Enter to confirm`);
445
614
  switch (this.state) {
446
615
  case "submit": {
447
616
  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 ?? "";
448
- return `${hdr}${dim(S_BAR2)} ${bld(val || dim("none"))}`;
617
+ return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
449
618
  }
450
619
  case "cancel":
451
- return `${hdr}${dim(S_BAR2)}`;
620
+ return `${hdr}${dim(S_BAR3)}`;
452
621
  case "error":
453
622
  return [
454
623
  hdr.trimEnd(),
455
- `${ylw(S_BAR2)} ${dim("/")} ${hint}`,
456
- buildList2(
457
- filtered,
458
- cursor,
459
- scrollOffset,
460
- multi ? selected : null
461
- ),
462
- `${ylw(S_BAR_END2)} ${ylw(this.error)}`,
624
+ `${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
625
+ buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
626
+ footer,
627
+ `${ylw(S_BAR_END3)} ${ylw(this.error)}`,
463
628
  ""
464
629
  ].join("\n");
465
630
  default:
466
631
  return [
467
632
  hdr.trimEnd(),
468
- `${info(S_BAR2)} ${dim("/")} ${hint}`,
469
- buildList2(
470
- filtered,
471
- cursor,
472
- scrollOffset,
473
- multi ? selected : null
474
- ),
475
- `${info(S_BAR_END2)}`,
633
+ `${info(S_BAR3)} ${dim("/")} ${inputHint}`,
634
+ buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
635
+ footer,
636
+ `${info(S_BAR_END3)}`,
476
637
  ""
477
638
  ].join("\n");
478
639
  }
@@ -529,7 +690,7 @@ ${symbol2(this.state)} ${message}
529
690
  }
530
691
  });
531
692
  const result = await prompt.prompt();
532
- if (isCancel2(result)) return null;
693
+ if (isCancel4(result)) return null;
533
694
  return result;
534
695
  }
535
696
  async function searchSelectLocale(options, message, initialValue) {
@@ -551,7 +712,7 @@ async function searchMultiSelectLocales(options, message, initialValues) {
551
712
  if (result === null) return null;
552
713
  const picks = result;
553
714
  if (picks.length === 0) {
554
- p.log.warn(
715
+ p2.log.warn(
555
716
  "At least one target language is required. Please select at least one."
556
717
  );
557
718
  return searchMultiSelectLocales(options, message, initialValues);
@@ -579,22 +740,23 @@ function buildLanguageOptions(locales) {
579
740
  return Array.from(byFamily.values());
580
741
  }
581
742
  async function runProjectCreate(params) {
582
- const { api, userToken, organizationId, repoCanonical } = params;
743
+ const { api, userToken, organizationId, repoCanonical, repoRoot } = params;
583
744
  const projectName = (params.defaultName ?? "my-project").trim();
584
- p2.log.success(`Project: ${chalk2.bold(projectName)}`);
745
+ p3.log.success(`Project: ${chalk2.bold(projectName)}`);
585
746
  let sourceLocales;
586
747
  try {
587
748
  ({ sourceLocales } = await api.listLocales(userToken));
588
749
  } catch {
589
- p2.log.error(
750
+ p3.log.error(
590
751
  "Failed to fetch supported locales. Check your connection and try again."
591
752
  );
592
753
  return null;
593
754
  }
594
755
  const languageOptions = buildLanguageOptions(sourceLocales);
595
- const appDir = params.defaultAppDir ?? "";
596
- if (appDir) {
597
- p2.log.success(`App directory: ${chalk2.bold(appDir)}`);
756
+ const appDirs = await collectAppDirs({ cwd: repoRoot, maxDirs: params.maxAppDirs });
757
+ if (appDirs === null) return null;
758
+ if (appDirs.length > 0) {
759
+ p3.log.success(`App directories: ${appDirs.map((d) => chalk2.bold(d)).join(", ")}`);
598
760
  }
599
761
  const sourceLocale = await searchSelectLocale(
600
762
  languageOptions,
@@ -606,7 +768,7 @@ async function runProjectCreate(params) {
606
768
  try {
607
769
  compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
608
770
  } catch {
609
- p2.log.error(
771
+ p3.log.error(
610
772
  "Failed to fetch compatible target locales. Check your connection and try again."
611
773
  );
612
774
  return null;
@@ -621,7 +783,7 @@ async function runProjectCreate(params) {
621
783
  );
622
784
  if (targetLocales === null) return null;
623
785
  if (targetLocales.length === 0) {
624
- p2.log.warn(
786
+ p3.log.warn(
625
787
  "No target languages selected \u2014 you can add them later from the dashboard."
626
788
  );
627
789
  }
@@ -639,7 +801,7 @@ async function runProjectCreate(params) {
639
801
  });
640
802
  if (result === null) return null;
641
803
  if (result.length === 0) {
642
- p2.log.warn(
804
+ p3.log.warn(
643
805
  "At least one branch is required. Please select at least one."
644
806
  );
645
807
  initial = [detected.defaultBranch];
@@ -656,38 +818,45 @@ async function runProjectCreate(params) {
656
818
  sourceLocale,
657
819
  targetLocales,
658
820
  targetBranches,
659
- appDirs: appDir ? [appDir] : [],
821
+ appDirs,
660
822
  repoCanonical
661
823
  });
662
- p2.log.success(`Project ${chalk2.bold(result.projectName)} created!`);
663
- return result;
824
+ p3.log.success(`Project ${chalk2.bold(result.projectName)} created!`);
825
+ return {
826
+ projectId: result.projectId,
827
+ projectName: result.projectName,
828
+ apiKey: result.apiKey,
829
+ sourceLocale,
830
+ targetLocales,
831
+ targetBranches,
832
+ repositoryBound: result.repositoryBound,
833
+ configureUrl: result.configureUrl,
834
+ apps: result.apps
835
+ };
664
836
  } catch (error) {
665
837
  const message = error instanceof Error ? error.message : "Unknown error";
666
- p2.log.error(`Failed to create project: ${message}`);
838
+ p3.log.error(`Failed to create project: ${message}`);
667
839
  return null;
668
840
  }
669
841
  }
670
842
  async function runAppCreate(params) {
671
843
  const { api, userToken, projectId, projectName, repoCanonical } = params;
672
- const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
844
+ const existingDirs = params.existingApps.map((a) => a.appDir);
845
+ const appDir = await promptSingleAppDir({ existingDirs });
846
+ if (appDir === null) return null;
847
+ if (appDir) {
848
+ p3.log.success(`App directory: ${chalk2.bold(appDir)}`);
849
+ }
673
850
  let sourceLocales;
674
851
  try {
675
852
  ({ sourceLocales } = await api.listLocales(userToken));
676
853
  } catch {
677
- p2.log.error(
854
+ p3.log.error(
678
855
  "Failed to fetch supported locales. Check your connection and try again."
679
856
  );
680
857
  return null;
681
858
  }
682
859
  const languageOptions = buildLanguageOptions(sourceLocales);
683
- const appDir = params.defaultAppDir ?? "";
684
- if (existingScopes.has(appDir)) {
685
- p2.log.error(`App directory "${appDir}" is already configured for this project.`);
686
- return null;
687
- }
688
- if (appDir) {
689
- p2.log.success(`App directory: ${chalk2.bold(appDir)}`);
690
- }
691
860
  const sourceLocale = await searchSelectLocale(
692
861
  languageOptions,
693
862
  "Source language",
@@ -698,7 +867,7 @@ async function runAppCreate(params) {
698
867
  try {
699
868
  compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
700
869
  } catch {
701
- p2.log.error(
870
+ p3.log.error(
702
871
  "Failed to fetch compatible target locales. Check your connection and try again."
703
872
  );
704
873
  return null;
@@ -712,7 +881,7 @@ async function runAppCreate(params) {
712
881
  );
713
882
  if (targetLocales === null) return null;
714
883
  if (targetLocales.length === 0) {
715
- p2.log.warn(
884
+ p3.log.warn(
716
885
  "No target languages selected \u2014 you can add them later from the dashboard."
717
886
  );
718
887
  }
@@ -729,7 +898,7 @@ async function runAppCreate(params) {
729
898
  });
730
899
  if (result === null) return null;
731
900
  if (result.length === 0) {
732
- p2.log.warn("At least one branch is required.");
901
+ p3.log.warn("At least one branch is required.");
733
902
  initial = [detectedApp.defaultBranch];
734
903
  } else {
735
904
  appPushBranches = result;
@@ -738,7 +907,7 @@ async function runAppCreate(params) {
738
907
  }
739
908
  const targetBranches = appPushBranches;
740
909
  try {
741
- const result = await api.createProject(userToken, {
910
+ const result = await api.createApp(userToken, {
742
911
  projectId,
743
912
  appDir,
744
913
  sourceLocale,
@@ -746,35 +915,35 @@ async function runAppCreate(params) {
746
915
  targetBranches,
747
916
  repoCanonical: repoCanonical ?? ""
748
917
  });
749
- p2.log.success(
750
- `App ${chalk2.bold(appDir)} added to ${chalk2.bold(projectName)}!`
918
+ p3.log.success(
919
+ `App ${chalk2.bold(appDir || "(root)")} added to ${chalk2.bold(projectName)}!`
751
920
  );
752
921
  return {
753
922
  projectId: result.projectId,
754
923
  projectName: result.projectName,
755
- apiKey: result.apiKey,
756
924
  appDir: result.appDir,
925
+ appId: result.appId,
757
926
  sourceLocale,
758
927
  targetLocales,
759
928
  targetBranches
760
929
  };
761
930
  } catch (error) {
762
931
  const message = error instanceof Error ? error.message : "Unknown error";
763
- p2.log.error(`Failed to add app: ${message}`);
932
+ p3.log.error(`Failed to add app: ${message}`);
764
933
  return null;
765
934
  }
766
935
  }
767
936
 
768
937
  // src/utils/github-connect.ts
769
- import { spawn } from "child_process";
770
- import * as p3 from "@clack/prompts";
938
+ import * as p4 from "@clack/prompts";
771
939
  import chalk3 from "chalk";
940
+ import { spawn } from "child_process";
772
941
 
773
942
  // src/utils/local-server.ts
774
943
  import { createServer } from "http";
775
944
  import { URL as URL2 } from "url";
776
945
  function startCallbackServer() {
777
- return new Promise((resolve2, reject) => {
946
+ return new Promise((resolve3, reject) => {
778
947
  let settled = false;
779
948
  let callbackResolve = null;
780
949
  let callbackReject = null;
@@ -825,7 +994,7 @@ function startCallbackServer() {
825
994
  if (settled) return;
826
995
  settled = true;
827
996
  const port = server.address().port;
828
- resolve2({
997
+ resolve3({
829
998
  port,
830
999
  waitForCallback: () => callbackPromise,
831
1000
  close: () => server.close()
@@ -852,7 +1021,7 @@ async function tryOpenBrowser(url) {
852
1021
  command = "xdg-open";
853
1022
  args = [url];
854
1023
  }
855
- return new Promise((resolve2) => {
1024
+ return new Promise((resolve3) => {
856
1025
  try {
857
1026
  const child = spawn(command, args, {
858
1027
  detached: true,
@@ -864,20 +1033,20 @@ async function tryOpenBrowser(url) {
864
1033
  if (settled) return;
865
1034
  settled = true;
866
1035
  child.unref();
867
- resolve2(true);
1036
+ resolve3(true);
868
1037
  });
869
1038
  child.once("error", () => {
870
1039
  if (settled) return;
871
1040
  settled = true;
872
- resolve2(false);
1041
+ resolve3(false);
873
1042
  });
874
1043
  setTimeout(() => {
875
1044
  if (settled) return;
876
1045
  settled = true;
877
- resolve2(false);
1046
+ resolve3(false);
878
1047
  }, 300);
879
1048
  } catch {
880
- resolve2(false);
1049
+ resolve3(false);
881
1050
  }
882
1051
  });
883
1052
  }
@@ -894,24 +1063,23 @@ async function runGitHubInstallFlow(params) {
894
1063
  callbackPort: server?.port
895
1064
  }
896
1065
  );
897
- p3.log.info("Opening GitHub to install the Vocoder App...");
898
- p3.note(installUrl, "Install URL");
1066
+ p4.log.info("Opening GitHub to install the Vocoder App...");
899
1067
  if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
900
- const shouldOpen = params.yes ? true : await p3.confirm({ message: "Open in your browser?" });
901
- if (p3.isCancel(shouldOpen)) {
1068
+ const shouldOpen = params.yes ? true : await p4.confirm({ message: "Open in your browser?" });
1069
+ if (p4.isCancel(shouldOpen)) {
902
1070
  server?.close();
903
1071
  return null;
904
1072
  }
905
1073
  if (shouldOpen) {
906
1074
  const opened = await tryOpenBrowser(installUrl);
907
1075
  if (!opened) {
908
- p3.log.info(
1076
+ p4.log.info(
909
1077
  "Could not open a browser automatically. Use the URL above."
910
1078
  );
911
1079
  }
912
1080
  }
913
1081
  }
914
- const connectSpinner = p3.spinner();
1082
+ const connectSpinner = p4.spinner();
915
1083
  connectSpinner.start("Waiting for GitHub App installation...");
916
1084
  if (server) {
917
1085
  try {
@@ -919,26 +1087,26 @@ async function runGitHubInstallFlow(params) {
919
1087
  const callbackParams = await Promise.race([
920
1088
  server.waitForCallback(),
921
1089
  new Promise(
922
- (resolve2) => setTimeout(() => resolve2(null), params_timeout)
1090
+ (resolve3) => setTimeout(() => resolve3(null), params_timeout)
923
1091
  )
924
1092
  ]);
925
1093
  server.close();
926
1094
  if (!callbackParams) {
927
1095
  connectSpinner.stop("GitHub App installation timed out");
928
- p3.log.error(
1096
+ p4.log.error(
929
1097
  "The installation flow timed out. Run `vocoder init` again."
930
1098
  );
931
1099
  return null;
932
1100
  }
933
1101
  if (callbackParams.error) {
934
1102
  connectSpinner.stop("GitHub App installation failed");
935
- p3.log.error(callbackParams.error);
1103
+ p4.log.error(callbackParams.error);
936
1104
  return null;
937
1105
  }
938
1106
  const { organizationId, connectionLabel, workspace_created } = callbackParams;
939
1107
  if (!organizationId || !connectionLabel) {
940
1108
  connectSpinner.stop("GitHub App installation incomplete");
941
- p3.log.error("Missing organization or connection data from callback.");
1109
+ p4.log.error("Missing organization or connection data from callback.");
942
1110
  return null;
943
1111
  }
944
1112
  connectSpinner.stop(
@@ -957,7 +1125,7 @@ async function runGitHubInstallFlow(params) {
957
1125
  }
958
1126
  }
959
1127
  connectSpinner.stop("Could not detect GitHub App installation automatically");
960
- p3.log.warn(
1128
+ p4.log.warn(
961
1129
  "Complete the installation in your browser, then run `vocoder init` again."
962
1130
  );
963
1131
  return null;
@@ -972,22 +1140,22 @@ async function runGitHubDiscoveryFlow(params) {
972
1140
  organizationId: params.organizationId,
973
1141
  callbackPort: server?.port
974
1142
  });
975
- p3.log.info("Opening GitHub to authorize your account...");
976
- p3.note("Complete authorization in your browser.");
1143
+ p4.log.info("Opening GitHub to authorize your account...");
1144
+ p4.note("Complete authorization in your browser.");
977
1145
  if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
978
- const shouldOpen = params.yes ? true : await p3.confirm({ message: "Open in your browser?" });
979
- if (p3.isCancel(shouldOpen)) {
1146
+ const shouldOpen = params.yes ? true : await p4.confirm({ message: "Open in your browser?" });
1147
+ if (p4.isCancel(shouldOpen)) {
980
1148
  server?.close();
981
1149
  return null;
982
1150
  }
983
1151
  if (shouldOpen) {
984
1152
  const opened = await tryOpenBrowser(oauthUrl);
985
1153
  if (!opened) {
986
- p3.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
1154
+ p4.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
987
1155
  }
988
1156
  }
989
1157
  }
990
- const oauthSpinner = p3.spinner();
1158
+ const oauthSpinner = p4.spinner();
991
1159
  oauthSpinner.start("Waiting for GitHub authorization...");
992
1160
  if (server) {
993
1161
  try {
@@ -995,7 +1163,7 @@ async function runGitHubDiscoveryFlow(params) {
995
1163
  const callbackParams = await Promise.race([
996
1164
  server.waitForCallback(),
997
1165
  new Promise(
998
- (resolve2) => setTimeout(() => resolve2(null), timeoutMs)
1166
+ (resolve3) => setTimeout(() => resolve3(null), timeoutMs)
999
1167
  )
1000
1168
  ]);
1001
1169
  server.close();
@@ -1005,7 +1173,7 @@ async function runGitHubDiscoveryFlow(params) {
1005
1173
  }
1006
1174
  if (callbackParams.error) {
1007
1175
  oauthSpinner.stop("GitHub authorization failed");
1008
- p3.log.error(callbackParams.error);
1176
+ p4.log.error(callbackParams.error);
1009
1177
  return null;
1010
1178
  }
1011
1179
  } catch {
@@ -1036,23 +1204,21 @@ async function selectGitHubInstallation(installations, canInstallNew) {
1036
1204
  label: `Install on a new account ${chalk3.dim("(creates a new personal workspace)")}`
1037
1205
  });
1038
1206
  }
1039
- const selected = await p3.select({
1207
+ const selected = await p4.select({
1040
1208
  message: "Select a GitHub installation",
1041
1209
  options
1042
1210
  });
1043
- if (p3.isCancel(selected)) return null;
1211
+ if (p4.isCancel(selected)) return null;
1044
1212
  if (selected === "install_new") return "install_new";
1045
1213
  return Number(selected);
1046
1214
  }
1047
1215
 
1048
1216
  // src/commands/init.ts
1049
1217
  import chalk5 from "chalk";
1050
- import { join as join2 } from "path";
1051
1218
  import { config as loadEnv } from "dotenv";
1052
1219
 
1053
1220
  // src/utils/git-identity.ts
1054
1221
  import { execSync as execSync2 } from "child_process";
1055
- import { relative, resolve } from "path";
1056
1222
  var SHA_REGEX = /^[0-9a-f]{40}$/i;
1057
1223
  function detectCommitSha() {
1058
1224
  if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
@@ -1130,21 +1296,13 @@ function resolveGitRepositoryIdentity() {
1130
1296
  if (!parsed) {
1131
1297
  return null;
1132
1298
  }
1133
- const repositoryRoot = safeExec("git rev-parse --show-toplevel");
1134
- const currentDirectory = process.cwd();
1135
- let repoAppDir = "";
1136
- if (repositoryRoot) {
1137
- const relativePath = relative(
1138
- resolve(repositoryRoot),
1139
- resolve(currentDirectory)
1140
- ).replace(/\\/g, "/").trim();
1141
- if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
1142
- repoAppDir = relativePath;
1143
- }
1299
+ const repoRoot = safeExec("git rev-parse --show-toplevel");
1300
+ if (!repoRoot) {
1301
+ return null;
1144
1302
  }
1145
1303
  return {
1146
1304
  repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
1147
- repoAppDir
1305
+ repoRoot
1148
1306
  };
1149
1307
  }
1150
1308
  function resolveGitContext() {
@@ -1158,47 +1316,47 @@ function resolveGitContext() {
1158
1316
  return { identity, warnings };
1159
1317
  }
1160
1318
 
1161
- // src/utils/workspace.ts
1162
- import * as p4 from "@clack/prompts";
1319
+ // src/utils/organization.ts
1320
+ import * as p5 from "@clack/prompts";
1163
1321
  import chalk4 from "chalk";
1164
- async function selectWorkspace(result) {
1165
- const { workspaces, canCreateWorkspace } = result;
1166
- if (workspaces.length === 0) {
1322
+ async function selectOrganization(result) {
1323
+ const { organizations, canCreateOrganization } = result;
1324
+ if (organizations.length === 0) {
1167
1325
  return { action: "create" };
1168
1326
  }
1169
- const options = workspaces.map((ws) => ({
1170
- value: ws.id,
1171
- label: ws.name,
1327
+ const options = organizations.map((org) => ({
1328
+ value: org.id,
1329
+ label: org.name,
1172
1330
  hint: [
1173
- ws.projectCount > 0 ? `${ws.projectCount} project${ws.projectCount !== 1 ? "s" : ""}` : "",
1174
- ws.connectionLabel ? `GitHub: ${ws.connectionLabel}` : ""
1331
+ org.projectCount > 0 ? `${org.projectCount} project${org.projectCount !== 1 ? "s" : ""}` : "",
1332
+ org.connectionLabel ? `GitHub: ${org.connectionLabel}` : ""
1175
1333
  ].filter(Boolean).join(" \xB7 ") || void 0
1176
1334
  }));
1177
- if (canCreateWorkspace) {
1335
+ if (canCreateOrganization) {
1178
1336
  options.push({ value: "create", label: "Create new workspace" });
1179
1337
  }
1180
- const selected = await p4.select({
1338
+ const selected = await p5.select({
1181
1339
  message: "Select workspace",
1182
1340
  options
1183
1341
  });
1184
- if (p4.isCancel(selected)) {
1342
+ if (p5.isCancel(selected)) {
1185
1343
  return { action: "cancelled" };
1186
1344
  }
1187
1345
  if (selected === "create") {
1188
1346
  return { action: "create" };
1189
1347
  }
1190
- const workspace = workspaces.find((ws) => ws.id === selected);
1191
- if (!workspace) {
1348
+ const organization = organizations.find((org) => org.id === selected);
1349
+ if (!organization) {
1192
1350
  return { action: "cancelled" };
1193
1351
  }
1194
- return { action: "use", workspace };
1352
+ return { action: "use", organization };
1195
1353
  }
1196
1354
 
1197
1355
  // src/commands/init.ts
1198
1356
  loadEnv();
1199
1357
  var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
1200
1358
  async function sleep(ms) {
1201
- await new Promise((resolve2) => setTimeout(resolve2, ms));
1359
+ await new Promise((resolve3) => setTimeout(resolve3, ms));
1202
1360
  }
1203
1361
  async function tryOpenBrowser2(url) {
1204
1362
  if (!process.stdout.isTTY || process.env.CI === "true") {
@@ -1216,7 +1374,7 @@ async function tryOpenBrowser2(url) {
1216
1374
  command = "xdg-open";
1217
1375
  args = [url];
1218
1376
  }
1219
- return await new Promise((resolve2) => {
1377
+ return await new Promise((resolve3) => {
1220
1378
  try {
1221
1379
  const child = spawn2(command, args, {
1222
1380
  detached: true,
@@ -1228,20 +1386,20 @@ async function tryOpenBrowser2(url) {
1228
1386
  if (settled) return;
1229
1387
  settled = true;
1230
1388
  child.unref();
1231
- resolve2(true);
1389
+ resolve3(true);
1232
1390
  });
1233
1391
  child.once("error", () => {
1234
1392
  if (settled) return;
1235
1393
  settled = true;
1236
- resolve2(false);
1394
+ resolve3(false);
1237
1395
  });
1238
1396
  setTimeout(() => {
1239
1397
  if (settled) return;
1240
1398
  settled = true;
1241
- resolve2(false);
1399
+ resolve3(false);
1242
1400
  }, 300);
1243
1401
  } catch {
1244
- resolve2(false);
1402
+ resolve3(false);
1245
1403
  }
1246
1404
  });
1247
1405
  }
@@ -1253,24 +1411,24 @@ function getSubscriptionSettingsUrl(apiUrl) {
1253
1411
  return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
1254
1412
  }
1255
1413
  function printPlanLimitMessage(apiUrl, message) {
1256
- p5.log.error(`You are over your plan limits.
1414
+ p6.log.error(`You are over your plan limits.
1257
1415
  ${message}`);
1258
- p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
1416
+ p6.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
1259
1417
  }
1260
1418
  function runScaffold(params) {
1261
- const { sourceLocale, targetBranches, appDir } = params;
1419
+ const { sourceLocale, targetBranches } = params;
1262
1420
  const detection = detectLocalEcosystem();
1263
1421
  const useTypeScript = detection.isTypeScript;
1264
1422
  if (detection.ecosystem) {
1265
1423
  const frameworkLabel = detection.framework ?? detection.ecosystem;
1266
1424
  const pmLabel = detection.packageManager;
1267
- p5.log.info(`Detected: ${chalk5.bold(frameworkLabel)} (${pmLabel})`);
1425
+ p6.log.info(`Detected: ${chalk5.bold(frameworkLabel)} (${pmLabel})`);
1268
1426
  }
1269
1427
  const { devPackages, runtimePackages } = getPackagesToInstall(detection);
1270
1428
  const allPackages = [...devPackages, ...runtimePackages];
1271
1429
  if (allPackages.length > 0) {
1272
- p5.log.info("");
1273
- const installSpinner = p5.spinner();
1430
+ p6.log.info("");
1431
+ const installSpinner = p6.spinner();
1274
1432
  installSpinner.start(`Installing ${allPackages.join(", ")}...`);
1275
1433
  try {
1276
1434
  if (devPackages.length > 0) {
@@ -1290,19 +1448,22 @@ function runScaffold(params) {
1290
1448
  installSpinner.stop("Package installation failed");
1291
1449
  const cmds = [
1292
1450
  devPackages.length > 0 ? buildInstallCommand(detection.packageManager, devPackages, true) : null,
1293
- runtimePackages.length > 0 ? buildInstallCommand(detection.packageManager, runtimePackages, false) : null
1451
+ runtimePackages.length > 0 ? buildInstallCommand(
1452
+ detection.packageManager,
1453
+ runtimePackages,
1454
+ false
1455
+ ) : null
1294
1456
  ].filter(Boolean).join(" && ");
1295
- p5.log.warn(`Run manually: ${highlight(cmds)}`);
1457
+ p6.log.warn(`Run manually: ${highlight(cmds)}`);
1296
1458
  }
1297
1459
  } else if (detection.ecosystem) {
1298
- p5.log.info(`Packages: ${chalk5.green("already installed")}`);
1460
+ p6.log.info(`Packages: ${chalk5.green("already installed")}`);
1299
1461
  }
1300
1462
  const snippets = getSetupSnippets({
1301
1463
  framework: detection.framework,
1302
1464
  ecosystem: detection.ecosystem,
1303
1465
  sourceLocale,
1304
- targetBranches,
1305
- appDir
1466
+ targetBranches
1306
1467
  });
1307
1468
  const steps = [];
1308
1469
  if (snippets.pluginStep) {
@@ -1324,36 +1485,23 @@ function runScaffold(params) {
1324
1485
  hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
1325
1486
  code: snippets.wrapStep.code
1326
1487
  });
1327
- p5.log.message("");
1328
- p5.log.message(chalk5.bold("Finish setup in your code"));
1329
- p5.log.message("");
1488
+ p6.log.message("");
1489
+ p6.log.message(chalk5.bold("Finish setup in your code"));
1490
+ p6.log.message("");
1330
1491
  for (let i = 0; i < steps.length; i++) {
1331
1492
  const step = steps[i];
1332
- p5.log.step(
1333
- `${chalk5.bold(step.label)} ${chalk5.dim(`\u2014 ${step.hint}`)}`
1334
- );
1493
+ p6.log.step(`${chalk5.bold(step.label)} ${chalk5.dim(`\u2014 ${step.hint}`)}`);
1335
1494
  printCodeBlock(step.code);
1336
- if (i < steps.length - 1) p5.log.message("");
1337
- }
1338
- const written = writeVocoderConfig({ targetBranches, useTypeScript });
1339
- if (written) {
1340
- p5.log.success(`Created ${highlight(written)}`);
1341
- } else if (!findExistingConfig(process.cwd())) {
1342
- const ext = useTypeScript ? "ts" : "js";
1343
- p5.log.warn(
1344
- `Could not write vocoder.config.${ext} \u2014 create it manually with your extraction patterns.`
1345
- );
1495
+ if (i < steps.length - 1) p6.log.message("");
1346
1496
  }
1347
- p5.log.message("");
1497
+ p6.log.message("");
1348
1498
  const branchList = targetBranches.length > 0 ? targetBranches.map((b) => highlight(b)).join(" or ") : highlight("your target branch");
1349
- p5.log.success(
1350
- `Push to ${branchList} to trigger your first translation run.`
1351
- );
1352
- p5.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
1499
+ p6.log.success(`Push to ${branchList} to trigger your first translation run.`);
1500
+ p6.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
1353
1501
  }
1354
- function writeApiKeyToEnv(apiKey) {
1355
- const envPath = join2(process.cwd(), ".env");
1356
- if (!existsSync2(envPath)) return false;
1502
+ function writeApiKeyToEnv(apiKey, repoRoot) {
1503
+ const envPath = join2(repoRoot ?? process.cwd(), ".env");
1504
+ if (!existsSync3(envPath)) return false;
1357
1505
  try {
1358
1506
  const content = readFileSync(envPath, "utf-8");
1359
1507
  const keyLine = `VOCODER_API_KEY=${apiKey}`;
@@ -1371,15 +1519,36 @@ function writeApiKeyToEnv(apiKey) {
1371
1519
  return false;
1372
1520
  }
1373
1521
  }
1374
- function printApiKey(apiKey) {
1375
- const saved = writeApiKeyToEnv(apiKey);
1376
- p5.log.message("");
1377
- p5.log.message(chalk5.bold("Your API Key"));
1522
+ function printApiKey(apiKey, repoRoot) {
1523
+ const saved = writeApiKeyToEnv(apiKey, repoRoot);
1524
+ p6.log.message("");
1525
+ p6.log.message(chalk5.bold("Your API Key"));
1378
1526
  printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
1379
1527
  if (saved) {
1380
- p5.log.success(chalk5.dim("Saved to .env"));
1528
+ p6.log.success(chalk5.dim("Saved to .env"));
1381
1529
  } else {
1382
- p5.log.message(chalk5.dim(" Add the above to your .env file"));
1530
+ p6.log.message(chalk5.dim(" Add the above to your .env file"));
1531
+ }
1532
+ }
1533
+ function writeAppConfigs(apps, targetBranches, useTypeScript, repoRoot) {
1534
+ const base = repoRoot ?? process.cwd();
1535
+ for (const app of apps) {
1536
+ const dir = app.appDir ? resolve2(base, app.appDir) : base;
1537
+ const written = writeVocoderConfig({
1538
+ targetBranches,
1539
+ appId: app.appId,
1540
+ cwd: dir,
1541
+ useTypeScript
1542
+ });
1543
+ if (written) {
1544
+ const displayPath = app.appDir ? `${app.appDir}/${written}` : written;
1545
+ p6.log.success(`Created ${highlight(displayPath)}`);
1546
+ } else if (!findExistingConfig(dir)) {
1547
+ const ext = useTypeScript ? "ts" : "js";
1548
+ p6.log.warn(
1549
+ `Could not write ${app.appDir ? `${app.appDir}/` : ""}vocoder.config.${ext} \u2014 create it manually.`
1550
+ );
1551
+ }
1383
1552
  }
1384
1553
  }
1385
1554
  function printCodeBlock(code) {
@@ -1416,7 +1585,6 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1416
1585
  const session = await api.startCliAuthSession(server?.port, repoCanonical);
1417
1586
  const browserUrl = reauth ? session.verificationUrl : session.installUrl ?? session.verificationUrl;
1418
1587
  const expiresAt = new Date(session.expiresAt).getTime();
1419
- p5.log.info(browserUrl);
1420
1588
  if (options.ci) {
1421
1589
  process.stdout.write(`VOCODER_AUTH_URL: ${browserUrl}
1422
1590
  `);
@@ -1425,23 +1593,23 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1425
1593
  } else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
1426
1594
  if (reauth) {
1427
1595
  if (!options.yes) {
1428
- const shouldOpen = await p5.confirm({
1596
+ const shouldOpen = await p6.confirm({
1429
1597
  message: "Open your browser to sign in again?"
1430
1598
  });
1431
- if (p5.isCancel(shouldOpen)) {
1599
+ if (p6.isCancel(shouldOpen)) {
1432
1600
  server?.close();
1433
- p5.cancel("Setup cancelled.");
1601
+ p6.cancel("Setup cancelled.");
1434
1602
  return null;
1435
1603
  }
1436
1604
  if (!shouldOpen) {
1437
1605
  server?.close();
1438
- p5.cancel("Setup cancelled.");
1606
+ p6.cancel("Setup cancelled.");
1439
1607
  return null;
1440
1608
  } else {
1441
1609
  const opened = await tryOpenBrowser2(browserUrl);
1442
1610
  if (!opened) {
1443
- p5.note(browserUrl, "Sign In");
1444
- p5.log.info("Open the URL above manually to continue.");
1611
+ p6.note(browserUrl, "Sign In");
1612
+ p6.log.info("Open the URL above manually to continue.");
1445
1613
  }
1446
1614
  }
1447
1615
  } else {
@@ -1450,20 +1618,24 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1450
1618
  } else {
1451
1619
  let isLinkFlow = false;
1452
1620
  if (!options.yes) {
1453
- const connectChoice = await p5.select({
1621
+ const connectChoice = await p6.select({
1454
1622
  message: "Vocoder needs to be installed on your GitHub account to get started",
1455
1623
  options: [
1456
1624
  {
1457
1625
  value: "install",
1458
1626
  label: "Install GitHub App",
1459
- hint: "recommended"
1627
+ hint: "new user"
1460
1628
  },
1461
- { value: "link", label: "Already installed? Link your account" }
1629
+ {
1630
+ value: "link",
1631
+ label: "Already installed? Link your account",
1632
+ hint: "returning user"
1633
+ }
1462
1634
  ]
1463
1635
  });
1464
- if (p5.isCancel(connectChoice)) {
1636
+ if (p6.isCancel(connectChoice)) {
1465
1637
  server?.close();
1466
- p5.cancel("Setup cancelled.");
1638
+ p6.cancel("Setup cancelled.");
1467
1639
  return null;
1468
1640
  }
1469
1641
  isLinkFlow = connectChoice === "link";
@@ -1482,13 +1654,13 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1482
1654
  }
1483
1655
  const opened = await tryOpenBrowser2(urlToOpen);
1484
1656
  if (!opened) {
1485
- p5.log.warn("Could not open your browser automatically.");
1486
- p5.note(urlToOpen, "GitHub");
1487
- p5.log.info("Open the URL above to continue.");
1657
+ p6.log.warn("Could not open your browser automatically.");
1658
+ p6.note(urlToOpen, "GitHub");
1659
+ p6.log.info("Open the URL above to continue.");
1488
1660
  }
1489
1661
  }
1490
1662
  }
1491
- const authSpinner = p5.spinner();
1663
+ const authSpinner = p6.spinner();
1492
1664
  authSpinner.start("Waiting for GitHub authorization...");
1493
1665
  let rawToken = null;
1494
1666
  let callbackOrganizationId;
@@ -1509,19 +1681,19 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1509
1681
  }
1510
1682
  return null;
1511
1683
  })();
1512
- const winner = await new Promise((resolve2) => {
1684
+ const winner = await new Promise((resolve3) => {
1513
1685
  let done = false;
1514
1686
  serverCallback.then((params) => {
1515
1687
  if (done || params === null || typeof params.token !== "string") return;
1516
1688
  done = true;
1517
- resolve2({ kind: "server", params });
1689
+ resolve3({ kind: "server", params });
1518
1690
  }).catch(() => {
1519
1691
  });
1520
1692
  sessionPoll.then((result) => {
1521
1693
  if (done || result === null) return;
1522
1694
  if (result.status === "complete" || result.status === "failed") {
1523
1695
  done = true;
1524
- resolve2({
1696
+ resolve3({
1525
1697
  kind: "poll",
1526
1698
  result
1527
1699
  });
@@ -1532,7 +1704,7 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1532
1704
  () => {
1533
1705
  if (!done) {
1534
1706
  done = true;
1535
- resolve2(null);
1707
+ resolve3(null);
1536
1708
  }
1537
1709
  },
1538
1710
  Math.max(0, deadline - Date.now())
@@ -1556,13 +1728,13 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1556
1728
  }
1557
1729
  } else {
1558
1730
  authSpinner.stop();
1559
- p5.log.error(winner.result.reason);
1731
+ p6.log.error(winner.result.reason);
1560
1732
  return null;
1561
1733
  }
1562
1734
  }
1563
1735
  if (!rawToken) {
1564
1736
  authSpinner.stop();
1565
- p5.log.error("The authentication link expired. Run `vocoder init` again.");
1737
+ p6.log.error("The authentication link expired. Run `vocoder init` again.");
1566
1738
  return null;
1567
1739
  }
1568
1740
  const userInfo = await api.getCliUserInfo(rawToken);
@@ -1576,34 +1748,44 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1576
1748
  }
1577
1749
  async function init(options = {}) {
1578
1750
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
1579
- p5.intro(chalk5.bold("Vocoder Setup"));
1751
+ p6.intro(chalk5.bold("Vocoder Setup"));
1580
1752
  try {
1581
1753
  const gitContext = resolveGitContext();
1582
1754
  const identity = gitContext.identity;
1583
1755
  if (gitContext.warnings.length > 0) {
1584
1756
  for (const warning of gitContext.warnings) {
1585
- p5.log.warn(warning);
1757
+ p6.log.warn(warning);
1586
1758
  }
1587
1759
  }
1588
1760
  let existingAppsForRepo = [];
1589
1761
  let repoProjectId = null;
1590
1762
  let repoProjectName = null;
1763
+ let lookup = null;
1591
1764
  if (identity) {
1592
1765
  const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
1593
- const lookup = await anonApi.lookupAppByRepo({
1766
+ lookup = await anonApi.lookupAppByRepo({
1594
1767
  repoCanonical: identity.repoCanonical,
1595
- appDir: identity.repoAppDir
1768
+ appDir: ""
1596
1769
  });
1597
- if (lookup.exactMatch) {
1598
- const { exactMatch } = lookup;
1599
- p5.log.success(`Project: ${chalk5.bold(exactMatch.projectName)}`);
1600
- p5.log.info(
1601
- `Branches: ${highlight((exactMatch.targetBranches ?? ["main"]).join(", "))}`
1770
+ if (lookup.existingApps.length > 0) {
1771
+ const allApps = lookup.existingApps;
1772
+ const firstApp = allApps[0];
1773
+ p6.log.success(`Project: ${chalk5.bold(firstApp.projectName)}`);
1774
+ p6.log.info(
1775
+ `Configured apps: ${allApps.map((a) => highlight(a.appDir || "(entire repo)")).join(", ")}`
1602
1776
  );
1603
- const needsKey = await p5.confirm({
1604
- message: "Need to regenerate your API key?"
1777
+ const routeAction = await p6.select({
1778
+ message: "This repo is already set up. What would you like to do?",
1779
+ options: [
1780
+ { value: "key", label: "Get an API key for this project" },
1781
+ { value: "add", label: "Add a new app directory" }
1782
+ ]
1605
1783
  });
1606
- if (!p5.isCancel(needsKey) && needsKey) {
1784
+ if (p6.isCancel(routeAction)) {
1785
+ p6.cancel("Setup cancelled.");
1786
+ return 1;
1787
+ }
1788
+ if (routeAction === "key") {
1607
1789
  const anonApi2 = new VocoderAPI({ apiUrl, apiKey: "" });
1608
1790
  const authResult = await runAuthFlow(
1609
1791
  anonApi2,
@@ -1612,44 +1794,37 @@ async function init(options = {}) {
1612
1794
  true
1613
1795
  );
1614
1796
  if (!authResult) return 1;
1615
- const spinner7 = p5.spinner();
1616
- spinner7.start("Generating new API key...");
1797
+ const spinner7 = p6.spinner();
1798
+ spinner7.start("Generating API key...");
1799
+ let apiKey;
1617
1800
  try {
1618
- const { apiKey } = await anonApi2.regenerateProjectApiKey(
1801
+ ({ apiKey } = await anonApi2.regenerateProjectApiKey(
1619
1802
  authResult.token,
1620
- exactMatch.projectId
1621
- );
1622
- spinner7.stop("New API key generated");
1623
- printApiKey(apiKey);
1803
+ firstApp.projectId
1804
+ ));
1805
+ spinner7.stop("API key ready");
1624
1806
  } catch (err) {
1625
1807
  spinner7.stop("Failed to generate key");
1626
1808
  const msg = err instanceof Error ? err.message : String(err);
1627
- p5.log.error(`Could not generate API key: ${msg}`);
1628
- p5.log.info("Try again or generate one from the dashboard.");
1809
+ p6.log.error(`Could not generate API key: ${msg}`);
1810
+ p6.log.info("Try again or generate one from the dashboard.");
1629
1811
  return 1;
1630
1812
  }
1631
- }
1632
- const isTs = detectLocalEcosystem().isTypeScript;
1633
- const written = writeVocoderConfig({ targetBranches: exactMatch.targetBranches ?? ["main"], useTypeScript: isTs });
1634
- if (written) p5.log.success(`Created ${highlight(written)}`);
1635
- p5.outro("Vocoder is already set up for this repository.");
1636
- return 0;
1637
- }
1638
- if (lookup.hasWholeRepoApp) {
1639
- const wholeRepo = lookup.existingApps.find((a) => a.appDir === "");
1640
- if (wholeRepo) {
1641
- p5.log.success(`Project: ${chalk5.bold(wholeRepo.projectName)}`);
1642
- const isTs = detectLocalEcosystem().isTypeScript;
1643
- const written = writeVocoderConfig({ targetBranches: ["main"], useTypeScript: isTs });
1644
- if (written) p5.log.success(`Created ${highlight(written)}`);
1645
- p5.outro("Vocoder is already set up for this repository.");
1813
+ printApiKey(apiKey, identity.repoRoot);
1814
+ const detection2 = detectLocalEcosystem();
1815
+ const targetBranches = lookup.exactMatch?.targetBranches ?? ["main"];
1816
+ writeAppConfigs(
1817
+ allApps.map((a) => ({ appDir: a.appDir, appId: a.appId })),
1818
+ targetBranches,
1819
+ detection2.isTypeScript,
1820
+ identity.repoRoot
1821
+ );
1822
+ p6.outro("Vocoder is set up for this repository.");
1646
1823
  return 0;
1647
1824
  }
1648
- }
1649
- if (lookup.existingApps.length > 0) {
1650
- existingAppsForRepo = lookup.existingApps;
1651
- repoProjectId = lookup.existingApps[0]?.projectId ?? null;
1652
- repoProjectName = lookup.existingApps[0]?.projectName ?? null;
1825
+ existingAppsForRepo = allApps;
1826
+ repoProjectId = firstApp.projectId;
1827
+ repoProjectName = firstApp.projectName;
1653
1828
  }
1654
1829
  }
1655
1830
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
@@ -1659,16 +1834,16 @@ async function init(options = {}) {
1659
1834
  let authOrganizationId;
1660
1835
  const storedAuth = await verifyStoredAuth(api);
1661
1836
  if (storedAuth.status === "valid") {
1662
- p5.log.success(`Authenticated as ${chalk5.bold(storedAuth.email)}`);
1837
+ p6.log.success(`Authenticated as ${chalk5.bold(storedAuth.email)}`);
1663
1838
  userToken = storedAuth.token;
1664
1839
  userEmail = storedAuth.email;
1665
1840
  userName = storedAuth.name;
1666
1841
  } else {
1667
1842
  const reauth = storedAuth.status === "expired";
1668
1843
  if (reauth) {
1669
- p5.log.warn("Stored credentials expired \u2014 signing in again");
1844
+ p6.log.warn("Stored credentials expired \u2014 signing in again");
1670
1845
  } else if (storedAuth.status === "gone") {
1671
- p5.log.warn("Account not found \u2014 starting fresh setup");
1846
+ p6.log.warn("Account not found \u2014 starting fresh setup");
1672
1847
  }
1673
1848
  const authResult = await runAuthFlow(
1674
1849
  api,
@@ -1689,101 +1864,57 @@ async function init(options = {}) {
1689
1864
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1690
1865
  });
1691
1866
  }
1692
- let selectedWorkspaceId;
1693
- let selectedWorkspaceName;
1867
+ let selectedOrganizationId;
1868
+ let selectedOrganizationName;
1869
+ const repoOrgContext = identity ? lookup?.organizationContext ?? null : null;
1694
1870
  if (authOrganizationId) {
1695
- const workspaceData = await api.listWorkspaces(userToken);
1696
- const ws = workspaceData.workspaces.find(
1871
+ const organizationData = await api.listOrganizations(userToken);
1872
+ const ws = organizationData.organizations.find(
1697
1873
  (w) => w.id === authOrganizationId
1698
1874
  );
1699
- selectedWorkspaceId = authOrganizationId;
1700
- selectedWorkspaceName = ws?.name ?? userEmail;
1701
- p5.log.success(
1702
- `Connected as ${chalk5.bold(userEmail)} \u2014 workspace: ${chalk5.bold(selectedWorkspaceName)}`
1875
+ selectedOrganizationId = authOrganizationId;
1876
+ selectedOrganizationName = ws?.name ?? userEmail;
1877
+ p6.log.success(
1878
+ `Connected as ${chalk5.bold(userEmail)} \u2014 workspace: ${chalk5.bold(selectedOrganizationName)}`
1703
1879
  );
1880
+ } else if (repoOrgContext && !repoProjectId) {
1881
+ selectedOrganizationId = repoOrgContext.organizationId;
1882
+ selectedOrganizationName = repoOrgContext.organizationName;
1883
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
1704
1884
  } else {
1705
- const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
1706
- const cachedInstallations = discoveryResult?.installations ?? [];
1707
- if (cachedInstallations.length > 0) {
1708
- if (identity?.repoCanonical) {
1709
- const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
1710
- if (repoOwner) {
1711
- const hasMatchingAccount = cachedInstallations.some(
1712
- (i) => i.accountLogin.toLowerCase() === repoOwner
1713
- );
1714
- if (!hasMatchingAccount) {
1715
- p5.log.warn(
1716
- `None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
1717
- The project will be created but translations won't trigger automatically.
1718
- To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
1719
- );
1720
- }
1721
- }
1722
- }
1723
- const validInstallations = cachedInstallations.filter(
1724
- (i) => !i.isSuspended && !i.conflictLabel
1725
- );
1726
- let selectedInstallationId = null;
1727
- if (validInstallations.length === 1 && cachedInstallations.length === 1) {
1728
- selectedInstallationId = validInstallations[0].installationId;
1729
- } else {
1730
- selectedInstallationId = await selectGitHubInstallation(
1731
- cachedInstallations.map((inst) => ({
1732
- installationId: inst.installationId,
1733
- accountLogin: inst.accountLogin,
1734
- accountType: inst.accountType,
1735
- isSuspended: inst.isSuspended,
1736
- conflictLabel: inst.conflictLabel
1737
- })),
1738
- false
1739
- );
1740
- }
1741
- if (selectedInstallationId === null || selectedInstallationId === "install_new") {
1742
- p5.cancel(
1743
- "Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
1744
- );
1745
- return 1;
1746
- }
1747
- const claimResult = await api.claimCliGitHubInstallation(userToken, {
1748
- installationId: String(selectedInstallationId),
1749
- organizationId: null
1750
- });
1751
- selectedWorkspaceId = claimResult.organizationId;
1752
- selectedWorkspaceName = claimResult.organizationName;
1753
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
1754
- } else {
1755
- const workspaceData = await api.listWorkspaces(userToken, {
1756
- repo: identity?.repoCanonical
1757
- });
1885
+ const organizationData = await api.listOrganizations(userToken, {
1886
+ repo: identity?.repoCanonical
1887
+ });
1888
+ {
1758
1889
  const repoCanonical = identity?.repoCanonical ?? null;
1759
- const covering = repoCanonical ? workspaceData.workspaces.filter((w) => w.coversRepo === true) : [];
1760
- const connected = workspaceData.workspaces.filter(
1890
+ const covering = repoCanonical ? organizationData.organizations.filter((w) => w.coversRepo === true) : [];
1891
+ const connected = organizationData.organizations.filter(
1761
1892
  (w) => w.hasGitHubConnection
1762
1893
  );
1763
1894
  if (repoCanonical && covering.length === 1) {
1764
1895
  const ws = covering[0];
1765
- selectedWorkspaceId = ws.id;
1766
- selectedWorkspaceName = ws.name;
1767
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
1896
+ selectedOrganizationId = ws.id;
1897
+ selectedOrganizationName = ws.name;
1898
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
1768
1899
  } else if (repoCanonical && covering.length > 1) {
1769
- const choice = await p5.select({
1900
+ const choice = await p6.select({
1770
1901
  message: "Select workspace for this repo",
1771
1902
  options: covering.map((w) => ({
1772
1903
  value: w.id,
1773
- label: `${w.name} ${chalk5.dim(`(${w.projectCount} project${w.projectCount !== 1 ? "s" : ""})`)}`
1904
+ label: `${w.name} ${chalk5.dim(`(${w.appCount} app${w.appCount !== 1 ? "s" : ""})`)}`
1774
1905
  }))
1775
1906
  });
1776
- if (p5.isCancel(choice)) {
1777
- p5.cancel("Setup cancelled.");
1907
+ if (p6.isCancel(choice)) {
1908
+ p6.cancel("Setup cancelled.");
1778
1909
  return 1;
1779
1910
  }
1780
1911
  const ws = covering.find((w) => w.id === choice);
1781
- selectedWorkspaceId = ws.id;
1782
- selectedWorkspaceName = ws.name;
1783
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
1912
+ selectedOrganizationId = ws.id;
1913
+ selectedOrganizationName = ws.name;
1914
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
1784
1915
  } else if (repoCanonical && covering.length === 0 && connected.length > 0) {
1785
1916
  const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
1786
- p5.log.warn(
1917
+ p6.log.warn(
1787
1918
  `${chalk5.bold(shortRepo)} isn't accessible from your Vocoder installation.
1788
1919
  Grant access to this repository or install on the account that owns it.`
1789
1920
  );
@@ -1801,18 +1932,18 @@ async function init(options = {}) {
1801
1932
  label: `Install on a different GitHub account ${chalk5.dim("(creates a new personal workspace)")}`
1802
1933
  });
1803
1934
  fixOptions.push({ value: "cancel", label: "Cancel" });
1804
- const fix = await p5.select({
1935
+ const fix = await p6.select({
1805
1936
  message: "How would you like to fix this?",
1806
1937
  options: fixOptions
1807
1938
  });
1808
- if (p5.isCancel(fix) || fix === "cancel") {
1809
- p5.cancel("Setup cancelled.");
1939
+ if (p6.isCancel(fix) || fix === "cancel") {
1940
+ p6.cancel("Setup cancelled.");
1810
1941
  return 1;
1811
1942
  }
1812
1943
  if (fix.startsWith("grant:")) {
1813
1944
  const ws = connected.find((w) => `grant:${w.id}` === fix);
1814
1945
  await tryOpenBrowser2(ws.installationConfigureUrl);
1815
- p5.cancel(
1946
+ p6.cancel(
1816
1947
  `Grant access to ${chalk5.bold(shortRepo)} in your browser,
1817
1948
  then re-run ${chalk5.bold("vocoder init")}.`
1818
1949
  );
@@ -1824,40 +1955,94 @@ async function init(options = {}) {
1824
1955
  yes: options.yes
1825
1956
  });
1826
1957
  if (!connectResult) {
1827
- p5.log.error(
1958
+ p6.log.error(
1828
1959
  "GitHub App installation did not complete. Run `vocoder init` again."
1829
1960
  );
1830
1961
  return 1;
1831
1962
  }
1832
- selectedWorkspaceId = connectResult.organizationId;
1833
- selectedWorkspaceName = connectResult.organizationName;
1834
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
1963
+ selectedOrganizationId = connectResult.organizationId;
1964
+ selectedOrganizationName = connectResult.organizationName;
1965
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
1835
1966
  } else {
1836
- if (workspaceData.workspaces.length === 1 && !workspaceData.canCreateWorkspace) {
1837
- const ws = workspaceData.workspaces[0];
1838
- selectedWorkspaceId = ws.id;
1839
- selectedWorkspaceName = ws.name;
1840
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
1967
+ const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
1968
+ const cachedInstallations = discoveryResult?.installations ?? [];
1969
+ if (cachedInstallations.length > 0) {
1970
+ if (identity?.repoCanonical) {
1971
+ const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
1972
+ if (repoOwner) {
1973
+ const hasMatchingAccount = cachedInstallations.some(
1974
+ (i) => i.accountLogin.toLowerCase() === repoOwner
1975
+ );
1976
+ if (!hasMatchingAccount) {
1977
+ p6.log.warn(
1978
+ `None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
1979
+ The project will be created but translations won't trigger automatically.
1980
+ To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
1981
+ );
1982
+ }
1983
+ }
1984
+ }
1985
+ const validInstallations = cachedInstallations.filter(
1986
+ (i) => !i.isSuspended && !i.conflictLabel
1987
+ );
1988
+ let selectedInstallationId = null;
1989
+ if (validInstallations.length === 1 && cachedInstallations.length === 1) {
1990
+ selectedInstallationId = validInstallations[0].installationId;
1991
+ } else {
1992
+ selectedInstallationId = await selectGitHubInstallation(
1993
+ cachedInstallations.map((inst) => ({
1994
+ installationId: inst.installationId,
1995
+ accountLogin: inst.accountLogin,
1996
+ accountType: inst.accountType,
1997
+ isSuspended: inst.isSuspended,
1998
+ conflictLabel: inst.conflictLabel
1999
+ })),
2000
+ false
2001
+ );
2002
+ }
2003
+ if (selectedInstallationId === null || selectedInstallationId === "install_new") {
2004
+ p6.cancel(
2005
+ "Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
2006
+ );
2007
+ return 1;
2008
+ }
2009
+ const claimResult = await api.claimCliGitHubInstallation(
2010
+ userToken,
2011
+ {
2012
+ installationId: String(selectedInstallationId),
2013
+ organizationId: null
2014
+ }
2015
+ );
2016
+ selectedOrganizationId = claimResult.organizationId;
2017
+ selectedOrganizationName = claimResult.organizationName;
2018
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
2019
+ } else if (organizationData.organizations.length === 1 && !organizationData.canCreateOrganization) {
2020
+ const ws = organizationData.organizations[0];
2021
+ selectedOrganizationId = ws.id;
2022
+ selectedOrganizationName = ws.name;
2023
+ p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
1841
2024
  } else {
1842
- const workspaceResult = await selectWorkspace(workspaceData);
1843
- if (workspaceResult.action === "cancelled") {
1844
- p5.cancel("Setup cancelled.");
2025
+ const organizationResult = await selectOrganization(organizationData);
2026
+ if (organizationResult.action === "cancelled") {
2027
+ p6.cancel("Setup cancelled.");
1845
2028
  return 1;
1846
2029
  }
1847
- if (workspaceResult.action === "use") {
1848
- selectedWorkspaceId = workspaceResult.workspace.id;
1849
- selectedWorkspaceName = workspaceResult.workspace.name;
1850
- p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
2030
+ if (organizationResult.action === "use") {
2031
+ selectedOrganizationId = organizationResult.organization.id;
2032
+ selectedOrganizationName = organizationResult.organization.name;
2033
+ p6.log.success(
2034
+ `Workspace: ${chalk5.bold(selectedOrganizationName)}`
2035
+ );
1851
2036
  } else {
1852
- const connectChoice = await p5.select({
2037
+ const connectChoice = await p6.select({
1853
2038
  message: "Connect your new workspace to GitHub",
1854
2039
  options: [
1855
2040
  { value: "install", label: "Install the Vocoder GitHub App" },
1856
2041
  { value: "link", label: "Link an existing installation" }
1857
2042
  ]
1858
2043
  });
1859
- if (p5.isCancel(connectChoice)) {
1860
- p5.cancel("Setup cancelled.");
2044
+ if (p6.isCancel(connectChoice)) {
2045
+ p6.cancel("Setup cancelled.");
1861
2046
  return 1;
1862
2047
  }
1863
2048
  if (connectChoice === "install") {
@@ -1867,15 +2052,15 @@ async function init(options = {}) {
1867
2052
  yes: options.yes
1868
2053
  });
1869
2054
  if (!connectResult) {
1870
- p5.log.error(
2055
+ p6.log.error(
1871
2056
  "GitHub App installation did not complete. Run `vocoder init` again."
1872
2057
  );
1873
2058
  return 1;
1874
2059
  }
1875
- selectedWorkspaceId = connectResult.organizationId;
1876
- selectedWorkspaceName = connectResult.organizationName;
1877
- p5.log.success(
1878
- `Workspace: ${chalk5.bold(selectedWorkspaceName)}`
2060
+ selectedOrganizationId = connectResult.organizationId;
2061
+ selectedOrganizationName = connectResult.organizationName;
2062
+ p6.log.success(
2063
+ `Workspace: ${chalk5.bold(selectedOrganizationName)}`
1879
2064
  );
1880
2065
  } else {
1881
2066
  const installations = await runGitHubDiscoveryFlow({
@@ -1885,21 +2070,21 @@ async function init(options = {}) {
1885
2070
  });
1886
2071
  if (!installations) return 1;
1887
2072
  if (installations.length === 0) {
1888
- p5.log.warn(
2073
+ p6.log.warn(
1889
2074
  "No GitHub installations found. Install the Vocoder GitHub App first."
1890
2075
  );
1891
- const installNow = await p5.confirm({
2076
+ const installNow = await p6.confirm({
1892
2077
  message: "Open GitHub to install the App?"
1893
2078
  });
1894
- if (p5.isCancel(installNow) || !installNow) return 1;
2079
+ if (p6.isCancel(installNow) || !installNow) return 1;
1895
2080
  const connectResult = await runGitHubInstallFlow({
1896
2081
  api,
1897
2082
  userToken,
1898
2083
  yes: options.yes
1899
2084
  });
1900
2085
  if (!connectResult) return 1;
1901
- selectedWorkspaceId = connectResult.organizationId;
1902
- selectedWorkspaceName = connectResult.organizationName;
2086
+ selectedOrganizationId = connectResult.organizationId;
2087
+ selectedOrganizationName = connectResult.organizationName;
1903
2088
  } else {
1904
2089
  const selectedInstallationId = await selectGitHubInstallation(
1905
2090
  installations.map((inst) => ({
@@ -1912,7 +2097,7 @@ async function init(options = {}) {
1912
2097
  true
1913
2098
  );
1914
2099
  if (selectedInstallationId === null) {
1915
- p5.cancel("Setup cancelled.");
2100
+ p6.cancel("Setup cancelled.");
1916
2101
  return 1;
1917
2102
  }
1918
2103
  if (selectedInstallationId === "install_new") {
@@ -1922,8 +2107,8 @@ async function init(options = {}) {
1922
2107
  yes: options.yes
1923
2108
  });
1924
2109
  if (!connectResult) return 1;
1925
- selectedWorkspaceId = connectResult.organizationId;
1926
- selectedWorkspaceName = connectResult.organizationName;
2110
+ selectedOrganizationId = connectResult.organizationId;
2111
+ selectedOrganizationName = connectResult.organizationName;
1927
2112
  } else {
1928
2113
  const claimResult = await api.claimCliGitHubInstallation(
1929
2114
  userToken,
@@ -1932,12 +2117,12 @@ async function init(options = {}) {
1932
2117
  organizationId: null
1933
2118
  }
1934
2119
  );
1935
- selectedWorkspaceId = claimResult.organizationId;
1936
- selectedWorkspaceName = claimResult.organizationName;
2120
+ selectedOrganizationId = claimResult.organizationId;
2121
+ selectedOrganizationName = claimResult.organizationName;
1937
2122
  }
1938
2123
  }
1939
- p5.log.success(
1940
- `Workspace: ${chalk5.bold(selectedWorkspaceName)}`
2124
+ p6.log.success(
2125
+ `Workspace: ${chalk5.bold(selectedOrganizationName)}`
1941
2126
  );
1942
2127
  }
1943
2128
  }
@@ -1946,116 +2131,85 @@ async function init(options = {}) {
1946
2131
  }
1947
2132
  }
1948
2133
  if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
1949
- p5.log.info(
1950
- `${chalk5.bold(repoProjectName)} is already set up for this repo.
1951
- Configured apps: ${existingAppsForRepo.map((a) => highlight(a.appDir || "(entire repo)")).join(", ")}`
1952
- );
1953
2134
  const appResult = await runAppCreate({
1954
2135
  api,
1955
2136
  userToken,
1956
2137
  projectId: repoProjectId,
1957
2138
  projectName: repoProjectName,
1958
- organizationName: selectedWorkspaceName,
2139
+ organizationName: selectedOrganizationName,
1959
2140
  repoCanonical: identity?.repoCanonical,
1960
- defaultAppDir: identity?.repoAppDir,
1961
2141
  existingApps: existingAppsForRepo
1962
2142
  });
1963
2143
  if (!appResult) {
1964
- p5.log.error("App setup failed. Run `vocoder init` again.");
2144
+ p6.log.error("App setup failed. Run `vocoder init` again.");
1965
2145
  return 1;
1966
2146
  }
2147
+ const detection2 = detectLocalEcosystem();
1967
2148
  runScaffold({
1968
2149
  sourceLocale: appResult.sourceLocale,
1969
- targetBranches: appResult.targetBranches,
1970
- appDir: identity?.repoAppDir
2150
+ targetBranches: appResult.targetBranches
1971
2151
  });
1972
- p5.outro("You're all set.");
2152
+ writeAppConfigs(
2153
+ [{ appDir: appResult.appDir, appId: appResult.appId }],
2154
+ appResult.targetBranches,
2155
+ detection2.isTypeScript,
2156
+ identity?.repoRoot
2157
+ );
2158
+ p6.log.info(
2159
+ chalk5.dim("Use the VOCODER_API_KEY already in your root .env")
2160
+ );
2161
+ p6.outro("You're all set.");
1973
2162
  return 0;
1974
2163
  }
2164
+ let remainingApps;
1975
2165
  try {
1976
- const wsCheck = await api.listWorkspaces(userToken);
1977
- const ws = wsCheck.workspaces.find((w) => w.id === selectedWorkspaceId);
1978
- if (ws && ws.maxProjects !== -1 && ws.projectCount >= ws.maxProjects) {
1979
- p5.log.warn(
1980
- `Project limit reached \u2014 ${ws.projectCount}/${ws.maxProjects} on your ${chalk5.bold(ws.planId)} plan.`
1981
- );
1982
- const options2 = [];
1983
- if (repoProjectId) {
1984
- options2.push({
1985
- value: "connect",
1986
- label: `Reconnect this repo to ${chalk5.bold(repoProjectName ?? "existing project")}`
2166
+ const wsCheck = await api.listOrganizations(userToken);
2167
+ const ws = wsCheck.organizations.find(
2168
+ (w) => w.id === selectedOrganizationId
2169
+ );
2170
+ if (ws) {
2171
+ if (ws.maxApps !== -1 && ws.appCount >= ws.maxApps) {
2172
+ p6.log.warn(
2173
+ `App limit reached \u2014 ${ws.appCount}/${ws.maxApps} on your ${chalk5.bold(ws.planId)} plan.`
2174
+ );
2175
+ const limitAction = await p6.select({
2176
+ message: "What would you like to do?",
2177
+ options: [
2178
+ { value: "upgrade", label: "Upgrade plan" },
2179
+ { value: "cancel", label: "Cancel" }
2180
+ ]
1987
2181
  });
1988
- }
1989
- options2.push({ value: "upgrade", label: "Upgrade plan" });
1990
- options2.push({ value: "cancel", label: "Cancel" });
1991
- const limitAction = await p5.select({
1992
- message: "What would you like to do?",
1993
- options: options2
1994
- });
1995
- if (p5.isCancel(limitAction) || limitAction === "cancel") {
1996
- p5.cancel("Setup cancelled.");
1997
- return 1;
1998
- }
1999
- if (limitAction === "upgrade") {
2182
+ if (p6.isCancel(limitAction) || limitAction === "cancel") {
2183
+ p6.cancel("Setup cancelled.");
2184
+ return 1;
2185
+ }
2000
2186
  await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
2001
- p5.cancel(
2187
+ p6.cancel(
2002
2188
  "Upgrade your plan in the browser, then re-run `vocoder init`."
2003
2189
  );
2004
2190
  return 1;
2005
2191
  }
2006
- const existingProjects = await api.listApps(
2007
- userToken,
2008
- selectedWorkspaceId
2009
- );
2010
- const chosenProject = existingProjects.find(
2011
- (proj) => proj.id === repoProjectId
2012
- );
2013
- if (!chosenProject) {
2014
- p5.log.error("Could not find the project. Try again.");
2015
- return 1;
2016
- }
2017
- try {
2018
- const appResult = await api.createApp(userToken, {
2019
- projectId: chosenProject.id,
2020
- appDir: identity?.repoAppDir ?? "",
2021
- sourceLocale: chosenProject.sourceLocale,
2022
- targetLocales: chosenProject.targetLocales,
2023
- targetBranches: chosenProject.targetBranches,
2024
- repoCanonical: identity?.repoCanonical ?? ""
2025
- });
2026
- p5.log.success(`Connected to project: ${chalk5.bold(chosenProject.name)}`);
2027
- printApiKey(appResult.apiKey);
2028
- runScaffold({
2029
- sourceLocale: chosenProject.sourceLocale,
2030
- targetBranches: chosenProject.targetBranches,
2031
- appDir: identity?.repoAppDir
2032
- });
2033
- } catch (err) {
2034
- const msg = err instanceof Error ? err.message : String(err);
2035
- p5.log.error(`Failed to create app binding: ${msg}`);
2036
- return 1;
2037
- }
2038
- p5.outro("You're all set.");
2039
- return 0;
2192
+ remainingApps = ws.maxApps === -1 ? void 0 : Math.max(0, ws.maxApps - ws.appCount);
2040
2193
  }
2041
2194
  } catch {
2042
2195
  }
2043
2196
  const projectResult = await runProjectCreate({
2044
2197
  api,
2045
2198
  userToken,
2046
- organizationId: selectedWorkspaceId,
2199
+ organizationId: selectedOrganizationId,
2047
2200
  defaultName: identity?.repoCanonical ? identity.repoCanonical.split("/").pop() : void 0,
2048
2201
  defaultSourceLocale: "en",
2049
2202
  repoCanonical: identity?.repoCanonical,
2203
+ repoRoot: identity?.repoRoot,
2050
2204
  defaultBranches: ["main"],
2051
- defaultAppDir: identity?.repoAppDir
2205
+ maxAppDirs: remainingApps
2052
2206
  });
2053
2207
  if (!projectResult) {
2054
- p5.log.error("Project creation failed. Run `vocoder init` again.");
2208
+ p6.log.error("Project creation failed. Run `vocoder init` again.");
2055
2209
  return 1;
2056
2210
  }
2057
2211
  if (!projectResult.repositoryBound && identity?.repoCanonical) {
2058
- p5.log.warn(
2212
+ p6.log.warn(
2059
2213
  `This repository isn't accessible to your GitHub App installation.
2060
2214
  Translations won't run automatically until you grant access.
2061
2215
 
@@ -2066,13 +2220,19 @@ Translations won't run automatically until you grant access.
2066
2220
  ` : "")
2067
2221
  );
2068
2222
  }
2223
+ const detection = detectLocalEcosystem();
2069
2224
  runScaffold({
2070
2225
  sourceLocale: projectResult.sourceLocale,
2071
- targetBranches: projectResult.targetBranches,
2072
- appDir: identity?.repoAppDir
2226
+ targetBranches: projectResult.targetBranches
2073
2227
  });
2074
- printApiKey(projectResult.apiKey);
2075
- p5.outro("You're all set.");
2228
+ writeAppConfigs(
2229
+ projectResult.apps,
2230
+ projectResult.targetBranches,
2231
+ detection.isTypeScript,
2232
+ identity?.repoRoot
2233
+ );
2234
+ printApiKey(projectResult.apiKey, identity?.repoRoot);
2235
+ p6.outro("You're all set.");
2076
2236
  return 0;
2077
2237
  } catch (error) {
2078
2238
  if (error instanceof Error) {
@@ -2080,24 +2240,25 @@ Translations won't run automatically until you grant access.
2080
2240
  printPlanLimitMessage(apiUrl, error.message);
2081
2241
  return 1;
2082
2242
  }
2083
- p5.log.error(`Error: ${error.message}`);
2243
+ p6.log.error(`Error: ${error.message}`);
2084
2244
  } else {
2085
- p5.log.error("Unknown setup error");
2245
+ p6.log.error("Unknown setup error");
2086
2246
  }
2087
2247
  return 1;
2088
2248
  }
2089
2249
  }
2090
2250
 
2091
2251
  // src/commands/locales.ts
2092
- import * as p8 from "@clack/prompts";
2252
+ import * as p9 from "@clack/prompts";
2093
2253
  import chalk7 from "chalk";
2094
2254
  import { config as loadEnv3 } from "dotenv";
2255
+ import { readFileSync as readFileSync3 } from "fs";
2095
2256
 
2096
2257
  // src/commands/sync.ts
2097
- import { createHash, randomUUID } from "crypto";
2098
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2258
+ import { randomUUID } from "crypto";
2259
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2099
2260
  import { join as join3 } from "path";
2100
- import * as p7 from "@clack/prompts";
2261
+ import * as p8 from "@clack/prompts";
2101
2262
  import chalk6 from "chalk";
2102
2263
 
2103
2264
  // src/utils/branch.ts
@@ -2167,7 +2328,7 @@ function matchBranchPattern(branch, pattern) {
2167
2328
  }
2168
2329
 
2169
2330
  // src/utils/config.ts
2170
- import * as p6 from "@clack/prompts";
2331
+ import * as p7 from "@clack/prompts";
2171
2332
  import { config as loadEnv2 } from "dotenv";
2172
2333
  loadEnv2();
2173
2334
  function extractShortCodeFromApiKey(apiKey) {
@@ -2223,7 +2384,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2223
2384
  };
2224
2385
  const fileConfig = loadVocoderConfig(process.cwd());
2225
2386
  if (!fileConfig) {
2226
- p6.log.warn(
2387
+ p7.log.warn(
2227
2388
  `No ${highlight("vocoder.config.ts")} found \u2014 run ${highlight("npx @vocoder/cli init")} to generate one.`
2228
2389
  );
2229
2390
  }
@@ -2254,7 +2415,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2254
2415
  excludePattern = fileConfig.exclude;
2255
2416
  configSources.excludePattern = "vocoder.config";
2256
2417
  } else if (envExcludePattern) {
2257
- excludePattern = envExcludePattern.split(",").map((p14) => p14.trim()).filter(Boolean);
2418
+ excludePattern = envExcludePattern.split(",").map((p15) => p15.trim()).filter(Boolean);
2258
2419
  configSources.excludePattern = "environment";
2259
2420
  } else {
2260
2421
  excludePattern = defaults.excludePattern;
@@ -2311,7 +2472,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2311
2472
  ...maxWaitMs ? [`Max wait: ${highlight(String(configSources.maxWaitMs))}`] : [],
2312
2473
  `No fallback: ${highlight(String(configSources.noFallback))}`
2313
2474
  ];
2314
- p6.note(lines.join("\n"), "Configuration sources");
2475
+ p7.note(lines.join("\n"), "Configuration sources");
2315
2476
  }
2316
2477
  return {
2317
2478
  includePattern,
@@ -2326,10 +2487,6 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2326
2487
  }
2327
2488
 
2328
2489
  // src/commands/sync.ts
2329
- function computeFingerprint(shortCode, texts) {
2330
- const sorted = [...texts].sort();
2331
- return createHash("sha256").update(`${shortCode}:${sorted.join("\0")}`).digest("hex").slice(0, 12);
2332
- }
2333
2490
  function isRecord(value) {
2334
2491
  return typeof value === "object" && value !== null && !Array.isArray(value);
2335
2492
  }
@@ -2377,15 +2534,6 @@ function getCacheFilePath(projectRoot, fingerprint) {
2377
2534
  return join3(projectRoot, "node_modules", ".vocoder", "cache", `${fingerprint}.json`);
2378
2535
  }
2379
2536
  function buildTranslationData(params) {
2380
- const textToHash = new Map(params.stringEntries.map((e) => [e.text, e.key]));
2381
- const hashKeyed = {};
2382
- for (const [locale, localeMap] of Object.entries(params.translations)) {
2383
- hashKeyed[locale] = {};
2384
- for (const [text, translation] of Object.entries(localeMap)) {
2385
- const hash = textToHash.get(text);
2386
- if (hash) hashKeyed[locale][hash] = translation;
2387
- }
2388
- }
2389
2537
  const locales = {};
2390
2538
  for (const code of [params.sourceLocale, ...params.targetLocales]) {
2391
2539
  const meta = params.localeMetadata?.[code];
@@ -2393,13 +2541,13 @@ function buildTranslationData(params) {
2393
2541
  }
2394
2542
  return {
2395
2543
  config: { sourceLocale: params.sourceLocale, targetLocales: params.targetLocales, locales },
2396
- translations: hashKeyed,
2544
+ translations: params.translations,
2397
2545
  updatedAt: params.updatedAt
2398
2546
  };
2399
2547
  }
2400
2548
  function readLocalCache(params) {
2401
2549
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
2402
- if (!existsSync3(cacheFilePath)) return null;
2550
+ if (!existsSync4(cacheFilePath)) return null;
2403
2551
  try {
2404
2552
  const raw = readFileSync2(cacheFilePath, "utf-8");
2405
2553
  const parsed = JSON.parse(raw);
@@ -2458,9 +2606,10 @@ function normalizeTranslations(params) {
2458
2606
  if (!merged[params.sourceLocale]) {
2459
2607
  merged[params.sourceLocale] = {};
2460
2608
  }
2461
- for (const sourceText of params.sourceStrings) {
2462
- if (!(sourceText in merged[params.sourceLocale])) {
2463
- merged[params.sourceLocale][sourceText] = sourceText;
2609
+ for (const entry of params.stringEntries) {
2610
+ if (!entry.text) continue;
2611
+ if (!(entry.key in merged[params.sourceLocale])) {
2612
+ merged[params.sourceLocale][entry.key] = entry.text;
2464
2613
  }
2465
2614
  }
2466
2615
  return merged;
@@ -2536,11 +2685,11 @@ function mergeContext(current, incoming) {
2536
2685
  return Array.from(merged).join(" | ");
2537
2686
  }
2538
2687
  function buildStringEntries(extractedStrings) {
2539
- const byText = /* @__PURE__ */ new Map();
2688
+ const byKey = /* @__PURE__ */ new Map();
2540
2689
  for (const str of extractedStrings) {
2541
- const existing = byText.get(str.text);
2690
+ const existing = byKey.get(str.key);
2542
2691
  if (!existing) {
2543
- byText.set(str.text, {
2692
+ byKey.set(str.key, {
2544
2693
  key: str.key,
2545
2694
  text: str.text,
2546
2695
  ...str.context ? { context: str.context } : {},
@@ -2555,11 +2704,8 @@ function buildStringEntries(extractedStrings) {
2555
2704
  } else if (existing.formality && str.formality && existing.formality !== str.formality) {
2556
2705
  existing.formality = "auto";
2557
2706
  }
2558
- if (str.key < existing.key) {
2559
- existing.key = str.key;
2560
- }
2561
2707
  }
2562
- return Array.from(byText.values());
2708
+ return Array.from(byKey.values());
2563
2709
  }
2564
2710
  async function fetchApiSnapshot(api, params) {
2565
2711
  const snapshot = await api.getTranslationSnapshot({
@@ -2580,19 +2726,19 @@ async function fetchApiSnapshot(api, params) {
2580
2726
  async function sync(options = {}) {
2581
2727
  const startTime = Date.now();
2582
2728
  const projectRoot = process.cwd();
2583
- p7.intro(chalk6.bold("Vocoder Sync"));
2729
+ p8.intro(chalk6.bold("Vocoder Sync"));
2584
2730
  const mergedConfig = await getMergedConfig(options, options.verbose);
2585
2731
  if (!mergedConfig.apiKey) {
2586
- p7.log.warn("No API key found. Run init to get started:");
2587
- p7.log.info(" npx @vocoder/cli init");
2588
- p7.log.info("");
2589
- p7.log.info(
2732
+ p8.log.warn("No API key found. Run init to get started:");
2733
+ p8.log.info(" npx @vocoder/cli init");
2734
+ p8.log.info("");
2735
+ p8.log.info(
2590
2736
  " Or add your key to .env: VOCODER_API_KEY=vca_..."
2591
2737
  );
2592
- p7.outro("Run `npx @vocoder/cli init` to set up your project.");
2738
+ p8.outro("Run `npx @vocoder/cli init` to set up your project.");
2593
2739
  return 1;
2594
2740
  }
2595
- const spinner7 = p7.spinner();
2741
+ const spinner7 = p8.spinner();
2596
2742
  try {
2597
2743
  const branch = detectBranch(options.branch);
2598
2744
  spinner7.start("Loading project configuration");
@@ -2621,12 +2767,12 @@ async function sync(options = {}) {
2621
2767
  };
2622
2768
  spinner7.stop(`Branch: ${highlight(branch)}`);
2623
2769
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
2624
- p7.log.warn(
2770
+ p8.log.warn(
2625
2771
  `Skipping translations (${highlight(branch)} is not a target branch)`
2626
2772
  );
2627
- p7.log.info(`Target branches: ${config.targetBranches.map((b) => highlight(b)).join(", ")}`);
2628
- p7.log.info("Use --force to translate anyway");
2629
- p7.outro("");
2773
+ p8.log.info(`Target branches: ${config.targetBranches.map((b) => highlight(b)).join(", ")}`);
2774
+ p8.log.info("Use --force to translate anyway");
2775
+ p8.outro("");
2630
2776
  return 0;
2631
2777
  }
2632
2778
  const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
@@ -2639,10 +2785,10 @@ async function sync(options = {}) {
2639
2785
  );
2640
2786
  if (extractedStrings.length === 0) {
2641
2787
  spinner7.stop("No translatable strings found");
2642
- p7.log.warn(
2788
+ p8.log.warn(
2643
2789
  "Make sure you are wrapping translatable strings with Vocoder"
2644
2790
  );
2645
- p7.outro("");
2791
+ p8.outro("");
2646
2792
  return 0;
2647
2793
  }
2648
2794
  spinner7.stop(
@@ -2653,10 +2799,10 @@ async function sync(options = {}) {
2653
2799
  if (extractedStrings.length > 5) {
2654
2800
  sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
2655
2801
  }
2656
- p7.note(sampleLines.join("\n"), "Sample strings");
2802
+ p8.note(sampleLines.join("\n"), "Sample strings");
2657
2803
  }
2658
2804
  if (options.dryRun) {
2659
- p7.note(
2805
+ p8.note(
2660
2806
  [
2661
2807
  `Strings: ${extractedStrings.length}`,
2662
2808
  `Branch: ${branch}`,
@@ -2667,36 +2813,36 @@ async function sync(options = {}) {
2667
2813
  ].join("\n"),
2668
2814
  "Dry run - would translate"
2669
2815
  );
2670
- p7.outro("No API calls made.");
2816
+ p8.outro("No API calls made.");
2671
2817
  return 0;
2672
2818
  }
2673
2819
  const repoIdentity = resolveGitRepositoryIdentity();
2674
2820
  if (!repoIdentity && options.verbose) {
2675
- p7.log.warn(
2821
+ p8.log.warn(
2676
2822
  "Could not detect git remote origin. Sync will continue without repo metadata."
2677
2823
  );
2678
2824
  }
2679
2825
  const commitSha = detectCommitSha() ?? void 0;
2680
2826
  const stringEntries = buildStringEntries(extractedStrings);
2681
- const sourceStrings = stringEntries.map((entry) => entry.text);
2682
2827
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
2683
- p7.log.info(
2684
- `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
2828
+ p8.log.info(
2829
+ `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique strings`
2685
2830
  );
2686
2831
  }
2687
- const fingerprint = computeFingerprint(extractShortCodeFromApiKey(localConfig.apiKey), sourceStrings);
2832
+ const sourceKeys = stringEntries.map((entry) => entry.key);
2833
+ const fingerprint = computeFingerprint(extractShortCodeFromApiKey(localConfig.apiKey), sourceKeys);
2688
2834
  if (!options.force) {
2689
2835
  const cacheFile = getCacheFilePath(projectRoot, fingerprint);
2690
- if (existsSync3(cacheFile)) {
2836
+ if (existsSync4(cacheFile)) {
2691
2837
  if (options.verbose) {
2692
- p7.log.info(`Cache hit: ${chalk6.dim(cacheFile)} (fingerprint ${highlight(fingerprint)})`);
2838
+ p8.log.info(`Cache hit: ${chalk6.dim(cacheFile)} (fingerprint ${highlight(fingerprint)})`);
2693
2839
  }
2694
2840
  const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
2695
- p7.outro(`Up to date (${duration2}s)`);
2841
+ p8.outro(`Up to date (${duration2}s)`);
2696
2842
  return 0;
2697
2843
  }
2698
2844
  if (options.verbose) {
2699
- p7.log.info(`No cache for fingerprint ${highlight(fingerprint)} \u2014 will submit to API`);
2845
+ p8.log.info(`No cache for fingerprint ${highlight(fingerprint)} \u2014 will submit to API`);
2700
2846
  }
2701
2847
  }
2702
2848
  spinner7.start("Submitting strings to Vocoder API");
@@ -2721,26 +2867,26 @@ async function sync(options = {}) {
2721
2867
  policy: config.syncPolicy
2722
2868
  });
2723
2869
  if (options.verbose) {
2724
- p7.log.info(`Batch: ${chalk6.dim(batchResponse.batchId)}`);
2725
- p7.log.info(`Requested mode: ${requestedMode}`);
2726
- p7.log.info(`Effective mode: ${effectiveMode}`);
2727
- p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
2870
+ p8.log.info(`Batch: ${chalk6.dim(batchResponse.batchId)}`);
2871
+ p8.log.info(`Requested mode: ${requestedMode}`);
2872
+ p8.log.info(`Effective mode: ${effectiveMode}`);
2873
+ p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
2728
2874
  if (batchResponse.queueStatus) {
2729
- p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
2875
+ p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
2730
2876
  }
2731
2877
  }
2732
2878
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
2733
- p7.log.success(`Up to date \u2014 ${highlight(batchResponse.totalStrings)} strings, no changes`);
2879
+ p8.log.success(`Up to date \u2014 ${highlight(batchResponse.totalStrings)} strings, no changes`);
2734
2880
  } else if (batchResponse.newStrings === 0) {
2735
2881
  const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk6.yellow(batchResponse.deletedStrings)} archived` : "";
2736
- p7.log.success(`No new strings \u2014 ${highlight(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
2882
+ p8.log.success(`No new strings \u2014 ${highlight(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
2737
2883
  } else {
2738
2884
  const statParts = [`${highlight(batchResponse.newStrings)} new, ${highlight(batchResponse.totalStrings)} total`];
2739
2885
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
2740
2886
  statParts.push(`${chalk6.yellow(batchResponse.deletedStrings)} archived`);
2741
2887
  }
2742
2888
  const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
2743
- p7.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.map((l) => highlight(l)).join(", ")}${estTime}`);
2889
+ p8.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.map((l) => highlight(l)).join(", ")}${estTime}`);
2744
2890
  }
2745
2891
  let artifacts = null;
2746
2892
  if (batchResponse.translations) {
@@ -2778,7 +2924,7 @@ async function sync(options = {}) {
2778
2924
  if (effectiveMode === "required") {
2779
2925
  throw waitError;
2780
2926
  }
2781
- p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
2927
+ p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
2782
2928
  }
2783
2929
  }
2784
2930
  if (!artifacts) {
@@ -2811,7 +2957,7 @@ async function sync(options = {}) {
2811
2957
  spinner7.stop("Failed to fetch API snapshot");
2812
2958
  if (options.verbose) {
2813
2959
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
2814
- p7.log.warn(`Snapshot fetch error: ${message}`);
2960
+ p8.log.warn(`Snapshot fetch error: ${message}`);
2815
2961
  }
2816
2962
  }
2817
2963
  }
@@ -2829,72 +2975,71 @@ async function sync(options = {}) {
2829
2975
  const finalTranslations = normalizeTranslations({
2830
2976
  sourceLocale: config.sourceLocale,
2831
2977
  targetLocales: config.targetLocales,
2832
- sourceStrings,
2978
+ stringEntries,
2833
2979
  translations: artifacts.translations
2834
2980
  });
2835
2981
  try {
2836
2982
  const data = buildTranslationData({
2837
2983
  sourceLocale: config.sourceLocale,
2838
2984
  targetLocales: config.targetLocales,
2839
- stringEntries,
2840
2985
  translations: finalTranslations,
2841
2986
  localeMetadata: artifacts.localeMetadata,
2842
2987
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2843
2988
  });
2844
2989
  const cachePath = writeCache({ projectRoot, fingerprint, data });
2845
2990
  if (options.verbose) {
2846
- p7.log.info(`Cache written: ${highlight(cachePath)}`);
2991
+ p8.log.info(`Cache written: ${highlight(cachePath)}`);
2847
2992
  }
2848
2993
  } catch (error) {
2849
2994
  if (options.verbose) {
2850
2995
  const message = error instanceof Error ? error.message : "Unknown cache write error";
2851
- p7.log.warn(`Failed to write cache: ${message}`);
2996
+ p8.log.warn(`Failed to write cache: ${message}`);
2852
2997
  }
2853
2998
  }
2854
2999
  if (artifacts.source !== "fresh") {
2855
3000
  const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
2856
- p7.log.warn(
3001
+ p8.log.warn(
2857
3002
  `Using ${sourceLabel}. New strings may appear after the background sync completes.`
2858
3003
  );
2859
3004
  }
2860
3005
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
2861
- p7.outro(`Sync complete! (${duration}s)`);
3006
+ p8.outro(`Sync complete! (${duration}s)`);
2862
3007
  return 0;
2863
3008
  } catch (error) {
2864
3009
  spinner7.stop();
2865
3010
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
2866
- p7.log.error(error.syncPolicyError.message);
3011
+ p8.log.error(error.syncPolicyError.message);
2867
3012
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
2868
3013
  for (const line of guidance) {
2869
- p7.log.info(line);
3014
+ p8.log.info(line);
2870
3015
  }
2871
3016
  return 1;
2872
3017
  }
2873
3018
  if (error instanceof VocoderAPIError && error.limitError) {
2874
3019
  const { limitError } = error;
2875
- p7.log.error(limitError.message);
3020
+ p8.log.error(limitError.message);
2876
3021
  const guidance = getLimitErrorGuidance(limitError);
2877
3022
  for (const line of guidance) {
2878
- p7.log.info(line);
3023
+ p8.log.info(line);
2879
3024
  }
2880
3025
  return 1;
2881
3026
  }
2882
3027
  if (error instanceof Error) {
2883
- p7.log.error(error.message);
3028
+ p8.log.error(error.message);
2884
3029
  const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
2885
3030
  if (isInvalidKey) {
2886
- p7.log.warn(
3031
+ p8.log.warn(
2887
3032
  "API key rejected \u2014 the project may have been deleted or the key revoked."
2888
3033
  );
2889
- p7.log.info(
3034
+ p8.log.info(
2890
3035
  " Run `npx @vocoder/cli init` to create a new project and key."
2891
3036
  );
2892
3037
  } else if (error.message.includes("git branch")) {
2893
- p7.log.warn("Run from a git repository, or use:");
2894
- p7.log.info(" vocoder sync --branch main");
3038
+ p8.log.warn("Run from a git repository, or use:");
3039
+ p8.log.info(" vocoder sync --branch main");
2895
3040
  }
2896
3041
  if (options.verbose) {
2897
- p7.log.info(`Full error: ${error.stack ?? error}`);
3042
+ p8.log.info(`Full error: ${error.stack ?? error}`);
2898
3043
  }
2899
3044
  }
2900
3045
  return 1;
@@ -2903,10 +3048,21 @@ async function sync(options = {}) {
2903
3048
 
2904
3049
  // src/commands/locales.ts
2905
3050
  loadEnv3();
3051
+ function readLocalAppId() {
3052
+ const configPath = findExistingConfig(process.cwd());
3053
+ if (!configPath) return void 0;
3054
+ try {
3055
+ const content = readFileSync3(configPath, "utf-8");
3056
+ const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
3057
+ return match?.[1];
3058
+ } catch {
3059
+ return void 0;
3060
+ }
3061
+ }
2906
3062
  function getApiConfig(options) {
2907
3063
  const apiKey = process.env.VOCODER_API_KEY;
2908
3064
  if (!apiKey) {
2909
- p8.log.error(
3065
+ p9.log.error(
2910
3066
  "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
2911
3067
  );
2912
3068
  return null;
@@ -2922,19 +3078,19 @@ async function listProjectLocales(options = {}) {
2922
3078
  const api = new VocoderAPI(config);
2923
3079
  try {
2924
3080
  const projectConfig = await api.getAppConfig();
2925
- p8.log.info(
3081
+ p9.log.info(
2926
3082
  `Source locale: ${highlight(projectConfig.sourceLocale)}`
2927
3083
  );
2928
3084
  if (projectConfig.targetLocales.length === 0) {
2929
- p8.log.info("Target locales: (none configured)");
3085
+ p9.log.info("Target locales: (none configured)");
2930
3086
  } else {
2931
- p8.log.info(
3087
+ p9.log.info(
2932
3088
  `Target locales: ${projectConfig.targetLocales.map((l) => highlight(l)).join(", ")}`
2933
3089
  );
2934
3090
  }
2935
3091
  return 0;
2936
3092
  } catch (error) {
2937
- p8.log.error(
3093
+ p9.log.error(
2938
3094
  error instanceof Error ? error.message : "Failed to fetch project locales."
2939
3095
  );
2940
3096
  return 1;
@@ -2942,19 +3098,20 @@ async function listProjectLocales(options = {}) {
2942
3098
  }
2943
3099
  async function addLocales(locales, options = {}) {
2944
3100
  if (locales.length === 0) {
2945
- p8.log.error("No locale codes provided.");
3101
+ p9.log.error("No locale codes provided.");
2946
3102
  return 1;
2947
3103
  }
2948
3104
  const config = getApiConfig(options);
2949
3105
  if (!config) return 1;
2950
3106
  const api = new VocoderAPI(config);
3107
+ const appId = readLocalAppId();
2951
3108
  let lastTargetLocales = [];
2952
3109
  let hadError = false;
2953
3110
  for (const locale of locales) {
2954
- const spinner7 = p8.spinner();
3111
+ const spinner7 = p9.spinner();
2955
3112
  spinner7.start(`Adding ${locale}\u2026`);
2956
3113
  try {
2957
- const result = await api.addLocale(locale);
3114
+ const result = await api.addLocale(locale, void 0, appId);
2958
3115
  lastTargetLocales = result.targetLocales;
2959
3116
  spinner7.stop(`Added ${highlight(locale)}`);
2960
3117
  } catch (error) {
@@ -2962,19 +3119,19 @@ async function addLocales(locales, options = {}) {
2962
3119
  hadError = true;
2963
3120
  if (error instanceof VocoderAPIError && error.limitError) {
2964
3121
  const { limitError } = error;
2965
- p8.log.error(limitError.message);
3122
+ p9.log.error(limitError.message);
2966
3123
  for (const line of getLimitErrorGuidance(limitError)) {
2967
- p8.log.info(line);
3124
+ p9.log.info(line);
2968
3125
  }
2969
3126
  break;
2970
3127
  }
2971
- p8.log.error(
3128
+ p9.log.error(
2972
3129
  error instanceof Error ? error.message : "Unknown error"
2973
3130
  );
2974
3131
  }
2975
3132
  }
2976
3133
  if (lastTargetLocales.length > 0) {
2977
- p8.log.info(
3134
+ p9.log.info(
2978
3135
  `Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
2979
3136
  );
2980
3137
  }
@@ -2982,35 +3139,36 @@ async function addLocales(locales, options = {}) {
2982
3139
  }
2983
3140
  async function removeLocales(locales, options = {}) {
2984
3141
  if (locales.length === 0) {
2985
- p8.log.error("No locale codes provided.");
3142
+ p9.log.error("No locale codes provided.");
2986
3143
  return 1;
2987
3144
  }
2988
3145
  const config = getApiConfig(options);
2989
3146
  if (!config) return 1;
2990
3147
  const api = new VocoderAPI(config);
3148
+ const appId = readLocalAppId();
2991
3149
  let lastTargetLocales = [];
2992
3150
  let hadError = false;
2993
3151
  for (const locale of locales) {
2994
- const spinner7 = p8.spinner();
3152
+ const spinner7 = p9.spinner();
2995
3153
  spinner7.start(`Removing ${locale}\u2026`);
2996
3154
  try {
2997
- const result = await api.removeLocale(locale);
3155
+ const result = await api.removeLocale(locale, void 0, appId);
2998
3156
  lastTargetLocales = result.targetLocales;
2999
3157
  spinner7.stop(`Removed ${highlight(locale)}`);
3000
3158
  } catch (error) {
3001
3159
  spinner7.stop(`Failed to remove ${chalk7.red(locale)}`);
3002
3160
  hadError = true;
3003
- p8.log.error(
3161
+ p9.log.error(
3004
3162
  error instanceof Error ? error.message : "Unknown error"
3005
3163
  );
3006
3164
  }
3007
3165
  }
3008
3166
  if (lastTargetLocales.length > 0) {
3009
- p8.log.info(
3167
+ p9.log.info(
3010
3168
  `Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
3011
3169
  );
3012
3170
  } else if (!hadError) {
3013
- p8.log.info("Target locales now: (none configured)");
3171
+ p9.log.info("Target locales now: (none configured)");
3014
3172
  }
3015
3173
  return hadError ? 1 : 0;
3016
3174
  }
@@ -3020,14 +3178,14 @@ async function listSupportedLocales(options = {}) {
3020
3178
  const api = new VocoderAPI(config);
3021
3179
  try {
3022
3180
  const result = await api.listLocales(config.apiKey);
3023
- p8.log.info(chalk7.bold("Source locales:"));
3181
+ p9.log.info(chalk7.bold("Source locales:"));
3024
3182
  printLocaleTable(result.sourceLocales);
3025
- p8.log.info("");
3026
- p8.log.info(chalk7.bold("Target locales:"));
3183
+ p9.log.info("");
3184
+ p9.log.info(chalk7.bold("Target locales:"));
3027
3185
  printLocaleTable(result.targetLocales);
3028
3186
  return 0;
3029
3187
  } catch (error) {
3030
- p8.log.error(
3188
+ p9.log.error(
3031
3189
  error instanceof Error ? error.message : "Failed to fetch supported locales."
3032
3190
  );
3033
3191
  return 1;
@@ -3036,16 +3194,16 @@ async function listSupportedLocales(options = {}) {
3036
3194
  function printLocaleTable(locales) {
3037
3195
  for (const locale of locales) {
3038
3196
  const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
3039
- p8.log.info(` ${highlight(locale.code.padEnd(10))} ${locale.name}${native}`);
3197
+ p9.log.info(` ${highlight(locale.code.padEnd(10))} ${locale.name}${native}`);
3040
3198
  }
3041
3199
  }
3042
3200
 
3043
3201
  // src/commands/logout.ts
3044
- import * as p9 from "@clack/prompts";
3202
+ import * as p10 from "@clack/prompts";
3045
3203
  async function logout(options = {}) {
3046
3204
  const stored = readAuthData();
3047
3205
  if (!stored) {
3048
- p9.log.info("Not currently authenticated.");
3206
+ p10.log.info("Not currently authenticated.");
3049
3207
  return 0;
3050
3208
  }
3051
3209
  const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
@@ -3055,19 +3213,19 @@ async function logout(options = {}) {
3055
3213
  } catch {
3056
3214
  }
3057
3215
  clearAuthData();
3058
- p9.log.success(`Logged out (was ${stored.email})`);
3216
+ p10.log.success(`Logged out (was ${stored.email})`);
3059
3217
  return 0;
3060
3218
  }
3061
3219
 
3062
3220
  // src/commands/app-config.ts
3063
- import * as p10 from "@clack/prompts";
3221
+ import * as p11 from "@clack/prompts";
3064
3222
  import chalk8 from "chalk";
3065
3223
  import { config as loadEnv4 } from "dotenv";
3066
3224
  loadEnv4();
3067
3225
  async function appConfig(options = {}) {
3068
3226
  const apiKey = process.env.VOCODER_API_KEY;
3069
3227
  if (!apiKey) {
3070
- p10.log.error(
3228
+ p11.log.error(
3071
3229
  "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
3072
3230
  );
3073
3231
  return 1;
@@ -3089,10 +3247,10 @@ async function appConfig(options = {}) {
3089
3247
  ` Non-blocking mode: ${highlight(config.syncPolicy.nonBlockingMode)}`,
3090
3248
  ` Max wait: ${highlight(String(config.syncPolicy.defaultMaxWaitMs))} ms`
3091
3249
  ];
3092
- p10.note(lines.join("\n"), `${config.projectName} \u2014 app config`);
3250
+ p11.note(lines.join("\n"), `${config.projectName} \u2014 app config`);
3093
3251
  return 0;
3094
3252
  } catch (error) {
3095
- p10.log.error(
3253
+ p11.log.error(
3096
3254
  error instanceof Error ? error.message : "Failed to fetch project config."
3097
3255
  );
3098
3256
  return 1;
@@ -3102,13 +3260,13 @@ async function appConfig(options = {}) {
3102
3260
  // src/commands/translations.ts
3103
3261
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
3104
3262
  import { join as join4 } from "path";
3105
- import * as p11 from "@clack/prompts";
3263
+ import * as p12 from "@clack/prompts";
3106
3264
  import { config as loadEnv5 } from "dotenv";
3107
3265
  loadEnv5();
3108
3266
  async function getTranslations(options = {}) {
3109
3267
  const apiKey = process.env.VOCODER_API_KEY;
3110
3268
  if (!apiKey) {
3111
- p11.log.error(
3269
+ p12.log.error(
3112
3270
  "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
3113
3271
  );
3114
3272
  return 1;
@@ -3119,25 +3277,25 @@ async function getTranslations(options = {}) {
3119
3277
  try {
3120
3278
  branch = detectBranch(options.branch);
3121
3279
  } catch (error) {
3122
- p11.log.error(
3280
+ p12.log.error(
3123
3281
  error instanceof Error ? error.message : "Failed to detect branch."
3124
3282
  );
3125
3283
  return 1;
3126
3284
  }
3127
- const spinner7 = p11.spinner();
3285
+ const spinner7 = p12.spinner();
3128
3286
  spinner7.start(`Fetching translations for ${highlight(branch)}\u2026`);
3129
3287
  try {
3130
3288
  const projectConfig = await api.getAppConfig();
3131
3289
  const targetLocales = options.locale ? [options.locale] : projectConfig.targetLocales;
3132
3290
  if (targetLocales.length === 0) {
3133
3291
  spinner7.stop("No target locales configured.");
3134
- p11.log.info("Add target locales with `vocoder locales add <code>`.");
3292
+ p12.log.info("Add target locales with `vocoder locales add <code>`.");
3135
3293
  return 1;
3136
3294
  }
3137
3295
  const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
3138
3296
  spinner7.stop(`Fetched translations for ${highlight(branch)}`);
3139
3297
  if (snapshot.status === "NOT_FOUND") {
3140
- p11.log.warn(
3298
+ p12.log.warn(
3141
3299
  `No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
3142
3300
  );
3143
3301
  return 1;
@@ -3152,7 +3310,7 @@ async function getTranslations(options = {}) {
3152
3310
  return 0;
3153
3311
  } catch (error) {
3154
3312
  spinner7.stop("Failed to fetch translations.");
3155
- p11.log.error(
3313
+ p12.log.error(
3156
3314
  error instanceof Error ? error.message : "Unknown error."
3157
3315
  );
3158
3316
  return 1;
@@ -3163,19 +3321,19 @@ function writeLocaleFiles(translations, outputDir) {
3163
3321
  for (const [locale, strings] of Object.entries(translations)) {
3164
3322
  const filePath = join4(outputDir, `${locale}.json`);
3165
3323
  writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
3166
- p11.log.success(`Wrote ${highlight(filePath)}`);
3324
+ p12.log.success(`Wrote ${highlight(filePath)}`);
3167
3325
  }
3168
3326
  }
3169
3327
 
3170
3328
  // src/commands/create-app.ts
3171
- import * as p12 from "@clack/prompts";
3329
+ import * as p13 from "@clack/prompts";
3172
3330
  import chalk9 from "chalk";
3173
3331
  import { config as loadEnv6 } from "dotenv";
3174
3332
  loadEnv6();
3175
3333
  async function createApp(options) {
3176
3334
  const authData = readAuthData();
3177
3335
  if (!authData) {
3178
- p12.log.error(
3336
+ p13.log.error(
3179
3337
  "Not logged in. Run `npx @vocoder/cli init` to authenticate first."
3180
3338
  );
3181
3339
  return 1;
@@ -3194,18 +3352,18 @@ async function createApp(options) {
3194
3352
  appDir = identity.repoAppDir;
3195
3353
  }
3196
3354
  } else {
3197
- p12.log.warn(
3355
+ p13.log.warn(
3198
3356
  "Could not detect a git remote. The project will be created without repo binding \u2014 sync-on-push will not function until a repository is connected via the Vocoder dashboard."
3199
3357
  );
3200
3358
  }
3201
3359
  }
3202
3360
  const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
3203
3361
  const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
3204
- const spinner7 = p12.spinner();
3362
+ const spinner7 = p13.spinner();
3205
3363
  spinner7.start(`Creating app "${options.name}"\u2026`);
3206
3364
  try {
3207
3365
  const result = await api.createProject(authData.token, {
3208
- organizationId: options.workspace,
3366
+ organizationId: options.organization,
3209
3367
  name: options.name,
3210
3368
  sourceLocale: options.sourceLocale,
3211
3369
  targetLocales,
@@ -3224,9 +3382,9 @@ async function createApp(options) {
3224
3382
  `Add this to your .env file:`,
3225
3383
  ` ${chalk9.bold("VOCODER_API_KEY")}=${highlight(result.apiKey)}`
3226
3384
  ];
3227
- p12.note(lines.join("\n"), "Project created");
3385
+ p13.note(lines.join("\n"), "Project created");
3228
3386
  if (!result.repositoryBound && repoCanonical) {
3229
- p12.log.warn(
3387
+ p13.log.warn(
3230
3388
  `Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
3231
3389
  );
3232
3390
  }
@@ -3235,13 +3393,13 @@ async function createApp(options) {
3235
3393
  spinner7.stop("Failed to create project.");
3236
3394
  if (error instanceof VocoderAPIError && error.limitError) {
3237
3395
  const { limitError } = error;
3238
- p12.log.error(limitError.message);
3396
+ p13.log.error(limitError.message);
3239
3397
  for (const line of getLimitErrorGuidance(limitError)) {
3240
- p12.log.info(line);
3398
+ p13.log.info(line);
3241
3399
  }
3242
3400
  return 1;
3243
3401
  }
3244
- p12.log.error(
3402
+ p13.log.error(
3245
3403
  error instanceof Error ? error.message : "Unknown error."
3246
3404
  );
3247
3405
  return 1;
@@ -3249,26 +3407,26 @@ async function createApp(options) {
3249
3407
  }
3250
3408
 
3251
3409
  // src/commands/whoami.ts
3252
- import * as p13 from "@clack/prompts";
3410
+ import * as p14 from "@clack/prompts";
3253
3411
  import chalk10 from "chalk";
3254
3412
  async function whoami(options = {}) {
3255
3413
  const stored = readAuthData();
3256
3414
  if (!stored) {
3257
- p13.log.info("Not logged in. Run `vocoder init` to authenticate.");
3415
+ p14.log.info("Not logged in. Run `vocoder init` to authenticate.");
3258
3416
  return 1;
3259
3417
  }
3260
3418
  const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
3261
3419
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
3262
3420
  try {
3263
3421
  const info2 = await api.getCliUserInfo(stored.token);
3264
- p13.log.info(`Logged in as ${chalk10.bold(info2.email)}`);
3422
+ p14.log.info(`Logged in as ${chalk10.bold(info2.email)}`);
3265
3423
  if (info2.name) {
3266
- p13.log.info(`Name: ${info2.name}`);
3424
+ p14.log.info(`Name: ${info2.name}`);
3267
3425
  }
3268
- p13.log.info(`API: ${apiUrl}`);
3426
+ p14.log.info(`API: ${apiUrl}`);
3269
3427
  return 0;
3270
3428
  } catch {
3271
- p13.log.error(
3429
+ p14.log.error(
3272
3430
  "Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
3273
3431
  );
3274
3432
  return 1;
@@ -3310,7 +3468,7 @@ localesCmd.command("remove <codes...>").description("Remove one or more target l
3310
3468
  localesCmd.command("supported").description("List all locales supported by Vocoder").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listSupportedLocales, options));
3311
3469
  program.command("project").description("Show current app configuration").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(appConfig, options));
3312
3470
  program.command("translations").description("Download the current translation snapshot").option("--branch <branch>", "Git branch (auto-detected if omitted)").option("--locale <locale>", "Fetch a specific locale only").option("--output <dir>", "Write locale JSON files to this directory").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(getTranslations, options));
3313
- program.command("create-app").description("Create a new Vocoder app (requires prior `vocoder init`)").requiredOption("--name <name>", "App display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--workspace <org-id>", "Workspace organization ID").option(
3471
+ program.command("create-app").description("Create a new Vocoder app (requires prior `vocoder init`)").requiredOption("--name <name>", "App display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--organization <org-id>", "Organization ID").option(
3314
3472
  "--target-locales <codes>",
3315
3473
  "Comma-separated target locale codes (e.g. fr,de,pt-BR)"
3316
3474
  ).option(
@@ -3329,7 +3487,7 @@ program.command("create-app").description("Create a new Vocoder app (requires pr
3329
3487
  sourceLocale: options.sourceLocale,
3330
3488
  targetLocales: options.targetLocales,
3331
3489
  targetBranches: options.targetBranches,
3332
- workspace: options.workspace
3490
+ organization: options.organization
3333
3491
  };
3334
3492
  return runCommand(createApp, translated);
3335
3493
  });