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