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