@vocoder/cli 0.15.0 → 0.16.1
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 +1996 -1980
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-62KCB6C6.mjs → chunk-2JERZ6DL.mjs} +79 -237
- package/dist/chunk-2JERZ6DL.mjs.map +1 -0
- package/dist/lib.d.mts +7 -7
- package/dist/lib.mjs +159 -2
- package/dist/lib.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-62KCB6C6.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -9,1047 +9,641 @@ import {
|
|
|
9
9
|
computeFingerprint,
|
|
10
10
|
detectLocalEcosystem,
|
|
11
11
|
getPackagesToInstall,
|
|
12
|
-
getSetupSnippets,
|
|
13
12
|
loadVocoderConfig,
|
|
14
13
|
readAuthData,
|
|
15
14
|
verifyStoredAuth,
|
|
16
15
|
writeAuthData
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-2JERZ6DL.mjs";
|
|
18
17
|
|
|
19
18
|
// src/bin.ts
|
|
20
19
|
import { Command } from "commander";
|
|
21
20
|
|
|
22
21
|
// src/commands/init.ts
|
|
23
|
-
import * as
|
|
24
|
-
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
25
|
-
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
26
|
-
|
|
27
|
-
// src/utils/write-config.ts
|
|
28
|
-
import { existsSync, writeFileSync } from "fs";
|
|
29
|
-
import { join } from "path";
|
|
30
|
-
function findExistingConfig(cwd = process.cwd()) {
|
|
31
|
-
for (const name of [
|
|
32
|
-
"vocoder.config.ts",
|
|
33
|
-
"vocoder.config.js",
|
|
34
|
-
"vocoder.config.json"
|
|
35
|
-
]) {
|
|
36
|
-
const candidate = join(cwd, name);
|
|
37
|
-
if (existsSync(candidate)) return candidate;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
function writeVocoderConfig(options) {
|
|
42
|
-
const {
|
|
43
|
-
targetBranches = ["main"],
|
|
44
|
-
useTypeScript = true,
|
|
45
|
-
cwd = process.cwd(),
|
|
46
|
-
appId
|
|
47
|
-
} = options;
|
|
48
|
-
if (findExistingConfig(cwd)) return null;
|
|
49
|
-
const ext = useTypeScript ? "ts" : "js";
|
|
50
|
-
const configPath = join(cwd, `vocoder.config.${ext}`);
|
|
51
|
-
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
52
|
-
const includes = ["**/*.{tsx,jsx,ts,js}"];
|
|
53
|
-
const includesStr = includes.map((p15) => `'${p15}'`).join(", ");
|
|
54
|
-
const appIdLine = appId ? ` appId: '${appId}',
|
|
55
|
-
` : "";
|
|
56
|
-
const content = `import { defineConfig } from '@vocoder/config'
|
|
57
|
-
|
|
58
|
-
export default defineConfig({
|
|
59
|
-
${appIdLine} targetBranches: [${branchesStr}],
|
|
60
|
-
include: [${includesStr}],
|
|
61
|
-
})
|
|
62
|
-
`;
|
|
63
|
-
try {
|
|
64
|
-
writeFileSync(configPath, content, "utf-8");
|
|
65
|
-
return `vocoder.config.${ext}`;
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
22
|
+
import * as p12 from "@clack/prompts";
|
|
70
23
|
|
|
71
|
-
// src/utils/
|
|
24
|
+
// src/utils/auth-flow.ts
|
|
25
|
+
import * as p from "@clack/prompts";
|
|
72
26
|
import chalk from "chalk";
|
|
73
|
-
var ORANGE = "#FC5206";
|
|
74
|
-
var PINK = "#D51977";
|
|
75
|
-
var BLUE = "#2450A9";
|
|
76
|
-
var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
|
|
77
|
-
var hex = (color) => (s) => noColor ? s : chalk.hex(color)(s);
|
|
78
|
-
var dim = (s) => noColor ? s : chalk.dim(s);
|
|
79
|
-
var bld = (s) => noColor ? s : chalk.bold(s);
|
|
80
|
-
var grn = (s) => noColor ? s : chalk.green(s);
|
|
81
|
-
var ylw = (s) => noColor ? s : chalk.yellow(s);
|
|
82
|
-
var red = (s) => noColor ? s : chalk.red(s);
|
|
83
|
-
var highlight = hex(PINK);
|
|
84
|
-
var info = hex(BLUE);
|
|
85
|
-
var active = hex(ORANGE);
|
|
86
|
-
|
|
87
|
-
// src/commands/init.ts
|
|
88
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
89
|
-
|
|
90
|
-
// src/utils/project-create.ts
|
|
91
|
-
import * as p3 from "@clack/prompts";
|
|
92
|
-
import chalk2 from "chalk";
|
|
93
27
|
|
|
94
|
-
// src/utils/
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
var S_BAR = "\u2502";
|
|
100
|
-
var S_BAR_END = "\u2514";
|
|
101
|
-
var S_ACTIVE = "\u25C6";
|
|
102
|
-
var S_SUBMIT = "\u25C6";
|
|
103
|
-
var S_CANCEL = "\u25A0";
|
|
104
|
-
var S_ERROR = "\u25B2";
|
|
105
|
-
function symbol(state) {
|
|
106
|
-
switch (state) {
|
|
107
|
-
case "submit":
|
|
108
|
-
return grn(S_SUBMIT);
|
|
109
|
-
case "cancel":
|
|
110
|
-
return red(S_CANCEL);
|
|
111
|
-
case "error":
|
|
112
|
-
return ylw(S_ERROR);
|
|
113
|
-
default:
|
|
114
|
-
return active(S_ACTIVE);
|
|
28
|
+
// src/utils/browser.ts
|
|
29
|
+
import { spawn } from "child_process";
|
|
30
|
+
async function tryOpenBrowser(url) {
|
|
31
|
+
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
32
|
+
return false;
|
|
115
33
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
);
|
|
128
|
-
if (nested) return `"${val}" overlaps with already-added "${nested}"`;
|
|
129
|
-
if (val !== "") {
|
|
130
|
-
const abs = resolve(opts.cwd ?? process.cwd(), val);
|
|
131
|
-
if (!existsSync2(abs)) return `Directory not found: ${val}`;
|
|
132
|
-
if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
|
|
34
|
+
let command;
|
|
35
|
+
let args;
|
|
36
|
+
if (process.platform === "darwin") {
|
|
37
|
+
command = "open";
|
|
38
|
+
args = [url];
|
|
39
|
+
} else if (process.platform === "win32") {
|
|
40
|
+
command = "rundll32";
|
|
41
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
42
|
+
} else {
|
|
43
|
+
command = "xdg-open";
|
|
44
|
+
args = [url];
|
|
133
45
|
}
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
async function collectAppDirs(opts = {}) {
|
|
137
|
-
const added = [];
|
|
138
|
-
let filter = "";
|
|
139
|
-
let cursor = 0;
|
|
140
|
-
let addCursor = false;
|
|
141
|
-
const isNewDir = () => {
|
|
142
|
-
const t = filter.trim();
|
|
143
|
-
return t.length > 0 && !added.includes(t);
|
|
144
|
-
};
|
|
145
|
-
const clampCursor = () => {
|
|
146
|
-
const max = added.length - 1;
|
|
147
|
-
if (cursor > max) cursor = Math.max(0, max);
|
|
148
|
-
};
|
|
149
|
-
const prompt = new Prompt(
|
|
150
|
-
{
|
|
151
|
-
validate() {
|
|
152
|
-
return void 0;
|
|
153
|
-
},
|
|
154
|
-
render() {
|
|
155
|
-
const trimmed = filter.trim();
|
|
156
|
-
const hdr = `${dim(S_BAR)}
|
|
157
|
-
${symbol(this.state)} App directories
|
|
158
|
-
`;
|
|
159
|
-
switch (this.state) {
|
|
160
|
-
case "submit": {
|
|
161
|
-
const summary = added.length > 0 ? bld(added.join(", ")) : dim("none (single-app project)");
|
|
162
|
-
return `${hdr}${dim(S_BAR)} ${summary}`;
|
|
163
|
-
}
|
|
164
|
-
case "cancel":
|
|
165
|
-
return `${hdr}${dim(S_BAR)}`;
|
|
166
|
-
default: {
|
|
167
|
-
const inputHint = filter.length > 0 ? filter : added.length === 0 ? dim("e.g. apps/web") : dim("e.g. apps/api");
|
|
168
|
-
const lines = [
|
|
169
|
-
hdr.trimEnd(),
|
|
170
|
-
`${info(S_BAR)} ${dim("/")} ${inputHint}`,
|
|
171
|
-
info(S_BAR)
|
|
172
|
-
];
|
|
173
|
-
for (let i = 0; i < added.length; i++) {
|
|
174
|
-
const isCursor = i === cursor && !addCursor;
|
|
175
|
-
const icon = isCursor ? active("\u25FC") : info("\u25FC");
|
|
176
|
-
const label = isCursor ? bld(added[i]) : added[i];
|
|
177
|
-
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
178
|
-
}
|
|
179
|
-
const atLimit = opts.maxDirs !== void 0 && added.length >= opts.maxDirs;
|
|
180
|
-
if (atLimit) {
|
|
181
|
-
lines.push(`${info(S_BAR)} ${dim(`App limit reached (${added.length}/${opts.maxDirs} on your plan)`)}`);
|
|
182
|
-
} else if (isNewDir()) {
|
|
183
|
-
const err = validateAppDirPath(trimmed, added, opts);
|
|
184
|
-
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
185
|
-
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}"`;
|
|
186
|
-
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
187
|
-
}
|
|
188
|
-
lines.push(info(S_BAR));
|
|
189
|
-
if (atLimit) {
|
|
190
|
-
lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
191
|
-
} else if (added.length === 0 && !isNewDir()) {
|
|
192
|
-
lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
|
|
193
|
-
lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
|
|
194
|
-
} else if (added.length > 0) {
|
|
195
|
-
lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
196
|
-
}
|
|
197
|
-
const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
|
|
198
|
-
if (this.state === "error") {
|
|
199
|
-
lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
|
|
200
|
-
} else {
|
|
201
|
-
lines.push(barEnd);
|
|
202
|
-
}
|
|
203
|
-
lines.push("");
|
|
204
|
-
return lines.join("\n");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
false
|
|
210
|
-
);
|
|
211
|
-
prompt.on("key", (key) => {
|
|
212
|
-
if (!key || key === " ") return;
|
|
213
|
-
const cp = key.codePointAt(0) ?? 0;
|
|
214
|
-
if (cp === 127 || cp === 8) {
|
|
215
|
-
filter = filter.slice(0, -1);
|
|
216
|
-
addCursor = false;
|
|
217
|
-
} else if (cp >= 32 && cp !== 127) {
|
|
218
|
-
filter += key;
|
|
219
|
-
cursor = 0;
|
|
220
|
-
addCursor = false;
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
prompt.on("cursor", (action) => {
|
|
224
|
-
switch (action) {
|
|
225
|
-
case "up":
|
|
226
|
-
if (addCursor) {
|
|
227
|
-
addCursor = false;
|
|
228
|
-
cursor = Math.max(0, added.length - 1);
|
|
229
|
-
} else {
|
|
230
|
-
cursor = Math.max(0, cursor - 1);
|
|
231
|
-
}
|
|
232
|
-
break;
|
|
233
|
-
case "down":
|
|
234
|
-
if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
|
|
235
|
-
addCursor = true;
|
|
236
|
-
} else if (!addCursor) {
|
|
237
|
-
cursor = Math.min(added.length - 1, cursor + 1);
|
|
238
|
-
}
|
|
239
|
-
break;
|
|
240
|
-
case "space": {
|
|
241
|
-
if (addCursor || filter.trim().length > 0 && isNewDir()) {
|
|
242
|
-
if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
|
|
243
|
-
const trimmed = filter.trim();
|
|
244
|
-
const err = validateAppDirPath(trimmed, added, opts);
|
|
245
|
-
if (!err) {
|
|
246
|
-
added.push(trimmed);
|
|
247
|
-
filter = "";
|
|
248
|
-
addCursor = false;
|
|
249
|
-
cursor = 0;
|
|
250
|
-
}
|
|
251
|
-
} else if (added.length > 0 && !isNewDir()) {
|
|
252
|
-
clampCursor();
|
|
253
|
-
added.splice(cursor, 1);
|
|
254
|
-
if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
|
|
255
|
-
}
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
prompt.on("finalize", () => {
|
|
261
|
-
if (prompt.state === "submit") {
|
|
262
|
-
prompt.value = [...added];
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
const result = await prompt.prompt();
|
|
266
|
-
if (isCancel(result)) return null;
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
async function promptSingleAppDir(params) {
|
|
270
|
-
const { existingDirs, cwd } = params;
|
|
271
|
-
const input = await p.text({
|
|
272
|
-
message: "App directory to add",
|
|
273
|
-
placeholder: "apps/web",
|
|
274
|
-
validate(val) {
|
|
275
|
-
const err = validateAppDirPath(val ?? "", existingDirs, { cwd });
|
|
276
|
-
if (err) return err;
|
|
277
|
-
if (!val) return "Directory is required";
|
|
278
|
-
return void 0;
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
if (p.isCancel(input)) return null;
|
|
282
|
-
return input;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// src/utils/branch-select.ts
|
|
286
|
-
import { execSync } from "child_process";
|
|
287
|
-
import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
|
|
288
|
-
var S_BAR2 = "\u2502";
|
|
289
|
-
var S_BAR_END2 = "\u2514";
|
|
290
|
-
var S_ACTIVE2 = "\u25C6";
|
|
291
|
-
var S_SUBMIT2 = "\u25C6";
|
|
292
|
-
var S_CANCEL2 = "\u25A0";
|
|
293
|
-
var S_ERROR2 = "\u25B2";
|
|
294
|
-
function symbol2(state) {
|
|
295
|
-
switch (state) {
|
|
296
|
-
case "submit":
|
|
297
|
-
return grn(S_SUBMIT2);
|
|
298
|
-
case "cancel":
|
|
299
|
-
return red(S_CANCEL2);
|
|
300
|
-
case "error":
|
|
301
|
-
return ylw(S_ERROR2);
|
|
302
|
-
default:
|
|
303
|
-
return active(S_ACTIVE2);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function detectGitBranches(cwd) {
|
|
307
|
-
const workDir = cwd ?? process.cwd();
|
|
308
|
-
try {
|
|
309
|
-
const localOut = execSync("git branch", {
|
|
310
|
-
cwd: workDir,
|
|
311
|
-
stdio: "pipe"
|
|
312
|
-
}).toString();
|
|
313
|
-
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
314
|
-
let remoteBranches = [];
|
|
315
|
-
try {
|
|
316
|
-
const remoteOut = execSync("git branch -r", {
|
|
317
|
-
cwd: workDir,
|
|
318
|
-
stdio: "pipe"
|
|
319
|
-
}).toString();
|
|
320
|
-
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
321
|
-
} catch {
|
|
322
|
-
}
|
|
323
|
-
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
324
|
-
let defaultBranch = "main";
|
|
46
|
+
return new Promise((resolve3) => {
|
|
325
47
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
stdio: "
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
var MAX_VISIBLE = 10;
|
|
352
|
-
function buildItems(branches, defaultBranch, customPatterns) {
|
|
353
|
-
const items = branches.map((b) => ({
|
|
354
|
-
value: b,
|
|
355
|
-
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
356
|
-
}));
|
|
357
|
-
for (const pt of customPatterns) {
|
|
358
|
-
if (!branches.includes(pt)) {
|
|
359
|
-
items.push({ value: pt, label: pt, isCustom: true });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return items;
|
|
363
|
-
}
|
|
364
|
-
function filterItems(items, query) {
|
|
365
|
-
if (!query.trim()) return items;
|
|
366
|
-
const lower = query.toLowerCase();
|
|
367
|
-
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
368
|
-
}
|
|
369
|
-
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
370
|
-
const lines = [info(S_BAR2)];
|
|
371
|
-
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
372
|
-
for (let i = scrollOffset; i < end; i++) {
|
|
373
|
-
const item = filtered[i];
|
|
374
|
-
const isCursor = i === cursor && !addCursor;
|
|
375
|
-
const isChecked = selected.has(item.value);
|
|
376
|
-
const icon = isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
|
|
377
|
-
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
378
|
-
if (isCursor) label = bld(label);
|
|
379
|
-
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
380
|
-
}
|
|
381
|
-
const trimmed = filter.trim();
|
|
382
|
-
const isNewPattern = trimmed.length > 0 && !filtered.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
383
|
-
if (isNewPattern) {
|
|
384
|
-
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
385
|
-
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
386
|
-
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
387
|
-
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
388
|
-
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
389
|
-
lines.push(dim(`${S_BAR2} No branches detected`));
|
|
390
|
-
}
|
|
391
|
-
const hidden = filtered.length - (end - scrollOffset);
|
|
392
|
-
if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more`));
|
|
393
|
-
return lines.join("\n");
|
|
394
|
-
}
|
|
395
|
-
async function filterableBranchSelect(params) {
|
|
396
|
-
const { message, branches, defaultBranch } = params;
|
|
397
|
-
const optional = params.optional ?? false;
|
|
398
|
-
const excludedSet = new Set(params.excludedPatterns ?? []);
|
|
399
|
-
let filter = "";
|
|
400
|
-
let cursor = 0;
|
|
401
|
-
let scrollOffset = 0;
|
|
402
|
-
let addCursor = false;
|
|
403
|
-
const customPatterns = [];
|
|
404
|
-
const selected = new Set(params.initialValues ?? [defaultBranch]);
|
|
405
|
-
const getItems = () => buildItems(branches, defaultBranch, customPatterns);
|
|
406
|
-
const getFiltered = () => filterItems(getItems(), filter);
|
|
407
|
-
const isNewPattern = () => {
|
|
408
|
-
const t = filter.trim();
|
|
409
|
-
if (!t) return false;
|
|
410
|
-
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
411
|
-
};
|
|
412
|
-
const clampCursor = (filtered) => {
|
|
413
|
-
const hasAdd = isNewPattern();
|
|
414
|
-
const max = filtered.length - 1 + (hasAdd ? 1 : 0);
|
|
415
|
-
if (cursor > max && !addCursor) cursor = Math.max(0, max);
|
|
416
|
-
if (!addCursor) {
|
|
417
|
-
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
418
|
-
if (cursor >= scrollOffset + MAX_VISIBLE)
|
|
419
|
-
scrollOffset = cursor - MAX_VISIBLE + 1;
|
|
420
|
-
if (scrollOffset < 0) scrollOffset = 0;
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
const prompt = new Prompt2(
|
|
424
|
-
{
|
|
425
|
-
validate() {
|
|
426
|
-
if (!optional && selected.size === 0)
|
|
427
|
-
return "At least one branch is required.";
|
|
428
|
-
return void 0;
|
|
429
|
-
},
|
|
430
|
-
render() {
|
|
431
|
-
const filtered = getFiltered();
|
|
432
|
-
clampCursor(filtered);
|
|
433
|
-
const hdr = `${dim(S_BAR2)}
|
|
434
|
-
${symbol2(this.state)} ${message}
|
|
435
|
-
`;
|
|
436
|
-
const inputHint = filter.length > 0 ? filter : dim("type to filter \xB7 type a custom pattern to add it");
|
|
437
|
-
const footer = selected.size > 0 ? dim(`${S_BAR2} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : optional ? dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to skip`) : dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`);
|
|
438
|
-
switch (this.state) {
|
|
439
|
-
case "submit": {
|
|
440
|
-
const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
|
|
441
|
-
return `${hdr}${dim(S_BAR2)} ${summary}`;
|
|
442
|
-
}
|
|
443
|
-
case "cancel":
|
|
444
|
-
return `${hdr}${dim(S_BAR2)}`;
|
|
445
|
-
case "error":
|
|
446
|
-
return [
|
|
447
|
-
hdr.trimEnd(),
|
|
448
|
-
`${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
449
|
-
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
450
|
-
footer,
|
|
451
|
-
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
452
|
-
""
|
|
453
|
-
].join("\n");
|
|
454
|
-
default:
|
|
455
|
-
return [
|
|
456
|
-
hdr.trimEnd(),
|
|
457
|
-
`${info(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
458
|
-
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
459
|
-
footer,
|
|
460
|
-
`${info(S_BAR_END2)}`,
|
|
461
|
-
""
|
|
462
|
-
].join("\n");
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
false
|
|
467
|
-
);
|
|
468
|
-
prompt.on("key", (key) => {
|
|
469
|
-
if (!key || key === " ") return;
|
|
470
|
-
const cp = key.codePointAt(0) ?? 0;
|
|
471
|
-
if (cp === 127 || cp === 8) {
|
|
472
|
-
filter = filter.slice(0, -1);
|
|
473
|
-
cursor = 0;
|
|
474
|
-
scrollOffset = 0;
|
|
475
|
-
addCursor = false;
|
|
476
|
-
} else if (cp >= 32 && cp !== 127) {
|
|
477
|
-
filter += key;
|
|
478
|
-
cursor = 0;
|
|
479
|
-
scrollOffset = 0;
|
|
480
|
-
addCursor = false;
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
prompt.on("cursor", (action) => {
|
|
484
|
-
const filtered = getFiltered();
|
|
485
|
-
const hasAdd = isNewPattern();
|
|
486
|
-
switch (action) {
|
|
487
|
-
case "up":
|
|
488
|
-
if (addCursor) {
|
|
489
|
-
addCursor = false;
|
|
490
|
-
cursor = Math.max(0, filtered.length - 1);
|
|
491
|
-
} else cursor = Math.max(0, cursor - 1);
|
|
492
|
-
break;
|
|
493
|
-
case "down":
|
|
494
|
-
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
495
|
-
addCursor = true;
|
|
496
|
-
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
497
|
-
break;
|
|
498
|
-
case "space":
|
|
499
|
-
if (addCursor) {
|
|
500
|
-
const t = filter.trim();
|
|
501
|
-
const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
|
|
502
|
-
if (!err) {
|
|
503
|
-
customPatterns.push(t);
|
|
504
|
-
selected.add(t);
|
|
505
|
-
filter = "";
|
|
506
|
-
cursor = 0;
|
|
507
|
-
scrollOffset = 0;
|
|
508
|
-
addCursor = false;
|
|
509
|
-
}
|
|
510
|
-
} else {
|
|
511
|
-
const item = filtered[cursor];
|
|
512
|
-
if (item) {
|
|
513
|
-
if (selected.has(item.value)) selected.delete(item.value);
|
|
514
|
-
else selected.add(item.value);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
prompt.on("finalize", () => {
|
|
521
|
-
if (prompt.state === "submit") {
|
|
522
|
-
prompt.value = Array.from(selected);
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
const result = await prompt.prompt();
|
|
526
|
-
if (isCancel3(result)) return null;
|
|
527
|
-
return result;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/utils/locale-search.ts
|
|
531
|
-
import { isCancel as isCancel4, Prompt as Prompt3 } from "@clack/core";
|
|
532
|
-
import * as p2 from "@clack/prompts";
|
|
533
|
-
var S_BAR3 = "\u2502";
|
|
534
|
-
var S_BAR_END3 = "\u2514";
|
|
535
|
-
var S_ACTIVE3 = "\u25C6";
|
|
536
|
-
var S_SUBMIT3 = "\u25C6";
|
|
537
|
-
var S_CANCEL3 = "\u25A0";
|
|
538
|
-
var S_ERROR3 = "\u25B2";
|
|
539
|
-
function symbol3(state) {
|
|
540
|
-
switch (state) {
|
|
541
|
-
case "submit":
|
|
542
|
-
return grn(S_SUBMIT3);
|
|
543
|
-
case "cancel":
|
|
544
|
-
return red(S_CANCEL3);
|
|
545
|
-
case "error":
|
|
546
|
-
return ylw(S_ERROR3);
|
|
547
|
-
default:
|
|
548
|
-
return active(S_ACTIVE3);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
var MAX_VISIBLE2 = 12;
|
|
552
|
-
function filterLocales(options, query) {
|
|
553
|
-
if (!query.trim()) return options;
|
|
554
|
-
const lower = query.toLowerCase();
|
|
555
|
-
return options.filter(
|
|
556
|
-
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
560
|
-
const isMulti = selected !== null;
|
|
561
|
-
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
562
|
-
const visibleLines = [info(S_BAR3)];
|
|
563
|
-
for (let i = scrollOffset; i < end; i++) {
|
|
564
|
-
const opt = filtered[i];
|
|
565
|
-
const isCursor = i === cursor;
|
|
566
|
-
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
567
|
-
const icon = isMulti ? isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
568
|
-
visibleLines.push(
|
|
569
|
-
`${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
const hidden = filtered.length - (end - scrollOffset);
|
|
573
|
-
if (hidden > 0)
|
|
574
|
-
visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
|
|
575
|
-
if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
|
|
576
|
-
return visibleLines.join("\n");
|
|
577
|
-
}
|
|
578
|
-
async function runFilterablePrompt(opts) {
|
|
579
|
-
const { message, options, multi } = opts;
|
|
580
|
-
let filter = "";
|
|
581
|
-
let cursor = 0;
|
|
582
|
-
let scrollOffset = 0;
|
|
583
|
-
const selected = new Set(multi ? opts.initialValues ?? [] : []);
|
|
584
|
-
if (!multi && opts.initialValue) {
|
|
585
|
-
const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
|
|
586
|
-
if (idx >= 0) cursor = idx;
|
|
587
|
-
}
|
|
588
|
-
const getFiltered = () => filterLocales(options, filter);
|
|
589
|
-
const clampCursor = (filtered) => {
|
|
590
|
-
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
|
|
591
|
-
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
592
|
-
if (cursor >= scrollOffset + MAX_VISIBLE2)
|
|
593
|
-
scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
594
|
-
if (scrollOffset < 0) scrollOffset = 0;
|
|
595
|
-
};
|
|
596
|
-
const prompt = new Prompt3(
|
|
597
|
-
{
|
|
598
|
-
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
599
|
-
validate() {
|
|
600
|
-
const f = getFiltered();
|
|
601
|
-
if (multi && selected.size === 0)
|
|
602
|
-
return "At least one target language is required.";
|
|
603
|
-
if (!multi && !f[cursor]) return "Please select a language.";
|
|
604
|
-
return void 0;
|
|
605
|
-
},
|
|
606
|
-
render() {
|
|
607
|
-
const filtered = getFiltered();
|
|
608
|
-
clampCursor(filtered);
|
|
609
|
-
const hdr = `${dim(S_BAR3)}
|
|
610
|
-
${symbol3(this.state)} ${message}
|
|
611
|
-
`;
|
|
612
|
-
const inputHint = filter.length > 0 ? filter : dim("type to filter");
|
|
613
|
-
const footer = multi ? selected.size > 0 ? dim(`${S_BAR3} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Enter to confirm`);
|
|
614
|
-
switch (this.state) {
|
|
615
|
-
case "submit": {
|
|
616
|
-
const val = multi ? Array.from(selected).map((id) => options.find((o) => o.bcp47 === id)?.label ?? id).join(", ") : options.find((o) => o.bcp47 === this.value)?.label ?? "";
|
|
617
|
-
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
618
|
-
}
|
|
619
|
-
case "cancel":
|
|
620
|
-
return `${hdr}${dim(S_BAR3)}`;
|
|
621
|
-
case "error":
|
|
622
|
-
return [
|
|
623
|
-
hdr.trimEnd(),
|
|
624
|
-
`${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
625
|
-
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
626
|
-
footer,
|
|
627
|
-
`${ylw(S_BAR_END3)} ${ylw(this.error)}`,
|
|
628
|
-
""
|
|
629
|
-
].join("\n");
|
|
630
|
-
default:
|
|
631
|
-
return [
|
|
632
|
-
hdr.trimEnd(),
|
|
633
|
-
`${info(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
634
|
-
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
635
|
-
footer,
|
|
636
|
-
`${info(S_BAR_END3)}`,
|
|
637
|
-
""
|
|
638
|
-
].join("\n");
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
},
|
|
642
|
-
false
|
|
643
|
-
// trackValue=false — we manage value manually
|
|
644
|
-
);
|
|
645
|
-
prompt.on("key", (key) => {
|
|
646
|
-
if (!key || key === " ") return;
|
|
647
|
-
const cp = key.codePointAt(0) ?? 0;
|
|
648
|
-
if (cp === 127 || cp === 8) {
|
|
649
|
-
filter = filter.slice(0, -1);
|
|
650
|
-
cursor = 0;
|
|
651
|
-
scrollOffset = 0;
|
|
652
|
-
} else if (cp >= 32 && cp !== 127) {
|
|
653
|
-
filter += key;
|
|
654
|
-
cursor = 0;
|
|
655
|
-
scrollOffset = 0;
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
prompt.on("cursor", (action) => {
|
|
659
|
-
const filtered = getFiltered();
|
|
660
|
-
switch (action) {
|
|
661
|
-
case "up":
|
|
662
|
-
cursor = Math.max(0, cursor - 1);
|
|
663
|
-
break;
|
|
664
|
-
case "down":
|
|
665
|
-
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
666
|
-
break;
|
|
667
|
-
case "space":
|
|
668
|
-
if (multi) {
|
|
669
|
-
const opt = filtered[cursor];
|
|
670
|
-
if (opt) {
|
|
671
|
-
if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
|
|
672
|
-
else selected.add(opt.bcp47);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
if (!multi) {
|
|
678
|
-
const opt = getFiltered()[cursor];
|
|
679
|
-
prompt.value = opt?.bcp47 ?? null;
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
prompt.on("finalize", () => {
|
|
683
|
-
if (prompt.state === "submit") {
|
|
684
|
-
if (multi) {
|
|
685
|
-
prompt.value = Array.from(selected);
|
|
686
|
-
} else {
|
|
687
|
-
const f = getFiltered();
|
|
688
|
-
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
689
|
-
}
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: "ignore",
|
|
51
|
+
windowsHide: true
|
|
52
|
+
});
|
|
53
|
+
let settled = false;
|
|
54
|
+
child.once("spawn", () => {
|
|
55
|
+
if (settled) return;
|
|
56
|
+
settled = true;
|
|
57
|
+
child.unref();
|
|
58
|
+
resolve3(true);
|
|
59
|
+
});
|
|
60
|
+
child.once("error", () => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
resolve3(false);
|
|
64
|
+
});
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
if (settled) return;
|
|
67
|
+
settled = true;
|
|
68
|
+
resolve3(false);
|
|
69
|
+
}, 300);
|
|
70
|
+
} catch {
|
|
71
|
+
resolve3(false);
|
|
690
72
|
}
|
|
691
73
|
});
|
|
692
|
-
const result = await prompt.prompt();
|
|
693
|
-
if (isCancel4(result)) return null;
|
|
694
|
-
return result;
|
|
695
|
-
}
|
|
696
|
-
async function searchSelectLocale(options, message, initialValue) {
|
|
697
|
-
const result = await runFilterablePrompt({
|
|
698
|
-
message,
|
|
699
|
-
options,
|
|
700
|
-
multi: false,
|
|
701
|
-
initialValue
|
|
702
|
-
});
|
|
703
|
-
return typeof result === "string" ? result : null;
|
|
704
74
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
75
|
+
|
|
76
|
+
// src/utils/local-server.ts
|
|
77
|
+
import { createServer } from "http";
|
|
78
|
+
import { URL as URL2 } from "url";
|
|
79
|
+
function startCallbackServer() {
|
|
80
|
+
return new Promise((resolve3, reject) => {
|
|
81
|
+
let settled = false;
|
|
82
|
+
let callbackResolve = null;
|
|
83
|
+
let callbackReject = null;
|
|
84
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
85
|
+
callbackResolve = res;
|
|
86
|
+
callbackReject = rej;
|
|
87
|
+
});
|
|
88
|
+
const server = createServer((req, res) => {
|
|
89
|
+
if (!req.url) {
|
|
90
|
+
res.writeHead(400);
|
|
91
|
+
res.end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let pathname;
|
|
95
|
+
let params;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL2(req.url, "http://localhost");
|
|
98
|
+
pathname = parsed.pathname;
|
|
99
|
+
params = Object.fromEntries(parsed.searchParams.entries());
|
|
100
|
+
} catch {
|
|
101
|
+
res.writeHead(400);
|
|
102
|
+
res.end("Bad request");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (pathname !== "/callback") {
|
|
106
|
+
res.writeHead(404);
|
|
107
|
+
res.end("Not found");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
111
|
+
res.end(
|
|
112
|
+
'<!DOCTYPE html><html><head><title>Authenticated</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;"><h2>Authenticated</h2><p>Return to your terminal to continue. You can close this tab.</p></body></html>'
|
|
113
|
+
);
|
|
114
|
+
if (callbackResolve) {
|
|
115
|
+
callbackResolve(params);
|
|
116
|
+
callbackResolve = null;
|
|
117
|
+
}
|
|
118
|
+
setImmediate(() => server.close());
|
|
119
|
+
});
|
|
120
|
+
server.on("error", (err) => {
|
|
121
|
+
if (!settled) {
|
|
122
|
+
settled = true;
|
|
123
|
+
if (callbackReject) callbackReject(err);
|
|
124
|
+
reject(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.listen(0, "127.0.0.1", () => {
|
|
128
|
+
if (settled) return;
|
|
129
|
+
settled = true;
|
|
130
|
+
const port = server.address().port;
|
|
131
|
+
resolve3({
|
|
132
|
+
port,
|
|
133
|
+
waitForCallback: () => callbackPromise,
|
|
134
|
+
close: () => server.close()
|
|
135
|
+
});
|
|
136
|
+
});
|
|
711
137
|
});
|
|
712
|
-
if (result === null) return null;
|
|
713
|
-
const picks = result;
|
|
714
|
-
if (picks.length === 0) {
|
|
715
|
-
p2.log.warn(
|
|
716
|
-
"At least one target language is required. Please select at least one."
|
|
717
|
-
);
|
|
718
|
-
return searchMultiSelectLocales(options, message, initialValues);
|
|
719
|
-
}
|
|
720
|
-
return picks;
|
|
721
138
|
}
|
|
722
139
|
|
|
723
|
-
// src/utils/
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
bcp47: l.code,
|
|
727
|
-
label: `${l.name} \u2014 ${l.code}`
|
|
728
|
-
}));
|
|
140
|
+
// src/utils/auth-flow.ts
|
|
141
|
+
async function sleep(ms) {
|
|
142
|
+
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
729
143
|
}
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (!existing || l.code.length < existing.bcp47.length) {
|
|
737
|
-
byFamily.set(family, opt);
|
|
144
|
+
async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
145
|
+
let server = null;
|
|
146
|
+
if (!options.ci) {
|
|
147
|
+
try {
|
|
148
|
+
server = await startCallbackServer();
|
|
149
|
+
} catch {
|
|
738
150
|
}
|
|
739
151
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
152
|
+
const session = await api.startCliAuthSession(server?.port, repoCanonical);
|
|
153
|
+
const browserUrl = reauth ? session.verificationUrl : session.installUrl ?? session.verificationUrl;
|
|
154
|
+
const expiresAt = new Date(session.expiresAt).getTime();
|
|
155
|
+
p.log.info(browserUrl);
|
|
156
|
+
if (options.ci) {
|
|
157
|
+
process.stdout.write(`VOCODER_AUTH_URL: ${browserUrl}
|
|
158
|
+
`);
|
|
159
|
+
process.stdout.write(`VOCODER_SESSION_ID: ${session.sessionId}
|
|
160
|
+
`);
|
|
161
|
+
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
162
|
+
if (reauth) {
|
|
163
|
+
if (!options.yes) {
|
|
164
|
+
const shouldOpen = await p.confirm({
|
|
165
|
+
message: "Open your browser to sign in again?"
|
|
166
|
+
});
|
|
167
|
+
if (p.isCancel(shouldOpen)) {
|
|
168
|
+
server?.close();
|
|
169
|
+
p.cancel("Setup cancelled.");
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (!shouldOpen) {
|
|
173
|
+
server?.close();
|
|
174
|
+
p.cancel("Setup cancelled.");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const opened = await tryOpenBrowser(browserUrl);
|
|
178
|
+
if (!opened) {
|
|
179
|
+
p.note(browserUrl, "Sign In");
|
|
180
|
+
p.log.info("Open the URL above manually to continue.");
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
await tryOpenBrowser(browserUrl);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
let isLinkFlow = false;
|
|
187
|
+
if (!options.yes) {
|
|
188
|
+
const connectChoice = await p.select({
|
|
189
|
+
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
190
|
+
options: [
|
|
191
|
+
{
|
|
192
|
+
value: "install",
|
|
193
|
+
label: "Install GitHub App",
|
|
194
|
+
hint: "new user"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
value: "link",
|
|
198
|
+
label: "Already installed? Link your account",
|
|
199
|
+
hint: "returning user"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
if (p.isCancel(connectChoice)) {
|
|
204
|
+
server?.close();
|
|
205
|
+
p.cancel("Setup cancelled.");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
isLinkFlow = connectChoice === "link";
|
|
209
|
+
}
|
|
210
|
+
let urlToOpen = browserUrl;
|
|
211
|
+
if (isLinkFlow) {
|
|
212
|
+
try {
|
|
213
|
+
const linkSession = await api.startCliGitHubLinkSession(
|
|
214
|
+
session.sessionId,
|
|
215
|
+
server?.port
|
|
216
|
+
);
|
|
217
|
+
urlToOpen = linkSession.oauthUrl;
|
|
218
|
+
} catch {
|
|
219
|
+
urlToOpen = browserUrl;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const opened = await tryOpenBrowser(urlToOpen);
|
|
223
|
+
if (!opened) {
|
|
224
|
+
p.log.warn("Could not open your browser automatically.");
|
|
225
|
+
p.note(urlToOpen, "GitHub");
|
|
226
|
+
p.log.info("Open the URL above to continue.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
760
229
|
}
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
let
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
230
|
+
const authSpinner = p.spinner();
|
|
231
|
+
authSpinner.start("Waiting for GitHub authorization...");
|
|
232
|
+
let rawToken = null;
|
|
233
|
+
let callbackOrganizationId;
|
|
234
|
+
let callbackDiscoveryReady = false;
|
|
235
|
+
const deadline = Math.min(expiresAt, Date.now() + 10 * 60 * 1e3);
|
|
236
|
+
let stopPolling = false;
|
|
237
|
+
const serverCallback = server ? server.waitForCallback().catch(() => null) : Promise.resolve(null);
|
|
238
|
+
const sessionPoll = (async () => {
|
|
239
|
+
while (!stopPolling && Date.now() < expiresAt) {
|
|
240
|
+
try {
|
|
241
|
+
const result = await api.pollCliAuthSession(session.sessionId);
|
|
242
|
+
if (result.status === "complete" || result.status === "failed") {
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
if (!stopPolling) await sleep(2e3);
|
|
248
|
+
}
|
|
774
249
|
return null;
|
|
775
|
-
}
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
250
|
+
})();
|
|
251
|
+
const winner = await new Promise((resolve3) => {
|
|
252
|
+
let done = false;
|
|
253
|
+
serverCallback.then((params) => {
|
|
254
|
+
if (done || params === null || typeof params.token !== "string") return;
|
|
255
|
+
done = true;
|
|
256
|
+
resolve3({ kind: "server", params });
|
|
257
|
+
}).catch(() => {
|
|
258
|
+
});
|
|
259
|
+
sessionPoll.then((result) => {
|
|
260
|
+
if (done || result === null) return;
|
|
261
|
+
if (result.status === "complete" || result.status === "failed") {
|
|
262
|
+
done = true;
|
|
263
|
+
resolve3({
|
|
264
|
+
kind: "poll",
|
|
265
|
+
result
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}).catch(() => {
|
|
269
|
+
});
|
|
270
|
+
setTimeout(
|
|
271
|
+
() => {
|
|
272
|
+
if (!done) {
|
|
273
|
+
done = true;
|
|
274
|
+
resolve3(null);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
Math.max(0, deadline - Date.now())
|
|
788
278
|
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (result.
|
|
804
|
-
|
|
805
|
-
"At least one branch is required. Please select at least one."
|
|
806
|
-
);
|
|
807
|
-
initial = [detected.defaultBranch];
|
|
808
|
-
} else {
|
|
809
|
-
pushBranches = result;
|
|
279
|
+
});
|
|
280
|
+
stopPolling = true;
|
|
281
|
+
server?.close();
|
|
282
|
+
if (winner !== null) {
|
|
283
|
+
if (winner.kind === "server") {
|
|
284
|
+
rawToken = winner.params.token;
|
|
285
|
+
if (typeof winner.params.organizationId === "string" && winner.params.organizationId) {
|
|
286
|
+
callbackOrganizationId = winner.params.organizationId;
|
|
287
|
+
}
|
|
288
|
+
if (winner.params.discovery_ready === "1") {
|
|
289
|
+
callbackDiscoveryReady = true;
|
|
290
|
+
}
|
|
291
|
+
} else if (winner.result.status === "complete") {
|
|
292
|
+
rawToken = winner.result.token;
|
|
293
|
+
if (winner.result.organizationId) {
|
|
294
|
+
callbackOrganizationId = winner.result.organizationId;
|
|
810
295
|
}
|
|
296
|
+
} else {
|
|
297
|
+
authSpinner.stop();
|
|
298
|
+
p.log.error(winner.result.reason);
|
|
299
|
+
return null;
|
|
811
300
|
}
|
|
812
301
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
organizationId,
|
|
817
|
-
name: projectName,
|
|
818
|
-
sourceLocale,
|
|
819
|
-
targetLocales,
|
|
820
|
-
targetBranches,
|
|
821
|
-
appDirs,
|
|
822
|
-
repoCanonical
|
|
823
|
-
});
|
|
824
|
-
p3.log.success(`Project ${chalk2.bold(result.projectName)} created!`);
|
|
825
|
-
return {
|
|
826
|
-
projectId: result.projectId,
|
|
827
|
-
projectName: result.projectName,
|
|
828
|
-
apiKey: result.apiKey,
|
|
829
|
-
sourceLocale,
|
|
830
|
-
targetLocales,
|
|
831
|
-
targetBranches,
|
|
832
|
-
repositoryBound: result.repositoryBound,
|
|
833
|
-
configureUrl: result.configureUrl,
|
|
834
|
-
apps: result.apps
|
|
835
|
-
};
|
|
836
|
-
} catch (error) {
|
|
837
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
838
|
-
p3.log.error(`Failed to create project: ${message}`);
|
|
302
|
+
if (!rawToken) {
|
|
303
|
+
authSpinner.stop();
|
|
304
|
+
p.log.error("The authentication link expired. Run `vocoder init` again.");
|
|
839
305
|
return null;
|
|
840
306
|
}
|
|
307
|
+
const userInfo = await api.getCliUserInfo(rawToken);
|
|
308
|
+
authSpinner.stop(`Authenticated as ${chalk.bold(userInfo.email)}`);
|
|
309
|
+
return {
|
|
310
|
+
token: rawToken,
|
|
311
|
+
...userInfo,
|
|
312
|
+
organizationId: callbackOrganizationId,
|
|
313
|
+
discoveryReady: callbackDiscoveryReady
|
|
314
|
+
};
|
|
841
315
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
316
|
+
|
|
317
|
+
// src/utils/output.ts
|
|
318
|
+
import * as p2 from "@clack/prompts";
|
|
319
|
+
import chalk2 from "chalk";
|
|
320
|
+
import { execSync } from "child_process";
|
|
321
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
322
|
+
import { join } from "path";
|
|
323
|
+
function tryClipboard(text2) {
|
|
324
|
+
const tools = [
|
|
325
|
+
{ cmd: "pbcopy" },
|
|
326
|
+
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
327
|
+
{ cmd: "xsel", args: ["--clipboard", "--input"] },
|
|
328
|
+
{ cmd: "wl-copy" },
|
|
329
|
+
{ cmd: "clip" }
|
|
330
|
+
];
|
|
331
|
+
for (const { cmd, args = [] } of tools) {
|
|
332
|
+
try {
|
|
333
|
+
execSync([cmd, ...args].join(" "), {
|
|
334
|
+
input: text2,
|
|
335
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
336
|
+
});
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
849
341
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
function printCommand(cmd) {
|
|
345
|
+
const copied = tryClipboard(cmd);
|
|
346
|
+
process.stdout.write("\n");
|
|
347
|
+
process.stdout.write(` ${chalk2.dim("$")} ${chalk2.cyan(cmd)}
|
|
348
|
+
`);
|
|
349
|
+
if (copied) process.stdout.write(` ${chalk2.dim("\u2191 copied to clipboard")}
|
|
350
|
+
`);
|
|
351
|
+
process.stdout.write("\n");
|
|
352
|
+
}
|
|
353
|
+
function printCodeBlock(code) {
|
|
354
|
+
process.stdout.write("\n");
|
|
355
|
+
for (const line of code.split("\n")) {
|
|
356
|
+
process.stdout.write(` ${line}
|
|
357
|
+
`);
|
|
858
358
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
);
|
|
865
|
-
if (sourceLocale === null) return null;
|
|
866
|
-
let compatibleTargets;
|
|
359
|
+
process.stdout.write("\n");
|
|
360
|
+
}
|
|
361
|
+
function writeApiKeyToEnv(apiKey, repoRoot) {
|
|
362
|
+
const envPath = join(repoRoot ?? process.cwd(), ".env");
|
|
363
|
+
if (!existsSync(envPath)) return false;
|
|
867
364
|
try {
|
|
868
|
-
|
|
365
|
+
const content = readFileSync(envPath, "utf-8");
|
|
366
|
+
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
367
|
+
let updated;
|
|
368
|
+
if (/^VOCODER_API_KEY=/m.test(content)) {
|
|
369
|
+
updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
|
|
370
|
+
} else {
|
|
371
|
+
const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
372
|
+
updated = `${content}${sep}${keyLine}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
writeFileSync(envPath, updated);
|
|
376
|
+
return true;
|
|
869
377
|
} catch {
|
|
870
|
-
|
|
871
|
-
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
872
|
-
);
|
|
873
|
-
return null;
|
|
378
|
+
return false;
|
|
874
379
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
)
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
886
|
-
);
|
|
380
|
+
}
|
|
381
|
+
function printApiKey(apiKey, repoRoot) {
|
|
382
|
+
const saved = writeApiKeyToEnv(apiKey, repoRoot);
|
|
383
|
+
p2.log.message("");
|
|
384
|
+
p2.log.message(chalk2.bold("Your API Key"));
|
|
385
|
+
printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
|
|
386
|
+
if (saved) {
|
|
387
|
+
p2.log.success(chalk2.dim("Saved to .env"));
|
|
388
|
+
} else {
|
|
389
|
+
p2.log.message(chalk2.dim(" Add the above to your .env file"));
|
|
887
390
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/utils/scaffold.ts
|
|
394
|
+
import * as p3 from "@clack/prompts";
|
|
395
|
+
import chalk4 from "chalk";
|
|
396
|
+
import { execSync as execSync2 } from "child_process";
|
|
397
|
+
import { resolve } from "path";
|
|
398
|
+
|
|
399
|
+
// src/utils/theme.ts
|
|
400
|
+
import chalk3 from "chalk";
|
|
401
|
+
var ORANGE = "#FC5206";
|
|
402
|
+
var PINK = "#D51977";
|
|
403
|
+
var BLUE = "#2450A9";
|
|
404
|
+
var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
|
|
405
|
+
var hex = (color) => (s) => noColor ? s : chalk3.hex(color)(s);
|
|
406
|
+
var dim = (s) => noColor ? s : chalk3.dim(s);
|
|
407
|
+
var bld = (s) => noColor ? s : chalk3.bold(s);
|
|
408
|
+
var grn = (s) => noColor ? s : chalk3.green(s);
|
|
409
|
+
var ylw = (s) => noColor ? s : chalk3.yellow(s);
|
|
410
|
+
var red = (s) => noColor ? s : chalk3.red(s);
|
|
411
|
+
var highlight = hex(PINK);
|
|
412
|
+
var info = hex(BLUE);
|
|
413
|
+
var active = hex(ORANGE);
|
|
414
|
+
|
|
415
|
+
// src/utils/write-config.ts
|
|
416
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
417
|
+
import { join as join2 } from "path";
|
|
418
|
+
function findExistingConfig(cwd = process.cwd()) {
|
|
419
|
+
for (const name of [
|
|
420
|
+
"vocoder.config.ts",
|
|
421
|
+
"vocoder.config.js",
|
|
422
|
+
"vocoder.config.json"
|
|
423
|
+
]) {
|
|
424
|
+
const candidate = join2(cwd, name);
|
|
425
|
+
if (existsSync2(candidate)) return candidate;
|
|
907
426
|
}
|
|
908
|
-
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
function writeVocoderConfig(options) {
|
|
430
|
+
const {
|
|
431
|
+
targetBranches = ["main"],
|
|
432
|
+
useTypeScript = true,
|
|
433
|
+
cwd = process.cwd(),
|
|
434
|
+
appId
|
|
435
|
+
} = options;
|
|
436
|
+
if (findExistingConfig(cwd)) return null;
|
|
437
|
+
const ext = useTypeScript ? "ts" : "js";
|
|
438
|
+
const configPath = join2(cwd, `vocoder.config.${ext}`);
|
|
439
|
+
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
440
|
+
const includes = ["**/*.{tsx,jsx,ts,js}"];
|
|
441
|
+
const includesStr = includes.map((p21) => `'${p21}'`).join(", ");
|
|
442
|
+
const appIdLine = appId ? ` appId: '${appId}',
|
|
443
|
+
` : "";
|
|
444
|
+
const content = `import { defineConfig } from '@vocoder/config'
|
|
445
|
+
|
|
446
|
+
export default defineConfig({
|
|
447
|
+
${appIdLine} targetBranches: [${branchesStr}],
|
|
448
|
+
include: [${includesStr}],
|
|
449
|
+
})
|
|
450
|
+
`;
|
|
909
451
|
try {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
sourceLocale,
|
|
914
|
-
targetLocales,
|
|
915
|
-
targetBranches,
|
|
916
|
-
repoCanonical: repoCanonical ?? ""
|
|
917
|
-
});
|
|
918
|
-
p3.log.success(
|
|
919
|
-
`App ${chalk2.bold(appDir || "(root)")} added to ${chalk2.bold(projectName)}!`
|
|
920
|
-
);
|
|
921
|
-
return {
|
|
922
|
-
projectId: result.projectId,
|
|
923
|
-
projectName: result.projectName,
|
|
924
|
-
appDir: result.appDir,
|
|
925
|
-
appId: result.appId,
|
|
926
|
-
sourceLocale,
|
|
927
|
-
targetLocales,
|
|
928
|
-
targetBranches
|
|
929
|
-
};
|
|
930
|
-
} catch (error) {
|
|
931
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
932
|
-
p3.log.error(`Failed to add app: ${message}`);
|
|
452
|
+
writeFileSync2(configPath, content, "utf-8");
|
|
453
|
+
return `vocoder.config.${ext}`;
|
|
454
|
+
} catch {
|
|
933
455
|
return null;
|
|
934
456
|
}
|
|
935
457
|
}
|
|
936
458
|
|
|
937
|
-
// src/utils/
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
let pathname;
|
|
961
|
-
let params;
|
|
962
|
-
try {
|
|
963
|
-
const parsed = new URL2(req.url, "http://localhost");
|
|
964
|
-
pathname = parsed.pathname;
|
|
965
|
-
params = Object.fromEntries(parsed.searchParams.entries());
|
|
966
|
-
} catch {
|
|
967
|
-
res.writeHead(400);
|
|
968
|
-
res.end("Bad request");
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (pathname !== "/callback") {
|
|
972
|
-
res.writeHead(404);
|
|
973
|
-
res.end("Not found");
|
|
974
|
-
return;
|
|
459
|
+
// src/utils/scaffold.ts
|
|
460
|
+
function runScaffold(params) {
|
|
461
|
+
const { targetBranches } = params;
|
|
462
|
+
const detection = detectLocalEcosystem();
|
|
463
|
+
if (detection.ecosystem) {
|
|
464
|
+
const frameworkLabel = detection.framework ?? detection.ecosystem;
|
|
465
|
+
const pmLabel = detection.packageManager;
|
|
466
|
+
p3.log.info(`Detected: ${chalk4.bold(frameworkLabel)} (${pmLabel})`);
|
|
467
|
+
}
|
|
468
|
+
const { devPackages, runtimePackages } = getPackagesToInstall(detection);
|
|
469
|
+
const allPackages = [...devPackages, ...runtimePackages];
|
|
470
|
+
if (allPackages.length > 0) {
|
|
471
|
+
p3.log.info("");
|
|
472
|
+
const installSpinner = p3.spinner();
|
|
473
|
+
installSpinner.start(`Installing ${allPackages.join(", ")}...`);
|
|
474
|
+
try {
|
|
475
|
+
if (devPackages.length > 0) {
|
|
476
|
+
execSync2(
|
|
477
|
+
buildInstallCommand(detection.packageManager, devPackages, true),
|
|
478
|
+
{ stdio: "pipe", cwd: process.cwd() }
|
|
479
|
+
);
|
|
975
480
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
callbackResolve(params);
|
|
982
|
-
callbackResolve = null;
|
|
481
|
+
if (runtimePackages.length > 0) {
|
|
482
|
+
execSync2(
|
|
483
|
+
buildInstallCommand(detection.packageManager, runtimePackages, false),
|
|
484
|
+
{ stdio: "pipe", cwd: process.cwd() }
|
|
485
|
+
);
|
|
983
486
|
}
|
|
984
|
-
|
|
487
|
+
installSpinner.stop(`Installed ${allPackages.join(", ")}`);
|
|
488
|
+
} catch {
|
|
489
|
+
installSpinner.stop("Package installation failed");
|
|
490
|
+
const cmds = [
|
|
491
|
+
devPackages.length > 0 ? buildInstallCommand(detection.packageManager, devPackages, true) : null,
|
|
492
|
+
runtimePackages.length > 0 ? buildInstallCommand(detection.packageManager, runtimePackages, false) : null
|
|
493
|
+
].filter(Boolean).join(" && ");
|
|
494
|
+
p3.log.warn(`Run manually: ${highlight(cmds)}`);
|
|
495
|
+
}
|
|
496
|
+
} else if (detection.ecosystem) {
|
|
497
|
+
p3.log.info(`Packages: ${chalk4.green("already installed")}`);
|
|
498
|
+
}
|
|
499
|
+
const branchList = targetBranches.length > 0 ? targetBranches.map((b) => highlight(b)).join(" or ") : highlight("your target branch");
|
|
500
|
+
p3.log.message("");
|
|
501
|
+
p3.log.success(`Push to ${branchList} to trigger your first translation run.`);
|
|
502
|
+
p3.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
|
|
503
|
+
}
|
|
504
|
+
function writeAppConfigs(apps, targetBranches, useTypeScript, repoRoot) {
|
|
505
|
+
const base = repoRoot ?? process.cwd();
|
|
506
|
+
for (const app of apps) {
|
|
507
|
+
const dir = app.appDir ? resolve(base, app.appDir) : base;
|
|
508
|
+
const written = writeVocoderConfig({
|
|
509
|
+
targetBranches,
|
|
510
|
+
appId: app.appId,
|
|
511
|
+
cwd: dir,
|
|
512
|
+
useTypeScript
|
|
985
513
|
});
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
514
|
+
if (written) {
|
|
515
|
+
const displayPath = app.appDir ? `${app.appDir}/${written}` : written;
|
|
516
|
+
p3.log.success(`Created ${highlight(displayPath)}`);
|
|
517
|
+
} else if (!findExistingConfig(dir)) {
|
|
518
|
+
const ext = useTypeScript ? "ts" : "js";
|
|
519
|
+
p3.log.warn(
|
|
520
|
+
`Could not write ${app.appDir ? `${app.appDir}/` : ""}vocoder.config.${ext} \u2014 create it manually.`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/utils/mcp-setup.ts
|
|
527
|
+
import * as p4 from "@clack/prompts";
|
|
528
|
+
import chalk5 from "chalk";
|
|
529
|
+
import { execSync as execSync3 } from "child_process";
|
|
530
|
+
var MCP_DOCS_URL = "https://vocoder.app/docs/mcp";
|
|
531
|
+
function mcpServerJson(apiKey) {
|
|
532
|
+
return JSON.stringify(
|
|
533
|
+
{
|
|
534
|
+
mcpServers: {
|
|
535
|
+
vocoder: {
|
|
536
|
+
type: "stdio",
|
|
537
|
+
command: "npx",
|
|
538
|
+
args: ["-y", "@vocoder/mcp"],
|
|
539
|
+
env: { VOCODER_API_KEY: apiKey }
|
|
540
|
+
}
|
|
991
541
|
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
542
|
+
},
|
|
543
|
+
null,
|
|
544
|
+
2
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
async function runMcpSetup(apiKey) {
|
|
548
|
+
const tool = await p4.select({
|
|
549
|
+
message: "Which AI editor?",
|
|
550
|
+
options: [
|
|
551
|
+
{ value: "claude", label: "Claude Code" },
|
|
552
|
+
{ value: "cursor", label: "Cursor" },
|
|
553
|
+
{ value: "windsurf", label: "Windsurf" },
|
|
554
|
+
{ value: "vscode", label: "VS Code (GitHub Copilot)" },
|
|
555
|
+
{ value: "other", label: "Other \u2014 show the config JSON" }
|
|
556
|
+
]
|
|
1003
557
|
});
|
|
558
|
+
if (p4.isCancel(tool)) return;
|
|
559
|
+
if (tool === "claude") {
|
|
560
|
+
try {
|
|
561
|
+
execSync3(
|
|
562
|
+
`claude mcp add --scope user --transport stdio --env VOCODER_API_KEY=${apiKey} vocoder -- npx -y @vocoder/mcp`,
|
|
563
|
+
{ stdio: "pipe" }
|
|
564
|
+
);
|
|
565
|
+
p4.log.success("Vocoder MCP server registered in Claude Code.");
|
|
566
|
+
} catch {
|
|
567
|
+
p4.log.message("Run this to register the MCP server:");
|
|
568
|
+
printCommand(
|
|
569
|
+
`claude mcp add --scope user --transport stdio --env VOCODER_API_KEY=${apiKey} vocoder -- npx -y @vocoder/mcp`
|
|
570
|
+
);
|
|
571
|
+
p4.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const configPaths = {
|
|
576
|
+
cursor: { path: "~/.cursor/mcp.json", merge: true },
|
|
577
|
+
windsurf: { path: "~/.codeium/windsurf/mcp_config.json", merge: true },
|
|
578
|
+
vscode: { path: ".vscode/mcp.json", merge: true },
|
|
579
|
+
other: { path: ".mcp.json", merge: false }
|
|
580
|
+
};
|
|
581
|
+
const { path: configPath, merge } = configPaths[tool];
|
|
582
|
+
const mergeNote = merge ? chalk5.dim(` Merge into ${configPath} (create if missing):`) : chalk5.dim(` Add to ${configPath}:`);
|
|
583
|
+
p4.log.message(mergeNote);
|
|
584
|
+
printCodeBlock(mcpServerJson(apiKey));
|
|
585
|
+
p4.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
1004
586
|
}
|
|
1005
587
|
|
|
1006
|
-
// src/utils/
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (settled) return;
|
|
1040
|
-
settled = true;
|
|
1041
|
-
resolve3(false);
|
|
588
|
+
// src/utils/plan-check.ts
|
|
589
|
+
import * as p5 from "@clack/prompts";
|
|
590
|
+
import chalk6 from "chalk";
|
|
591
|
+
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
592
|
+
function getSubscriptionSettingsUrl(apiUrl) {
|
|
593
|
+
return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
|
|
594
|
+
}
|
|
595
|
+
function isPlanLimitFailure(message) {
|
|
596
|
+
if (!message) return false;
|
|
597
|
+
return /limit|upgrade/i.test(message);
|
|
598
|
+
}
|
|
599
|
+
function printPlanLimitMessage(apiUrl, message) {
|
|
600
|
+
p5.log.error(`You are over your plan limits.
|
|
601
|
+
${message}`);
|
|
602
|
+
p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
603
|
+
}
|
|
604
|
+
async function checkPlanLimits(api, userToken, organizationId, apiUrl) {
|
|
605
|
+
try {
|
|
606
|
+
const { organizations } = await api.listOrganizations(userToken);
|
|
607
|
+
const organization = organizations.find((o) => o.id === organizationId);
|
|
608
|
+
if (!organization) {
|
|
609
|
+
return { atLimit: false };
|
|
610
|
+
}
|
|
611
|
+
if (organization.maxApps !== -1 && organization.appCount >= organization.maxApps) {
|
|
612
|
+
p5.log.warn(
|
|
613
|
+
`App limit reached \u2014 ${organization.appCount}/${organization.maxApps} on your ${chalk6.bold(organization.planId)} plan.`
|
|
614
|
+
);
|
|
615
|
+
const limitAction = await p5.select({
|
|
616
|
+
message: "What would you like to do?",
|
|
617
|
+
options: [
|
|
618
|
+
{ value: "upgrade", label: "Upgrade plan" },
|
|
619
|
+
{ value: "cancel", label: "Cancel" }
|
|
620
|
+
]
|
|
1042
621
|
});
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
622
|
+
if (p5.isCancel(limitAction) || limitAction === "cancel") {
|
|
623
|
+
p5.cancel("Setup cancelled.");
|
|
624
|
+
return { atLimit: true };
|
|
625
|
+
}
|
|
626
|
+
await tryOpenBrowser(getSubscriptionSettingsUrl(apiUrl));
|
|
627
|
+
p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
|
|
628
|
+
return { atLimit: true };
|
|
1050
629
|
}
|
|
1051
|
-
|
|
630
|
+
const remaining = organization.maxApps === -1 ? void 0 : Math.max(0, organization.maxApps - organization.appCount);
|
|
631
|
+
return { atLimit: false, remaining };
|
|
632
|
+
} catch {
|
|
633
|
+
p5.log.warn(
|
|
634
|
+
"Could not verify plan limits \u2014 proceeding, the server will enforce them."
|
|
635
|
+
);
|
|
636
|
+
return { atLimit: false };
|
|
637
|
+
}
|
|
1052
638
|
}
|
|
639
|
+
|
|
640
|
+
// src/utils/organization-select.ts
|
|
641
|
+
import * as p8 from "@clack/prompts";
|
|
642
|
+
import chalk9 from "chalk";
|
|
643
|
+
|
|
644
|
+
// src/utils/github-connect.ts
|
|
645
|
+
import * as p6 from "@clack/prompts";
|
|
646
|
+
import chalk7 from "chalk";
|
|
1053
647
|
async function runGitHubInstallFlow(params) {
|
|
1054
648
|
let server = null;
|
|
1055
649
|
try {
|
|
@@ -1063,23 +657,23 @@ async function runGitHubInstallFlow(params) {
|
|
|
1063
657
|
callbackPort: server?.port
|
|
1064
658
|
}
|
|
1065
659
|
);
|
|
1066
|
-
|
|
660
|
+
p6.log.info("Opening GitHub to install the Vocoder App...");
|
|
1067
661
|
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1068
|
-
const shouldOpen = params.yes ? true : await
|
|
1069
|
-
if (
|
|
662
|
+
const shouldOpen = params.yes ? true : await p6.confirm({ message: "Open in your browser?" });
|
|
663
|
+
if (p6.isCancel(shouldOpen)) {
|
|
1070
664
|
server?.close();
|
|
1071
665
|
return null;
|
|
1072
666
|
}
|
|
1073
667
|
if (shouldOpen) {
|
|
1074
668
|
const opened = await tryOpenBrowser(installUrl);
|
|
1075
669
|
if (!opened) {
|
|
1076
|
-
|
|
670
|
+
p6.log.info(
|
|
1077
671
|
"Could not open a browser automatically. Use the URL above."
|
|
1078
672
|
);
|
|
1079
673
|
}
|
|
1080
674
|
}
|
|
1081
675
|
}
|
|
1082
|
-
const connectSpinner =
|
|
676
|
+
const connectSpinner = p6.spinner();
|
|
1083
677
|
connectSpinner.start("Waiting for GitHub App installation...");
|
|
1084
678
|
if (server) {
|
|
1085
679
|
try {
|
|
@@ -1093,24 +687,24 @@ async function runGitHubInstallFlow(params) {
|
|
|
1093
687
|
server.close();
|
|
1094
688
|
if (!callbackParams) {
|
|
1095
689
|
connectSpinner.stop("GitHub App installation timed out");
|
|
1096
|
-
|
|
690
|
+
p6.log.error(
|
|
1097
691
|
"The installation flow timed out. Run `vocoder init` again."
|
|
1098
692
|
);
|
|
1099
693
|
return null;
|
|
1100
694
|
}
|
|
1101
695
|
if (callbackParams.error) {
|
|
1102
696
|
connectSpinner.stop("GitHub App installation failed");
|
|
1103
|
-
|
|
697
|
+
p6.log.error(callbackParams.error);
|
|
1104
698
|
return null;
|
|
1105
699
|
}
|
|
1106
700
|
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
1107
701
|
if (!organizationId || !connectionLabel) {
|
|
1108
702
|
connectSpinner.stop("GitHub App installation incomplete");
|
|
1109
|
-
|
|
703
|
+
p6.log.error("Missing organization or connection data from callback.");
|
|
1110
704
|
return null;
|
|
1111
705
|
}
|
|
1112
706
|
connectSpinner.stop(
|
|
1113
|
-
`Connected to GitHub as ${
|
|
707
|
+
`Connected to GitHub as ${chalk7.bold(connectionLabel)}`
|
|
1114
708
|
);
|
|
1115
709
|
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
1116
710
|
return {
|
|
@@ -1125,7 +719,7 @@ async function runGitHubInstallFlow(params) {
|
|
|
1125
719
|
}
|
|
1126
720
|
}
|
|
1127
721
|
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
1128
|
-
|
|
722
|
+
p6.log.warn(
|
|
1129
723
|
"Complete the installation in your browser, then run `vocoder init` again."
|
|
1130
724
|
);
|
|
1131
725
|
return null;
|
|
@@ -1140,22 +734,21 @@ async function runGitHubDiscoveryFlow(params) {
|
|
|
1140
734
|
organizationId: params.organizationId,
|
|
1141
735
|
callbackPort: server?.port
|
|
1142
736
|
});
|
|
1143
|
-
|
|
1144
|
-
p4.note("Complete authorization in your browser.");
|
|
737
|
+
p6.log.info("Opening GitHub to authorize your account...");
|
|
1145
738
|
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1146
|
-
const shouldOpen = params.yes ? true : await
|
|
1147
|
-
if (
|
|
739
|
+
const shouldOpen = params.yes ? true : await p6.confirm({ message: "Open in your browser?" });
|
|
740
|
+
if (p6.isCancel(shouldOpen)) {
|
|
1148
741
|
server?.close();
|
|
1149
742
|
return null;
|
|
1150
743
|
}
|
|
1151
744
|
if (shouldOpen) {
|
|
1152
745
|
const opened = await tryOpenBrowser(oauthUrl);
|
|
1153
746
|
if (!opened) {
|
|
1154
|
-
|
|
747
|
+
p6.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
1155
748
|
}
|
|
1156
749
|
}
|
|
1157
750
|
}
|
|
1158
|
-
const oauthSpinner =
|
|
751
|
+
const oauthSpinner = p6.spinner();
|
|
1159
752
|
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
1160
753
|
if (server) {
|
|
1161
754
|
try {
|
|
@@ -1173,7 +766,7 @@ async function runGitHubDiscoveryFlow(params) {
|
|
|
1173
766
|
}
|
|
1174
767
|
if (callbackParams.error) {
|
|
1175
768
|
oauthSpinner.stop("GitHub authorization failed");
|
|
1176
|
-
|
|
769
|
+
p6.log.error(callbackParams.error);
|
|
1177
770
|
return null;
|
|
1178
771
|
}
|
|
1179
772
|
} catch {
|
|
@@ -1193,7 +786,7 @@ async function selectGitHubInstallation(installations, canInstallNew) {
|
|
|
1193
786
|
value: String(inst.installationId),
|
|
1194
787
|
label: inst.accountLogin,
|
|
1195
788
|
hint: [
|
|
1196
|
-
inst.accountType === "Organization" ? "
|
|
789
|
+
inst.accountType === "Organization" ? "GitHub org" : "personal account",
|
|
1197
790
|
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
1198
791
|
inst.isSuspended ? "suspended" : ""
|
|
1199
792
|
].filter(Boolean).join(" \xB7 ") || void 0
|
|
@@ -1201,561 +794,1272 @@ async function selectGitHubInstallation(installations, canInstallNew) {
|
|
|
1201
794
|
if (canInstallNew) {
|
|
1202
795
|
options.push({
|
|
1203
796
|
value: "install_new",
|
|
1204
|
-
label: `Install on a new account ${
|
|
797
|
+
label: `Install on a new account ${chalk7.dim("(creates a new workspace)")}`
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const selected = await p6.select({
|
|
801
|
+
message: "Which GitHub account should this workspace connect to?",
|
|
802
|
+
options
|
|
803
|
+
});
|
|
804
|
+
if (p6.isCancel(selected)) return null;
|
|
805
|
+
if (selected === "install_new") return "install_new";
|
|
806
|
+
return Number(selected);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/utils/organization.ts
|
|
810
|
+
import * as p7 from "@clack/prompts";
|
|
811
|
+
import chalk8 from "chalk";
|
|
812
|
+
async function selectOrganization(result) {
|
|
813
|
+
const { organizations, canCreateOrganization } = result;
|
|
814
|
+
if (organizations.length === 0) {
|
|
815
|
+
return { action: "create" };
|
|
816
|
+
}
|
|
817
|
+
const options = organizations.map((org) => {
|
|
818
|
+
const atLimit = org.maxApps !== -1 && org.appCount >= org.maxApps;
|
|
819
|
+
const hint = [
|
|
820
|
+
org.projectCount > 0 ? `${org.projectCount} project${org.projectCount !== 1 ? "s" : ""}` : "",
|
|
821
|
+
org.connectionLabel ? `GitHub: ${org.connectionLabel}` : "",
|
|
822
|
+
atLimit ? chalk8.yellow(`${org.appCount}/${org.maxApps} apps \u2014 upgrade for more`) : ""
|
|
823
|
+
].filter(Boolean).join(" \xB7 ") || void 0;
|
|
824
|
+
return { value: org.id, label: org.name, hint };
|
|
825
|
+
});
|
|
826
|
+
if (canCreateOrganization) {
|
|
827
|
+
options.push({ value: "create", label: "Create new workspace" });
|
|
828
|
+
}
|
|
829
|
+
const selected = await p7.select({
|
|
830
|
+
message: "Select workspace",
|
|
831
|
+
options
|
|
832
|
+
});
|
|
833
|
+
if (p7.isCancel(selected)) {
|
|
834
|
+
return { action: "cancelled" };
|
|
835
|
+
}
|
|
836
|
+
if (selected === "create") {
|
|
837
|
+
return { action: "create" };
|
|
838
|
+
}
|
|
839
|
+
const organization = organizations.find((org) => org.id === selected);
|
|
840
|
+
if (!organization) {
|
|
841
|
+
return { action: "cancelled" };
|
|
842
|
+
}
|
|
843
|
+
return { action: "use", organization };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/utils/organization-select.ts
|
|
847
|
+
async function selectOrganizationForInit(params) {
|
|
848
|
+
const { api, userToken, userEmail, identity, lookup, repoProjectId, options } = params;
|
|
849
|
+
if (params.authOrganizationId) {
|
|
850
|
+
const organizationData2 = await api.listOrganizations(userToken);
|
|
851
|
+
const organization = organizationData2.organizations.find(
|
|
852
|
+
(o) => o.id === params.authOrganizationId
|
|
853
|
+
);
|
|
854
|
+
const organizationName = organization?.name ?? userEmail;
|
|
855
|
+
p8.log.success(
|
|
856
|
+
`Connected as ${chalk9.bold(userEmail)} \u2014 workspace: ${chalk9.bold(organizationName)}`
|
|
857
|
+
);
|
|
858
|
+
return { organizationId: params.authOrganizationId, organizationName };
|
|
859
|
+
}
|
|
860
|
+
const repoOrgContext = identity ? lookup?.organizationContext ?? null : null;
|
|
861
|
+
if (repoOrgContext && !repoProjectId) {
|
|
862
|
+
p8.log.success(`Workspace: ${chalk9.bold(repoOrgContext.organizationName)}`);
|
|
863
|
+
return {
|
|
864
|
+
organizationId: repoOrgContext.organizationId,
|
|
865
|
+
organizationName: repoOrgContext.organizationName
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
const organizationData = await api.listOrganizations(userToken, {
|
|
869
|
+
repo: identity?.repoCanonical
|
|
870
|
+
});
|
|
871
|
+
const repoCanonical = identity?.repoCanonical ?? null;
|
|
872
|
+
const covering = repoCanonical ? organizationData.organizations.filter((o) => o.coversRepo === true) : [];
|
|
873
|
+
const connected = organizationData.organizations.filter(
|
|
874
|
+
(o) => o.hasGitHubConnection
|
|
875
|
+
);
|
|
876
|
+
if (repoCanonical && covering.length === 1) {
|
|
877
|
+
const organization = covering[0];
|
|
878
|
+
p8.log.success(`Workspace: ${chalk9.bold(organization.name)}`);
|
|
879
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
880
|
+
}
|
|
881
|
+
if (repoCanonical && covering.length > 1) {
|
|
882
|
+
const choice = await p8.select({
|
|
883
|
+
message: "Select workspace for this repo",
|
|
884
|
+
options: covering.map((o) => ({
|
|
885
|
+
value: o.id,
|
|
886
|
+
label: `${o.name} ${chalk9.dim(`(${o.appCount} app${o.appCount !== 1 ? "s" : ""})`)}`
|
|
887
|
+
}))
|
|
888
|
+
});
|
|
889
|
+
if (p8.isCancel(choice)) {
|
|
890
|
+
p8.cancel("Setup cancelled.");
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const organization = covering.find((o) => o.id === choice);
|
|
894
|
+
p8.log.success(`Workspace: ${chalk9.bold(organization.name)}`);
|
|
895
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
896
|
+
}
|
|
897
|
+
if (repoCanonical && covering.length === 0 && connected.length > 0) {
|
|
898
|
+
const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
|
|
899
|
+
p8.log.warn(
|
|
900
|
+
`${chalk9.bold(shortRepo)} isn't accessible from your Vocoder installation.
|
|
901
|
+
Grant access to this repository or install on the account that owns it.`
|
|
902
|
+
);
|
|
903
|
+
const fixOptions = [];
|
|
904
|
+
for (const organization of connected) {
|
|
905
|
+
if (organization.installationConfigureUrl) {
|
|
906
|
+
fixOptions.push({
|
|
907
|
+
value: `grant:${organization.id}`,
|
|
908
|
+
label: `Configure ${chalk9.bold(organization.connectionLabel ?? organization.name)}'s GitHub App installation`
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
fixOptions.push({
|
|
913
|
+
value: "install_new",
|
|
914
|
+
label: `Install on a different GitHub account ${chalk9.dim("(creates a new personal workspace)")}`
|
|
915
|
+
});
|
|
916
|
+
fixOptions.push({ value: "cancel", label: "Cancel" });
|
|
917
|
+
const fix = await p8.select({
|
|
918
|
+
message: "How would you like to fix this?",
|
|
919
|
+
options: fixOptions
|
|
920
|
+
});
|
|
921
|
+
if (p8.isCancel(fix) || fix === "cancel") {
|
|
922
|
+
p8.cancel("Setup cancelled.");
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
if (fix.startsWith("grant:")) {
|
|
926
|
+
const organization = connected.find((o) => `grant:${o.id}` === fix);
|
|
927
|
+
await tryOpenBrowser(organization.installationConfigureUrl);
|
|
928
|
+
p8.cancel(
|
|
929
|
+
`Grant access to ${chalk9.bold(shortRepo)} in your browser,
|
|
930
|
+
then re-run ${chalk9.bold("vocoder init")}.`
|
|
931
|
+
);
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const connectResult = await runGitHubInstallFlow({
|
|
935
|
+
api,
|
|
936
|
+
userToken,
|
|
937
|
+
yes: options.yes
|
|
938
|
+
});
|
|
939
|
+
if (!connectResult) {
|
|
940
|
+
p8.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
p8.log.success(`Workspace: ${chalk9.bold(connectResult.organizationName)}`);
|
|
944
|
+
return {
|
|
945
|
+
organizationId: connectResult.organizationId,
|
|
946
|
+
organizationName: connectResult.organizationName
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
950
|
+
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
951
|
+
if (cachedInstallations.length > 0) {
|
|
952
|
+
if (repoCanonical) {
|
|
953
|
+
const repoOwner = repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
954
|
+
if (repoOwner) {
|
|
955
|
+
const hasMatchingAccount = cachedInstallations.some(
|
|
956
|
+
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
957
|
+
);
|
|
958
|
+
if (!hasMatchingAccount) {
|
|
959
|
+
p8.log.warn(
|
|
960
|
+
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
961
|
+
The project will be created but translations won't trigger automatically.
|
|
962
|
+
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const validInstallations = cachedInstallations.filter(
|
|
968
|
+
(i) => !i.isSuspended && !i.conflictLabel
|
|
969
|
+
);
|
|
970
|
+
let selectedInstallationId2 = null;
|
|
971
|
+
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
972
|
+
selectedInstallationId2 = validInstallations[0].installationId;
|
|
973
|
+
} else {
|
|
974
|
+
selectedInstallationId2 = await selectGitHubInstallation(
|
|
975
|
+
cachedInstallations.map((inst) => ({
|
|
976
|
+
installationId: inst.installationId,
|
|
977
|
+
accountLogin: inst.accountLogin,
|
|
978
|
+
accountType: inst.accountType,
|
|
979
|
+
isSuspended: inst.isSuspended,
|
|
980
|
+
conflictLabel: inst.conflictLabel
|
|
981
|
+
})),
|
|
982
|
+
false
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (selectedInstallationId2 === null || selectedInstallationId2 === "install_new") {
|
|
986
|
+
p8.cancel("Setup cancelled. Re-run `vocoder init` and choose Install GitHub App.");
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const claimResult2 = await api.claimCliGitHubInstallation(userToken, {
|
|
990
|
+
installationId: String(selectedInstallationId2),
|
|
991
|
+
organizationId: null
|
|
992
|
+
});
|
|
993
|
+
p8.log.success(`Workspace: ${chalk9.bold(claimResult2.organizationName)}`);
|
|
994
|
+
return {
|
|
995
|
+
organizationId: claimResult2.organizationId,
|
|
996
|
+
organizationName: claimResult2.organizationName
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
if (organizationData.organizations.length === 1 && !organizationData.canCreateOrganization) {
|
|
1000
|
+
const organization = organizationData.organizations[0];
|
|
1001
|
+
p8.log.success(`Workspace: ${chalk9.bold(organization.name)}`);
|
|
1002
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
1003
|
+
}
|
|
1004
|
+
const organizationResult = await selectOrganization(organizationData);
|
|
1005
|
+
if (organizationResult.action === "cancelled") {
|
|
1006
|
+
p8.cancel("Setup cancelled.");
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
if (organizationResult.action === "use") {
|
|
1010
|
+
const { organization } = organizationResult;
|
|
1011
|
+
p8.log.success(`Workspace: ${chalk9.bold(organization.name)}`);
|
|
1012
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
1013
|
+
}
|
|
1014
|
+
const connectChoice = await p8.select({
|
|
1015
|
+
message: "Connect your new workspace to GitHub",
|
|
1016
|
+
options: [
|
|
1017
|
+
{ value: "install", label: "Install the Vocoder GitHub App" },
|
|
1018
|
+
{ value: "link", label: "Link an existing installation" }
|
|
1019
|
+
]
|
|
1020
|
+
});
|
|
1021
|
+
if (p8.isCancel(connectChoice)) {
|
|
1022
|
+
p8.cancel("Setup cancelled.");
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
if (connectChoice === "install") {
|
|
1026
|
+
const connectResult = await runGitHubInstallFlow({
|
|
1027
|
+
api,
|
|
1028
|
+
userToken,
|
|
1029
|
+
yes: options.yes
|
|
1030
|
+
});
|
|
1031
|
+
if (!connectResult) {
|
|
1032
|
+
p8.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
p8.log.success(`Workspace: ${chalk9.bold(connectResult.organizationName)}`);
|
|
1036
|
+
return {
|
|
1037
|
+
organizationId: connectResult.organizationId,
|
|
1038
|
+
organizationName: connectResult.organizationName
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
const installations = await runGitHubDiscoveryFlow({
|
|
1042
|
+
api,
|
|
1043
|
+
userToken,
|
|
1044
|
+
yes: options.yes
|
|
1045
|
+
});
|
|
1046
|
+
if (!installations) return null;
|
|
1047
|
+
if (installations.length === 0) {
|
|
1048
|
+
p8.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
|
|
1049
|
+
const installNow = await p8.confirm({
|
|
1050
|
+
message: "Open GitHub to install the App?"
|
|
1051
|
+
});
|
|
1052
|
+
if (p8.isCancel(installNow) || !installNow) return null;
|
|
1053
|
+
const connectResult = await runGitHubInstallFlow({
|
|
1054
|
+
api,
|
|
1055
|
+
userToken,
|
|
1056
|
+
yes: options.yes
|
|
1057
|
+
});
|
|
1058
|
+
if (!connectResult) return null;
|
|
1059
|
+
p8.log.success(`Workspace: ${chalk9.bold(connectResult.organizationName)}`);
|
|
1060
|
+
return {
|
|
1061
|
+
organizationId: connectResult.organizationId,
|
|
1062
|
+
organizationName: connectResult.organizationName
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const selectedInstallationId = await selectGitHubInstallation(
|
|
1066
|
+
installations.map((inst) => ({
|
|
1067
|
+
installationId: inst.installationId,
|
|
1068
|
+
accountLogin: inst.accountLogin,
|
|
1069
|
+
accountType: inst.accountType,
|
|
1070
|
+
isSuspended: inst.isSuspended,
|
|
1071
|
+
conflictLabel: inst.conflictLabel
|
|
1072
|
+
})),
|
|
1073
|
+
true
|
|
1074
|
+
);
|
|
1075
|
+
if (selectedInstallationId === null) {
|
|
1076
|
+
p8.cancel("Setup cancelled.");
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
if (selectedInstallationId === "install_new") {
|
|
1080
|
+
const connectResult = await runGitHubInstallFlow({
|
|
1081
|
+
api,
|
|
1082
|
+
userToken,
|
|
1083
|
+
yes: options.yes
|
|
1205
1084
|
});
|
|
1085
|
+
if (!connectResult) return null;
|
|
1086
|
+
p8.log.success(`Workspace: ${chalk9.bold(connectResult.organizationName)}`);
|
|
1087
|
+
return {
|
|
1088
|
+
organizationId: connectResult.organizationId,
|
|
1089
|
+
organizationName: connectResult.organizationName
|
|
1090
|
+
};
|
|
1206
1091
|
}
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1092
|
+
const claimResult = await api.claimCliGitHubInstallation(userToken, {
|
|
1093
|
+
installationId: String(selectedInstallationId),
|
|
1094
|
+
organizationId: null
|
|
1210
1095
|
});
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1096
|
+
p8.log.success(`Workspace: ${chalk9.bold(claimResult.organizationName)}`);
|
|
1097
|
+
return {
|
|
1098
|
+
organizationId: claimResult.organizationId,
|
|
1099
|
+
organizationName: claimResult.organizationName
|
|
1100
|
+
};
|
|
1214
1101
|
}
|
|
1215
1102
|
|
|
1216
|
-
// src/
|
|
1217
|
-
import
|
|
1218
|
-
import
|
|
1103
|
+
// src/utils/project-create.ts
|
|
1104
|
+
import * as p11 from "@clack/prompts";
|
|
1105
|
+
import chalk10 from "chalk";
|
|
1219
1106
|
|
|
1220
|
-
// src/utils/
|
|
1221
|
-
import {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1107
|
+
// src/utils/app-dir-select.ts
|
|
1108
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
1109
|
+
import { resolve as resolve2 } from "path";
|
|
1110
|
+
import { isCancel as isCancel7, Prompt } from "@clack/core";
|
|
1111
|
+
import * as p9 from "@clack/prompts";
|
|
1112
|
+
var S_BAR = "\u2502";
|
|
1113
|
+
var S_BAR_END = "\u2514";
|
|
1114
|
+
var S_ACTIVE = "\u25C6";
|
|
1115
|
+
var S_SUBMIT = "\u25C6";
|
|
1116
|
+
var S_CANCEL = "\u25A0";
|
|
1117
|
+
var S_ERROR = "\u25B2";
|
|
1118
|
+
function symbol(state) {
|
|
1119
|
+
switch (state) {
|
|
1120
|
+
case "submit":
|
|
1121
|
+
return grn(S_SUBMIT);
|
|
1122
|
+
case "cancel":
|
|
1123
|
+
return red(S_CANCEL);
|
|
1124
|
+
case "error":
|
|
1125
|
+
return ylw(S_ERROR);
|
|
1126
|
+
default:
|
|
1127
|
+
return active(S_ACTIVE);
|
|
1240
1128
|
}
|
|
1241
1129
|
}
|
|
1242
|
-
function
|
|
1243
|
-
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1130
|
+
function validateAppDirPath(val, existing, opts = {}) {
|
|
1131
|
+
if (val.startsWith("/")) return "Must be a relative path (e.g. apps/web)";
|
|
1132
|
+
if (val.includes("..")) return "Path traversal not allowed";
|
|
1133
|
+
const hasWholeRepo = existing.includes("");
|
|
1134
|
+
const hasScoped = existing.some((d) => d !== "");
|
|
1135
|
+
if (val === "" && hasScoped) return "Cannot add whole-repo scope to a monorepo project";
|
|
1136
|
+
if (val !== "" && hasWholeRepo) return "Cannot add a scoped directory to a whole-repo project";
|
|
1137
|
+
if (existing.includes(val)) return `Already added: ${val}`;
|
|
1138
|
+
const nested = existing.find(
|
|
1139
|
+
(d) => d !== "" && (val.startsWith(d + "/") || d.startsWith(val + "/"))
|
|
1140
|
+
);
|
|
1141
|
+
if (nested) return `"${val}" overlaps with already-added "${nested}"`;
|
|
1142
|
+
if (val !== "") {
|
|
1143
|
+
const abs = resolve2(opts.cwd ?? process.cwd(), val);
|
|
1144
|
+
if (!existsSync3(abs)) return `Directory not found: ${val}`;
|
|
1145
|
+
if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
|
|
1246
1146
|
}
|
|
1247
|
-
return
|
|
1147
|
+
return null;
|
|
1248
1148
|
}
|
|
1249
|
-
function
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1149
|
+
async function collectAppDirs(opts = {}) {
|
|
1150
|
+
const added = [];
|
|
1151
|
+
let filter = "";
|
|
1152
|
+
let cursor = 0;
|
|
1153
|
+
let addCursor = false;
|
|
1154
|
+
const isNewDir = () => {
|
|
1155
|
+
const t = filter.trim();
|
|
1156
|
+
return t.length > 0 && !added.includes(t);
|
|
1157
|
+
};
|
|
1158
|
+
const clampCursor = () => {
|
|
1159
|
+
const max = added.length - 1;
|
|
1160
|
+
if (cursor > max) cursor = Math.max(0, max);
|
|
1161
|
+
};
|
|
1162
|
+
const prompt = new Prompt(
|
|
1163
|
+
{
|
|
1164
|
+
validate() {
|
|
1165
|
+
return void 0;
|
|
1166
|
+
},
|
|
1167
|
+
render() {
|
|
1168
|
+
const trimmed = filter.trim();
|
|
1169
|
+
const hdr = `${dim(S_BAR)}
|
|
1170
|
+
${symbol(this.state)} App directories
|
|
1171
|
+
`;
|
|
1172
|
+
switch (this.state) {
|
|
1173
|
+
case "submit": {
|
|
1174
|
+
const summary = added.length > 0 ? bld(added.join(", ")) : dim("none (single-app project)");
|
|
1175
|
+
return `${hdr}${dim(S_BAR)} ${summary}`;
|
|
1176
|
+
}
|
|
1177
|
+
case "cancel":
|
|
1178
|
+
return `${hdr}${dim(S_BAR)}`;
|
|
1179
|
+
default: {
|
|
1180
|
+
const inputHint = filter.length > 0 ? filter : added.length === 0 ? dim("e.g. apps/web") : dim("e.g. apps/api");
|
|
1181
|
+
const lines = [
|
|
1182
|
+
hdr.trimEnd(),
|
|
1183
|
+
`${info(S_BAR)} ${dim("/")} ${inputHint}`,
|
|
1184
|
+
info(S_BAR)
|
|
1185
|
+
];
|
|
1186
|
+
for (let i = 0; i < added.length; i++) {
|
|
1187
|
+
const isCursor = i === cursor && !addCursor;
|
|
1188
|
+
const icon = active("\u25FC");
|
|
1189
|
+
const label = isCursor ? bld(added[i]) : added[i];
|
|
1190
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
1191
|
+
}
|
|
1192
|
+
const atLimit = opts.maxDirs !== void 0 && added.length >= opts.maxDirs;
|
|
1193
|
+
if (atLimit) {
|
|
1194
|
+
lines.push(`${info(S_BAR)} ${dim(`App limit reached (${added.length}/${opts.maxDirs} on your plan)`)}`);
|
|
1195
|
+
} else if (isNewDir()) {
|
|
1196
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
1197
|
+
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
1198
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}"`;
|
|
1199
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
1200
|
+
}
|
|
1201
|
+
lines.push(info(S_BAR));
|
|
1202
|
+
if (atLimit) {
|
|
1203
|
+
lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
1204
|
+
} else if (added.length === 0 && !isNewDir()) {
|
|
1205
|
+
lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
|
|
1206
|
+
lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
|
|
1207
|
+
} else if (added.length > 0) {
|
|
1208
|
+
lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
1209
|
+
}
|
|
1210
|
+
const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
|
|
1211
|
+
if (this.state === "error") {
|
|
1212
|
+
lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
|
|
1213
|
+
} else {
|
|
1214
|
+
lines.push(barEnd);
|
|
1215
|
+
}
|
|
1216
|
+
lines.push("");
|
|
1217
|
+
return lines.join("\n");
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1261
1220
|
}
|
|
1262
|
-
|
|
1221
|
+
},
|
|
1222
|
+
false
|
|
1223
|
+
);
|
|
1224
|
+
prompt.on("key", (key) => {
|
|
1225
|
+
if (!key || key === " ") return;
|
|
1226
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
1227
|
+
if (cp === 127 || cp === 8) {
|
|
1228
|
+
filter = filter.slice(0, -1);
|
|
1229
|
+
addCursor = false;
|
|
1230
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
1231
|
+
filter += key;
|
|
1232
|
+
cursor = 0;
|
|
1233
|
+
addCursor = false;
|
|
1263
1234
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1235
|
+
});
|
|
1236
|
+
prompt.on("cursor", (action) => {
|
|
1237
|
+
switch (action) {
|
|
1238
|
+
case "up":
|
|
1239
|
+
if (addCursor) {
|
|
1240
|
+
addCursor = false;
|
|
1241
|
+
cursor = Math.max(0, added.length - 1);
|
|
1242
|
+
} else {
|
|
1243
|
+
cursor = Math.max(0, cursor - 1);
|
|
1244
|
+
}
|
|
1245
|
+
break;
|
|
1246
|
+
case "down":
|
|
1247
|
+
if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
|
|
1248
|
+
addCursor = true;
|
|
1249
|
+
} else if (!addCursor) {
|
|
1250
|
+
cursor = Math.min(added.length - 1, cursor + 1);
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
case "space": {
|
|
1254
|
+
if (addCursor || filter.trim().length > 0 && isNewDir()) {
|
|
1255
|
+
if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
|
|
1256
|
+
const trimmed = filter.trim();
|
|
1257
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
1258
|
+
if (!err) {
|
|
1259
|
+
added.push(trimmed);
|
|
1260
|
+
filter = "";
|
|
1261
|
+
addCursor = false;
|
|
1262
|
+
cursor = 0;
|
|
1263
|
+
}
|
|
1264
|
+
} else if (added.length > 0 && !isNewDir()) {
|
|
1265
|
+
clampCursor();
|
|
1266
|
+
added.splice(cursor, 1);
|
|
1267
|
+
if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
|
|
1268
|
+
}
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1272
1271
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1272
|
+
});
|
|
1273
|
+
prompt.on("finalize", () => {
|
|
1274
|
+
if (prompt.state === "submit") {
|
|
1275
|
+
prompt.value = [...added];
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
const result = await prompt.prompt();
|
|
1279
|
+
if (isCancel7(result)) return null;
|
|
1280
|
+
return result;
|
|
1277
1281
|
}
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1282
|
+
async function promptSingleAppDir(params) {
|
|
1283
|
+
const { existingDirs, cwd } = params;
|
|
1284
|
+
const input = await p9.text({
|
|
1285
|
+
message: "App directory to add",
|
|
1286
|
+
placeholder: "apps/web",
|
|
1287
|
+
validate(val) {
|
|
1288
|
+
const err = validateAppDirPath(val ?? "", existingDirs, { cwd });
|
|
1289
|
+
if (err) return err;
|
|
1290
|
+
if (!val) return "Directory is required";
|
|
1291
|
+
return void 0;
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
if (p9.isCancel(input)) return null;
|
|
1295
|
+
return input;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/utils/branch-select.ts
|
|
1299
|
+
import { execSync as execSync4 } from "child_process";
|
|
1300
|
+
import { isCancel as isCancel9, Prompt as Prompt2 } from "@clack/core";
|
|
1301
|
+
var S_BAR2 = "\u2502";
|
|
1302
|
+
var S_BAR_END2 = "\u2514";
|
|
1303
|
+
var S_ACTIVE2 = "\u25C6";
|
|
1304
|
+
var S_SUBMIT2 = "\u25C6";
|
|
1305
|
+
var S_CANCEL2 = "\u25A0";
|
|
1306
|
+
var S_ERROR2 = "\u25B2";
|
|
1307
|
+
function symbol2(state) {
|
|
1308
|
+
switch (state) {
|
|
1309
|
+
case "submit":
|
|
1310
|
+
return grn(S_SUBMIT2);
|
|
1311
|
+
case "cancel":
|
|
1312
|
+
return red(S_CANCEL2);
|
|
1313
|
+
case "error":
|
|
1314
|
+
return ylw(S_ERROR2);
|
|
1315
|
+
default:
|
|
1316
|
+
return active(S_ACTIVE2);
|
|
1287
1317
|
}
|
|
1288
|
-
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
1289
1318
|
}
|
|
1290
|
-
function
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1319
|
+
function detectGitBranches(cwd) {
|
|
1320
|
+
const workDir = cwd ?? process.cwd();
|
|
1321
|
+
try {
|
|
1322
|
+
const localOut = execSync4("git branch", {
|
|
1323
|
+
cwd: workDir,
|
|
1324
|
+
stdio: "pipe"
|
|
1325
|
+
}).toString();
|
|
1326
|
+
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
1327
|
+
let remoteBranches = [];
|
|
1328
|
+
try {
|
|
1329
|
+
const remoteOut = execSync4("git branch -r", {
|
|
1330
|
+
cwd: workDir,
|
|
1331
|
+
stdio: "pipe"
|
|
1332
|
+
}).toString();
|
|
1333
|
+
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
1337
|
+
let defaultBranch = "main";
|
|
1338
|
+
try {
|
|
1339
|
+
const ref = execSync4("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
1340
|
+
cwd: workDir,
|
|
1341
|
+
stdio: "pipe"
|
|
1342
|
+
}).toString().trim();
|
|
1343
|
+
defaultBranch = ref.split("/").pop() ?? "main";
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
return {
|
|
1347
|
+
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
1348
|
+
defaultBranch
|
|
1349
|
+
};
|
|
1350
|
+
} catch {
|
|
1351
|
+
return { branches: ["main"], defaultBranch: "main" };
|
|
1302
1352
|
}
|
|
1303
|
-
return {
|
|
1304
|
-
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
1305
|
-
repoRoot
|
|
1306
|
-
};
|
|
1307
1353
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
if (!
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
return
|
|
1354
|
+
var INVALID_CHARS = /[\s?^~:[\]\\]/;
|
|
1355
|
+
function validateBranchPattern(pattern) {
|
|
1356
|
+
const t = pattern.trim();
|
|
1357
|
+
if (!t) return "Pattern cannot be empty";
|
|
1358
|
+
if (INVALID_CHARS.test(t))
|
|
1359
|
+
return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
1360
|
+
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
1361
|
+
if (t.includes("//")) return "Cannot contain //";
|
|
1362
|
+
return null;
|
|
1317
1363
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const { organizations, canCreateOrganization } = result;
|
|
1324
|
-
if (organizations.length === 0) {
|
|
1325
|
-
return { action: "create" };
|
|
1326
|
-
}
|
|
1327
|
-
const options = organizations.map((org) => ({
|
|
1328
|
-
value: org.id,
|
|
1329
|
-
label: org.name,
|
|
1330
|
-
hint: [
|
|
1331
|
-
org.projectCount > 0 ? `${org.projectCount} project${org.projectCount !== 1 ? "s" : ""}` : "",
|
|
1332
|
-
org.connectionLabel ? `GitHub: ${org.connectionLabel}` : ""
|
|
1333
|
-
].filter(Boolean).join(" \xB7 ") || void 0
|
|
1364
|
+
var MAX_VISIBLE = 10;
|
|
1365
|
+
function buildItems(branches, defaultBranch, customPatterns) {
|
|
1366
|
+
const items = branches.map((b) => ({
|
|
1367
|
+
value: b,
|
|
1368
|
+
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
1334
1369
|
}));
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
message: "Select workspace",
|
|
1340
|
-
options
|
|
1341
|
-
});
|
|
1342
|
-
if (p5.isCancel(selected)) {
|
|
1343
|
-
return { action: "cancelled" };
|
|
1344
|
-
}
|
|
1345
|
-
if (selected === "create") {
|
|
1346
|
-
return { action: "create" };
|
|
1347
|
-
}
|
|
1348
|
-
const organization = organizations.find((org) => org.id === selected);
|
|
1349
|
-
if (!organization) {
|
|
1350
|
-
return { action: "cancelled" };
|
|
1370
|
+
for (const pt of customPatterns) {
|
|
1371
|
+
if (!branches.includes(pt)) {
|
|
1372
|
+
items.push({ value: pt, label: pt, isCustom: true });
|
|
1373
|
+
}
|
|
1351
1374
|
}
|
|
1352
|
-
return
|
|
1375
|
+
return items;
|
|
1353
1376
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
async function sleep(ms) {
|
|
1359
|
-
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1377
|
+
function filterItems(items, query) {
|
|
1378
|
+
if (!query.trim()) return items;
|
|
1379
|
+
const lower = query.toLowerCase();
|
|
1380
|
+
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
1360
1381
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1382
|
+
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
1383
|
+
const lines = [info(S_BAR2)];
|
|
1384
|
+
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
1385
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
1386
|
+
const item = filtered[i];
|
|
1387
|
+
const isCursor = i === cursor && !addCursor;
|
|
1388
|
+
const isChecked = selected.has(item.value);
|
|
1389
|
+
const icon = isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
|
|
1390
|
+
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
1391
|
+
if (isCursor) label = bld(label);
|
|
1392
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
1364
1393
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
command = "xdg-open";
|
|
1375
|
-
args = [url];
|
|
1394
|
+
const trimmed = filter.trim();
|
|
1395
|
+
const isNewPattern = trimmed.length > 0 && !filtered.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
1396
|
+
if (isNewPattern) {
|
|
1397
|
+
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
1398
|
+
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
1399
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
1400
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
1401
|
+
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
1402
|
+
lines.push(dim(`${S_BAR2} No branches detected`));
|
|
1376
1403
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1404
|
+
const hidden = filtered.length - (end - scrollOffset);
|
|
1405
|
+
if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
|
|
1406
|
+
return lines.join("\n");
|
|
1407
|
+
}
|
|
1408
|
+
async function filterableBranchSelect(params) {
|
|
1409
|
+
const { message, branches, defaultBranch } = params;
|
|
1410
|
+
const optional = params.optional ?? false;
|
|
1411
|
+
const excludedSet = new Set(params.excludedPatterns ?? []);
|
|
1412
|
+
let filter = "";
|
|
1413
|
+
let cursor = 0;
|
|
1414
|
+
let scrollOffset = 0;
|
|
1415
|
+
let addCursor = false;
|
|
1416
|
+
const customPatterns = [];
|
|
1417
|
+
const selected = new Set(params.initialValues ?? [defaultBranch]);
|
|
1418
|
+
const getItems = () => buildItems(branches, defaultBranch, customPatterns);
|
|
1419
|
+
const getFiltered = () => filterItems(getItems(), filter);
|
|
1420
|
+
const isNewPattern = () => {
|
|
1421
|
+
const t = filter.trim();
|
|
1422
|
+
if (!t) return false;
|
|
1423
|
+
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
1424
|
+
};
|
|
1425
|
+
const clampCursor = (filtered) => {
|
|
1426
|
+
const hasAdd = isNewPattern();
|
|
1427
|
+
const max = filtered.length - 1 + (hasAdd ? 1 : 0);
|
|
1428
|
+
if (cursor > max && !addCursor) cursor = Math.max(0, max);
|
|
1429
|
+
if (!addCursor) {
|
|
1430
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
1431
|
+
if (cursor >= scrollOffset + MAX_VISIBLE)
|
|
1432
|
+
scrollOffset = cursor - MAX_VISIBLE + 1;
|
|
1433
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
const prompt = new Prompt2(
|
|
1437
|
+
{
|
|
1438
|
+
validate() {
|
|
1439
|
+
if (!optional && selected.size === 0)
|
|
1440
|
+
return "At least one branch is required.";
|
|
1441
|
+
return void 0;
|
|
1442
|
+
},
|
|
1443
|
+
render() {
|
|
1444
|
+
const filtered = getFiltered();
|
|
1445
|
+
clampCursor(filtered);
|
|
1446
|
+
const hdr = `${dim(S_BAR2)}
|
|
1447
|
+
${symbol2(this.state)} ${message}
|
|
1448
|
+
`;
|
|
1449
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter \xB7 type a custom pattern to add it");
|
|
1450
|
+
const trimmedFilter = filter.trim();
|
|
1451
|
+
const footer = (() => {
|
|
1452
|
+
if (trimmedFilter.length > 0 && isNewPattern()) {
|
|
1453
|
+
return dim(`${S_BAR2} Space to add "${trimmedFilter}" \xB7 \u2191\u2193 navigate \xB7 Enter to confirm`);
|
|
1454
|
+
}
|
|
1455
|
+
if (selected.size > 0) {
|
|
1456
|
+
return dim(`${S_BAR2} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`);
|
|
1457
|
+
}
|
|
1458
|
+
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`);
|
|
1459
|
+
})();
|
|
1460
|
+
switch (this.state) {
|
|
1461
|
+
case "submit": {
|
|
1462
|
+
const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
|
|
1463
|
+
return `${hdr}${dim(S_BAR2)} ${summary}`;
|
|
1464
|
+
}
|
|
1465
|
+
case "cancel":
|
|
1466
|
+
return `${hdr}${dim(S_BAR2)}`;
|
|
1467
|
+
case "error":
|
|
1468
|
+
return [
|
|
1469
|
+
hdr.trimEnd(),
|
|
1470
|
+
`${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
1471
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
1472
|
+
footer,
|
|
1473
|
+
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
1474
|
+
""
|
|
1475
|
+
].join("\n");
|
|
1476
|
+
default:
|
|
1477
|
+
return [
|
|
1478
|
+
hdr.trimEnd(),
|
|
1479
|
+
`${info(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
1480
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
1481
|
+
footer,
|
|
1482
|
+
`${info(S_BAR_END2)}`,
|
|
1483
|
+
""
|
|
1484
|
+
].join("\n");
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
false
|
|
1489
|
+
);
|
|
1490
|
+
prompt.on("key", (key) => {
|
|
1491
|
+
if (!key || key === " ") return;
|
|
1492
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
1493
|
+
if (cp === 127 || cp === 8) {
|
|
1494
|
+
filter = filter.slice(0, -1);
|
|
1495
|
+
cursor = 0;
|
|
1496
|
+
scrollOffset = 0;
|
|
1497
|
+
addCursor = false;
|
|
1498
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
1499
|
+
filter += key;
|
|
1500
|
+
cursor = 0;
|
|
1501
|
+
scrollOffset = 0;
|
|
1502
|
+
addCursor = false;
|
|
1503
|
+
}
|
|
1504
|
+
if (isNewPattern()) {
|
|
1505
|
+
addCursor = true;
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
prompt.on("cursor", (action) => {
|
|
1509
|
+
const filtered = getFiltered();
|
|
1510
|
+
const hasAdd = isNewPattern();
|
|
1511
|
+
switch (action) {
|
|
1512
|
+
case "up":
|
|
1513
|
+
if (addCursor) {
|
|
1514
|
+
addCursor = false;
|
|
1515
|
+
cursor = Math.max(0, filtered.length - 1);
|
|
1516
|
+
} else cursor = Math.max(0, cursor - 1);
|
|
1517
|
+
break;
|
|
1518
|
+
case "down":
|
|
1519
|
+
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
1520
|
+
addCursor = true;
|
|
1521
|
+
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1522
|
+
break;
|
|
1523
|
+
case "space": {
|
|
1524
|
+
const t = filter.trim();
|
|
1525
|
+
if (addCursor || t.length > 0 && isNewPattern()) {
|
|
1526
|
+
const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
|
|
1527
|
+
if (!err) {
|
|
1528
|
+
customPatterns.push(t);
|
|
1529
|
+
selected.add(t);
|
|
1530
|
+
filter = "";
|
|
1531
|
+
cursor = 0;
|
|
1532
|
+
scrollOffset = 0;
|
|
1533
|
+
addCursor = false;
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
const item = filtered[cursor];
|
|
1537
|
+
if (item) {
|
|
1538
|
+
if (selected.has(item.value)) selected.delete(item.value);
|
|
1539
|
+
else selected.add(item.value);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
prompt.on("finalize", () => {
|
|
1547
|
+
if (prompt.state === "submit") {
|
|
1548
|
+
prompt.value = Array.from(selected);
|
|
1403
1549
|
}
|
|
1404
1550
|
});
|
|
1551
|
+
const result = await prompt.prompt();
|
|
1552
|
+
if (isCancel9(result)) return null;
|
|
1553
|
+
return result;
|
|
1405
1554
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1555
|
+
|
|
1556
|
+
// src/utils/locale-search.ts
|
|
1557
|
+
import { isCancel as isCancel10, Prompt as Prompt3 } from "@clack/core";
|
|
1558
|
+
import * as p10 from "@clack/prompts";
|
|
1559
|
+
var S_BAR3 = "\u2502";
|
|
1560
|
+
var S_BAR_END3 = "\u2514";
|
|
1561
|
+
var S_ACTIVE3 = "\u25C6";
|
|
1562
|
+
var S_SUBMIT3 = "\u25C6";
|
|
1563
|
+
var S_CANCEL3 = "\u25A0";
|
|
1564
|
+
var S_ERROR3 = "\u25B2";
|
|
1565
|
+
function symbol3(state) {
|
|
1566
|
+
switch (state) {
|
|
1567
|
+
case "submit":
|
|
1568
|
+
return grn(S_SUBMIT3);
|
|
1569
|
+
case "cancel":
|
|
1570
|
+
return red(S_CANCEL3);
|
|
1571
|
+
case "error":
|
|
1572
|
+
return ylw(S_ERROR3);
|
|
1573
|
+
default:
|
|
1574
|
+
return active(S_ACTIVE3);
|
|
1575
|
+
}
|
|
1409
1576
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1577
|
+
var MAX_VISIBLE2 = 12;
|
|
1578
|
+
function filterLocales(options, query) {
|
|
1579
|
+
if (!query.trim()) return options;
|
|
1580
|
+
const lower = query.toLowerCase();
|
|
1581
|
+
return options.filter(
|
|
1582
|
+
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1583
|
+
);
|
|
1412
1584
|
}
|
|
1413
|
-
function
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1585
|
+
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
1586
|
+
const isMulti = selected !== null;
|
|
1587
|
+
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
1588
|
+
const visibleLines = [info(S_BAR3)];
|
|
1589
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
1590
|
+
const opt = filtered[i];
|
|
1591
|
+
const isCursor = i === cursor;
|
|
1592
|
+
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
1593
|
+
const icon = isMulti ? isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
1594
|
+
visibleLines.push(
|
|
1595
|
+
`${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const hidden = filtered.length - (end - scrollOffset);
|
|
1599
|
+
if (hidden > 0) visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
|
|
1600
|
+
if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
|
|
1601
|
+
return visibleLines.join("\n");
|
|
1417
1602
|
}
|
|
1418
|
-
function
|
|
1419
|
-
const {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1603
|
+
async function runFilterablePrompt(opts) {
|
|
1604
|
+
const { message, options, multi } = opts;
|
|
1605
|
+
let filter = "";
|
|
1606
|
+
let cursor = 0;
|
|
1607
|
+
let scrollOffset = 0;
|
|
1608
|
+
const selected = new Set(multi ? opts.initialValues ?? [] : []);
|
|
1609
|
+
if (!multi && opts.initialValue) {
|
|
1610
|
+
const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
|
|
1611
|
+
if (idx >= 0) cursor = idx;
|
|
1426
1612
|
}
|
|
1427
|
-
const
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1613
|
+
const getFiltered = () => filterLocales(options, filter);
|
|
1614
|
+
const clampCursor = (filtered) => {
|
|
1615
|
+
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
|
|
1616
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
1617
|
+
if (cursor >= scrollOffset + MAX_VISIBLE2)
|
|
1618
|
+
scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
1619
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
1620
|
+
};
|
|
1621
|
+
const prompt = new Prompt3(
|
|
1622
|
+
{
|
|
1623
|
+
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
1624
|
+
validate() {
|
|
1625
|
+
const f = getFiltered();
|
|
1626
|
+
if (multi && selected.size === 0)
|
|
1627
|
+
return "At least one target language is required.";
|
|
1628
|
+
if (!multi && !f[cursor]) return "Please select a language.";
|
|
1629
|
+
return void 0;
|
|
1630
|
+
},
|
|
1631
|
+
render() {
|
|
1632
|
+
const filtered = getFiltered();
|
|
1633
|
+
clampCursor(filtered);
|
|
1634
|
+
const hdr = `${dim(S_BAR3)}
|
|
1635
|
+
${symbol3(this.state)} ${message}
|
|
1636
|
+
`;
|
|
1637
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter");
|
|
1638
|
+
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`);
|
|
1639
|
+
switch (this.state) {
|
|
1640
|
+
case "submit": {
|
|
1641
|
+
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 ?? "";
|
|
1642
|
+
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
1643
|
+
}
|
|
1644
|
+
case "cancel":
|
|
1645
|
+
return `${hdr}${dim(S_BAR3)}`;
|
|
1646
|
+
case "error":
|
|
1647
|
+
return [
|
|
1648
|
+
hdr.trimEnd(),
|
|
1649
|
+
`${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
1650
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
1651
|
+
footer,
|
|
1652
|
+
`${ylw(S_BAR_END3)} ${ylw(this.error)}`,
|
|
1653
|
+
""
|
|
1654
|
+
].join("\n");
|
|
1655
|
+
default:
|
|
1656
|
+
return [
|
|
1657
|
+
hdr.trimEnd(),
|
|
1658
|
+
`${info(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
1659
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
1660
|
+
footer,
|
|
1661
|
+
`${info(S_BAR_END3)}`,
|
|
1662
|
+
""
|
|
1663
|
+
].join("\n");
|
|
1664
|
+
}
|
|
1439
1665
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1666
|
+
},
|
|
1667
|
+
false
|
|
1668
|
+
// trackValue=false — we manage value manually
|
|
1669
|
+
);
|
|
1670
|
+
prompt.on("key", (key) => {
|
|
1671
|
+
if (!key || key === " ") return;
|
|
1672
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
1673
|
+
if (cp === 127 || cp === 8) {
|
|
1674
|
+
filter = filter.slice(0, -1);
|
|
1675
|
+
cursor = 0;
|
|
1676
|
+
scrollOffset = 0;
|
|
1677
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
1678
|
+
filter += key;
|
|
1679
|
+
cursor = 0;
|
|
1680
|
+
scrollOffset = 0;
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
prompt.on("cursor", (action) => {
|
|
1684
|
+
const filtered = getFiltered();
|
|
1685
|
+
switch (action) {
|
|
1686
|
+
case "up":
|
|
1687
|
+
cursor = Math.max(0, cursor - 1);
|
|
1688
|
+
break;
|
|
1689
|
+
case "down":
|
|
1690
|
+
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
1691
|
+
break;
|
|
1692
|
+
case "space":
|
|
1693
|
+
if (multi) {
|
|
1694
|
+
const opt = filtered[cursor];
|
|
1695
|
+
if (opt) {
|
|
1696
|
+
if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
|
|
1697
|
+
else selected.add(opt.bcp47);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
if (!multi) {
|
|
1703
|
+
const opt = getFiltered()[cursor];
|
|
1704
|
+
prompt.value = opt?.bcp47 ?? null;
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
prompt.on("finalize", () => {
|
|
1708
|
+
if (prompt.state === "submit") {
|
|
1709
|
+
if (multi) {
|
|
1710
|
+
prompt.value = Array.from(selected);
|
|
1711
|
+
} else {
|
|
1712
|
+
const f = getFiltered();
|
|
1713
|
+
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
1445
1714
|
}
|
|
1446
|
-
installSpinner.stop(`Installed ${allPackages.join(", ")}`);
|
|
1447
|
-
} catch {
|
|
1448
|
-
installSpinner.stop("Package installation failed");
|
|
1449
|
-
const cmds = [
|
|
1450
|
-
devPackages.length > 0 ? buildInstallCommand(detection.packageManager, devPackages, true) : null,
|
|
1451
|
-
runtimePackages.length > 0 ? buildInstallCommand(
|
|
1452
|
-
detection.packageManager,
|
|
1453
|
-
runtimePackages,
|
|
1454
|
-
false
|
|
1455
|
-
) : null
|
|
1456
|
-
].filter(Boolean).join(" && ");
|
|
1457
|
-
p6.log.warn(`Run manually: ${highlight(cmds)}`);
|
|
1458
1715
|
}
|
|
1459
|
-
} else if (detection.ecosystem) {
|
|
1460
|
-
p6.log.info(`Packages: ${chalk5.green("already installed")}`);
|
|
1461
|
-
}
|
|
1462
|
-
const snippets = getSetupSnippets({
|
|
1463
|
-
framework: detection.framework,
|
|
1464
|
-
ecosystem: detection.ecosystem,
|
|
1465
|
-
sourceLocale,
|
|
1466
|
-
targetBranches
|
|
1467
1716
|
});
|
|
1468
|
-
const
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1717
|
+
const result = await prompt.prompt();
|
|
1718
|
+
if (isCancel10(result)) return null;
|
|
1719
|
+
return result;
|
|
1720
|
+
}
|
|
1721
|
+
async function searchSelectLocale(options, message, initialValue) {
|
|
1722
|
+
const result = await runFilterablePrompt({
|
|
1723
|
+
message,
|
|
1724
|
+
options,
|
|
1725
|
+
multi: false,
|
|
1726
|
+
initialValue
|
|
1727
|
+
});
|
|
1728
|
+
return typeof result === "string" ? result : null;
|
|
1729
|
+
}
|
|
1730
|
+
async function searchMultiSelectLocales(options, message, initialValues) {
|
|
1731
|
+
const result = await runFilterablePrompt({
|
|
1732
|
+
message,
|
|
1733
|
+
options,
|
|
1734
|
+
multi: true,
|
|
1735
|
+
initialValues
|
|
1736
|
+
});
|
|
1737
|
+
if (result === null) return null;
|
|
1738
|
+
const picks = result;
|
|
1739
|
+
if (picks.length === 0) {
|
|
1740
|
+
p10.log.warn(
|
|
1741
|
+
"At least one target language is required. Please select at least one."
|
|
1742
|
+
);
|
|
1743
|
+
return searchMultiSelectLocales(options, message, initialValues);
|
|
1482
1744
|
}
|
|
1483
|
-
|
|
1484
|
-
label: "wrap translatable text",
|
|
1485
|
-
hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
|
|
1486
|
-
code: snippets.wrapStep.code
|
|
1487
|
-
});
|
|
1488
|
-
p6.log.message("");
|
|
1489
|
-
p6.log.message(chalk5.bold("Finish setup in your code"));
|
|
1490
|
-
p6.log.message("");
|
|
1491
|
-
for (let i = 0; i < steps.length; i++) {
|
|
1492
|
-
const step = steps[i];
|
|
1493
|
-
p6.log.step(`${chalk5.bold(step.label)} ${chalk5.dim(`\u2014 ${step.hint}`)}`);
|
|
1494
|
-
printCodeBlock(step.code);
|
|
1495
|
-
if (i < steps.length - 1) p6.log.message("");
|
|
1496
|
-
}
|
|
1497
|
-
p6.log.message("");
|
|
1498
|
-
const branchList = targetBranches.length > 0 ? targetBranches.map((b) => highlight(b)).join(" or ") : highlight("your target branch");
|
|
1499
|
-
p6.log.success(`Push to ${branchList} to trigger your first translation run.`);
|
|
1500
|
-
p6.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
|
|
1745
|
+
return picks;
|
|
1501
1746
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1747
|
+
|
|
1748
|
+
// src/utils/project-create.ts
|
|
1749
|
+
function buildLocaleOptions(locales) {
|
|
1750
|
+
return locales.map((l) => ({
|
|
1751
|
+
bcp47: l.code,
|
|
1752
|
+
label: `${l.name} \u2014 ${l.code}`
|
|
1753
|
+
}));
|
|
1754
|
+
}
|
|
1755
|
+
function buildLanguageOptions(locales) {
|
|
1756
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
1757
|
+
for (const l of locales) {
|
|
1758
|
+
const family = l.code.split("-")[0].toLowerCase();
|
|
1759
|
+
const opt = { bcp47: l.code, label: `${l.name} \u2014 ${l.code}` };
|
|
1760
|
+
const existing = byFamily.get(family);
|
|
1761
|
+
if (!existing || l.code.length < existing.bcp47.length) {
|
|
1762
|
+
byFamily.set(family, opt);
|
|
1515
1763
|
}
|
|
1516
|
-
writeFileSync2(envPath, updated);
|
|
1517
|
-
return true;
|
|
1518
|
-
} catch {
|
|
1519
|
-
return false;
|
|
1520
1764
|
}
|
|
1765
|
+
return Array.from(byFamily.values());
|
|
1521
1766
|
}
|
|
1522
|
-
function
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1767
|
+
async function runProjectCreate(params) {
|
|
1768
|
+
const { api, userToken, organizationId, repoCanonical, repoRoot } = params;
|
|
1769
|
+
const projectName = (params.defaultName ?? "my-project").trim();
|
|
1770
|
+
p11.log.success(`Project: ${chalk10.bold(projectName)}`);
|
|
1771
|
+
let sourceLocales;
|
|
1772
|
+
try {
|
|
1773
|
+
({ sourceLocales } = await api.listLocales(userToken));
|
|
1774
|
+
} catch {
|
|
1775
|
+
p11.log.error(
|
|
1776
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1777
|
+
);
|
|
1778
|
+
return null;
|
|
1531
1779
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
const written = writeVocoderConfig({
|
|
1538
|
-
targetBranches,
|
|
1539
|
-
appId: app.appId,
|
|
1540
|
-
cwd: dir,
|
|
1541
|
-
useTypeScript
|
|
1542
|
-
});
|
|
1543
|
-
if (written) {
|
|
1544
|
-
const displayPath = app.appDir ? `${app.appDir}/${written}` : written;
|
|
1545
|
-
p6.log.success(`Created ${highlight(displayPath)}`);
|
|
1546
|
-
} else if (!findExistingConfig(dir)) {
|
|
1547
|
-
const ext = useTypeScript ? "ts" : "js";
|
|
1548
|
-
p6.log.warn(
|
|
1549
|
-
`Could not write ${app.appDir ? `${app.appDir}/` : ""}vocoder.config.${ext} \u2014 create it manually.`
|
|
1550
|
-
);
|
|
1551
|
-
}
|
|
1780
|
+
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
1781
|
+
const appDirs = await collectAppDirs({ cwd: repoRoot, maxDirs: params.maxAppDirs });
|
|
1782
|
+
if (appDirs === null) return null;
|
|
1783
|
+
if (appDirs.length > 0) {
|
|
1784
|
+
p11.log.success(`App directories: ${appDirs.map((d) => chalk10.bold(d)).join(", ")}`);
|
|
1552
1785
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
(max, line) => Math.max(max, line.length),
|
|
1558
|
-
0
|
|
1559
|
-
);
|
|
1560
|
-
const bar = chalk5.gray("\u2502");
|
|
1561
|
-
const pad = (s) => s + " ".repeat(maxLen - s.length);
|
|
1562
|
-
process.stdout.write(`${chalk5.gray("\u2502")}
|
|
1563
|
-
`);
|
|
1564
|
-
process.stdout.write(
|
|
1565
|
-
`${chalk5.gray("\u2502")} ${chalk5.gray(`\u250C${"\u2500".repeat(maxLen + 2)}\u2510`)}
|
|
1566
|
-
`
|
|
1786
|
+
const sourceLocale = await searchSelectLocale(
|
|
1787
|
+
languageOptions,
|
|
1788
|
+
"Source language (the language your code is written in)",
|
|
1789
|
+
params.defaultSourceLocale ?? "en"
|
|
1567
1790
|
);
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1791
|
+
if (sourceLocale === null) return null;
|
|
1792
|
+
let compatibleTargets;
|
|
1793
|
+
try {
|
|
1794
|
+
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
1795
|
+
} catch {
|
|
1796
|
+
p11.log.error(
|
|
1797
|
+
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
1798
|
+
);
|
|
1799
|
+
return null;
|
|
1571
1800
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1801
|
+
const localeOptions = buildLocaleOptions(compatibleTargets);
|
|
1802
|
+
const targetOptions = localeOptions.filter(
|
|
1803
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1575
1804
|
);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1805
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
1806
|
+
targetOptions,
|
|
1807
|
+
"Target languages (languages to translate into)"
|
|
1808
|
+
);
|
|
1809
|
+
if (targetLocales === null) return null;
|
|
1810
|
+
if (targetLocales.length === 0) {
|
|
1811
|
+
p11.log.warn(
|
|
1812
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1813
|
+
);
|
|
1584
1814
|
}
|
|
1585
|
-
const
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
if (!shouldOpen) {
|
|
1605
|
-
server?.close();
|
|
1606
|
-
p6.cancel("Setup cancelled.");
|
|
1607
|
-
return null;
|
|
1608
|
-
} else {
|
|
1609
|
-
const opened = await tryOpenBrowser2(browserUrl);
|
|
1610
|
-
if (!opened) {
|
|
1611
|
-
p6.note(browserUrl, "Sign In");
|
|
1612
|
-
p6.log.info("Open the URL above manually to continue.");
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1815
|
+
const detected = detectGitBranches();
|
|
1816
|
+
const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
|
|
1817
|
+
let pushBranches = [];
|
|
1818
|
+
{
|
|
1819
|
+
let initial = initialBranches;
|
|
1820
|
+
while (pushBranches.length === 0) {
|
|
1821
|
+
const result2 = await filterableBranchSelect({
|
|
1822
|
+
message: "Which branches should trigger translations?",
|
|
1823
|
+
branches: detected.branches,
|
|
1824
|
+
defaultBranch: detected.defaultBranch,
|
|
1825
|
+
initialValues: initial
|
|
1826
|
+
});
|
|
1827
|
+
if (result2 === null) return null;
|
|
1828
|
+
if (result2.length === 0) {
|
|
1829
|
+
p11.log.warn(
|
|
1830
|
+
"At least one branch is required. Please select at least one."
|
|
1831
|
+
);
|
|
1832
|
+
initial = [detected.defaultBranch];
|
|
1615
1833
|
} else {
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
} else {
|
|
1619
|
-
let isLinkFlow = false;
|
|
1620
|
-
if (!options.yes) {
|
|
1621
|
-
const connectChoice = await p6.select({
|
|
1622
|
-
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
1623
|
-
options: [
|
|
1624
|
-
{
|
|
1625
|
-
value: "install",
|
|
1626
|
-
label: "Install GitHub App",
|
|
1627
|
-
hint: "new user"
|
|
1628
|
-
},
|
|
1629
|
-
{
|
|
1630
|
-
value: "link",
|
|
1631
|
-
label: "Already installed? Link your account",
|
|
1632
|
-
hint: "returning user"
|
|
1633
|
-
}
|
|
1634
|
-
]
|
|
1635
|
-
});
|
|
1636
|
-
if (p6.isCancel(connectChoice)) {
|
|
1637
|
-
server?.close();
|
|
1638
|
-
p6.cancel("Setup cancelled.");
|
|
1639
|
-
return null;
|
|
1640
|
-
}
|
|
1641
|
-
isLinkFlow = connectChoice === "link";
|
|
1642
|
-
}
|
|
1643
|
-
let urlToOpen = browserUrl;
|
|
1644
|
-
if (isLinkFlow) {
|
|
1645
|
-
try {
|
|
1646
|
-
const linkSession = await api.startCliGitHubLinkSession(
|
|
1647
|
-
session.sessionId,
|
|
1648
|
-
server?.port
|
|
1649
|
-
);
|
|
1650
|
-
urlToOpen = linkSession.oauthUrl;
|
|
1651
|
-
} catch {
|
|
1652
|
-
urlToOpen = browserUrl;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
const opened = await tryOpenBrowser2(urlToOpen);
|
|
1656
|
-
if (!opened) {
|
|
1657
|
-
p6.log.warn("Could not open your browser automatically.");
|
|
1658
|
-
p6.note(urlToOpen, "GitHub");
|
|
1659
|
-
p6.log.info("Open the URL above to continue.");
|
|
1834
|
+
pushBranches = result2;
|
|
1660
1835
|
}
|
|
1661
1836
|
}
|
|
1662
1837
|
}
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1838
|
+
const targetBranches = pushBranches;
|
|
1839
|
+
const result = await api.createProject(userToken, {
|
|
1840
|
+
organizationId,
|
|
1841
|
+
name: projectName,
|
|
1842
|
+
sourceLocale,
|
|
1843
|
+
targetLocales,
|
|
1844
|
+
targetBranches,
|
|
1845
|
+
appDirs,
|
|
1846
|
+
repoCanonical
|
|
1847
|
+
});
|
|
1848
|
+
p11.log.success(`Project ${chalk10.bold(result.projectName)} created!`);
|
|
1849
|
+
return {
|
|
1850
|
+
projectId: result.projectId,
|
|
1851
|
+
projectName: result.projectName,
|
|
1852
|
+
apiKey: result.apiKey,
|
|
1853
|
+
sourceLocale,
|
|
1854
|
+
targetLocales,
|
|
1855
|
+
targetBranches,
|
|
1856
|
+
repositoryBound: result.repositoryBound,
|
|
1857
|
+
configureUrl: result.configureUrl,
|
|
1858
|
+
apps: result.apps
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
async function runAppCreate(params) {
|
|
1862
|
+
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
1863
|
+
const existingDirs = params.existingApps.map((a) => a.appDir);
|
|
1864
|
+
const appDir = await promptSingleAppDir({ existingDirs });
|
|
1865
|
+
if (appDir === null) return null;
|
|
1866
|
+
if (appDir) {
|
|
1867
|
+
p11.log.success(`App directory: ${chalk10.bold(appDir)}`);
|
|
1868
|
+
}
|
|
1869
|
+
let sourceLocales;
|
|
1870
|
+
try {
|
|
1871
|
+
({ sourceLocales } = await api.listLocales(userToken));
|
|
1872
|
+
} catch {
|
|
1873
|
+
p11.log.error(
|
|
1874
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1875
|
+
);
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
1879
|
+
const sourceLocale = await searchSelectLocale(
|
|
1880
|
+
languageOptions,
|
|
1881
|
+
"Source language",
|
|
1882
|
+
"en"
|
|
1883
|
+
);
|
|
1884
|
+
if (sourceLocale === null) return null;
|
|
1885
|
+
let compatibleTargets;
|
|
1886
|
+
try {
|
|
1887
|
+
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
1888
|
+
} catch {
|
|
1889
|
+
p11.log.error(
|
|
1890
|
+
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
1891
|
+
);
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
const targetOptions = buildLocaleOptions(compatibleTargets).filter(
|
|
1895
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1896
|
+
);
|
|
1897
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
1898
|
+
targetOptions,
|
|
1899
|
+
"Target languages"
|
|
1900
|
+
);
|
|
1901
|
+
if (targetLocales === null) return null;
|
|
1902
|
+
if (targetLocales.length === 0) {
|
|
1903
|
+
p11.log.warn(
|
|
1904
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const detectedApp = detectGitBranches();
|
|
1908
|
+
let appPushBranches = [];
|
|
1909
|
+
{
|
|
1910
|
+
let initial = [detectedApp.defaultBranch];
|
|
1911
|
+
while (appPushBranches.length === 0) {
|
|
1912
|
+
const result2 = await filterableBranchSelect({
|
|
1913
|
+
message: "Which branches should trigger translations?",
|
|
1914
|
+
branches: detectedApp.branches,
|
|
1915
|
+
defaultBranch: detectedApp.defaultBranch,
|
|
1916
|
+
initialValues: initial
|
|
1917
|
+
});
|
|
1918
|
+
if (result2 === null) return null;
|
|
1919
|
+
if (result2.length === 0) {
|
|
1920
|
+
p11.log.warn("At least one branch is required.");
|
|
1921
|
+
initial = [detectedApp.defaultBranch];
|
|
1922
|
+
} else {
|
|
1923
|
+
appPushBranches = result2;
|
|
1679
1924
|
}
|
|
1680
|
-
if (!stopPolling) await sleep(2e3);
|
|
1681
1925
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
});
|
|
1692
|
-
sessionPoll.then((result) => {
|
|
1693
|
-
if (done || result === null) return;
|
|
1694
|
-
if (result.status === "complete" || result.status === "failed") {
|
|
1695
|
-
done = true;
|
|
1696
|
-
resolve3({
|
|
1697
|
-
kind: "poll",
|
|
1698
|
-
result
|
|
1699
|
-
});
|
|
1700
|
-
}
|
|
1701
|
-
}).catch(() => {
|
|
1702
|
-
});
|
|
1703
|
-
setTimeout(
|
|
1704
|
-
() => {
|
|
1705
|
-
if (!done) {
|
|
1706
|
-
done = true;
|
|
1707
|
-
resolve3(null);
|
|
1708
|
-
}
|
|
1709
|
-
},
|
|
1710
|
-
Math.max(0, deadline - Date.now())
|
|
1711
|
-
);
|
|
1926
|
+
}
|
|
1927
|
+
const targetBranches = appPushBranches;
|
|
1928
|
+
const result = await api.createApp(userToken, {
|
|
1929
|
+
projectId,
|
|
1930
|
+
appDir,
|
|
1931
|
+
sourceLocale,
|
|
1932
|
+
targetLocales,
|
|
1933
|
+
targetBranches,
|
|
1934
|
+
repoCanonical: repoCanonical ?? ""
|
|
1712
1935
|
});
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1936
|
+
p11.log.success(
|
|
1937
|
+
`App ${chalk10.bold(appDir || "(root)")} added to ${chalk10.bold(projectName)}!`
|
|
1938
|
+
);
|
|
1939
|
+
return {
|
|
1940
|
+
projectId: result.projectId,
|
|
1941
|
+
projectName: result.projectName,
|
|
1942
|
+
appDir: result.appDir,
|
|
1943
|
+
appId: result.appId,
|
|
1944
|
+
sourceLocale,
|
|
1945
|
+
targetLocales,
|
|
1946
|
+
targetBranches
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/commands/init.ts
|
|
1951
|
+
import chalk11 from "chalk";
|
|
1952
|
+
import { config as loadEnv } from "dotenv";
|
|
1953
|
+
|
|
1954
|
+
// src/utils/git-identity.ts
|
|
1955
|
+
import { execSync as execSync5 } from "child_process";
|
|
1956
|
+
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
1957
|
+
function detectCommitSha() {
|
|
1958
|
+
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
1959
|
+
return process.env.VOCODER_COMMIT_SHA;
|
|
1960
|
+
}
|
|
1961
|
+
const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
|
|
1962
|
+
if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
|
|
1963
|
+
return safeExec("git rev-parse HEAD");
|
|
1964
|
+
}
|
|
1965
|
+
function safeExec(command) {
|
|
1966
|
+
try {
|
|
1967
|
+
const output = execSync5(command, {
|
|
1968
|
+
encoding: "utf-8",
|
|
1969
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1970
|
+
}).trim();
|
|
1971
|
+
return output.length > 0 ? output : null;
|
|
1972
|
+
} catch {
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function normalizePath(pathname) {
|
|
1977
|
+
const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
|
|
1978
|
+
if (!cleaned || !cleaned.includes("/")) {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
return cleaned;
|
|
1982
|
+
}
|
|
1983
|
+
function parseRemoteUrl(remoteUrl) {
|
|
1984
|
+
const trimmed = remoteUrl.trim();
|
|
1985
|
+
if (!trimmed) {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
if (!trimmed.includes("://")) {
|
|
1989
|
+
const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
|
|
1990
|
+
if (scpMatch) {
|
|
1991
|
+
const host = (scpMatch[1] || "").toLowerCase();
|
|
1992
|
+
const ownerRepoPath = normalizePath(scpMatch[2] || "");
|
|
1993
|
+
if (!host || !ownerRepoPath) {
|
|
1994
|
+
return null;
|
|
1728
1995
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1996
|
+
return { host, ownerRepoPath };
|
|
1997
|
+
}
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
try {
|
|
2001
|
+
const parsed = new URL(trimmed);
|
|
2002
|
+
const host = parsed.hostname.toLowerCase();
|
|
2003
|
+
const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
|
|
2004
|
+
if (!host || !ownerRepoPath) {
|
|
1732
2005
|
return null;
|
|
1733
2006
|
}
|
|
2007
|
+
return { host, ownerRepoPath };
|
|
2008
|
+
} catch {
|
|
2009
|
+
return null;
|
|
1734
2010
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
2011
|
+
}
|
|
2012
|
+
function toCanonical(host, ownerRepoPath) {
|
|
2013
|
+
if (host.includes("github.com")) {
|
|
2014
|
+
return `github:${ownerRepoPath.toLowerCase()}`;
|
|
2015
|
+
}
|
|
2016
|
+
if (host.includes("gitlab.com")) {
|
|
2017
|
+
return `gitlab:${ownerRepoPath.toLowerCase()}`;
|
|
2018
|
+
}
|
|
2019
|
+
if (host.includes("bitbucket.org")) {
|
|
2020
|
+
return `bitbucket:${ownerRepoPath.toLowerCase()}`;
|
|
2021
|
+
}
|
|
2022
|
+
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
2023
|
+
}
|
|
2024
|
+
function resolveGitRepositoryIdentity() {
|
|
2025
|
+
const remoteUrl = safeExec("git config --get remote.origin.url");
|
|
2026
|
+
if (!remoteUrl) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
2030
|
+
if (!parsed) {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
const repoRoot = safeExec("git rev-parse --show-toplevel");
|
|
2034
|
+
if (!repoRoot) {
|
|
1738
2035
|
return null;
|
|
1739
2036
|
}
|
|
1740
|
-
const userInfo = await api.getCliUserInfo(rawToken);
|
|
1741
|
-
authSpinner.stop(`Authenticated as ${chalk5.bold(userInfo.email)}`);
|
|
1742
2037
|
return {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
organizationId: callbackOrganizationId,
|
|
1746
|
-
discoveryReady: callbackDiscoveryReady
|
|
2038
|
+
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
2039
|
+
repoRoot
|
|
1747
2040
|
};
|
|
1748
2041
|
}
|
|
2042
|
+
function resolveGitContext() {
|
|
2043
|
+
const warnings = [];
|
|
2044
|
+
const identity = resolveGitRepositoryIdentity();
|
|
2045
|
+
if (!identity) {
|
|
2046
|
+
warnings.push(
|
|
2047
|
+
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return { identity, warnings };
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/commands/init.ts
|
|
2054
|
+
loadEnv();
|
|
1749
2055
|
async function init(options = {}) {
|
|
1750
2056
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
1751
|
-
|
|
2057
|
+
p12.intro(chalk11.bold("Vocoder Setup"));
|
|
1752
2058
|
try {
|
|
1753
2059
|
const gitContext = resolveGitContext();
|
|
1754
2060
|
const identity = gitContext.identity;
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
p6.log.warn(warning);
|
|
1758
|
-
}
|
|
2061
|
+
for (const warning of gitContext.warnings) {
|
|
2062
|
+
p12.log.warn(warning);
|
|
1759
2063
|
}
|
|
1760
2064
|
let existingAppsForRepo = [];
|
|
1761
2065
|
let repoProjectId = null;
|
|
@@ -1770,19 +2074,19 @@ async function init(options = {}) {
|
|
|
1770
2074
|
if (lookup.existingApps.length > 0) {
|
|
1771
2075
|
const allApps = lookup.existingApps;
|
|
1772
2076
|
const firstApp = allApps[0];
|
|
1773
|
-
|
|
1774
|
-
|
|
2077
|
+
p12.log.success(`Project: ${chalk11.bold(firstApp.projectName)}`);
|
|
2078
|
+
p12.log.info(
|
|
1775
2079
|
`Configured apps: ${allApps.map((a) => highlight(a.appDir || "(entire repo)")).join(", ")}`
|
|
1776
2080
|
);
|
|
1777
|
-
const routeAction = await
|
|
2081
|
+
const routeAction = await p12.select({
|
|
1778
2082
|
message: "This repo is already set up. What would you like to do?",
|
|
1779
2083
|
options: [
|
|
1780
2084
|
{ value: "key", label: "Get an API key for this project" },
|
|
1781
2085
|
{ value: "add", label: "Add a new app directory" }
|
|
1782
2086
|
]
|
|
1783
2087
|
});
|
|
1784
|
-
if (
|
|
1785
|
-
|
|
2088
|
+
if (p12.isCancel(routeAction)) {
|
|
2089
|
+
p12.cancel("Setup cancelled.");
|
|
1786
2090
|
return 1;
|
|
1787
2091
|
}
|
|
1788
2092
|
if (routeAction === "key") {
|
|
@@ -1794,20 +2098,20 @@ async function init(options = {}) {
|
|
|
1794
2098
|
true
|
|
1795
2099
|
);
|
|
1796
2100
|
if (!authResult) return 1;
|
|
1797
|
-
const
|
|
1798
|
-
|
|
2101
|
+
const spinner9 = p12.spinner();
|
|
2102
|
+
spinner9.start("Generating API key...");
|
|
1799
2103
|
let apiKey;
|
|
1800
2104
|
try {
|
|
1801
2105
|
({ apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
1802
2106
|
authResult.token,
|
|
1803
2107
|
firstApp.projectId
|
|
1804
2108
|
));
|
|
1805
|
-
|
|
2109
|
+
spinner9.stop("API key ready");
|
|
1806
2110
|
} catch (err) {
|
|
1807
|
-
|
|
2111
|
+
spinner9.stop("Failed to generate key");
|
|
1808
2112
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1809
|
-
|
|
1810
|
-
|
|
2113
|
+
p12.log.error(`Could not generate API key: ${msg}`);
|
|
2114
|
+
p12.log.info("Try again or generate one from the dashboard.");
|
|
1811
2115
|
return 1;
|
|
1812
2116
|
}
|
|
1813
2117
|
printApiKey(apiKey, identity.repoRoot);
|
|
@@ -1819,7 +2123,7 @@ async function init(options = {}) {
|
|
|
1819
2123
|
detection2.isTypeScript,
|
|
1820
2124
|
identity.repoRoot
|
|
1821
2125
|
);
|
|
1822
|
-
|
|
2126
|
+
p12.outro("Vocoder is set up for this repository.");
|
|
1823
2127
|
return 0;
|
|
1824
2128
|
}
|
|
1825
2129
|
existingAppsForRepo = allApps;
|
|
@@ -1834,16 +2138,16 @@ async function init(options = {}) {
|
|
|
1834
2138
|
let authOrganizationId;
|
|
1835
2139
|
const storedAuth = await verifyStoredAuth(api);
|
|
1836
2140
|
if (storedAuth.status === "valid") {
|
|
1837
|
-
|
|
2141
|
+
p12.log.success(`Authenticated as ${chalk11.bold(storedAuth.email)}`);
|
|
1838
2142
|
userToken = storedAuth.token;
|
|
1839
2143
|
userEmail = storedAuth.email;
|
|
1840
2144
|
userName = storedAuth.name;
|
|
1841
2145
|
} else {
|
|
1842
2146
|
const reauth = storedAuth.status === "expired";
|
|
1843
2147
|
if (reauth) {
|
|
1844
|
-
|
|
2148
|
+
p12.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1845
2149
|
} else if (storedAuth.status === "gone") {
|
|
1846
|
-
|
|
2150
|
+
p12.log.warn("Account not found \u2014 starting fresh setup");
|
|
1847
2151
|
}
|
|
1848
2152
|
const authResult = await runAuthFlow(
|
|
1849
2153
|
api,
|
|
@@ -1864,272 +2168,18 @@ async function init(options = {}) {
|
|
|
1864
2168
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1865
2169
|
});
|
|
1866
2170
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
);
|
|
1880
|
-
} else if (repoOrgContext && !repoProjectId) {
|
|
1881
|
-
selectedOrganizationId = repoOrgContext.organizationId;
|
|
1882
|
-
selectedOrganizationName = repoOrgContext.organizationName;
|
|
1883
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1884
|
-
} else {
|
|
1885
|
-
const organizationData = await api.listOrganizations(userToken, {
|
|
1886
|
-
repo: identity?.repoCanonical
|
|
1887
|
-
});
|
|
1888
|
-
{
|
|
1889
|
-
const repoCanonical = identity?.repoCanonical ?? null;
|
|
1890
|
-
const covering = repoCanonical ? organizationData.organizations.filter((w) => w.coversRepo === true) : [];
|
|
1891
|
-
const connected = organizationData.organizations.filter(
|
|
1892
|
-
(w) => w.hasGitHubConnection
|
|
1893
|
-
);
|
|
1894
|
-
if (repoCanonical && covering.length === 1) {
|
|
1895
|
-
const ws = covering[0];
|
|
1896
|
-
selectedOrganizationId = ws.id;
|
|
1897
|
-
selectedOrganizationName = ws.name;
|
|
1898
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1899
|
-
} else if (repoCanonical && covering.length > 1) {
|
|
1900
|
-
const choice = await p6.select({
|
|
1901
|
-
message: "Select workspace for this repo",
|
|
1902
|
-
options: covering.map((w) => ({
|
|
1903
|
-
value: w.id,
|
|
1904
|
-
label: `${w.name} ${chalk5.dim(`(${w.appCount} app${w.appCount !== 1 ? "s" : ""})`)}`
|
|
1905
|
-
}))
|
|
1906
|
-
});
|
|
1907
|
-
if (p6.isCancel(choice)) {
|
|
1908
|
-
p6.cancel("Setup cancelled.");
|
|
1909
|
-
return 1;
|
|
1910
|
-
}
|
|
1911
|
-
const ws = covering.find((w) => w.id === choice);
|
|
1912
|
-
selectedOrganizationId = ws.id;
|
|
1913
|
-
selectedOrganizationName = ws.name;
|
|
1914
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1915
|
-
} else if (repoCanonical && covering.length === 0 && connected.length > 0) {
|
|
1916
|
-
const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
|
|
1917
|
-
p6.log.warn(
|
|
1918
|
-
`${chalk5.bold(shortRepo)} isn't accessible from your Vocoder installation.
|
|
1919
|
-
Grant access to this repository or install on the account that owns it.`
|
|
1920
|
-
);
|
|
1921
|
-
const fixOptions = [];
|
|
1922
|
-
for (const ws of connected) {
|
|
1923
|
-
if (ws.installationConfigureUrl) {
|
|
1924
|
-
fixOptions.push({
|
|
1925
|
-
value: `grant:${ws.id}`,
|
|
1926
|
-
label: `Configure ${chalk5.bold(ws.connectionLabel ?? ws.name)}'s GitHub App installation`
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
fixOptions.push({
|
|
1931
|
-
value: "install_new",
|
|
1932
|
-
label: `Install on a different GitHub account ${chalk5.dim("(creates a new personal workspace)")}`
|
|
1933
|
-
});
|
|
1934
|
-
fixOptions.push({ value: "cancel", label: "Cancel" });
|
|
1935
|
-
const fix = await p6.select({
|
|
1936
|
-
message: "How would you like to fix this?",
|
|
1937
|
-
options: fixOptions
|
|
1938
|
-
});
|
|
1939
|
-
if (p6.isCancel(fix) || fix === "cancel") {
|
|
1940
|
-
p6.cancel("Setup cancelled.");
|
|
1941
|
-
return 1;
|
|
1942
|
-
}
|
|
1943
|
-
if (fix.startsWith("grant:")) {
|
|
1944
|
-
const ws = connected.find((w) => `grant:${w.id}` === fix);
|
|
1945
|
-
await tryOpenBrowser2(ws.installationConfigureUrl);
|
|
1946
|
-
p6.cancel(
|
|
1947
|
-
`Grant access to ${chalk5.bold(shortRepo)} in your browser,
|
|
1948
|
-
then re-run ${chalk5.bold("vocoder init")}.`
|
|
1949
|
-
);
|
|
1950
|
-
return 1;
|
|
1951
|
-
}
|
|
1952
|
-
const connectResult = await runGitHubInstallFlow({
|
|
1953
|
-
api,
|
|
1954
|
-
userToken,
|
|
1955
|
-
yes: options.yes
|
|
1956
|
-
});
|
|
1957
|
-
if (!connectResult) {
|
|
1958
|
-
p6.log.error(
|
|
1959
|
-
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
1960
|
-
);
|
|
1961
|
-
return 1;
|
|
1962
|
-
}
|
|
1963
|
-
selectedOrganizationId = connectResult.organizationId;
|
|
1964
|
-
selectedOrganizationName = connectResult.organizationName;
|
|
1965
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1966
|
-
} else {
|
|
1967
|
-
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
1968
|
-
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
1969
|
-
if (cachedInstallations.length > 0) {
|
|
1970
|
-
if (identity?.repoCanonical) {
|
|
1971
|
-
const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
1972
|
-
if (repoOwner) {
|
|
1973
|
-
const hasMatchingAccount = cachedInstallations.some(
|
|
1974
|
-
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
1975
|
-
);
|
|
1976
|
-
if (!hasMatchingAccount) {
|
|
1977
|
-
p6.log.warn(
|
|
1978
|
-
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
1979
|
-
The project will be created but translations won't trigger automatically.
|
|
1980
|
-
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
1981
|
-
);
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
const validInstallations = cachedInstallations.filter(
|
|
1986
|
-
(i) => !i.isSuspended && !i.conflictLabel
|
|
1987
|
-
);
|
|
1988
|
-
let selectedInstallationId = null;
|
|
1989
|
-
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
1990
|
-
selectedInstallationId = validInstallations[0].installationId;
|
|
1991
|
-
} else {
|
|
1992
|
-
selectedInstallationId = await selectGitHubInstallation(
|
|
1993
|
-
cachedInstallations.map((inst) => ({
|
|
1994
|
-
installationId: inst.installationId,
|
|
1995
|
-
accountLogin: inst.accountLogin,
|
|
1996
|
-
accountType: inst.accountType,
|
|
1997
|
-
isSuspended: inst.isSuspended,
|
|
1998
|
-
conflictLabel: inst.conflictLabel
|
|
1999
|
-
})),
|
|
2000
|
-
false
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
if (selectedInstallationId === null || selectedInstallationId === "install_new") {
|
|
2004
|
-
p6.cancel(
|
|
2005
|
-
"Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
|
|
2006
|
-
);
|
|
2007
|
-
return 1;
|
|
2008
|
-
}
|
|
2009
|
-
const claimResult = await api.claimCliGitHubInstallation(
|
|
2010
|
-
userToken,
|
|
2011
|
-
{
|
|
2012
|
-
installationId: String(selectedInstallationId),
|
|
2013
|
-
organizationId: null
|
|
2014
|
-
}
|
|
2015
|
-
);
|
|
2016
|
-
selectedOrganizationId = claimResult.organizationId;
|
|
2017
|
-
selectedOrganizationName = claimResult.organizationName;
|
|
2018
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
2019
|
-
} else if (organizationData.organizations.length === 1 && !organizationData.canCreateOrganization) {
|
|
2020
|
-
const ws = organizationData.organizations[0];
|
|
2021
|
-
selectedOrganizationId = ws.id;
|
|
2022
|
-
selectedOrganizationName = ws.name;
|
|
2023
|
-
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
2024
|
-
} else {
|
|
2025
|
-
const organizationResult = await selectOrganization(organizationData);
|
|
2026
|
-
if (organizationResult.action === "cancelled") {
|
|
2027
|
-
p6.cancel("Setup cancelled.");
|
|
2028
|
-
return 1;
|
|
2029
|
-
}
|
|
2030
|
-
if (organizationResult.action === "use") {
|
|
2031
|
-
selectedOrganizationId = organizationResult.organization.id;
|
|
2032
|
-
selectedOrganizationName = organizationResult.organization.name;
|
|
2033
|
-
p6.log.success(
|
|
2034
|
-
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
2035
|
-
);
|
|
2036
|
-
} else {
|
|
2037
|
-
const connectChoice = await p6.select({
|
|
2038
|
-
message: "Connect your new workspace to GitHub",
|
|
2039
|
-
options: [
|
|
2040
|
-
{ value: "install", label: "Install the Vocoder GitHub App" },
|
|
2041
|
-
{ value: "link", label: "Link an existing installation" }
|
|
2042
|
-
]
|
|
2043
|
-
});
|
|
2044
|
-
if (p6.isCancel(connectChoice)) {
|
|
2045
|
-
p6.cancel("Setup cancelled.");
|
|
2046
|
-
return 1;
|
|
2047
|
-
}
|
|
2048
|
-
if (connectChoice === "install") {
|
|
2049
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2050
|
-
api,
|
|
2051
|
-
userToken,
|
|
2052
|
-
yes: options.yes
|
|
2053
|
-
});
|
|
2054
|
-
if (!connectResult) {
|
|
2055
|
-
p6.log.error(
|
|
2056
|
-
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
2057
|
-
);
|
|
2058
|
-
return 1;
|
|
2059
|
-
}
|
|
2060
|
-
selectedOrganizationId = connectResult.organizationId;
|
|
2061
|
-
selectedOrganizationName = connectResult.organizationName;
|
|
2062
|
-
p6.log.success(
|
|
2063
|
-
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
2064
|
-
);
|
|
2065
|
-
} else {
|
|
2066
|
-
const installations = await runGitHubDiscoveryFlow({
|
|
2067
|
-
api,
|
|
2068
|
-
userToken,
|
|
2069
|
-
yes: options.yes
|
|
2070
|
-
});
|
|
2071
|
-
if (!installations) return 1;
|
|
2072
|
-
if (installations.length === 0) {
|
|
2073
|
-
p6.log.warn(
|
|
2074
|
-
"No GitHub installations found. Install the Vocoder GitHub App first."
|
|
2075
|
-
);
|
|
2076
|
-
const installNow = await p6.confirm({
|
|
2077
|
-
message: "Open GitHub to install the App?"
|
|
2078
|
-
});
|
|
2079
|
-
if (p6.isCancel(installNow) || !installNow) return 1;
|
|
2080
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2081
|
-
api,
|
|
2082
|
-
userToken,
|
|
2083
|
-
yes: options.yes
|
|
2084
|
-
});
|
|
2085
|
-
if (!connectResult) return 1;
|
|
2086
|
-
selectedOrganizationId = connectResult.organizationId;
|
|
2087
|
-
selectedOrganizationName = connectResult.organizationName;
|
|
2088
|
-
} else {
|
|
2089
|
-
const selectedInstallationId = await selectGitHubInstallation(
|
|
2090
|
-
installations.map((inst) => ({
|
|
2091
|
-
installationId: inst.installationId,
|
|
2092
|
-
accountLogin: inst.accountLogin,
|
|
2093
|
-
accountType: inst.accountType,
|
|
2094
|
-
isSuspended: inst.isSuspended,
|
|
2095
|
-
conflictLabel: inst.conflictLabel
|
|
2096
|
-
})),
|
|
2097
|
-
true
|
|
2098
|
-
);
|
|
2099
|
-
if (selectedInstallationId === null) {
|
|
2100
|
-
p6.cancel("Setup cancelled.");
|
|
2101
|
-
return 1;
|
|
2102
|
-
}
|
|
2103
|
-
if (selectedInstallationId === "install_new") {
|
|
2104
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2105
|
-
api,
|
|
2106
|
-
userToken,
|
|
2107
|
-
yes: options.yes
|
|
2108
|
-
});
|
|
2109
|
-
if (!connectResult) return 1;
|
|
2110
|
-
selectedOrganizationId = connectResult.organizationId;
|
|
2111
|
-
selectedOrganizationName = connectResult.organizationName;
|
|
2112
|
-
} else {
|
|
2113
|
-
const claimResult = await api.claimCliGitHubInstallation(
|
|
2114
|
-
userToken,
|
|
2115
|
-
{
|
|
2116
|
-
installationId: String(selectedInstallationId),
|
|
2117
|
-
organizationId: null
|
|
2118
|
-
}
|
|
2119
|
-
);
|
|
2120
|
-
selectedOrganizationId = claimResult.organizationId;
|
|
2121
|
-
selectedOrganizationName = claimResult.organizationName;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
p6.log.success(
|
|
2125
|
-
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2171
|
+
const organizationResult = await selectOrganizationForInit({
|
|
2172
|
+
api,
|
|
2173
|
+
userToken,
|
|
2174
|
+
userEmail,
|
|
2175
|
+
identity: identity ?? null,
|
|
2176
|
+
lookup,
|
|
2177
|
+
repoProjectId,
|
|
2178
|
+
authOrganizationId,
|
|
2179
|
+
options
|
|
2180
|
+
});
|
|
2181
|
+
if (!organizationResult) return 1;
|
|
2182
|
+
const { organizationId: selectedOrganizationId, organizationName: selectedOrganizationName } = organizationResult;
|
|
2133
2183
|
if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
|
|
2134
2184
|
const appResult = await runAppCreate({
|
|
2135
2185
|
api,
|
|
@@ -2141,58 +2191,28 @@ async function init(options = {}) {
|
|
|
2141
2191
|
existingApps: existingAppsForRepo
|
|
2142
2192
|
});
|
|
2143
2193
|
if (!appResult) {
|
|
2144
|
-
|
|
2194
|
+
p12.log.error("App setup failed. Run `vocoder init` again.");
|
|
2145
2195
|
return 1;
|
|
2146
2196
|
}
|
|
2147
2197
|
const detection2 = detectLocalEcosystem();
|
|
2148
|
-
runScaffold({
|
|
2149
|
-
sourceLocale: appResult.sourceLocale,
|
|
2150
|
-
targetBranches: appResult.targetBranches
|
|
2151
|
-
});
|
|
2198
|
+
runScaffold({ targetBranches: appResult.targetBranches });
|
|
2152
2199
|
writeAppConfigs(
|
|
2153
2200
|
[{ appDir: appResult.appDir, appId: appResult.appId }],
|
|
2154
2201
|
appResult.targetBranches,
|
|
2155
2202
|
detection2.isTypeScript,
|
|
2156
2203
|
identity?.repoRoot
|
|
2157
2204
|
);
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
);
|
|
2161
|
-
p6.outro("You're all set.");
|
|
2205
|
+
p12.log.info(chalk11.dim("Use the VOCODER_API_KEY already in your root .env"));
|
|
2206
|
+
p12.outro("You're all set.");
|
|
2162
2207
|
return 0;
|
|
2163
2208
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
if (ws.maxApps !== -1 && ws.appCount >= ws.maxApps) {
|
|
2172
|
-
p6.log.warn(
|
|
2173
|
-
`App limit reached \u2014 ${ws.appCount}/${ws.maxApps} on your ${chalk5.bold(ws.planId)} plan.`
|
|
2174
|
-
);
|
|
2175
|
-
const limitAction = await p6.select({
|
|
2176
|
-
message: "What would you like to do?",
|
|
2177
|
-
options: [
|
|
2178
|
-
{ value: "upgrade", label: "Upgrade plan" },
|
|
2179
|
-
{ value: "cancel", label: "Cancel" }
|
|
2180
|
-
]
|
|
2181
|
-
});
|
|
2182
|
-
if (p6.isCancel(limitAction) || limitAction === "cancel") {
|
|
2183
|
-
p6.cancel("Setup cancelled.");
|
|
2184
|
-
return 1;
|
|
2185
|
-
}
|
|
2186
|
-
await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
|
|
2187
|
-
p6.cancel(
|
|
2188
|
-
"Upgrade your plan in the browser, then re-run `vocoder init`."
|
|
2189
|
-
);
|
|
2190
|
-
return 1;
|
|
2191
|
-
}
|
|
2192
|
-
remainingApps = ws.maxApps === -1 ? void 0 : Math.max(0, ws.maxApps - ws.appCount);
|
|
2193
|
-
}
|
|
2194
|
-
} catch {
|
|
2195
|
-
}
|
|
2209
|
+
const planCheck = await checkPlanLimits(
|
|
2210
|
+
api,
|
|
2211
|
+
userToken,
|
|
2212
|
+
selectedOrganizationId,
|
|
2213
|
+
apiUrl
|
|
2214
|
+
);
|
|
2215
|
+
if (planCheck.atLimit) return 1;
|
|
2196
2216
|
const projectResult = await runProjectCreate({
|
|
2197
2217
|
api,
|
|
2198
2218
|
userToken,
|
|
@@ -2202,29 +2222,23 @@ async function init(options = {}) {
|
|
|
2202
2222
|
repoCanonical: identity?.repoCanonical,
|
|
2203
2223
|
repoRoot: identity?.repoRoot,
|
|
2204
2224
|
defaultBranches: ["main"],
|
|
2205
|
-
maxAppDirs:
|
|
2225
|
+
maxAppDirs: planCheck.remaining
|
|
2206
2226
|
});
|
|
2207
|
-
if (!projectResult)
|
|
2208
|
-
p6.log.error("Project creation failed. Run `vocoder init` again.");
|
|
2209
|
-
return 1;
|
|
2210
|
-
}
|
|
2227
|
+
if (!projectResult) return 1;
|
|
2211
2228
|
if (!projectResult.repositoryBound && identity?.repoCanonical) {
|
|
2212
|
-
|
|
2229
|
+
p12.log.warn(
|
|
2213
2230
|
`This repository isn't accessible to your GitHub App installation.
|
|
2214
2231
|
Translations won't run automatically until you grant access.
|
|
2215
2232
|
|
|
2216
2233
|
To fix: go to your GitHub App installation settings and add this
|
|
2217
2234
|
repository to the allowed list, or switch to "All repositories".
|
|
2218
2235
|
` + (projectResult.configureUrl ? `
|
|
2219
|
-
${
|
|
2236
|
+
${chalk11.dim(projectResult.configureUrl)}
|
|
2220
2237
|
` : "")
|
|
2221
2238
|
);
|
|
2222
2239
|
}
|
|
2223
2240
|
const detection = detectLocalEcosystem();
|
|
2224
|
-
runScaffold({
|
|
2225
|
-
sourceLocale: projectResult.sourceLocale,
|
|
2226
|
-
targetBranches: projectResult.targetBranches
|
|
2227
|
-
});
|
|
2241
|
+
runScaffold({ targetBranches: projectResult.targetBranches });
|
|
2228
2242
|
writeAppConfigs(
|
|
2229
2243
|
projectResult.apps,
|
|
2230
2244
|
projectResult.targetBranches,
|
|
@@ -2232,25 +2246,28 @@ Translations won't run automatically until you grant access.
|
|
|
2232
2246
|
identity?.repoRoot
|
|
2233
2247
|
);
|
|
2234
2248
|
printApiKey(projectResult.apiKey, identity?.repoRoot);
|
|
2235
|
-
|
|
2249
|
+
const wantsMcp = await p12.confirm({
|
|
2250
|
+
message: "Set up the Vocoder MCP server for your AI editor?"
|
|
2251
|
+
});
|
|
2252
|
+
if (!p12.isCancel(wantsMcp) && wantsMcp) {
|
|
2253
|
+
await runMcpSetup(projectResult.apiKey);
|
|
2254
|
+
}
|
|
2255
|
+
p12.outro("You're all set.");
|
|
2236
2256
|
return 0;
|
|
2237
2257
|
} catch (error) {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return 1;
|
|
2242
|
-
}
|
|
2243
|
-
p6.log.error(`Error: ${error.message}`);
|
|
2258
|
+
const message = error instanceof Error ? error.message : "Unknown setup error";
|
|
2259
|
+
if (isPlanLimitFailure(message)) {
|
|
2260
|
+
printPlanLimitMessage(apiUrl, message);
|
|
2244
2261
|
} else {
|
|
2245
|
-
|
|
2262
|
+
p12.log.error(message);
|
|
2246
2263
|
}
|
|
2247
2264
|
return 1;
|
|
2248
2265
|
}
|
|
2249
2266
|
}
|
|
2250
2267
|
|
|
2251
2268
|
// src/commands/locales.ts
|
|
2252
|
-
import * as
|
|
2253
|
-
import
|
|
2269
|
+
import * as p15 from "@clack/prompts";
|
|
2270
|
+
import chalk13 from "chalk";
|
|
2254
2271
|
import { config as loadEnv3 } from "dotenv";
|
|
2255
2272
|
import { readFileSync as readFileSync3 } from "fs";
|
|
2256
2273
|
|
|
@@ -2258,11 +2275,11 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
2258
2275
|
import { randomUUID } from "crypto";
|
|
2259
2276
|
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2260
2277
|
import { join as join3 } from "path";
|
|
2261
|
-
import * as
|
|
2262
|
-
import
|
|
2278
|
+
import * as p14 from "@clack/prompts";
|
|
2279
|
+
import chalk12 from "chalk";
|
|
2263
2280
|
|
|
2264
2281
|
// src/utils/branch.ts
|
|
2265
|
-
import { execSync as
|
|
2282
|
+
import { execSync as execSync6 } from "child_process";
|
|
2266
2283
|
var REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
|
|
2267
2284
|
function escapeRegexChar(value) {
|
|
2268
2285
|
return value.replace(REGEX_SPECIAL_CHARS, "\\$&");
|
|
@@ -2284,7 +2301,7 @@ function detectBranch(override) {
|
|
|
2284
2301
|
return envBranch;
|
|
2285
2302
|
}
|
|
2286
2303
|
try {
|
|
2287
|
-
const branch =
|
|
2304
|
+
const branch = execSync6("git rev-parse --abbrev-ref HEAD", {
|
|
2288
2305
|
encoding: "utf-8",
|
|
2289
2306
|
stdio: ["pipe", "pipe", "ignore"]
|
|
2290
2307
|
}).trim();
|
|
@@ -2328,7 +2345,7 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2328
2345
|
}
|
|
2329
2346
|
|
|
2330
2347
|
// src/utils/config.ts
|
|
2331
|
-
import * as
|
|
2348
|
+
import * as p13 from "@clack/prompts";
|
|
2332
2349
|
import { config as loadEnv2 } from "dotenv";
|
|
2333
2350
|
loadEnv2();
|
|
2334
2351
|
function extractShortCodeFromApiKey(apiKey) {
|
|
@@ -2384,7 +2401,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2384
2401
|
};
|
|
2385
2402
|
const fileConfig = loadVocoderConfig(process.cwd());
|
|
2386
2403
|
if (!fileConfig) {
|
|
2387
|
-
|
|
2404
|
+
p13.log.warn(
|
|
2388
2405
|
`No ${highlight("vocoder.config.ts")} found \u2014 run ${highlight("npx @vocoder/cli init")} to generate one.`
|
|
2389
2406
|
);
|
|
2390
2407
|
}
|
|
@@ -2415,7 +2432,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2415
2432
|
excludePattern = fileConfig.exclude;
|
|
2416
2433
|
configSources.excludePattern = "vocoder.config";
|
|
2417
2434
|
} else if (envExcludePattern) {
|
|
2418
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2435
|
+
excludePattern = envExcludePattern.split(",").map((p21) => p21.trim()).filter(Boolean);
|
|
2419
2436
|
configSources.excludePattern = "environment";
|
|
2420
2437
|
} else {
|
|
2421
2438
|
excludePattern = defaults.excludePattern;
|
|
@@ -2472,7 +2489,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2472
2489
|
...maxWaitMs ? [`Max wait: ${highlight(String(configSources.maxWaitMs))}`] : [],
|
|
2473
2490
|
`No fallback: ${highlight(String(configSources.noFallback))}`
|
|
2474
2491
|
];
|
|
2475
|
-
|
|
2492
|
+
p13.note(lines.join("\n"), "Configuration sources");
|
|
2476
2493
|
}
|
|
2477
2494
|
return {
|
|
2478
2495
|
includePattern,
|
|
@@ -2726,22 +2743,22 @@ async function fetchApiSnapshot(api, params) {
|
|
|
2726
2743
|
async function sync(options = {}) {
|
|
2727
2744
|
const startTime = Date.now();
|
|
2728
2745
|
const projectRoot = process.cwd();
|
|
2729
|
-
|
|
2746
|
+
p14.intro(chalk12.bold("Vocoder Sync"));
|
|
2730
2747
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
2731
2748
|
if (!mergedConfig.apiKey) {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2749
|
+
p14.log.warn("No API key found. Run init to get started:");
|
|
2750
|
+
p14.log.info(" npx @vocoder/cli init");
|
|
2751
|
+
p14.log.info("");
|
|
2752
|
+
p14.log.info(
|
|
2736
2753
|
" Or add your key to .env: VOCODER_API_KEY=vca_..."
|
|
2737
2754
|
);
|
|
2738
|
-
|
|
2755
|
+
p14.outro("Run `npx @vocoder/cli init` to set up your project.");
|
|
2739
2756
|
return 1;
|
|
2740
2757
|
}
|
|
2741
|
-
const
|
|
2758
|
+
const spinner9 = p14.spinner();
|
|
2742
2759
|
try {
|
|
2743
2760
|
const branch = detectBranch(options.branch);
|
|
2744
|
-
|
|
2761
|
+
spinner9.start("Loading project configuration");
|
|
2745
2762
|
const localConfig = {
|
|
2746
2763
|
apiKey: mergedConfig.apiKey,
|
|
2747
2764
|
apiUrl: mergedConfig.apiUrl || "https://vocoder.app"
|
|
@@ -2762,21 +2779,21 @@ async function sync(options = {}) {
|
|
|
2762
2779
|
includePattern: mergedConfig.includePattern,
|
|
2763
2780
|
excludePattern: mergedConfig.excludePattern,
|
|
2764
2781
|
timeout: waitTimeoutMs,
|
|
2765
|
-
...fileConfig?.appIndustry ? {
|
|
2782
|
+
...fileConfig?.industry ?? fileConfig?.appIndustry ? { industry: fileConfig?.industry ?? fileConfig?.appIndustry } : {},
|
|
2766
2783
|
...fileConfig?.formality ? { formality: fileConfig.formality } : {}
|
|
2767
2784
|
};
|
|
2768
|
-
|
|
2785
|
+
spinner9.stop(`Branch: ${highlight(branch)}`);
|
|
2769
2786
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
2770
|
-
|
|
2787
|
+
p14.log.warn(
|
|
2771
2788
|
`Skipping translations (${highlight(branch)} is not a target branch)`
|
|
2772
2789
|
);
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2790
|
+
p14.log.info(`Target branches: ${config.targetBranches.map((b) => highlight(b)).join(", ")}`);
|
|
2791
|
+
p14.log.info("Use --force to translate anyway");
|
|
2792
|
+
p14.outro("");
|
|
2776
2793
|
return 0;
|
|
2777
2794
|
}
|
|
2778
2795
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
2779
|
-
|
|
2796
|
+
spinner9.start(`Extracting strings from ${patternsDisplay}`);
|
|
2780
2797
|
const extractor = new StringExtractor();
|
|
2781
2798
|
const extractedStrings = await extractor.extractFromProject(
|
|
2782
2799
|
config.includePattern,
|
|
@@ -2784,14 +2801,14 @@ async function sync(options = {}) {
|
|
|
2784
2801
|
config.excludePattern
|
|
2785
2802
|
);
|
|
2786
2803
|
if (extractedStrings.length === 0) {
|
|
2787
|
-
|
|
2788
|
-
|
|
2804
|
+
spinner9.stop("No translatable strings found");
|
|
2805
|
+
p14.log.warn(
|
|
2789
2806
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
2790
2807
|
);
|
|
2791
|
-
|
|
2808
|
+
p14.outro("");
|
|
2792
2809
|
return 0;
|
|
2793
2810
|
}
|
|
2794
|
-
|
|
2811
|
+
spinner9.stop(
|
|
2795
2812
|
`Extracted ${highlight(extractedStrings.length)} strings from ${highlight(patternsDisplay)}`
|
|
2796
2813
|
);
|
|
2797
2814
|
if (options.verbose) {
|
|
@@ -2799,10 +2816,10 @@ async function sync(options = {}) {
|
|
|
2799
2816
|
if (extractedStrings.length > 5) {
|
|
2800
2817
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
2801
2818
|
}
|
|
2802
|
-
|
|
2819
|
+
p14.note(sampleLines.join("\n"), "Sample strings");
|
|
2803
2820
|
}
|
|
2804
2821
|
if (options.dryRun) {
|
|
2805
|
-
|
|
2822
|
+
p14.note(
|
|
2806
2823
|
[
|
|
2807
2824
|
`Strings: ${extractedStrings.length}`,
|
|
2808
2825
|
`Branch: ${branch}`,
|
|
@@ -2813,19 +2830,19 @@ async function sync(options = {}) {
|
|
|
2813
2830
|
].join("\n"),
|
|
2814
2831
|
"Dry run - would translate"
|
|
2815
2832
|
);
|
|
2816
|
-
|
|
2833
|
+
p14.outro("No API calls made.");
|
|
2817
2834
|
return 0;
|
|
2818
2835
|
}
|
|
2819
2836
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
2820
2837
|
if (!repoIdentity && options.verbose) {
|
|
2821
|
-
|
|
2838
|
+
p14.log.warn(
|
|
2822
2839
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
2823
2840
|
);
|
|
2824
2841
|
}
|
|
2825
2842
|
const commitSha = detectCommitSha() ?? void 0;
|
|
2826
2843
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
2827
2844
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
2828
|
-
|
|
2845
|
+
p14.log.info(
|
|
2829
2846
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique strings`
|
|
2830
2847
|
);
|
|
2831
2848
|
}
|
|
@@ -2835,17 +2852,17 @@ async function sync(options = {}) {
|
|
|
2835
2852
|
const cacheFile = getCacheFilePath(projectRoot, fingerprint);
|
|
2836
2853
|
if (existsSync4(cacheFile)) {
|
|
2837
2854
|
if (options.verbose) {
|
|
2838
|
-
|
|
2855
|
+
p14.log.info(`Cache hit: ${chalk12.dim(cacheFile)} (fingerprint ${highlight(fingerprint)})`);
|
|
2839
2856
|
}
|
|
2840
2857
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2841
|
-
|
|
2858
|
+
p14.outro(`Up to date (${duration2}s)`);
|
|
2842
2859
|
return 0;
|
|
2843
2860
|
}
|
|
2844
2861
|
if (options.verbose) {
|
|
2845
|
-
|
|
2862
|
+
p14.log.info(`No cache for fingerprint ${highlight(fingerprint)} \u2014 will submit to API`);
|
|
2846
2863
|
}
|
|
2847
2864
|
}
|
|
2848
|
-
|
|
2865
|
+
spinner9.start("Submitting strings to Vocoder API");
|
|
2849
2866
|
const batchResponse = await api.submitTranslation(
|
|
2850
2867
|
branch,
|
|
2851
2868
|
stringEntries,
|
|
@@ -2855,38 +2872,37 @@ async function sync(options = {}) {
|
|
|
2855
2872
|
requestedMaxWaitMs: waitTimeoutMs,
|
|
2856
2873
|
clientRunId: randomUUID(),
|
|
2857
2874
|
force: options.force,
|
|
2858
|
-
|
|
2859
|
-
...config.appIndustry ? { appIndustry: config.appIndustry } : {}
|
|
2875
|
+
...config.industry ? { industry: config.industry } : {}
|
|
2860
2876
|
},
|
|
2861
2877
|
repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
|
|
2862
2878
|
);
|
|
2863
|
-
|
|
2879
|
+
spinner9.stop("Strings submitted");
|
|
2864
2880
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
2865
2881
|
branch,
|
|
2866
2882
|
requestedMode,
|
|
2867
2883
|
policy: config.syncPolicy
|
|
2868
2884
|
});
|
|
2869
2885
|
if (options.verbose) {
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2886
|
+
p14.log.info(`Batch: ${chalk12.dim(batchResponse.batchId)}`);
|
|
2887
|
+
p14.log.info(`Requested mode: ${requestedMode}`);
|
|
2888
|
+
p14.log.info(`Effective mode: ${effectiveMode}`);
|
|
2889
|
+
p14.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
2874
2890
|
if (batchResponse.queueStatus) {
|
|
2875
|
-
|
|
2891
|
+
p14.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
2876
2892
|
}
|
|
2877
2893
|
}
|
|
2878
2894
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
2879
|
-
|
|
2895
|
+
p14.log.success(`Up to date \u2014 ${highlight(batchResponse.totalStrings)} strings, no changes`);
|
|
2880
2896
|
} else if (batchResponse.newStrings === 0) {
|
|
2881
|
-
const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${
|
|
2882
|
-
|
|
2897
|
+
const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk12.yellow(batchResponse.deletedStrings)} archived` : "";
|
|
2898
|
+
p14.log.success(`No new strings \u2014 ${highlight(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
|
|
2883
2899
|
} else {
|
|
2884
2900
|
const statParts = [`${highlight(batchResponse.newStrings)} new, ${highlight(batchResponse.totalStrings)} total`];
|
|
2885
2901
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
2886
|
-
statParts.push(`${
|
|
2902
|
+
statParts.push(`${chalk12.yellow(batchResponse.deletedStrings)} archived`);
|
|
2887
2903
|
}
|
|
2888
2904
|
const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
|
|
2889
|
-
|
|
2905
|
+
p14.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.map((l) => highlight(l)).join(", ")}${estTime}`);
|
|
2890
2906
|
}
|
|
2891
2907
|
let artifacts = null;
|
|
2892
2908
|
if (batchResponse.translations) {
|
|
@@ -2898,7 +2914,7 @@ async function sync(options = {}) {
|
|
|
2898
2914
|
let waitError = null;
|
|
2899
2915
|
if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
|
|
2900
2916
|
const waitTimeoutSecs = Math.round(waitTimeoutMs / 1e3);
|
|
2901
|
-
|
|
2917
|
+
spinner9.start(`Waiting for translations (max ${waitTimeoutSecs}s)`);
|
|
2902
2918
|
let lastProgress = 0;
|
|
2903
2919
|
try {
|
|
2904
2920
|
const completion = await api.waitForCompletion(
|
|
@@ -2907,7 +2923,7 @@ async function sync(options = {}) {
|
|
|
2907
2923
|
(progress) => {
|
|
2908
2924
|
const percent = Math.round(progress * 100);
|
|
2909
2925
|
if (percent > lastProgress) {
|
|
2910
|
-
|
|
2926
|
+
spinner9.message(`Translating... ${percent}%`);
|
|
2911
2927
|
lastProgress = percent;
|
|
2912
2928
|
}
|
|
2913
2929
|
}
|
|
@@ -2917,14 +2933,14 @@ async function sync(options = {}) {
|
|
|
2917
2933
|
translations: completion.translations,
|
|
2918
2934
|
localeMetadata: completion.localeMetadata
|
|
2919
2935
|
};
|
|
2920
|
-
|
|
2936
|
+
spinner9.stop("Translations complete");
|
|
2921
2937
|
} catch (error) {
|
|
2922
|
-
|
|
2938
|
+
spinner9.stop("Translation wait incomplete");
|
|
2923
2939
|
waitError = error instanceof Error ? error : new Error(String(error));
|
|
2924
2940
|
if (effectiveMode === "required") {
|
|
2925
2941
|
throw waitError;
|
|
2926
2942
|
}
|
|
2927
|
-
|
|
2943
|
+
p14.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
2928
2944
|
}
|
|
2929
2945
|
}
|
|
2930
2946
|
if (!artifacts) {
|
|
@@ -2933,14 +2949,14 @@ async function sync(options = {}) {
|
|
|
2933
2949
|
"Fresh translations are not available and fallback is disabled (--no-fallback)."
|
|
2934
2950
|
);
|
|
2935
2951
|
}
|
|
2936
|
-
|
|
2952
|
+
spinner9.start("Loading fallback translations");
|
|
2937
2953
|
const localFallback = readLocalCache({
|
|
2938
2954
|
projectRoot,
|
|
2939
2955
|
fingerprint
|
|
2940
2956
|
});
|
|
2941
2957
|
if (localFallback) {
|
|
2942
2958
|
artifacts = localFallback;
|
|
2943
|
-
|
|
2959
|
+
spinner9.stop(`Using local cached snapshot (${fingerprint})`);
|
|
2944
2960
|
} else {
|
|
2945
2961
|
try {
|
|
2946
2962
|
const apiSnapshot = await fetchApiSnapshot(api, {
|
|
@@ -2949,15 +2965,15 @@ async function sync(options = {}) {
|
|
|
2949
2965
|
});
|
|
2950
2966
|
if (apiSnapshot) {
|
|
2951
2967
|
artifacts = apiSnapshot;
|
|
2952
|
-
|
|
2968
|
+
spinner9.stop("Using latest completed API snapshot");
|
|
2953
2969
|
} else {
|
|
2954
|
-
|
|
2970
|
+
spinner9.stop("No completed API snapshot available");
|
|
2955
2971
|
}
|
|
2956
2972
|
} catch (error) {
|
|
2957
|
-
|
|
2973
|
+
spinner9.stop("Failed to fetch API snapshot");
|
|
2958
2974
|
if (options.verbose) {
|
|
2959
2975
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
2960
|
-
|
|
2976
|
+
p14.log.warn(`Snapshot fetch error: ${message}`);
|
|
2961
2977
|
}
|
|
2962
2978
|
}
|
|
2963
2979
|
}
|
|
@@ -2988,58 +3004,58 @@ async function sync(options = {}) {
|
|
|
2988
3004
|
});
|
|
2989
3005
|
const cachePath = writeCache({ projectRoot, fingerprint, data });
|
|
2990
3006
|
if (options.verbose) {
|
|
2991
|
-
|
|
3007
|
+
p14.log.info(`Cache written: ${highlight(cachePath)}`);
|
|
2992
3008
|
}
|
|
2993
3009
|
} catch (error) {
|
|
2994
3010
|
if (options.verbose) {
|
|
2995
3011
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
2996
|
-
|
|
3012
|
+
p14.log.warn(`Failed to write cache: ${message}`);
|
|
2997
3013
|
}
|
|
2998
3014
|
}
|
|
2999
3015
|
if (artifacts.source !== "fresh") {
|
|
3000
3016
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
3001
|
-
|
|
3017
|
+
p14.log.warn(
|
|
3002
3018
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
3003
3019
|
);
|
|
3004
3020
|
}
|
|
3005
3021
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3006
|
-
|
|
3022
|
+
p14.outro(`Sync complete! (${duration}s)`);
|
|
3007
3023
|
return 0;
|
|
3008
3024
|
} catch (error) {
|
|
3009
|
-
|
|
3025
|
+
spinner9.stop();
|
|
3010
3026
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
3011
|
-
|
|
3027
|
+
p14.log.error(error.syncPolicyError.message);
|
|
3012
3028
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
3013
3029
|
for (const line of guidance) {
|
|
3014
|
-
|
|
3030
|
+
p14.log.info(line);
|
|
3015
3031
|
}
|
|
3016
3032
|
return 1;
|
|
3017
3033
|
}
|
|
3018
3034
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3019
3035
|
const { limitError } = error;
|
|
3020
|
-
|
|
3036
|
+
p14.log.error(limitError.message);
|
|
3021
3037
|
const guidance = getLimitErrorGuidance(limitError);
|
|
3022
3038
|
for (const line of guidance) {
|
|
3023
|
-
|
|
3039
|
+
p14.log.info(line);
|
|
3024
3040
|
}
|
|
3025
3041
|
return 1;
|
|
3026
3042
|
}
|
|
3027
3043
|
if (error instanceof Error) {
|
|
3028
|
-
|
|
3044
|
+
p14.log.error(error.message);
|
|
3029
3045
|
const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
|
|
3030
3046
|
if (isInvalidKey) {
|
|
3031
|
-
|
|
3047
|
+
p14.log.warn(
|
|
3032
3048
|
"API key rejected \u2014 the project may have been deleted or the key revoked."
|
|
3033
3049
|
);
|
|
3034
|
-
|
|
3050
|
+
p14.log.info(
|
|
3035
3051
|
" Run `npx @vocoder/cli init` to create a new project and key."
|
|
3036
3052
|
);
|
|
3037
3053
|
} else if (error.message.includes("git branch")) {
|
|
3038
|
-
|
|
3039
|
-
|
|
3054
|
+
p14.log.warn("Run from a git repository, or use:");
|
|
3055
|
+
p14.log.info(" vocoder sync --branch main");
|
|
3040
3056
|
}
|
|
3041
3057
|
if (options.verbose) {
|
|
3042
|
-
|
|
3058
|
+
p14.log.info(`Full error: ${error.stack ?? error}`);
|
|
3043
3059
|
}
|
|
3044
3060
|
}
|
|
3045
3061
|
return 1;
|
|
@@ -3062,7 +3078,7 @@ function readLocalAppId() {
|
|
|
3062
3078
|
function getApiConfig(options) {
|
|
3063
3079
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
3064
3080
|
if (!apiKey) {
|
|
3065
|
-
|
|
3081
|
+
p15.log.error(
|
|
3066
3082
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3067
3083
|
);
|
|
3068
3084
|
return null;
|
|
@@ -3078,19 +3094,19 @@ async function listProjectLocales(options = {}) {
|
|
|
3078
3094
|
const api = new VocoderAPI(config);
|
|
3079
3095
|
try {
|
|
3080
3096
|
const projectConfig = await api.getAppConfig();
|
|
3081
|
-
|
|
3097
|
+
p15.log.info(
|
|
3082
3098
|
`Source locale: ${highlight(projectConfig.sourceLocale)}`
|
|
3083
3099
|
);
|
|
3084
3100
|
if (projectConfig.targetLocales.length === 0) {
|
|
3085
|
-
|
|
3101
|
+
p15.log.info("Target locales: (none configured)");
|
|
3086
3102
|
} else {
|
|
3087
|
-
|
|
3103
|
+
p15.log.info(
|
|
3088
3104
|
`Target locales: ${projectConfig.targetLocales.map((l) => highlight(l)).join(", ")}`
|
|
3089
3105
|
);
|
|
3090
3106
|
}
|
|
3091
3107
|
return 0;
|
|
3092
3108
|
} catch (error) {
|
|
3093
|
-
|
|
3109
|
+
p15.log.error(
|
|
3094
3110
|
error instanceof Error ? error.message : "Failed to fetch project locales."
|
|
3095
3111
|
);
|
|
3096
3112
|
return 1;
|
|
@@ -3098,7 +3114,7 @@ async function listProjectLocales(options = {}) {
|
|
|
3098
3114
|
}
|
|
3099
3115
|
async function addLocales(locales, options = {}) {
|
|
3100
3116
|
if (locales.length === 0) {
|
|
3101
|
-
|
|
3117
|
+
p15.log.error("No locale codes provided.");
|
|
3102
3118
|
return 1;
|
|
3103
3119
|
}
|
|
3104
3120
|
const config = getApiConfig(options);
|
|
@@ -3108,30 +3124,30 @@ async function addLocales(locales, options = {}) {
|
|
|
3108
3124
|
let lastTargetLocales = [];
|
|
3109
3125
|
let hadError = false;
|
|
3110
3126
|
for (const locale of locales) {
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3127
|
+
const spinner9 = p15.spinner();
|
|
3128
|
+
spinner9.start(`Adding ${locale}\u2026`);
|
|
3113
3129
|
try {
|
|
3114
3130
|
const result = await api.addLocale(locale, void 0, appId);
|
|
3115
3131
|
lastTargetLocales = result.targetLocales;
|
|
3116
|
-
|
|
3132
|
+
spinner9.stop(`Added ${highlight(locale)}`);
|
|
3117
3133
|
} catch (error) {
|
|
3118
|
-
|
|
3134
|
+
spinner9.stop(`Failed to add ${chalk13.red(locale)}`);
|
|
3119
3135
|
hadError = true;
|
|
3120
3136
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3121
3137
|
const { limitError } = error;
|
|
3122
|
-
|
|
3138
|
+
p15.log.error(limitError.message);
|
|
3123
3139
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3124
|
-
|
|
3140
|
+
p15.log.info(line);
|
|
3125
3141
|
}
|
|
3126
3142
|
break;
|
|
3127
3143
|
}
|
|
3128
|
-
|
|
3144
|
+
p15.log.error(
|
|
3129
3145
|
error instanceof Error ? error.message : "Unknown error"
|
|
3130
3146
|
);
|
|
3131
3147
|
}
|
|
3132
3148
|
}
|
|
3133
3149
|
if (lastTargetLocales.length > 0) {
|
|
3134
|
-
|
|
3150
|
+
p15.log.info(
|
|
3135
3151
|
`Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
|
|
3136
3152
|
);
|
|
3137
3153
|
}
|
|
@@ -3139,7 +3155,7 @@ async function addLocales(locales, options = {}) {
|
|
|
3139
3155
|
}
|
|
3140
3156
|
async function removeLocales(locales, options = {}) {
|
|
3141
3157
|
if (locales.length === 0) {
|
|
3142
|
-
|
|
3158
|
+
p15.log.error("No locale codes provided.");
|
|
3143
3159
|
return 1;
|
|
3144
3160
|
}
|
|
3145
3161
|
const config = getApiConfig(options);
|
|
@@ -3149,26 +3165,26 @@ async function removeLocales(locales, options = {}) {
|
|
|
3149
3165
|
let lastTargetLocales = [];
|
|
3150
3166
|
let hadError = false;
|
|
3151
3167
|
for (const locale of locales) {
|
|
3152
|
-
const
|
|
3153
|
-
|
|
3168
|
+
const spinner9 = p15.spinner();
|
|
3169
|
+
spinner9.start(`Removing ${locale}\u2026`);
|
|
3154
3170
|
try {
|
|
3155
3171
|
const result = await api.removeLocale(locale, void 0, appId);
|
|
3156
3172
|
lastTargetLocales = result.targetLocales;
|
|
3157
|
-
|
|
3173
|
+
spinner9.stop(`Removed ${highlight(locale)}`);
|
|
3158
3174
|
} catch (error) {
|
|
3159
|
-
|
|
3175
|
+
spinner9.stop(`Failed to remove ${chalk13.red(locale)}`);
|
|
3160
3176
|
hadError = true;
|
|
3161
|
-
|
|
3177
|
+
p15.log.error(
|
|
3162
3178
|
error instanceof Error ? error.message : "Unknown error"
|
|
3163
3179
|
);
|
|
3164
3180
|
}
|
|
3165
3181
|
}
|
|
3166
3182
|
if (lastTargetLocales.length > 0) {
|
|
3167
|
-
|
|
3183
|
+
p15.log.info(
|
|
3168
3184
|
`Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
|
|
3169
3185
|
);
|
|
3170
3186
|
} else if (!hadError) {
|
|
3171
|
-
|
|
3187
|
+
p15.log.info("Target locales now: (none configured)");
|
|
3172
3188
|
}
|
|
3173
3189
|
return hadError ? 1 : 0;
|
|
3174
3190
|
}
|
|
@@ -3178,14 +3194,14 @@ async function listSupportedLocales(options = {}) {
|
|
|
3178
3194
|
const api = new VocoderAPI(config);
|
|
3179
3195
|
try {
|
|
3180
3196
|
const result = await api.listLocales(config.apiKey);
|
|
3181
|
-
|
|
3197
|
+
p15.log.info(chalk13.bold("Source locales:"));
|
|
3182
3198
|
printLocaleTable(result.sourceLocales);
|
|
3183
|
-
|
|
3184
|
-
|
|
3199
|
+
p15.log.info("");
|
|
3200
|
+
p15.log.info(chalk13.bold("Target locales:"));
|
|
3185
3201
|
printLocaleTable(result.targetLocales);
|
|
3186
3202
|
return 0;
|
|
3187
3203
|
} catch (error) {
|
|
3188
|
-
|
|
3204
|
+
p15.log.error(
|
|
3189
3205
|
error instanceof Error ? error.message : "Failed to fetch supported locales."
|
|
3190
3206
|
);
|
|
3191
3207
|
return 1;
|
|
@@ -3194,16 +3210,16 @@ async function listSupportedLocales(options = {}) {
|
|
|
3194
3210
|
function printLocaleTable(locales) {
|
|
3195
3211
|
for (const locale of locales) {
|
|
3196
3212
|
const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
|
|
3197
|
-
|
|
3213
|
+
p15.log.info(` ${highlight(locale.code.padEnd(10))} ${locale.name}${native}`);
|
|
3198
3214
|
}
|
|
3199
3215
|
}
|
|
3200
3216
|
|
|
3201
3217
|
// src/commands/logout.ts
|
|
3202
|
-
import * as
|
|
3218
|
+
import * as p16 from "@clack/prompts";
|
|
3203
3219
|
async function logout(options = {}) {
|
|
3204
3220
|
const stored = readAuthData();
|
|
3205
3221
|
if (!stored) {
|
|
3206
|
-
|
|
3222
|
+
p16.log.info("Not currently authenticated.");
|
|
3207
3223
|
return 0;
|
|
3208
3224
|
}
|
|
3209
3225
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
@@ -3213,19 +3229,19 @@ async function logout(options = {}) {
|
|
|
3213
3229
|
} catch {
|
|
3214
3230
|
}
|
|
3215
3231
|
clearAuthData();
|
|
3216
|
-
|
|
3232
|
+
p16.log.success(`Logged out (was ${stored.email})`);
|
|
3217
3233
|
return 0;
|
|
3218
3234
|
}
|
|
3219
3235
|
|
|
3220
3236
|
// src/commands/app-config.ts
|
|
3221
|
-
import * as
|
|
3222
|
-
import
|
|
3237
|
+
import * as p17 from "@clack/prompts";
|
|
3238
|
+
import chalk14 from "chalk";
|
|
3223
3239
|
import { config as loadEnv4 } from "dotenv";
|
|
3224
3240
|
loadEnv4();
|
|
3225
3241
|
async function appConfig(options = {}) {
|
|
3226
3242
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
3227
3243
|
if (!apiKey) {
|
|
3228
|
-
|
|
3244
|
+
p17.log.error(
|
|
3229
3245
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3230
3246
|
);
|
|
3231
3247
|
return 1;
|
|
@@ -3235,10 +3251,10 @@ async function appConfig(options = {}) {
|
|
|
3235
3251
|
try {
|
|
3236
3252
|
const config = await api.getAppConfig();
|
|
3237
3253
|
const lines = [
|
|
3238
|
-
`App: ${
|
|
3254
|
+
`App: ${chalk14.bold(config.projectName)}`,
|
|
3239
3255
|
`Organization: ${config.organizationName}`,
|
|
3240
3256
|
`Source locale: ${highlight(config.sourceLocale)}`,
|
|
3241
|
-
`Target locales: ${config.targetLocales.length > 0 ? config.targetLocales.map((l) => highlight(l)).join(", ") :
|
|
3257
|
+
`Target locales: ${config.targetLocales.length > 0 ? config.targetLocales.map((l) => highlight(l)).join(", ") : chalk14.dim("(none)")}`,
|
|
3242
3258
|
`Target branches: ${config.targetBranches.map((b) => highlight(b)).join(", ")}`,
|
|
3243
3259
|
...config.primaryBranch ? [`Primary branch: ${highlight(config.primaryBranch)}`] : [],
|
|
3244
3260
|
`Sync policy:`,
|
|
@@ -3247,10 +3263,10 @@ async function appConfig(options = {}) {
|
|
|
3247
3263
|
` Non-blocking mode: ${highlight(config.syncPolicy.nonBlockingMode)}`,
|
|
3248
3264
|
` Max wait: ${highlight(String(config.syncPolicy.defaultMaxWaitMs))} ms`
|
|
3249
3265
|
];
|
|
3250
|
-
|
|
3266
|
+
p17.note(lines.join("\n"), `${config.projectName} \u2014 app config`);
|
|
3251
3267
|
return 0;
|
|
3252
3268
|
} catch (error) {
|
|
3253
|
-
|
|
3269
|
+
p17.log.error(
|
|
3254
3270
|
error instanceof Error ? error.message : "Failed to fetch project config."
|
|
3255
3271
|
);
|
|
3256
3272
|
return 1;
|
|
@@ -3260,13 +3276,13 @@ async function appConfig(options = {}) {
|
|
|
3260
3276
|
// src/commands/translations.ts
|
|
3261
3277
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3262
3278
|
import { join as join4 } from "path";
|
|
3263
|
-
import * as
|
|
3279
|
+
import * as p18 from "@clack/prompts";
|
|
3264
3280
|
import { config as loadEnv5 } from "dotenv";
|
|
3265
3281
|
loadEnv5();
|
|
3266
3282
|
async function getTranslations(options = {}) {
|
|
3267
3283
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
3268
3284
|
if (!apiKey) {
|
|
3269
|
-
|
|
3285
|
+
p18.log.error(
|
|
3270
3286
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3271
3287
|
);
|
|
3272
3288
|
return 1;
|
|
@@ -3277,25 +3293,25 @@ async function getTranslations(options = {}) {
|
|
|
3277
3293
|
try {
|
|
3278
3294
|
branch = detectBranch(options.branch);
|
|
3279
3295
|
} catch (error) {
|
|
3280
|
-
|
|
3296
|
+
p18.log.error(
|
|
3281
3297
|
error instanceof Error ? error.message : "Failed to detect branch."
|
|
3282
3298
|
);
|
|
3283
3299
|
return 1;
|
|
3284
3300
|
}
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3301
|
+
const spinner9 = p18.spinner();
|
|
3302
|
+
spinner9.start(`Fetching translations for ${highlight(branch)}\u2026`);
|
|
3287
3303
|
try {
|
|
3288
3304
|
const projectConfig = await api.getAppConfig();
|
|
3289
3305
|
const targetLocales = options.locale ? [options.locale] : projectConfig.targetLocales;
|
|
3290
3306
|
if (targetLocales.length === 0) {
|
|
3291
|
-
|
|
3292
|
-
|
|
3307
|
+
spinner9.stop("No target locales configured.");
|
|
3308
|
+
p18.log.info("Add target locales with `vocoder locales add <code>`.");
|
|
3293
3309
|
return 1;
|
|
3294
3310
|
}
|
|
3295
3311
|
const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
|
|
3296
|
-
|
|
3312
|
+
spinner9.stop(`Fetched translations for ${highlight(branch)}`);
|
|
3297
3313
|
if (snapshot.status === "NOT_FOUND") {
|
|
3298
|
-
|
|
3314
|
+
p18.log.warn(
|
|
3299
3315
|
`No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
|
|
3300
3316
|
);
|
|
3301
3317
|
return 1;
|
|
@@ -3309,8 +3325,8 @@ async function getTranslations(options = {}) {
|
|
|
3309
3325
|
}
|
|
3310
3326
|
return 0;
|
|
3311
3327
|
} catch (error) {
|
|
3312
|
-
|
|
3313
|
-
|
|
3328
|
+
spinner9.stop("Failed to fetch translations.");
|
|
3329
|
+
p18.log.error(
|
|
3314
3330
|
error instanceof Error ? error.message : "Unknown error."
|
|
3315
3331
|
);
|
|
3316
3332
|
return 1;
|
|
@@ -3321,19 +3337,19 @@ function writeLocaleFiles(translations, outputDir) {
|
|
|
3321
3337
|
for (const [locale, strings] of Object.entries(translations)) {
|
|
3322
3338
|
const filePath = join4(outputDir, `${locale}.json`);
|
|
3323
3339
|
writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
|
|
3324
|
-
|
|
3340
|
+
p18.log.success(`Wrote ${highlight(filePath)}`);
|
|
3325
3341
|
}
|
|
3326
3342
|
}
|
|
3327
3343
|
|
|
3328
3344
|
// src/commands/create-app.ts
|
|
3329
|
-
import * as
|
|
3330
|
-
import
|
|
3345
|
+
import * as p19 from "@clack/prompts";
|
|
3346
|
+
import chalk15 from "chalk";
|
|
3331
3347
|
import { config as loadEnv6 } from "dotenv";
|
|
3332
3348
|
loadEnv6();
|
|
3333
3349
|
async function createApp(options) {
|
|
3334
3350
|
const authData = readAuthData();
|
|
3335
3351
|
if (!authData) {
|
|
3336
|
-
|
|
3352
|
+
p19.log.error(
|
|
3337
3353
|
"Not logged in. Run `npx @vocoder/cli init` to authenticate first."
|
|
3338
3354
|
);
|
|
3339
3355
|
return 1;
|
|
@@ -3352,15 +3368,15 @@ async function createApp(options) {
|
|
|
3352
3368
|
appDir = identity.repoAppDir;
|
|
3353
3369
|
}
|
|
3354
3370
|
} else {
|
|
3355
|
-
|
|
3371
|
+
p19.log.warn(
|
|
3356
3372
|
"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."
|
|
3357
3373
|
);
|
|
3358
3374
|
}
|
|
3359
3375
|
}
|
|
3360
3376
|
const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
|
|
3361
3377
|
const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3378
|
+
const spinner9 = p19.spinner();
|
|
3379
|
+
spinner9.start(`Creating app "${options.name}"\u2026`);
|
|
3364
3380
|
try {
|
|
3365
3381
|
const result = await api.createProject(authData.token, {
|
|
3366
3382
|
organizationId: options.organization,
|
|
@@ -3371,35 +3387,35 @@ async function createApp(options) {
|
|
|
3371
3387
|
appDirs: [appDir],
|
|
3372
3388
|
...repoCanonical ? { repoCanonical } : {}
|
|
3373
3389
|
});
|
|
3374
|
-
|
|
3390
|
+
spinner9.stop(`Created app ${chalk15.bold(result.projectName)}`);
|
|
3375
3391
|
const lines = [
|
|
3376
3392
|
`Project ID: ${result.projectId}`,
|
|
3377
3393
|
`Source locale: ${highlight(result.sourceLocale)}`,
|
|
3378
|
-
`Target locales: ${result.targetLocales.length > 0 ? result.targetLocales.map((l) => highlight(l)).join(", ") :
|
|
3394
|
+
`Target locales: ${result.targetLocales.length > 0 ? result.targetLocales.map((l) => highlight(l)).join(", ") : chalk15.dim("(none)")}`,
|
|
3379
3395
|
`Branches: ${result.targetBranches.map((b) => highlight(b)).join(", ")}`,
|
|
3380
3396
|
...repoCanonical ? [`Repository: ${highlight(repoCanonical)}${appDir !== "." ? ` (${appDir})` : ""}`] : [],
|
|
3381
3397
|
"",
|
|
3382
3398
|
`Add this to your .env file:`,
|
|
3383
|
-
` ${
|
|
3399
|
+
` ${chalk15.bold("VOCODER_API_KEY")}=${highlight(result.apiKey)}`
|
|
3384
3400
|
];
|
|
3385
|
-
|
|
3401
|
+
p19.note(lines.join("\n"), "Project created");
|
|
3386
3402
|
if (!result.repositoryBound && repoCanonical) {
|
|
3387
|
-
|
|
3403
|
+
p19.log.warn(
|
|
3388
3404
|
`Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
|
|
3389
3405
|
);
|
|
3390
3406
|
}
|
|
3391
3407
|
return 0;
|
|
3392
3408
|
} catch (error) {
|
|
3393
|
-
|
|
3409
|
+
spinner9.stop("Failed to create project.");
|
|
3394
3410
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3395
3411
|
const { limitError } = error;
|
|
3396
|
-
|
|
3412
|
+
p19.log.error(limitError.message);
|
|
3397
3413
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3398
|
-
|
|
3414
|
+
p19.log.info(line);
|
|
3399
3415
|
}
|
|
3400
3416
|
return 1;
|
|
3401
3417
|
}
|
|
3402
|
-
|
|
3418
|
+
p19.log.error(
|
|
3403
3419
|
error instanceof Error ? error.message : "Unknown error."
|
|
3404
3420
|
);
|
|
3405
3421
|
return 1;
|
|
@@ -3407,26 +3423,26 @@ async function createApp(options) {
|
|
|
3407
3423
|
}
|
|
3408
3424
|
|
|
3409
3425
|
// src/commands/whoami.ts
|
|
3410
|
-
import * as
|
|
3411
|
-
import
|
|
3426
|
+
import * as p20 from "@clack/prompts";
|
|
3427
|
+
import chalk16 from "chalk";
|
|
3412
3428
|
async function whoami(options = {}) {
|
|
3413
3429
|
const stored = readAuthData();
|
|
3414
3430
|
if (!stored) {
|
|
3415
|
-
|
|
3431
|
+
p20.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
3416
3432
|
return 1;
|
|
3417
3433
|
}
|
|
3418
3434
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3419
3435
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3420
3436
|
try {
|
|
3421
3437
|
const info2 = await api.getCliUserInfo(stored.token);
|
|
3422
|
-
|
|
3438
|
+
p20.log.info(`Logged in as ${chalk16.bold(info2.email)}`);
|
|
3423
3439
|
if (info2.name) {
|
|
3424
|
-
|
|
3440
|
+
p20.log.info(`Name: ${info2.name}`);
|
|
3425
3441
|
}
|
|
3426
|
-
|
|
3442
|
+
p20.log.info(`API: ${apiUrl}`);
|
|
3427
3443
|
return 0;
|
|
3428
3444
|
} catch {
|
|
3429
|
-
|
|
3445
|
+
p20.log.error(
|
|
3430
3446
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3431
3447
|
);
|
|
3432
3448
|
return 1;
|