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