@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 +942 -754
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-LLEMSC3X.mjs → chunk-IK4ZCESQ.mjs} +283 -423
- package/dist/chunk-IK4ZCESQ.mjs.map +1 -0
- package/dist/lib.d.mts +50 -27
- package/dist/lib.mjs +159 -2
- package/dist/lib.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-LLEMSC3X.mjs.map +0 -1
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-
|
|
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
|
|
22
|
+
import * as p6 from "@clack/prompts";
|
|
23
23
|
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
24
|
-
import { existsSync as
|
|
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((
|
|
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
|
|
90
|
+
import * as p3 from "@clack/prompts";
|
|
85
91
|
import chalk2 from "chalk";
|
|
86
92
|
|
|
87
|
-
// src/utils/
|
|
88
|
-
import {
|
|
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,
|
|
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 ?
|
|
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(
|
|
378
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
182
379
|
}
|
|
183
380
|
const trimmed = filter.trim();
|
|
184
|
-
const
|
|
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(
|
|
386
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
191
387
|
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
192
|
-
lines.push(dim(`${
|
|
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(`${
|
|
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
|
|
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(
|
|
242
|
-
${
|
|
432
|
+
const hdr = `${dim(S_BAR2)}
|
|
433
|
+
${symbol2(this.state)} ${message}
|
|
243
434
|
`;
|
|
244
|
-
const
|
|
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(
|
|
449
|
+
return `${hdr}${dim(S_BAR2)} ${summary}`;
|
|
249
450
|
}
|
|
250
451
|
case "cancel":
|
|
251
|
-
return `${hdr}${dim(
|
|
452
|
+
return `${hdr}${dim(S_BAR2)}`;
|
|
252
453
|
case "error":
|
|
253
454
|
return [
|
|
254
455
|
hdr.trimEnd(),
|
|
255
|
-
`${ylw(
|
|
256
|
-
buildList(
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
274
|
-
buildList(
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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 (
|
|
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
|
|
357
|
-
import * as
|
|
358
|
-
var
|
|
359
|
-
var
|
|
360
|
-
var
|
|
361
|
-
var
|
|
362
|
-
var
|
|
363
|
-
var
|
|
364
|
-
function
|
|
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(
|
|
554
|
+
return grn(S_SUBMIT3);
|
|
368
555
|
case "cancel":
|
|
369
|
-
return red(
|
|
556
|
+
return red(S_CANCEL3);
|
|
370
557
|
case "error":
|
|
371
|
-
return ylw(
|
|
558
|
+
return ylw(S_ERROR3);
|
|
372
559
|
default:
|
|
373
|
-
return active(
|
|
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 ?
|
|
579
|
+
const icon = isMulti ? isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
393
580
|
visibleLines.push(
|
|
394
|
-
`${info(
|
|
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
|
-
|
|
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
|
|
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(
|
|
440
|
-
${
|
|
620
|
+
const hdr = `${dim(S_BAR3)}
|
|
621
|
+
${symbol3(this.state)} ${message}
|
|
441
622
|
`;
|
|
442
|
-
const
|
|
443
|
-
|
|
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(
|
|
628
|
+
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
449
629
|
}
|
|
450
630
|
case "cancel":
|
|
451
|
-
return `${hdr}${dim(
|
|
631
|
+
return `${hdr}${dim(S_BAR3)}`;
|
|
452
632
|
case "error":
|
|
453
633
|
return [
|
|
454
634
|
hdr.trimEnd(),
|
|
455
|
-
`${ylw(
|
|
456
|
-
buildList2(
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
469
|
-
buildList2(
|
|
470
|
-
|
|
471
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
596
|
-
if (
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
641
|
-
if (
|
|
642
|
-
|
|
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 =
|
|
820
|
+
pushBranches = result2;
|
|
648
821
|
}
|
|
649
822
|
}
|
|
650
823
|
}
|
|
651
824
|
const targetBranches = pushBranches;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
731
|
-
if (
|
|
732
|
-
|
|
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 =
|
|
909
|
+
appPushBranches = result2;
|
|
736
910
|
}
|
|
737
911
|
}
|
|
738
912
|
}
|
|
739
913
|
const targetBranches = appPushBranches;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
1035
|
+
resolve3(true);
|
|
868
1036
|
});
|
|
869
1037
|
child.once("error", () => {
|
|
870
1038
|
if (settled) return;
|
|
871
1039
|
settled = true;
|
|
872
|
-
|
|
1040
|
+
resolve3(false);
|
|
873
1041
|
});
|
|
874
1042
|
setTimeout(() => {
|
|
875
1043
|
if (settled) return;
|
|
876
1044
|
settled = true;
|
|
877
|
-
|
|
1045
|
+
resolve3(false);
|
|
878
1046
|
}, 300);
|
|
879
1047
|
} catch {
|
|
880
|
-
|
|
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
|
-
|
|
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
|
|
901
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
979
|
-
if (
|
|
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
|
-
|
|
1152
|
+
p4.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
987
1153
|
}
|
|
988
1154
|
}
|
|
989
1155
|
}
|
|
990
|
-
const oauthSpinner =
|
|
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
|
-
(
|
|
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
|
-
|
|
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" ? "
|
|
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
|
|
1202
|
+
label: `Install on a new account ${chalk3.dim("(creates a new workspace)")}`
|
|
1037
1203
|
});
|
|
1038
1204
|
}
|
|
1039
|
-
const selected = await
|
|
1040
|
-
message: "
|
|
1205
|
+
const selected = await p4.select({
|
|
1206
|
+
message: "Which GitHub account should this workspace connect to?",
|
|
1041
1207
|
options
|
|
1042
1208
|
});
|
|
1043
|
-
if (
|
|
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
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
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/
|
|
1162
|
-
import * as
|
|
1317
|
+
// src/utils/organization.ts
|
|
1318
|
+
import * as p5 from "@clack/prompts";
|
|
1163
1319
|
import chalk4 from "chalk";
|
|
1164
|
-
async function
|
|
1165
|
-
const {
|
|
1166
|
-
if (
|
|
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 =
|
|
1170
|
-
value:
|
|
1171
|
-
label:
|
|
1325
|
+
const options = organizations.map((org) => ({
|
|
1326
|
+
value: org.id,
|
|
1327
|
+
label: org.name,
|
|
1172
1328
|
hint: [
|
|
1173
|
-
|
|
1174
|
-
|
|
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 (
|
|
1333
|
+
if (canCreateOrganization) {
|
|
1178
1334
|
options.push({ value: "create", label: "Create new workspace" });
|
|
1179
1335
|
}
|
|
1180
|
-
const selected = await
|
|
1336
|
+
const selected = await p5.select({
|
|
1181
1337
|
message: "Select workspace",
|
|
1182
1338
|
options
|
|
1183
1339
|
});
|
|
1184
|
-
if (
|
|
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
|
|
1191
|
-
if (!
|
|
1346
|
+
const organization = organizations.find((org) => org.id === selected);
|
|
1347
|
+
if (!organization) {
|
|
1192
1348
|
return { action: "cancelled" };
|
|
1193
1349
|
}
|
|
1194
|
-
return { action: "use",
|
|
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((
|
|
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((
|
|
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
|
-
|
|
1387
|
+
resolve3(true);
|
|
1232
1388
|
});
|
|
1233
1389
|
child.once("error", () => {
|
|
1234
1390
|
if (settled) return;
|
|
1235
1391
|
settled = true;
|
|
1236
|
-
|
|
1392
|
+
resolve3(false);
|
|
1237
1393
|
});
|
|
1238
1394
|
setTimeout(() => {
|
|
1239
1395
|
if (settled) return;
|
|
1240
1396
|
settled = true;
|
|
1241
|
-
|
|
1397
|
+
resolve3(false);
|
|
1242
1398
|
}, 300);
|
|
1243
1399
|
} catch {
|
|
1244
|
-
|
|
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
|
-
|
|
1412
|
+
p6.log.error(`You are over your plan limits.
|
|
1257
1413
|
${message}`);
|
|
1258
|
-
|
|
1414
|
+
p6.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
1259
1415
|
}
|
|
1260
1416
|
function runScaffold(params) {
|
|
1261
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
1273
|
-
const installSpinner =
|
|
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(
|
|
1449
|
+
runtimePackages.length > 0 ? buildInstallCommand(
|
|
1450
|
+
detection.packageManager,
|
|
1451
|
+
runtimePackages,
|
|
1452
|
+
false
|
|
1453
|
+
) : null
|
|
1294
1454
|
].filter(Boolean).join(" && ");
|
|
1295
|
-
|
|
1455
|
+
p6.log.warn(`Run manually: ${highlight(cmds)}`);
|
|
1296
1456
|
}
|
|
1297
1457
|
} else if (detection.ecosystem) {
|
|
1298
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
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 (!
|
|
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
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
1491
|
+
p6.log.success(chalk5.dim("Saved to .env"));
|
|
1381
1492
|
} else {
|
|
1382
|
-
|
|
1493
|
+
p6.log.message(chalk5.dim(" Add the above to your .env file"));
|
|
1383
1494
|
}
|
|
1384
1495
|
}
|
|
1385
|
-
function
|
|
1386
|
-
const
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
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
|
|
1632
|
+
const shouldOpen = await p6.confirm({
|
|
1429
1633
|
message: "Open your browser to sign in again?"
|
|
1430
1634
|
});
|
|
1431
|
-
if (
|
|
1635
|
+
if (p6.isCancel(shouldOpen)) {
|
|
1432
1636
|
server?.close();
|
|
1433
|
-
|
|
1637
|
+
p6.cancel("Setup cancelled.");
|
|
1434
1638
|
return null;
|
|
1435
1639
|
}
|
|
1436
1640
|
if (!shouldOpen) {
|
|
1437
1641
|
server?.close();
|
|
1438
|
-
|
|
1642
|
+
p6.cancel("Setup cancelled.");
|
|
1439
1643
|
return null;
|
|
1440
1644
|
} else {
|
|
1441
1645
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
1442
1646
|
if (!opened) {
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
|
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: "
|
|
1663
|
+
hint: "new user"
|
|
1460
1664
|
},
|
|
1461
|
-
{
|
|
1665
|
+
{
|
|
1666
|
+
value: "link",
|
|
1667
|
+
label: "Already installed? Link your account",
|
|
1668
|
+
hint: "returning user"
|
|
1669
|
+
}
|
|
1462
1670
|
]
|
|
1463
1671
|
});
|
|
1464
|
-
if (
|
|
1672
|
+
if (p6.isCancel(connectChoice)) {
|
|
1465
1673
|
server?.close();
|
|
1466
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1767
|
+
p6.log.error(winner.result.reason);
|
|
1560
1768
|
return null;
|
|
1561
1769
|
}
|
|
1562
1770
|
}
|
|
1563
1771
|
if (!rawToken) {
|
|
1564
1772
|
authSpinner.stop();
|
|
1565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1802
|
+
lookup = await anonApi.lookupAppByRepo({
|
|
1594
1803
|
repoCanonical: identity.repoCanonical,
|
|
1595
|
-
appDir:
|
|
1804
|
+
appDir: ""
|
|
1596
1805
|
});
|
|
1597
|
-
if (lookup.
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
|
1604
|
-
message: "
|
|
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 (
|
|
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 =
|
|
1616
|
-
spinner7.start("Generating
|
|
1833
|
+
const spinner7 = p6.spinner();
|
|
1834
|
+
spinner7.start("Generating API key...");
|
|
1835
|
+
let apiKey;
|
|
1617
1836
|
try {
|
|
1618
|
-
|
|
1837
|
+
({ apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
1619
1838
|
authResult.token,
|
|
1620
|
-
|
|
1621
|
-
);
|
|
1622
|
-
spinner7.stop("
|
|
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
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1880
|
+
p6.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1670
1881
|
} else if (storedAuth.status === "gone") {
|
|
1671
|
-
|
|
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
|
|
1693
|
-
let
|
|
1903
|
+
let selectedOrganizationId;
|
|
1904
|
+
let selectedOrganizationName;
|
|
1905
|
+
const repoOrgContext = identity ? lookup?.organizationContext ?? null : null;
|
|
1694
1906
|
if (authOrganizationId) {
|
|
1695
|
-
const
|
|
1696
|
-
const ws =
|
|
1907
|
+
const organizationData = await api.listOrganizations(userToken);
|
|
1908
|
+
const ws = organizationData.organizations.find(
|
|
1697
1909
|
(w) => w.id === authOrganizationId
|
|
1698
1910
|
);
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
`Connected as ${chalk5.bold(userEmail)} \u2014 workspace: ${chalk5.bold(
|
|
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
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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 ?
|
|
1760
|
-
const connected =
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
|
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.
|
|
1940
|
+
label: `${w.name} ${chalk5.dim(`(${w.appCount} app${w.appCount !== 1 ? "s" : ""})`)}`
|
|
1774
1941
|
}))
|
|
1775
1942
|
});
|
|
1776
|
-
if (
|
|
1777
|
-
|
|
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
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
-
|
|
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
|
|
1971
|
+
const fix = await p6.select({
|
|
1805
1972
|
message: "How would you like to fix this?",
|
|
1806
1973
|
options: fixOptions
|
|
1807
1974
|
});
|
|
1808
|
-
if (
|
|
1809
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1999
|
+
selectedOrganizationId = connectResult.organizationId;
|
|
2000
|
+
selectedOrganizationName = connectResult.organizationName;
|
|
2001
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1835
2002
|
} else {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
|
1843
|
-
if (
|
|
1844
|
-
|
|
2061
|
+
const organizationResult = await selectOrganization(organizationData);
|
|
2062
|
+
if (organizationResult.action === "cancelled") {
|
|
2063
|
+
p6.cancel("Setup cancelled.");
|
|
1845
2064
|
return 1;
|
|
1846
2065
|
}
|
|
1847
|
-
if (
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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
|
|
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 (
|
|
1860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
`Workspace: ${chalk5.bold(
|
|
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
|
-
|
|
2109
|
+
p6.log.warn(
|
|
1889
2110
|
"No GitHub installations found. Install the Vocoder GitHub App first."
|
|
1890
2111
|
);
|
|
1891
|
-
const installNow = await
|
|
2112
|
+
const installNow = await p6.confirm({
|
|
1892
2113
|
message: "Open GitHub to install the App?"
|
|
1893
2114
|
});
|
|
1894
|
-
if (
|
|
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
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
1936
|
-
|
|
2156
|
+
selectedOrganizationId = claimResult.organizationId;
|
|
2157
|
+
selectedOrganizationName = claimResult.organizationName;
|
|
1937
2158
|
}
|
|
1938
2159
|
}
|
|
1939
|
-
|
|
1940
|
-
`Workspace: ${chalk5.bold(
|
|
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:
|
|
2175
|
+
organizationName: selectedOrganizationName,
|
|
1959
2176
|
repoCanonical: identity?.repoCanonical,
|
|
1960
|
-
defaultAppDir: identity?.repoAppDir,
|
|
1961
2177
|
existingApps: existingAppsForRepo
|
|
1962
2178
|
});
|
|
1963
2179
|
if (!appResult) {
|
|
1964
|
-
|
|
2180
|
+
p6.log.error("App setup failed. Run `vocoder init` again.");
|
|
1965
2181
|
return 1;
|
|
1966
2182
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
appDir:
|
|
1971
|
-
|
|
1972
|
-
|
|
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.
|
|
1977
|
-
const ws = wsCheck.
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
)
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
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
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
2098
|
-
import { existsSync as
|
|
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
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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:
|
|
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 (!
|
|
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
|
|
2462
|
-
if (!
|
|
2463
|
-
|
|
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
|
|
2719
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2540
2720
|
for (const str of extractedStrings) {
|
|
2541
|
-
const existing =
|
|
2721
|
+
const existing = byKey.get(str.key);
|
|
2542
2722
|
if (!existing) {
|
|
2543
|
-
|
|
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(
|
|
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
|
-
|
|
2760
|
+
p8.intro(chalk6.bold("Vocoder Sync"));
|
|
2584
2761
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
2585
2762
|
if (!mergedConfig.apiKey) {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
2769
|
+
p8.outro("Run `npx @vocoder/cli init` to set up your project.");
|
|
2593
2770
|
return 1;
|
|
2594
2771
|
}
|
|
2595
|
-
const spinner7 =
|
|
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 ? {
|
|
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
|
-
|
|
2801
|
+
p8.log.warn(
|
|
2625
2802
|
`Skipping translations (${highlight(branch)} is not a target branch)`
|
|
2626
2803
|
);
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
-
|
|
2819
|
+
p8.log.warn(
|
|
2643
2820
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
2644
2821
|
);
|
|
2645
|
-
|
|
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
|
-
|
|
2833
|
+
p8.note(sampleLines.join("\n"), "Sample strings");
|
|
2657
2834
|
}
|
|
2658
2835
|
if (options.dryRun) {
|
|
2659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2684
|
-
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique
|
|
2859
|
+
p8.log.info(
|
|
2860
|
+
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique strings`
|
|
2685
2861
|
);
|
|
2686
2862
|
}
|
|
2687
|
-
const
|
|
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 (
|
|
2867
|
+
if (existsSync4(cacheFile)) {
|
|
2691
2868
|
if (options.verbose) {
|
|
2692
|
-
|
|
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
|
-
|
|
2872
|
+
p8.outro(`Up to date (${duration2}s)`);
|
|
2696
2873
|
return 0;
|
|
2697
2874
|
}
|
|
2698
2875
|
if (options.verbose) {
|
|
2699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
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
|
-
|
|
2905
|
+
p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
2730
2906
|
}
|
|
2731
2907
|
}
|
|
2732
2908
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
2733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3041
|
+
p8.log.error(error.syncPolicyError.message);
|
|
2867
3042
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
2868
3043
|
for (const line of guidance) {
|
|
2869
|
-
|
|
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
|
-
|
|
3050
|
+
p8.log.error(limitError.message);
|
|
2876
3051
|
const guidance = getLimitErrorGuidance(limitError);
|
|
2877
3052
|
for (const line of guidance) {
|
|
2878
|
-
|
|
3053
|
+
p8.log.info(line);
|
|
2879
3054
|
}
|
|
2880
3055
|
return 1;
|
|
2881
3056
|
}
|
|
2882
3057
|
if (error instanceof Error) {
|
|
2883
|
-
|
|
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
|
-
|
|
3061
|
+
p8.log.warn(
|
|
2887
3062
|
"API key rejected \u2014 the project may have been deleted or the key revoked."
|
|
2888
3063
|
);
|
|
2889
|
-
|
|
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
|
-
|
|
2894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3111
|
+
p9.log.info(
|
|
2926
3112
|
`Source locale: ${highlight(projectConfig.sourceLocale)}`
|
|
2927
3113
|
);
|
|
2928
3114
|
if (projectConfig.targetLocales.length === 0) {
|
|
2929
|
-
|
|
3115
|
+
p9.log.info("Target locales: (none configured)");
|
|
2930
3116
|
} else {
|
|
2931
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3152
|
+
p9.log.error(limitError.message);
|
|
2966
3153
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
2967
|
-
|
|
3154
|
+
p9.log.info(line);
|
|
2968
3155
|
}
|
|
2969
3156
|
break;
|
|
2970
3157
|
}
|
|
2971
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3197
|
+
p9.log.info(
|
|
3010
3198
|
`Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
|
|
3011
3199
|
);
|
|
3012
3200
|
} else if (!hadError) {
|
|
3013
|
-
|
|
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
|
-
|
|
3211
|
+
p9.log.info(chalk7.bold("Source locales:"));
|
|
3024
3212
|
printLocaleTable(result.sourceLocales);
|
|
3025
|
-
|
|
3026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3232
|
+
import * as p10 from "@clack/prompts";
|
|
3045
3233
|
async function logout(options = {}) {
|
|
3046
3234
|
const stored = readAuthData();
|
|
3047
3235
|
if (!stored) {
|
|
3048
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3280
|
+
p11.note(lines.join("\n"), `${config.projectName} \u2014 app config`);
|
|
3093
3281
|
return 0;
|
|
3094
3282
|
} catch (error) {
|
|
3095
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3354
|
+
p12.log.success(`Wrote ${highlight(filePath)}`);
|
|
3167
3355
|
}
|
|
3168
3356
|
}
|
|
3169
3357
|
|
|
3170
3358
|
// src/commands/create-app.ts
|
|
3171
|
-
import * as
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
3415
|
+
p13.note(lines.join("\n"), "Project created");
|
|
3228
3416
|
if (!result.repositoryBound && repoCanonical) {
|
|
3229
|
-
|
|
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
|
-
|
|
3426
|
+
p13.log.error(limitError.message);
|
|
3239
3427
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3240
|
-
|
|
3428
|
+
p13.log.info(line);
|
|
3241
3429
|
}
|
|
3242
3430
|
return 1;
|
|
3243
3431
|
}
|
|
3244
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3452
|
+
p14.log.info(`Logged in as ${chalk10.bold(info2.email)}`);
|
|
3265
3453
|
if (info2.name) {
|
|
3266
|
-
|
|
3454
|
+
p14.log.info(`Name: ${info2.name}`);
|
|
3267
3455
|
}
|
|
3268
|
-
|
|
3456
|
+
p14.log.info(`API: ${apiUrl}`);
|
|
3269
3457
|
return 0;
|
|
3270
3458
|
} catch {
|
|
3271
|
-
|
|
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("--
|
|
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
|
-
|
|
3520
|
+
organization: options.organization
|
|
3333
3521
|
};
|
|
3334
3522
|
return runCommand(createApp, translated);
|
|
3335
3523
|
});
|