@vocoder/cli 0.16.2 → 0.16.4
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 +1812 -1804
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-2JERZ6DL.mjs → chunk-OQWNYACE.mjs} +119 -119
- package/dist/{chunk-2JERZ6DL.mjs.map → chunk-OQWNYACE.mjs.map} +1 -1
- package/dist/lib.mjs +1 -1
- package/package.json +3 -3
package/dist/bin.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
readAuthData,
|
|
14
14
|
verifyStoredAuth,
|
|
15
15
|
writeAuthData
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OQWNYACE.mjs";
|
|
17
17
|
|
|
18
18
|
// src/bin.ts
|
|
19
19
|
import { Command } from "commander";
|
|
@@ -21,74 +21,10 @@ import { Command } from "commander";
|
|
|
21
21
|
// src/commands/init.ts
|
|
22
22
|
import * as p12 from "@clack/prompts";
|
|
23
23
|
|
|
24
|
-
// src/utils/
|
|
24
|
+
// src/utils/plan-check.ts
|
|
25
25
|
import * as p from "@clack/prompts";
|
|
26
26
|
import chalk from "chalk";
|
|
27
27
|
|
|
28
|
-
// src/utils/local-server.ts
|
|
29
|
-
import { createServer } from "http";
|
|
30
|
-
import { URL as URL2 } from "url";
|
|
31
|
-
function startCallbackServer() {
|
|
32
|
-
return new Promise((resolve3, reject) => {
|
|
33
|
-
let settled = false;
|
|
34
|
-
let callbackResolve = null;
|
|
35
|
-
let callbackReject = null;
|
|
36
|
-
const callbackPromise = new Promise((res, rej) => {
|
|
37
|
-
callbackResolve = res;
|
|
38
|
-
callbackReject = rej;
|
|
39
|
-
});
|
|
40
|
-
const server = createServer((req, res) => {
|
|
41
|
-
if (!req.url) {
|
|
42
|
-
res.writeHead(400);
|
|
43
|
-
res.end();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
let pathname;
|
|
47
|
-
let params;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL2(req.url, "http://localhost");
|
|
50
|
-
pathname = parsed.pathname;
|
|
51
|
-
params = Object.fromEntries(parsed.searchParams.entries());
|
|
52
|
-
} catch {
|
|
53
|
-
res.writeHead(400);
|
|
54
|
-
res.end("Bad request");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (pathname !== "/callback") {
|
|
58
|
-
res.writeHead(404);
|
|
59
|
-
res.end("Not found");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
63
|
-
res.end(
|
|
64
|
-
'<!DOCTYPE html><html><head><title>Authenticated</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;"><h2>Authenticated</h2><p>Return to your terminal to continue. You can close this tab.</p></body></html>'
|
|
65
|
-
);
|
|
66
|
-
if (callbackResolve) {
|
|
67
|
-
callbackResolve(params);
|
|
68
|
-
callbackResolve = null;
|
|
69
|
-
}
|
|
70
|
-
setImmediate(() => server.close());
|
|
71
|
-
});
|
|
72
|
-
server.on("error", (err) => {
|
|
73
|
-
if (!settled) {
|
|
74
|
-
settled = true;
|
|
75
|
-
if (callbackReject) callbackReject(err);
|
|
76
|
-
reject(err);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
server.listen(0, "127.0.0.1", () => {
|
|
80
|
-
if (settled) return;
|
|
81
|
-
settled = true;
|
|
82
|
-
const port = server.address().port;
|
|
83
|
-
resolve3({
|
|
84
|
-
port,
|
|
85
|
-
waitForCallback: () => callbackPromise,
|
|
86
|
-
close: () => server.close()
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
28
|
// src/utils/browser.ts
|
|
93
29
|
import { spawn } from "child_process";
|
|
94
30
|
async function tryOpenBrowser(url) {
|
|
@@ -137,1917 +73,1989 @@ async function tryOpenBrowser(url) {
|
|
|
137
73
|
});
|
|
138
74
|
}
|
|
139
75
|
|
|
140
|
-
// src/utils/
|
|
141
|
-
|
|
142
|
-
|
|
76
|
+
// src/utils/plan-check.ts
|
|
77
|
+
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
78
|
+
function getSubscriptionSettingsUrl(apiUrl) {
|
|
79
|
+
return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
|
|
143
80
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`);
|
|
160
|
-
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
161
|
-
if (reauth) {
|
|
162
|
-
if (!options.yes) {
|
|
163
|
-
const shouldOpen = await p.confirm({
|
|
164
|
-
message: "Open your browser to sign in again?"
|
|
165
|
-
});
|
|
166
|
-
if (p.isCancel(shouldOpen)) {
|
|
167
|
-
server?.close();
|
|
168
|
-
p.cancel("Setup cancelled.");
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
if (!shouldOpen) {
|
|
172
|
-
server?.close();
|
|
173
|
-
p.cancel("Setup cancelled.");
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
const opened = await tryOpenBrowser(browserUrl);
|
|
177
|
-
if (!opened) {
|
|
178
|
-
p.note(browserUrl, "Sign In");
|
|
179
|
-
p.log.info("Open the URL above manually to continue.");
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
await tryOpenBrowser(browserUrl);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
let isLinkFlow = false;
|
|
186
|
-
if (!options.yes) {
|
|
187
|
-
const connectChoice = await p.select({
|
|
188
|
-
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
189
|
-
options: [
|
|
190
|
-
{
|
|
191
|
-
value: "install",
|
|
192
|
-
label: "Install GitHub App",
|
|
193
|
-
hint: "new user"
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
value: "link",
|
|
197
|
-
label: "Already installed? Link your account",
|
|
198
|
-
hint: "returning user"
|
|
199
|
-
}
|
|
200
|
-
]
|
|
201
|
-
});
|
|
202
|
-
if (p.isCancel(connectChoice)) {
|
|
203
|
-
server?.close();
|
|
204
|
-
p.cancel("Setup cancelled.");
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
isLinkFlow = connectChoice === "link";
|
|
208
|
-
}
|
|
209
|
-
let urlToOpen = browserUrl;
|
|
210
|
-
if (isLinkFlow) {
|
|
211
|
-
try {
|
|
212
|
-
const linkSession = await api.startCliGitHubLinkSession(
|
|
213
|
-
session.sessionId,
|
|
214
|
-
server?.port
|
|
215
|
-
);
|
|
216
|
-
urlToOpen = linkSession.oauthUrl;
|
|
217
|
-
} catch {
|
|
218
|
-
urlToOpen = browserUrl;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
const opened = await tryOpenBrowser(urlToOpen);
|
|
222
|
-
if (!opened) {
|
|
223
|
-
p.log.warn("Could not open your browser automatically.");
|
|
224
|
-
p.note(urlToOpen, "GitHub");
|
|
225
|
-
p.log.info("Open the URL above to continue.");
|
|
226
|
-
}
|
|
81
|
+
function isPlanLimitFailure(message) {
|
|
82
|
+
if (!message) return false;
|
|
83
|
+
return /limit|upgrade/i.test(message);
|
|
84
|
+
}
|
|
85
|
+
function printPlanLimitMessage(apiUrl, message) {
|
|
86
|
+
p.log.error(`You are over your plan limits.
|
|
87
|
+
${message}`);
|
|
88
|
+
p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
89
|
+
}
|
|
90
|
+
async function checkPlanLimits(api, userToken, organizationId, apiUrl) {
|
|
91
|
+
try {
|
|
92
|
+
const { organizations } = await api.listOrganizations(userToken);
|
|
93
|
+
const organization = organizations.find((o) => o.id === organizationId);
|
|
94
|
+
if (!organization) {
|
|
95
|
+
return { atLimit: false };
|
|
227
96
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
} catch {
|
|
97
|
+
if (organization.maxApps !== -1 && organization.appCount >= organization.maxApps) {
|
|
98
|
+
p.log.warn(
|
|
99
|
+
`App limit reached \u2014 ${organization.appCount}/${organization.maxApps} on your ${chalk.bold(organization.planId)} plan.`
|
|
100
|
+
);
|
|
101
|
+
const limitAction = await p.select({
|
|
102
|
+
message: "What would you like to do?",
|
|
103
|
+
options: [
|
|
104
|
+
{ value: "upgrade", label: "Upgrade plan" },
|
|
105
|
+
{ value: "cancel", label: "Cancel" }
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
if (p.isCancel(limitAction) || limitAction === "cancel") {
|
|
109
|
+
p.cancel("Setup cancelled.");
|
|
110
|
+
return { atLimit: true };
|
|
245
111
|
}
|
|
246
|
-
|
|
112
|
+
await tryOpenBrowser(getSubscriptionSettingsUrl(apiUrl));
|
|
113
|
+
p.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
|
|
114
|
+
return { atLimit: true };
|
|
247
115
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (done || params === null || typeof params.token !== "string") return;
|
|
254
|
-
done = true;
|
|
255
|
-
resolve3({ kind: "server", params });
|
|
256
|
-
}).catch(() => {
|
|
257
|
-
});
|
|
258
|
-
sessionPoll.then((result) => {
|
|
259
|
-
if (done || result === null) return;
|
|
260
|
-
if (result.status === "complete" || result.status === "failed") {
|
|
261
|
-
done = true;
|
|
262
|
-
resolve3({
|
|
263
|
-
kind: "poll",
|
|
264
|
-
result
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}).catch(() => {
|
|
268
|
-
});
|
|
269
|
-
setTimeout(
|
|
270
|
-
() => {
|
|
271
|
-
if (!done) {
|
|
272
|
-
done = true;
|
|
273
|
-
resolve3(null);
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
Math.max(0, deadline - Date.now())
|
|
116
|
+
const remaining = organization.maxApps === -1 ? void 0 : Math.max(0, organization.maxApps - organization.appCount);
|
|
117
|
+
return { atLimit: false, remaining };
|
|
118
|
+
} catch {
|
|
119
|
+
p.log.warn(
|
|
120
|
+
"Could not verify plan limits \u2014 proceeding, the server will enforce them."
|
|
277
121
|
);
|
|
278
|
-
|
|
279
|
-
stopPolling = true;
|
|
280
|
-
server?.close();
|
|
281
|
-
if (winner !== null) {
|
|
282
|
-
if (winner.kind === "server") {
|
|
283
|
-
rawToken = winner.params.token;
|
|
284
|
-
if (typeof winner.params.organizationId === "string" && winner.params.organizationId) {
|
|
285
|
-
callbackOrganizationId = winner.params.organizationId;
|
|
286
|
-
}
|
|
287
|
-
if (winner.params.discovery_ready === "1") {
|
|
288
|
-
callbackDiscoveryReady = true;
|
|
289
|
-
}
|
|
290
|
-
} else if (winner.result.status === "complete") {
|
|
291
|
-
rawToken = winner.result.token;
|
|
292
|
-
if (winner.result.organizationId) {
|
|
293
|
-
callbackOrganizationId = winner.result.organizationId;
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
authSpinner.stop();
|
|
297
|
-
p.log.error(winner.result.reason);
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (!rawToken) {
|
|
302
|
-
authSpinner.stop();
|
|
303
|
-
p.log.error("The authentication link expired. Run `vocoder init` again.");
|
|
304
|
-
return null;
|
|
122
|
+
return { atLimit: false };
|
|
305
123
|
}
|
|
306
|
-
const userInfo = await api.getCliUserInfo(rawToken);
|
|
307
|
-
authSpinner.stop(`Authenticated as ${chalk.bold(userInfo.email)}`);
|
|
308
|
-
return {
|
|
309
|
-
token: rawToken,
|
|
310
|
-
...userInfo,
|
|
311
|
-
organizationId: callbackOrganizationId,
|
|
312
|
-
discoveryReady: callbackDiscoveryReady
|
|
313
|
-
};
|
|
314
124
|
}
|
|
315
125
|
|
|
316
|
-
// src/utils/
|
|
317
|
-
import * as
|
|
318
|
-
import
|
|
319
|
-
import { execSync } from "child_process";
|
|
320
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
321
|
-
import { join } from "path";
|
|
322
|
-
function tryClipboard(text2) {
|
|
323
|
-
const tools = [
|
|
324
|
-
{ cmd: "pbcopy" },
|
|
325
|
-
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
326
|
-
{ cmd: "xsel", args: ["--clipboard", "--input"] },
|
|
327
|
-
{ cmd: "wl-copy" },
|
|
328
|
-
{ cmd: "clip" }
|
|
329
|
-
];
|
|
330
|
-
for (const { cmd, args = [] } of tools) {
|
|
331
|
-
try {
|
|
332
|
-
execSync([cmd, ...args].join(" "), {
|
|
333
|
-
input: text2,
|
|
334
|
-
stdio: ["pipe", "ignore", "ignore"]
|
|
335
|
-
});
|
|
336
|
-
return true;
|
|
337
|
-
} catch {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
function printCommand(cmd) {
|
|
344
|
-
const copied = tryClipboard(cmd);
|
|
345
|
-
process.stdout.write("\n");
|
|
346
|
-
process.stdout.write(` ${chalk2.dim("$")} ${chalk2.cyan(cmd)}
|
|
347
|
-
`);
|
|
348
|
-
if (copied) process.stdout.write(` ${chalk2.dim("\u2191 copied to clipboard")}
|
|
349
|
-
`);
|
|
350
|
-
process.stdout.write("\n");
|
|
351
|
-
}
|
|
352
|
-
function printCodeBlock(code) {
|
|
353
|
-
process.stdout.write("\n");
|
|
354
|
-
for (const line of code.split("\n")) {
|
|
355
|
-
process.stdout.write(` ${line}
|
|
356
|
-
`);
|
|
357
|
-
}
|
|
358
|
-
process.stdout.write("\n");
|
|
359
|
-
}
|
|
360
|
-
function writeApiKeyToEnv(apiKey, repoRoot) {
|
|
361
|
-
const envPath = join(repoRoot ?? process.cwd(), ".env");
|
|
362
|
-
if (!existsSync(envPath)) return false;
|
|
363
|
-
try {
|
|
364
|
-
const content = readFileSync(envPath, "utf-8");
|
|
365
|
-
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
366
|
-
let updated;
|
|
367
|
-
if (/^VOCODER_API_KEY=/m.test(content)) {
|
|
368
|
-
updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
|
|
369
|
-
} else {
|
|
370
|
-
const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
371
|
-
updated = `${content}${sep}${keyLine}
|
|
372
|
-
`;
|
|
373
|
-
}
|
|
374
|
-
writeFileSync(envPath, updated);
|
|
375
|
-
return true;
|
|
376
|
-
} catch {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function printApiKey(apiKey, repoRoot) {
|
|
381
|
-
const saved = writeApiKeyToEnv(apiKey, repoRoot);
|
|
382
|
-
p2.log.message("");
|
|
383
|
-
p2.log.message(chalk2.bold("Your API Key"));
|
|
384
|
-
printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
|
|
385
|
-
if (saved) {
|
|
386
|
-
p2.log.success(chalk2.dim("Saved to .env"));
|
|
387
|
-
} else {
|
|
388
|
-
p2.log.message(chalk2.dim(" Add the above to your .env file"));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
126
|
+
// src/utils/project-create.ts
|
|
127
|
+
import * as p4 from "@clack/prompts";
|
|
128
|
+
import chalk3 from "chalk";
|
|
391
129
|
|
|
392
|
-
// src/utils/
|
|
393
|
-
import
|
|
394
|
-
import chalk4 from "chalk";
|
|
395
|
-
import { execSync as execSync2 } from "child_process";
|
|
130
|
+
// src/utils/app-dir-select.ts
|
|
131
|
+
import { existsSync, statSync } from "fs";
|
|
396
132
|
import { resolve } from "path";
|
|
133
|
+
import { isCancel as isCancel2, Prompt } from "@clack/core";
|
|
134
|
+
import * as p2 from "@clack/prompts";
|
|
397
135
|
|
|
398
136
|
// src/utils/theme.ts
|
|
399
|
-
import
|
|
137
|
+
import chalk2 from "chalk";
|
|
400
138
|
var ORANGE = "#FC5206";
|
|
401
139
|
var PINK = "#D51977";
|
|
402
140
|
var BLUE = "#2450A9";
|
|
403
141
|
var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
|
|
404
|
-
var hex = (color) => (s) => noColor ? s :
|
|
405
|
-
var dim = (s) => noColor ? s :
|
|
406
|
-
var bld = (s) => noColor ? s :
|
|
407
|
-
var grn = (s) => noColor ? s :
|
|
408
|
-
var ylw = (s) => noColor ? s :
|
|
409
|
-
var red = (s) => noColor ? s :
|
|
142
|
+
var hex = (color) => (s) => noColor ? s : chalk2.hex(color)(s);
|
|
143
|
+
var dim = (s) => noColor ? s : chalk2.dim(s);
|
|
144
|
+
var bld = (s) => noColor ? s : chalk2.bold(s);
|
|
145
|
+
var grn = (s) => noColor ? s : chalk2.green(s);
|
|
146
|
+
var ylw = (s) => noColor ? s : chalk2.yellow(s);
|
|
147
|
+
var red = (s) => noColor ? s : chalk2.red(s);
|
|
410
148
|
var highlight = hex(PINK);
|
|
411
149
|
var info = hex(BLUE);
|
|
412
150
|
var active = hex(ORANGE);
|
|
413
151
|
|
|
414
|
-
// src/utils/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
152
|
+
// src/utils/app-dir-select.ts
|
|
153
|
+
var S_BAR = "\u2502";
|
|
154
|
+
var S_BAR_END = "\u2514";
|
|
155
|
+
var S_ACTIVE = "\u25C6";
|
|
156
|
+
var S_SUBMIT = "\u25C6";
|
|
157
|
+
var S_CANCEL = "\u25A0";
|
|
158
|
+
var S_ERROR = "\u25B2";
|
|
159
|
+
function symbol(state) {
|
|
160
|
+
switch (state) {
|
|
161
|
+
case "submit":
|
|
162
|
+
return grn(S_SUBMIT);
|
|
163
|
+
case "cancel":
|
|
164
|
+
return red(S_CANCEL);
|
|
165
|
+
case "error":
|
|
166
|
+
return ylw(S_ERROR);
|
|
167
|
+
default:
|
|
168
|
+
return active(S_ACTIVE);
|
|
425
169
|
}
|
|
426
|
-
return null;
|
|
427
170
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
` :
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
export default defineConfig({
|
|
446
|
-
${appIdLine} targetBranches: [${branchesStr}],
|
|
447
|
-
include: [${includesStr}],
|
|
448
|
-
})
|
|
449
|
-
`;
|
|
450
|
-
try {
|
|
451
|
-
writeFileSync2(configPath, content, "utf-8");
|
|
452
|
-
return `vocoder.config.${ext}`;
|
|
453
|
-
} catch {
|
|
454
|
-
return null;
|
|
171
|
+
function validateAppDirPath(val, existing, opts = {}) {
|
|
172
|
+
if (val.startsWith("/")) return "Must be a relative path (e.g. apps/web)";
|
|
173
|
+
if (val.includes("..")) return "Path traversal not allowed";
|
|
174
|
+
const hasWholeRepo = existing.includes("");
|
|
175
|
+
const hasScoped = existing.some((d) => d !== "");
|
|
176
|
+
if (val === "" && hasScoped) return "Cannot add whole-repo scope to a monorepo project";
|
|
177
|
+
if (val !== "" && hasWholeRepo) return "Cannot add a scoped directory to a whole-repo project";
|
|
178
|
+
if (existing.includes(val)) return `Already added: ${val}`;
|
|
179
|
+
const nested = existing.find(
|
|
180
|
+
(d) => d !== "" && (val.startsWith(d + "/") || d.startsWith(val + "/"))
|
|
181
|
+
);
|
|
182
|
+
if (nested) return `"${val}" overlaps with already-added "${nested}"`;
|
|
183
|
+
if (val !== "") {
|
|
184
|
+
const abs = resolve(opts.cwd ?? process.cwd(), val);
|
|
185
|
+
if (!existsSync(abs)) return `Directory not found: ${val}`;
|
|
186
|
+
if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
|
|
455
187
|
}
|
|
188
|
+
return null;
|
|
456
189
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
190
|
+
async function collectAppDirs(opts = {}) {
|
|
191
|
+
const added = [];
|
|
192
|
+
let filter = "";
|
|
193
|
+
let cursor = 0;
|
|
194
|
+
let addCursor = false;
|
|
195
|
+
const isNewDir = () => {
|
|
196
|
+
const t = filter.trim();
|
|
197
|
+
return t.length > 0 && !added.includes(t);
|
|
198
|
+
};
|
|
199
|
+
const clampCursor = () => {
|
|
200
|
+
const max = added.length - 1;
|
|
201
|
+
if (cursor > max) cursor = Math.max(0, max);
|
|
202
|
+
};
|
|
203
|
+
const prompt = new Prompt(
|
|
204
|
+
{
|
|
205
|
+
validate() {
|
|
206
|
+
return void 0;
|
|
207
|
+
},
|
|
208
|
+
render() {
|
|
209
|
+
const trimmed = filter.trim();
|
|
210
|
+
const hdr = `${dim(S_BAR)}
|
|
211
|
+
${symbol(this.state)} App directories
|
|
212
|
+
`;
|
|
213
|
+
switch (this.state) {
|
|
214
|
+
case "submit": {
|
|
215
|
+
const summary = added.length > 0 ? bld(added.join(", ")) : dim("none (single-app project)");
|
|
216
|
+
return `${hdr}${dim(S_BAR)} ${summary}`;
|
|
217
|
+
}
|
|
218
|
+
case "cancel":
|
|
219
|
+
return `${hdr}${dim(S_BAR)}`;
|
|
220
|
+
default: {
|
|
221
|
+
const inputHint = filter.length > 0 ? filter : added.length === 0 ? dim("e.g. apps/web") : dim("e.g. apps/api");
|
|
222
|
+
const lines = [
|
|
223
|
+
hdr.trimEnd(),
|
|
224
|
+
`${info(S_BAR)} ${dim("/")} ${inputHint}`,
|
|
225
|
+
info(S_BAR)
|
|
226
|
+
];
|
|
227
|
+
for (let i = 0; i < added.length; i++) {
|
|
228
|
+
const isCursor = i === cursor && !addCursor;
|
|
229
|
+
const icon = active("\u25FC");
|
|
230
|
+
const label = isCursor ? bld(added[i]) : added[i];
|
|
231
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
232
|
+
}
|
|
233
|
+
const atLimit = opts.maxDirs !== void 0 && added.length >= opts.maxDirs;
|
|
234
|
+
if (atLimit) {
|
|
235
|
+
lines.push(`${info(S_BAR)} ${dim(`App limit reached (${added.length}/${opts.maxDirs} on your plan)`)}`);
|
|
236
|
+
} else if (isNewDir()) {
|
|
237
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
238
|
+
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
239
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}"`;
|
|
240
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push(info(S_BAR));
|
|
243
|
+
if (atLimit) {
|
|
244
|
+
lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
245
|
+
} else if (added.length === 0 && !isNewDir()) {
|
|
246
|
+
lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
|
|
247
|
+
lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
|
|
248
|
+
} else if (added.length > 0) {
|
|
249
|
+
lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
250
|
+
}
|
|
251
|
+
const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
|
|
252
|
+
if (this.state === "error") {
|
|
253
|
+
lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
|
|
254
|
+
} else {
|
|
255
|
+
lines.push(barEnd);
|
|
256
|
+
}
|
|
257
|
+
lines.push("");
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
485
261
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
262
|
+
},
|
|
263
|
+
false
|
|
264
|
+
);
|
|
265
|
+
prompt.on("key", (key) => {
|
|
266
|
+
if (!key || key === " ") return;
|
|
267
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
268
|
+
if (cp === 127 || cp === 8) {
|
|
269
|
+
filter = filter.slice(0, -1);
|
|
270
|
+
addCursor = false;
|
|
271
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
272
|
+
filter += key;
|
|
273
|
+
cursor = 0;
|
|
274
|
+
addCursor = false;
|
|
494
275
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
import { execSync as execSync3 } from "child_process";
|
|
529
|
-
var MCP_DOCS_URL = "https://vocoder.app/docs/mcp";
|
|
530
|
-
function mcpServerJson(apiKey) {
|
|
531
|
-
return JSON.stringify(
|
|
532
|
-
{
|
|
533
|
-
mcpServers: {
|
|
534
|
-
vocoder: {
|
|
535
|
-
type: "stdio",
|
|
536
|
-
command: "npx",
|
|
537
|
-
args: ["-y", "@vocoder/mcp"],
|
|
538
|
-
env: { VOCODER_API_KEY: apiKey }
|
|
276
|
+
});
|
|
277
|
+
prompt.on("cursor", (action) => {
|
|
278
|
+
switch (action) {
|
|
279
|
+
case "up":
|
|
280
|
+
if (addCursor) {
|
|
281
|
+
addCursor = false;
|
|
282
|
+
cursor = Math.max(0, added.length - 1);
|
|
283
|
+
} else {
|
|
284
|
+
cursor = Math.max(0, cursor - 1);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case "down":
|
|
288
|
+
if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
|
|
289
|
+
addCursor = true;
|
|
290
|
+
} else if (!addCursor) {
|
|
291
|
+
cursor = Math.min(added.length - 1, cursor + 1);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
case "space": {
|
|
295
|
+
if (addCursor || filter.trim().length > 0 && isNewDir()) {
|
|
296
|
+
if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
|
|
297
|
+
const trimmed = filter.trim();
|
|
298
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
299
|
+
if (!err) {
|
|
300
|
+
added.push(trimmed);
|
|
301
|
+
filter = "";
|
|
302
|
+
addCursor = false;
|
|
303
|
+
cursor = 0;
|
|
304
|
+
}
|
|
305
|
+
} else if (added.length > 0 && !isNewDir()) {
|
|
306
|
+
clampCursor();
|
|
307
|
+
added.splice(cursor, 1);
|
|
308
|
+
if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
|
|
539
309
|
}
|
|
310
|
+
break;
|
|
540
311
|
}
|
|
541
|
-
}
|
|
542
|
-
null,
|
|
543
|
-
2
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
async function runMcpSetup(apiKey) {
|
|
547
|
-
const tool = await p4.select({
|
|
548
|
-
message: "Which AI editor?",
|
|
549
|
-
options: [
|
|
550
|
-
{ value: "claude", label: "Claude Code" },
|
|
551
|
-
{ value: "cursor", label: "Cursor" },
|
|
552
|
-
{ value: "windsurf", label: "Windsurf" },
|
|
553
|
-
{ value: "vscode", label: "VS Code (GitHub Copilot)" },
|
|
554
|
-
{ value: "other", label: "Other \u2014 show the config JSON" }
|
|
555
|
-
]
|
|
312
|
+
}
|
|
556
313
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
execSync3(
|
|
561
|
-
`claude mcp add --scope user --transport stdio -e VOCODER_API_KEY=${apiKey} vocoder -- npx -y @vocoder/mcp`,
|
|
562
|
-
{ stdio: "pipe" }
|
|
563
|
-
);
|
|
564
|
-
p4.log.success("Vocoder MCP server registered in Claude Code.");
|
|
565
|
-
} catch {
|
|
566
|
-
p4.log.message("Run this to register the MCP server:");
|
|
567
|
-
printCommand(
|
|
568
|
-
`claude mcp add --scope user --transport stdio -e VOCODER_API_KEY=${apiKey} vocoder -- npx -y @vocoder/mcp`
|
|
569
|
-
);
|
|
570
|
-
p4.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
314
|
+
prompt.on("finalize", () => {
|
|
315
|
+
if (prompt.state === "submit") {
|
|
316
|
+
prompt.value = [...added];
|
|
571
317
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
windsurf: { path: "~/.codeium/windsurf/mcp_config.json", merge: true },
|
|
577
|
-
vscode: { path: ".vscode/mcp.json", merge: true },
|
|
578
|
-
other: { path: ".mcp.json", merge: false }
|
|
579
|
-
};
|
|
580
|
-
const { path: configPath, merge } = configPaths[tool];
|
|
581
|
-
const mergeNote = merge ? chalk5.dim(` Merge into ${configPath} (create if missing):`) : chalk5.dim(` Add to ${configPath}:`);
|
|
582
|
-
p4.log.message(mergeNote);
|
|
583
|
-
printCodeBlock(mcpServerJson(apiKey));
|
|
584
|
-
p4.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/utils/plan-check.ts
|
|
588
|
-
import * as p5 from "@clack/prompts";
|
|
589
|
-
import chalk6 from "chalk";
|
|
590
|
-
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
591
|
-
function getSubscriptionSettingsUrl(apiUrl) {
|
|
592
|
-
return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
|
|
318
|
+
});
|
|
319
|
+
const result = await prompt.prompt();
|
|
320
|
+
if (isCancel2(result)) return null;
|
|
321
|
+
return result;
|
|
593
322
|
}
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
323
|
+
async function promptSingleAppDir(params) {
|
|
324
|
+
const { existingDirs, cwd } = params;
|
|
325
|
+
const input = await p2.text({
|
|
326
|
+
message: "App directory to add",
|
|
327
|
+
placeholder: "apps/web",
|
|
328
|
+
validate(val) {
|
|
329
|
+
const err = validateAppDirPath(val ?? "", existingDirs, { cwd });
|
|
330
|
+
if (err) return err;
|
|
331
|
+
if (!val) return "Directory is required";
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
if (p2.isCancel(input)) return null;
|
|
336
|
+
return input;
|
|
597
337
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
338
|
+
|
|
339
|
+
// src/utils/branch-select.ts
|
|
340
|
+
import { execSync } from "child_process";
|
|
341
|
+
import { isCancel as isCancel4, Prompt as Prompt2 } from "@clack/core";
|
|
342
|
+
var S_BAR2 = "\u2502";
|
|
343
|
+
var S_BAR_END2 = "\u2514";
|
|
344
|
+
var S_ACTIVE2 = "\u25C6";
|
|
345
|
+
var S_SUBMIT2 = "\u25C6";
|
|
346
|
+
var S_CANCEL2 = "\u25A0";
|
|
347
|
+
var S_ERROR2 = "\u25B2";
|
|
348
|
+
function symbol2(state) {
|
|
349
|
+
switch (state) {
|
|
350
|
+
case "submit":
|
|
351
|
+
return grn(S_SUBMIT2);
|
|
352
|
+
case "cancel":
|
|
353
|
+
return red(S_CANCEL2);
|
|
354
|
+
case "error":
|
|
355
|
+
return ylw(S_ERROR2);
|
|
356
|
+
default:
|
|
357
|
+
return active(S_ACTIVE2);
|
|
358
|
+
}
|
|
602
359
|
}
|
|
603
|
-
|
|
360
|
+
function detectGitBranches(cwd) {
|
|
361
|
+
const workDir = cwd ?? process.cwd();
|
|
604
362
|
try {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
363
|
+
const localOut = execSync("git branch", {
|
|
364
|
+
cwd: workDir,
|
|
365
|
+
stdio: "pipe"
|
|
366
|
+
}).toString();
|
|
367
|
+
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
368
|
+
let remoteBranches = [];
|
|
369
|
+
try {
|
|
370
|
+
const remoteOut = execSync("git branch -r", {
|
|
371
|
+
cwd: workDir,
|
|
372
|
+
stdio: "pipe"
|
|
373
|
+
}).toString();
|
|
374
|
+
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
375
|
+
} catch {
|
|
609
376
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
]
|
|
620
|
-
});
|
|
621
|
-
if (p5.isCancel(limitAction) || limitAction === "cancel") {
|
|
622
|
-
p5.cancel("Setup cancelled.");
|
|
623
|
-
return { atLimit: true };
|
|
624
|
-
}
|
|
625
|
-
await tryOpenBrowser(getSubscriptionSettingsUrl(apiUrl));
|
|
626
|
-
p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
|
|
627
|
-
return { atLimit: true };
|
|
377
|
+
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
378
|
+
let defaultBranch = "main";
|
|
379
|
+
try {
|
|
380
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
381
|
+
cwd: workDir,
|
|
382
|
+
stdio: "pipe"
|
|
383
|
+
}).toString().trim();
|
|
384
|
+
defaultBranch = ref.split("/").pop() ?? "main";
|
|
385
|
+
} catch {
|
|
628
386
|
}
|
|
629
|
-
|
|
630
|
-
|
|
387
|
+
return {
|
|
388
|
+
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
389
|
+
defaultBranch
|
|
390
|
+
};
|
|
631
391
|
} catch {
|
|
632
|
-
|
|
633
|
-
"Could not verify plan limits \u2014 proceeding, the server will enforce them."
|
|
634
|
-
);
|
|
635
|
-
return { atLimit: false };
|
|
392
|
+
return { branches: ["main"], defaultBranch: "main" };
|
|
636
393
|
}
|
|
637
394
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
);
|
|
659
|
-
p6.log.info("Opening GitHub to install the Vocoder App...");
|
|
660
|
-
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
661
|
-
const shouldOpen = params.yes ? true : await p6.confirm({ message: "Open in your browser?" });
|
|
662
|
-
if (p6.isCancel(shouldOpen)) {
|
|
663
|
-
server?.close();
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
if (shouldOpen) {
|
|
667
|
-
const opened = await tryOpenBrowser(installUrl);
|
|
668
|
-
if (!opened) {
|
|
669
|
-
p6.log.info(
|
|
670
|
-
"Could not open a browser automatically. Use the URL above."
|
|
671
|
-
);
|
|
672
|
-
}
|
|
395
|
+
var INVALID_CHARS = /[\s?^~:[\]\\]/;
|
|
396
|
+
function validateBranchPattern(pattern) {
|
|
397
|
+
const t = pattern.trim();
|
|
398
|
+
if (!t) return "Pattern cannot be empty";
|
|
399
|
+
if (INVALID_CHARS.test(t))
|
|
400
|
+
return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
401
|
+
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
402
|
+
if (t.includes("//")) return "Cannot contain //";
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
var MAX_VISIBLE = 10;
|
|
406
|
+
function buildItems(branches, defaultBranch, customPatterns) {
|
|
407
|
+
const items = branches.map((b) => ({
|
|
408
|
+
value: b,
|
|
409
|
+
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
410
|
+
}));
|
|
411
|
+
for (const pt of customPatterns) {
|
|
412
|
+
if (!branches.includes(pt)) {
|
|
413
|
+
items.push({ value: pt, label: pt, isCustom: true });
|
|
673
414
|
}
|
|
674
415
|
}
|
|
675
|
-
|
|
676
|
-
connectSpinner.start("Waiting for GitHub App installation...");
|
|
677
|
-
if (server) {
|
|
678
|
-
try {
|
|
679
|
-
const params_timeout = 15 * 60 * 1e3;
|
|
680
|
-
const callbackParams = await Promise.race([
|
|
681
|
-
server.waitForCallback(),
|
|
682
|
-
new Promise(
|
|
683
|
-
(resolve3) => setTimeout(() => resolve3(null), params_timeout)
|
|
684
|
-
)
|
|
685
|
-
]);
|
|
686
|
-
server.close();
|
|
687
|
-
if (!callbackParams) {
|
|
688
|
-
connectSpinner.stop("GitHub App installation timed out");
|
|
689
|
-
p6.log.error(
|
|
690
|
-
"The installation flow timed out. Run `vocoder init` again."
|
|
691
|
-
);
|
|
692
|
-
return null;
|
|
693
|
-
}
|
|
694
|
-
if (callbackParams.error) {
|
|
695
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
696
|
-
p6.log.error(callbackParams.error);
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
700
|
-
if (!organizationId || !connectionLabel) {
|
|
701
|
-
connectSpinner.stop("GitHub App installation incomplete");
|
|
702
|
-
p6.log.error("Missing organization or connection data from callback.");
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
connectSpinner.stop(
|
|
706
|
-
`Connected to GitHub as ${chalk7.bold(connectionLabel)}`
|
|
707
|
-
);
|
|
708
|
-
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
709
|
-
return {
|
|
710
|
-
organizationId,
|
|
711
|
-
organizationName: orgName,
|
|
712
|
-
connectionLabel
|
|
713
|
-
};
|
|
714
|
-
} catch {
|
|
715
|
-
server.close();
|
|
716
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
717
|
-
return null;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
721
|
-
p6.log.warn(
|
|
722
|
-
"Complete the installation in your browser, then run `vocoder init` again."
|
|
723
|
-
);
|
|
724
|
-
return null;
|
|
416
|
+
return items;
|
|
725
417
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
418
|
+
function filterItems(items, query) {
|
|
419
|
+
if (!query.trim()) return items;
|
|
420
|
+
const lower = query.toLowerCase();
|
|
421
|
+
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
422
|
+
}
|
|
423
|
+
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
424
|
+
const lines = [info(S_BAR2)];
|
|
425
|
+
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
426
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
427
|
+
const item = filtered[i];
|
|
428
|
+
const isCursor = i === cursor && !addCursor;
|
|
429
|
+
const isChecked = selected.has(item.value);
|
|
430
|
+
const icon = isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
|
|
431
|
+
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
432
|
+
if (isCursor) label = bld(label);
|
|
433
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
731
434
|
}
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
435
|
+
const trimmed = filter.trim();
|
|
436
|
+
const isNewPattern = trimmed.length > 0 && !filtered.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
437
|
+
if (isNewPattern) {
|
|
438
|
+
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
439
|
+
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
440
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
441
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
442
|
+
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
443
|
+
lines.push(dim(`${S_BAR2} No branches detected`));
|
|
444
|
+
}
|
|
445
|
+
const hidden = filtered.length - (end - scrollOffset);
|
|
446
|
+
if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
|
449
|
+
async function filterableBranchSelect(params) {
|
|
450
|
+
const { message, branches, defaultBranch } = params;
|
|
451
|
+
const optional = params.optional ?? false;
|
|
452
|
+
const excludedSet = new Set(params.excludedPatterns ?? []);
|
|
453
|
+
let filter = "";
|
|
454
|
+
let cursor = 0;
|
|
455
|
+
let scrollOffset = 0;
|
|
456
|
+
let addCursor = false;
|
|
457
|
+
const customPatterns = [];
|
|
458
|
+
const selected = new Set(params.initialValues ?? [defaultBranch]);
|
|
459
|
+
const getItems = () => buildItems(branches, defaultBranch, customPatterns);
|
|
460
|
+
const getFiltered = () => filterItems(getItems(), filter);
|
|
461
|
+
const isNewPattern = () => {
|
|
462
|
+
const t = filter.trim();
|
|
463
|
+
if (!t) return false;
|
|
464
|
+
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
465
|
+
};
|
|
466
|
+
const clampCursor = (filtered) => {
|
|
467
|
+
const hasAdd = isNewPattern();
|
|
468
|
+
const max = filtered.length - 1 + (hasAdd ? 1 : 0);
|
|
469
|
+
if (cursor > max && !addCursor) cursor = Math.max(0, max);
|
|
470
|
+
if (!addCursor) {
|
|
471
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
472
|
+
if (cursor >= scrollOffset + MAX_VISIBLE)
|
|
473
|
+
scrollOffset = cursor - MAX_VISIBLE + 1;
|
|
474
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
742
475
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
476
|
+
};
|
|
477
|
+
const prompt = new Prompt2(
|
|
478
|
+
{
|
|
479
|
+
validate() {
|
|
480
|
+
if (!optional && selected.size === 0)
|
|
481
|
+
return "At least one branch is required.";
|
|
482
|
+
return void 0;
|
|
483
|
+
},
|
|
484
|
+
render() {
|
|
485
|
+
const filtered = getFiltered();
|
|
486
|
+
clampCursor(filtered);
|
|
487
|
+
const hdr = `${dim(S_BAR2)}
|
|
488
|
+
${symbol2(this.state)} ${message}
|
|
489
|
+
`;
|
|
490
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter \xB7 type a custom pattern to add it");
|
|
491
|
+
const trimmedFilter = filter.trim();
|
|
492
|
+
const footer = (() => {
|
|
493
|
+
if (trimmedFilter.length > 0 && isNewPattern()) {
|
|
494
|
+
return dim(`${S_BAR2} Space to add "${trimmedFilter}" \xB7 \u2191\u2193 navigate \xB7 Enter to confirm`);
|
|
495
|
+
}
|
|
496
|
+
if (selected.size > 0) {
|
|
497
|
+
return dim(`${S_BAR2} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`);
|
|
498
|
+
}
|
|
499
|
+
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`);
|
|
500
|
+
})();
|
|
501
|
+
switch (this.state) {
|
|
502
|
+
case "submit": {
|
|
503
|
+
const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
|
|
504
|
+
return `${hdr}${dim(S_BAR2)} ${summary}`;
|
|
505
|
+
}
|
|
506
|
+
case "cancel":
|
|
507
|
+
return `${hdr}${dim(S_BAR2)}`;
|
|
508
|
+
case "error":
|
|
509
|
+
return [
|
|
510
|
+
hdr.trimEnd(),
|
|
511
|
+
`${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
512
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
513
|
+
footer,
|
|
514
|
+
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
515
|
+
""
|
|
516
|
+
].join("\n");
|
|
517
|
+
default:
|
|
518
|
+
return [
|
|
519
|
+
hdr.trimEnd(),
|
|
520
|
+
`${info(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
521
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
522
|
+
footer,
|
|
523
|
+
`${info(S_BAR_END2)}`,
|
|
524
|
+
""
|
|
525
|
+
].join("\n");
|
|
526
|
+
}
|
|
747
527
|
}
|
|
528
|
+
},
|
|
529
|
+
false
|
|
530
|
+
);
|
|
531
|
+
prompt.on("key", (key) => {
|
|
532
|
+
if (!key || key === " ") return;
|
|
533
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
534
|
+
if (cp === 127 || cp === 8) {
|
|
535
|
+
filter = filter.slice(0, -1);
|
|
536
|
+
cursor = 0;
|
|
537
|
+
scrollOffset = 0;
|
|
538
|
+
addCursor = false;
|
|
539
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
540
|
+
filter += key;
|
|
541
|
+
cursor = 0;
|
|
542
|
+
scrollOffset = 0;
|
|
543
|
+
addCursor = false;
|
|
748
544
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
752
|
-
if (server) {
|
|
753
|
-
try {
|
|
754
|
-
const timeoutMs = 10 * 60 * 1e3;
|
|
755
|
-
const callbackParams = await Promise.race([
|
|
756
|
-
server.waitForCallback(),
|
|
757
|
-
new Promise(
|
|
758
|
-
(resolve3) => setTimeout(() => resolve3(null), timeoutMs)
|
|
759
|
-
)
|
|
760
|
-
]);
|
|
761
|
-
server.close();
|
|
762
|
-
if (!callbackParams) {
|
|
763
|
-
oauthSpinner.stop("GitHub authorization timed out");
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
if (callbackParams.error) {
|
|
767
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
768
|
-
p6.log.error(callbackParams.error);
|
|
769
|
-
return null;
|
|
770
|
-
}
|
|
771
|
-
} catch {
|
|
772
|
-
server.close();
|
|
773
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
774
|
-
return null;
|
|
545
|
+
if (isNewPattern()) {
|
|
546
|
+
addCursor = true;
|
|
775
547
|
}
|
|
776
|
-
}
|
|
777
|
-
oauthSpinner.stop("GitHub account authorized");
|
|
778
|
-
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
779
|
-
params.userToken
|
|
780
|
-
);
|
|
781
|
-
return discoveryResult.installations;
|
|
782
|
-
}
|
|
783
|
-
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
784
|
-
const options = installations.map((inst) => ({
|
|
785
|
-
value: String(inst.installationId),
|
|
786
|
-
label: inst.accountLogin,
|
|
787
|
-
hint: [
|
|
788
|
-
inst.accountType === "Organization" ? "GitHub org" : "personal account",
|
|
789
|
-
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
790
|
-
inst.isSuspended ? "suspended" : ""
|
|
791
|
-
].filter(Boolean).join(" \xB7 ") || void 0
|
|
792
|
-
}));
|
|
793
|
-
if (canInstallNew) {
|
|
794
|
-
options.push({
|
|
795
|
-
value: "install_new",
|
|
796
|
-
label: `Install on a new account ${chalk7.dim("(creates a new workspace)")}`
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
const selected = await p6.select({
|
|
800
|
-
message: "Which GitHub account should this workspace connect to?",
|
|
801
|
-
options
|
|
802
548
|
});
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
549
|
+
prompt.on("cursor", (action) => {
|
|
550
|
+
const filtered = getFiltered();
|
|
551
|
+
const hasAdd = isNewPattern();
|
|
552
|
+
switch (action) {
|
|
553
|
+
case "up":
|
|
554
|
+
if (addCursor) {
|
|
555
|
+
addCursor = false;
|
|
556
|
+
cursor = Math.max(0, filtered.length - 1);
|
|
557
|
+
} else cursor = Math.max(0, cursor - 1);
|
|
558
|
+
break;
|
|
559
|
+
case "down":
|
|
560
|
+
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
561
|
+
addCursor = true;
|
|
562
|
+
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
563
|
+
break;
|
|
564
|
+
case "space": {
|
|
565
|
+
const t = filter.trim();
|
|
566
|
+
if (addCursor || t.length > 0 && isNewPattern()) {
|
|
567
|
+
const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
|
|
568
|
+
if (!err) {
|
|
569
|
+
customPatterns.push(t);
|
|
570
|
+
selected.add(t);
|
|
571
|
+
filter = "";
|
|
572
|
+
cursor = 0;
|
|
573
|
+
scrollOffset = 0;
|
|
574
|
+
addCursor = false;
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
const item = filtered[cursor];
|
|
578
|
+
if (item) {
|
|
579
|
+
if (selected.has(item.value)) selected.delete(item.value);
|
|
580
|
+
else selected.add(item.value);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
824
586
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
message: "Select workspace",
|
|
830
|
-
options
|
|
587
|
+
prompt.on("finalize", () => {
|
|
588
|
+
if (prompt.state === "submit") {
|
|
589
|
+
prompt.value = Array.from(selected);
|
|
590
|
+
}
|
|
831
591
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (selected === "create") {
|
|
836
|
-
return { action: "create" };
|
|
837
|
-
}
|
|
838
|
-
const organization = organizations.find((org) => org.id === selected);
|
|
839
|
-
if (!organization) {
|
|
840
|
-
return { action: "cancelled" };
|
|
841
|
-
}
|
|
842
|
-
return { action: "use", organization };
|
|
592
|
+
const result = await prompt.prompt();
|
|
593
|
+
if (isCancel4(result)) return null;
|
|
594
|
+
return result;
|
|
843
595
|
}
|
|
844
596
|
|
|
845
|
-
// src/utils/
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
organizationName: repoOrgContext.organizationName
|
|
865
|
-
};
|
|
597
|
+
// src/utils/locale-search.ts
|
|
598
|
+
import { isCancel as isCancel5, Prompt as Prompt3 } from "@clack/core";
|
|
599
|
+
import * as p3 from "@clack/prompts";
|
|
600
|
+
var S_BAR3 = "\u2502";
|
|
601
|
+
var S_BAR_END3 = "\u2514";
|
|
602
|
+
var S_ACTIVE3 = "\u25C6";
|
|
603
|
+
var S_SUBMIT3 = "\u25C6";
|
|
604
|
+
var S_CANCEL3 = "\u25A0";
|
|
605
|
+
var S_ERROR3 = "\u25B2";
|
|
606
|
+
function symbol3(state) {
|
|
607
|
+
switch (state) {
|
|
608
|
+
case "submit":
|
|
609
|
+
return grn(S_SUBMIT3);
|
|
610
|
+
case "cancel":
|
|
611
|
+
return red(S_CANCEL3);
|
|
612
|
+
case "error":
|
|
613
|
+
return ylw(S_ERROR3);
|
|
614
|
+
default:
|
|
615
|
+
return active(S_ACTIVE3);
|
|
866
616
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
(o) => o.
|
|
617
|
+
}
|
|
618
|
+
var MAX_VISIBLE2 = 12;
|
|
619
|
+
function filterLocales(options, query) {
|
|
620
|
+
if (!query.trim()) return options;
|
|
621
|
+
const lower = query.toLowerCase();
|
|
622
|
+
return options.filter(
|
|
623
|
+
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
874
624
|
);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
625
|
+
}
|
|
626
|
+
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
627
|
+
const isMulti = selected !== null;
|
|
628
|
+
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
629
|
+
const visibleLines = [info(S_BAR3)];
|
|
630
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
631
|
+
const opt = filtered[i];
|
|
632
|
+
const isCursor = i === cursor;
|
|
633
|
+
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
634
|
+
const icon = isMulti ? isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
635
|
+
visibleLines.push(
|
|
636
|
+
`${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
|
|
637
|
+
);
|
|
879
638
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
return { organizationId: organization.id, organizationName: organization.name };
|
|
639
|
+
const hidden = filtered.length - (end - scrollOffset);
|
|
640
|
+
if (hidden > 0) visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
|
|
641
|
+
if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
|
|
642
|
+
return visibleLines.join("\n");
|
|
643
|
+
}
|
|
644
|
+
async function runFilterablePrompt(opts) {
|
|
645
|
+
const { message, options, multi } = opts;
|
|
646
|
+
let filter = "";
|
|
647
|
+
let cursor = 0;
|
|
648
|
+
let scrollOffset = 0;
|
|
649
|
+
const selected = new Set(multi ? opts.initialValues ?? [] : []);
|
|
650
|
+
if (!multi && opts.initialValue) {
|
|
651
|
+
const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
|
|
652
|
+
if (idx >= 0) cursor = idx;
|
|
895
653
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
654
|
+
const getFiltered = () => filterLocales(options, filter);
|
|
655
|
+
const clampCursor = (filtered) => {
|
|
656
|
+
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
|
|
657
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
658
|
+
if (cursor >= scrollOffset + MAX_VISIBLE2)
|
|
659
|
+
scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
660
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
661
|
+
};
|
|
662
|
+
const prompt = new Prompt3(
|
|
663
|
+
{
|
|
664
|
+
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
665
|
+
validate() {
|
|
666
|
+
const f = getFiltered();
|
|
667
|
+
if (multi && selected.size === 0)
|
|
668
|
+
return "At least one target language is required.";
|
|
669
|
+
if (!multi && !f[cursor]) return "Please select a language.";
|
|
670
|
+
return void 0;
|
|
671
|
+
},
|
|
672
|
+
render() {
|
|
673
|
+
const filtered = getFiltered();
|
|
674
|
+
clampCursor(filtered);
|
|
675
|
+
const hdr = `${dim(S_BAR3)}
|
|
676
|
+
${symbol3(this.state)} ${message}
|
|
677
|
+
`;
|
|
678
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter");
|
|
679
|
+
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`);
|
|
680
|
+
switch (this.state) {
|
|
681
|
+
case "submit": {
|
|
682
|
+
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 ?? "";
|
|
683
|
+
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
684
|
+
}
|
|
685
|
+
case "cancel":
|
|
686
|
+
return `${hdr}${dim(S_BAR3)}`;
|
|
687
|
+
case "error":
|
|
688
|
+
return [
|
|
689
|
+
hdr.trimEnd(),
|
|
690
|
+
`${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
691
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
692
|
+
footer,
|
|
693
|
+
`${ylw(S_BAR_END3)} ${ylw(this.error)}`,
|
|
694
|
+
""
|
|
695
|
+
].join("\n");
|
|
696
|
+
default:
|
|
697
|
+
return [
|
|
698
|
+
hdr.trimEnd(),
|
|
699
|
+
`${info(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
700
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
701
|
+
footer,
|
|
702
|
+
`${info(S_BAR_END3)}`,
|
|
703
|
+
""
|
|
704
|
+
].join("\n");
|
|
705
|
+
}
|
|
909
706
|
}
|
|
707
|
+
},
|
|
708
|
+
false
|
|
709
|
+
// trackValue=false — we manage value manually
|
|
710
|
+
);
|
|
711
|
+
prompt.on("key", (key) => {
|
|
712
|
+
if (!key || key === " ") return;
|
|
713
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
714
|
+
if (cp === 127 || cp === 8) {
|
|
715
|
+
filter = filter.slice(0, -1);
|
|
716
|
+
cursor = 0;
|
|
717
|
+
scrollOffset = 0;
|
|
718
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
719
|
+
filter += key;
|
|
720
|
+
cursor = 0;
|
|
721
|
+
scrollOffset = 0;
|
|
910
722
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
);
|
|
931
|
-
return null;
|
|
723
|
+
});
|
|
724
|
+
prompt.on("cursor", (action) => {
|
|
725
|
+
const filtered = getFiltered();
|
|
726
|
+
switch (action) {
|
|
727
|
+
case "up":
|
|
728
|
+
cursor = Math.max(0, cursor - 1);
|
|
729
|
+
break;
|
|
730
|
+
case "down":
|
|
731
|
+
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
732
|
+
break;
|
|
733
|
+
case "space":
|
|
734
|
+
if (multi) {
|
|
735
|
+
const opt = filtered[cursor];
|
|
736
|
+
if (opt) {
|
|
737
|
+
if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
|
|
738
|
+
else selected.add(opt.bcp47);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
932
742
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
yes: options.yes
|
|
937
|
-
});
|
|
938
|
-
if (!connectResult) {
|
|
939
|
-
p8.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
|
|
940
|
-
return null;
|
|
743
|
+
if (!multi) {
|
|
744
|
+
const opt = getFiltered()[cursor];
|
|
745
|
+
prompt.value = opt?.bcp47 ?? null;
|
|
941
746
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
if (cachedInstallations.length > 0) {
|
|
951
|
-
if (repoCanonical) {
|
|
952
|
-
const repoOwner = repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
953
|
-
if (repoOwner) {
|
|
954
|
-
const hasMatchingAccount = cachedInstallations.some(
|
|
955
|
-
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
956
|
-
);
|
|
957
|
-
if (!hasMatchingAccount) {
|
|
958
|
-
p8.log.warn(
|
|
959
|
-
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
960
|
-
The project will be created but translations won't trigger automatically.
|
|
961
|
-
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
962
|
-
);
|
|
963
|
-
}
|
|
747
|
+
});
|
|
748
|
+
prompt.on("finalize", () => {
|
|
749
|
+
if (prompt.state === "submit") {
|
|
750
|
+
if (multi) {
|
|
751
|
+
prompt.value = Array.from(selected);
|
|
752
|
+
} else {
|
|
753
|
+
const f = getFiltered();
|
|
754
|
+
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
964
755
|
}
|
|
965
756
|
}
|
|
966
|
-
|
|
967
|
-
|
|
757
|
+
});
|
|
758
|
+
const result = await prompt.prompt();
|
|
759
|
+
if (isCancel5(result)) return null;
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
async function searchSelectLocale(options, message, initialValue) {
|
|
763
|
+
const result = await runFilterablePrompt({
|
|
764
|
+
message,
|
|
765
|
+
options,
|
|
766
|
+
multi: false,
|
|
767
|
+
initialValue
|
|
768
|
+
});
|
|
769
|
+
return typeof result === "string" ? result : null;
|
|
770
|
+
}
|
|
771
|
+
async function searchMultiSelectLocales(options, message, initialValues) {
|
|
772
|
+
const result = await runFilterablePrompt({
|
|
773
|
+
message,
|
|
774
|
+
options,
|
|
775
|
+
multi: true,
|
|
776
|
+
initialValues
|
|
777
|
+
});
|
|
778
|
+
if (result === null) return null;
|
|
779
|
+
const picks = result;
|
|
780
|
+
if (picks.length === 0) {
|
|
781
|
+
p3.log.warn(
|
|
782
|
+
"At least one target language is required. Please select at least one."
|
|
968
783
|
);
|
|
969
|
-
|
|
970
|
-
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
971
|
-
selectedInstallationId2 = validInstallations[0].installationId;
|
|
972
|
-
} else {
|
|
973
|
-
selectedInstallationId2 = await selectGitHubInstallation(
|
|
974
|
-
cachedInstallations.map((inst) => ({
|
|
975
|
-
installationId: inst.installationId,
|
|
976
|
-
accountLogin: inst.accountLogin,
|
|
977
|
-
accountType: inst.accountType,
|
|
978
|
-
isSuspended: inst.isSuspended,
|
|
979
|
-
conflictLabel: inst.conflictLabel
|
|
980
|
-
})),
|
|
981
|
-
false
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (selectedInstallationId2 === null || selectedInstallationId2 === "install_new") {
|
|
985
|
-
p8.cancel("Setup cancelled. Re-run `vocoder init` and choose Install GitHub App.");
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
const claimResult2 = await api.claimCliGitHubInstallation(userToken, {
|
|
989
|
-
installationId: String(selectedInstallationId2),
|
|
990
|
-
organizationId: null
|
|
991
|
-
});
|
|
992
|
-
p8.log.success(`Workspace: ${chalk9.bold(claimResult2.organizationName)}`);
|
|
993
|
-
return {
|
|
994
|
-
organizationId: claimResult2.organizationId,
|
|
995
|
-
organizationName: claimResult2.organizationName
|
|
996
|
-
};
|
|
784
|
+
return searchMultiSelectLocales(options, message, initialValues);
|
|
997
785
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
786
|
+
return picks;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/utils/project-create.ts
|
|
790
|
+
function buildLocaleOptions(locales) {
|
|
791
|
+
return locales.map((l) => ({
|
|
792
|
+
bcp47: l.code,
|
|
793
|
+
label: `${l.name} \u2014 ${l.code}`
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
function buildLanguageOptions(locales) {
|
|
797
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
798
|
+
for (const l of locales) {
|
|
799
|
+
const family = l.code.split("-")[0].toLowerCase();
|
|
800
|
+
const opt = { bcp47: l.code, label: `${l.name} \u2014 ${l.code}` };
|
|
801
|
+
const existing = byFamily.get(family);
|
|
802
|
+
if (!existing || l.code.length < existing.bcp47.length) {
|
|
803
|
+
byFamily.set(family, opt);
|
|
804
|
+
}
|
|
1002
805
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
806
|
+
return Array.from(byFamily.values());
|
|
807
|
+
}
|
|
808
|
+
async function runProjectCreate(params) {
|
|
809
|
+
const { api, userToken, organizationId, repoCanonical, repoRoot } = params;
|
|
810
|
+
const projectName = (params.defaultName ?? "my-project").trim();
|
|
811
|
+
p4.log.success(`Project: ${chalk3.bold(projectName)}`);
|
|
812
|
+
let sourceLocales;
|
|
813
|
+
try {
|
|
814
|
+
({ sourceLocales } = await api.listLocales(userToken));
|
|
815
|
+
} catch {
|
|
816
|
+
p4.log.error(
|
|
817
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
818
|
+
);
|
|
1006
819
|
return null;
|
|
1007
820
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
821
|
+
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
822
|
+
const appDirs = await collectAppDirs({ cwd: repoRoot, maxDirs: params.maxAppDirs });
|
|
823
|
+
if (appDirs === null) return null;
|
|
824
|
+
if (appDirs.length > 0) {
|
|
825
|
+
p4.log.success(`App directories: ${appDirs.map((d) => chalk3.bold(d)).join(", ")}`);
|
|
1012
826
|
}
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
827
|
+
const sourceLocale = await searchSelectLocale(
|
|
828
|
+
languageOptions,
|
|
829
|
+
"Source language (the language your code is written in)",
|
|
830
|
+
params.defaultSourceLocale ?? "en"
|
|
831
|
+
);
|
|
832
|
+
if (sourceLocale === null) return null;
|
|
833
|
+
let compatibleTargets;
|
|
834
|
+
try {
|
|
835
|
+
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
836
|
+
} catch {
|
|
837
|
+
p4.log.error(
|
|
838
|
+
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
839
|
+
);
|
|
1022
840
|
return null;
|
|
1023
841
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
842
|
+
const localeOptions = buildLocaleOptions(compatibleTargets);
|
|
843
|
+
const targetOptions = localeOptions.filter(
|
|
844
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
845
|
+
);
|
|
846
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
847
|
+
targetOptions,
|
|
848
|
+
"Target languages (languages to translate into)"
|
|
849
|
+
);
|
|
850
|
+
if (targetLocales === null) return null;
|
|
851
|
+
if (targetLocales.length === 0) {
|
|
852
|
+
p4.log.warn(
|
|
853
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const detected = detectGitBranches();
|
|
857
|
+
const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
|
|
858
|
+
let pushBranches = [];
|
|
859
|
+
{
|
|
860
|
+
let initial = initialBranches;
|
|
861
|
+
while (pushBranches.length === 0) {
|
|
862
|
+
const result2 = await filterableBranchSelect({
|
|
863
|
+
message: "Which branches should trigger translations?",
|
|
864
|
+
branches: detected.branches,
|
|
865
|
+
defaultBranch: detected.defaultBranch,
|
|
866
|
+
initialValues: initial
|
|
867
|
+
});
|
|
868
|
+
if (result2 === null) return null;
|
|
869
|
+
if (result2.length === 0) {
|
|
870
|
+
p4.log.warn(
|
|
871
|
+
"At least one branch is required. Please select at least one."
|
|
872
|
+
);
|
|
873
|
+
initial = [detected.defaultBranch];
|
|
874
|
+
} else {
|
|
875
|
+
pushBranches = result2;
|
|
876
|
+
}
|
|
1033
877
|
}
|
|
1034
|
-
p8.log.success(`Workspace: ${chalk9.bold(connectResult.organizationName)}`);
|
|
1035
|
-
return {
|
|
1036
|
-
organizationId: connectResult.organizationId,
|
|
1037
|
-
organizationName: connectResult.organizationName
|
|
1038
|
-
};
|
|
1039
878
|
}
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
879
|
+
const targetBranches = pushBranches;
|
|
880
|
+
const result = await api.createProject(userToken, {
|
|
881
|
+
organizationId,
|
|
882
|
+
name: projectName,
|
|
883
|
+
sourceLocale,
|
|
884
|
+
targetLocales,
|
|
885
|
+
targetBranches,
|
|
886
|
+
appDirs,
|
|
887
|
+
repoCanonical
|
|
1044
888
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
889
|
+
p4.log.success(`Project ${chalk3.bold(result.projectName)} created!`);
|
|
890
|
+
return {
|
|
891
|
+
projectId: result.projectId,
|
|
892
|
+
projectName: result.projectName,
|
|
893
|
+
apiKey: result.apiKey,
|
|
894
|
+
sourceLocale,
|
|
895
|
+
targetLocales,
|
|
896
|
+
targetBranches,
|
|
897
|
+
repositoryBound: result.repositoryBound,
|
|
898
|
+
configureUrl: result.configureUrl,
|
|
899
|
+
apps: result.apps
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async function runAppCreate(params) {
|
|
903
|
+
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
904
|
+
const existingDirs = params.existingApps.map((a) => a.appDir);
|
|
905
|
+
const appDir = await promptSingleAppDir({ existingDirs });
|
|
906
|
+
if (appDir === null) return null;
|
|
907
|
+
if (appDir) {
|
|
908
|
+
p4.log.success(`App directory: ${chalk3.bold(appDir)}`);
|
|
1063
909
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
910
|
+
let sourceLocales;
|
|
911
|
+
try {
|
|
912
|
+
({ sourceLocales } = await api.listLocales(userToken));
|
|
913
|
+
} catch {
|
|
914
|
+
p4.log.error(
|
|
915
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
916
|
+
);
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
920
|
+
const sourceLocale = await searchSelectLocale(
|
|
921
|
+
languageOptions,
|
|
922
|
+
"Source language",
|
|
923
|
+
"en"
|
|
1073
924
|
);
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
925
|
+
if (sourceLocale === null) return null;
|
|
926
|
+
let compatibleTargets;
|
|
927
|
+
try {
|
|
928
|
+
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
929
|
+
} catch {
|
|
930
|
+
p4.log.error(
|
|
931
|
+
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
932
|
+
);
|
|
1076
933
|
return null;
|
|
1077
934
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
935
|
+
const targetOptions = buildLocaleOptions(compatibleTargets).filter(
|
|
936
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
937
|
+
);
|
|
938
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
939
|
+
targetOptions,
|
|
940
|
+
"Target languages"
|
|
941
|
+
);
|
|
942
|
+
if (targetLocales === null) return null;
|
|
943
|
+
if (targetLocales.length === 0) {
|
|
944
|
+
p4.log.warn(
|
|
945
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
946
|
+
);
|
|
1090
947
|
}
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
948
|
+
const detectedApp = detectGitBranches();
|
|
949
|
+
let appPushBranches = [];
|
|
950
|
+
{
|
|
951
|
+
let initial = [detectedApp.defaultBranch];
|
|
952
|
+
while (appPushBranches.length === 0) {
|
|
953
|
+
const result2 = await filterableBranchSelect({
|
|
954
|
+
message: "Which branches should trigger translations?",
|
|
955
|
+
branches: detectedApp.branches,
|
|
956
|
+
defaultBranch: detectedApp.defaultBranch,
|
|
957
|
+
initialValues: initial
|
|
958
|
+
});
|
|
959
|
+
if (result2 === null) return null;
|
|
960
|
+
if (result2.length === 0) {
|
|
961
|
+
p4.log.warn("At least one branch is required.");
|
|
962
|
+
initial = [detectedApp.defaultBranch];
|
|
963
|
+
} else {
|
|
964
|
+
appPushBranches = result2;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const targetBranches = appPushBranches;
|
|
969
|
+
const result = await api.createApp(userToken, {
|
|
970
|
+
projectId,
|
|
971
|
+
appDir,
|
|
972
|
+
sourceLocale,
|
|
973
|
+
targetLocales,
|
|
974
|
+
targetBranches,
|
|
975
|
+
repoCanonical: repoCanonical ?? ""
|
|
1094
976
|
});
|
|
1095
|
-
|
|
977
|
+
p4.log.success(
|
|
978
|
+
`App ${chalk3.bold(appDir || "(root)")} added to ${chalk3.bold(projectName)}!`
|
|
979
|
+
);
|
|
1096
980
|
return {
|
|
1097
|
-
|
|
1098
|
-
|
|
981
|
+
projectId: result.projectId,
|
|
982
|
+
projectName: result.projectName,
|
|
983
|
+
appDir: result.appDir,
|
|
984
|
+
appId: result.appId,
|
|
985
|
+
sourceLocale,
|
|
986
|
+
targetLocales,
|
|
987
|
+
targetBranches
|
|
1099
988
|
};
|
|
1100
989
|
}
|
|
1101
990
|
|
|
1102
|
-
// src/utils/
|
|
1103
|
-
import * as
|
|
1104
|
-
import
|
|
1105
|
-
|
|
1106
|
-
// src/utils/app-dir-select.ts
|
|
1107
|
-
import { existsSync as existsSync3, statSync } from "fs";
|
|
991
|
+
// src/utils/scaffold.ts
|
|
992
|
+
import * as p5 from "@clack/prompts";
|
|
993
|
+
import chalk4 from "chalk";
|
|
994
|
+
import { execSync as execSync2 } from "child_process";
|
|
1108
995
|
import { resolve as resolve2 } from "path";
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
case "cancel":
|
|
1122
|
-
return red(S_CANCEL);
|
|
1123
|
-
case "error":
|
|
1124
|
-
return ylw(S_ERROR);
|
|
1125
|
-
default:
|
|
1126
|
-
return active(S_ACTIVE);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
function validateAppDirPath(val, existing, opts = {}) {
|
|
1130
|
-
if (val.startsWith("/")) return "Must be a relative path (e.g. apps/web)";
|
|
1131
|
-
if (val.includes("..")) return "Path traversal not allowed";
|
|
1132
|
-
const hasWholeRepo = existing.includes("");
|
|
1133
|
-
const hasScoped = existing.some((d) => d !== "");
|
|
1134
|
-
if (val === "" && hasScoped) return "Cannot add whole-repo scope to a monorepo project";
|
|
1135
|
-
if (val !== "" && hasWholeRepo) return "Cannot add a scoped directory to a whole-repo project";
|
|
1136
|
-
if (existing.includes(val)) return `Already added: ${val}`;
|
|
1137
|
-
const nested = existing.find(
|
|
1138
|
-
(d) => d !== "" && (val.startsWith(d + "/") || d.startsWith(val + "/"))
|
|
1139
|
-
);
|
|
1140
|
-
if (nested) return `"${val}" overlaps with already-added "${nested}"`;
|
|
1141
|
-
if (val !== "") {
|
|
1142
|
-
const abs = resolve2(opts.cwd ?? process.cwd(), val);
|
|
1143
|
-
if (!existsSync3(abs)) return `Directory not found: ${val}`;
|
|
1144
|
-
if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
|
|
996
|
+
|
|
997
|
+
// src/utils/write-config.ts
|
|
998
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
999
|
+
import { join } from "path";
|
|
1000
|
+
function findExistingConfig(cwd = process.cwd()) {
|
|
1001
|
+
for (const name of [
|
|
1002
|
+
"vocoder.config.ts",
|
|
1003
|
+
"vocoder.config.js",
|
|
1004
|
+
"vocoder.config.json"
|
|
1005
|
+
]) {
|
|
1006
|
+
const candidate = join(cwd, name);
|
|
1007
|
+
if (existsSync2(candidate)) return candidate;
|
|
1145
1008
|
}
|
|
1146
1009
|
return null;
|
|
1147
1010
|
}
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
};
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
${symbol(this.state)} App directories
|
|
1011
|
+
function writeVocoderConfig(options) {
|
|
1012
|
+
const {
|
|
1013
|
+
targetBranches = ["main"],
|
|
1014
|
+
useTypeScript = true,
|
|
1015
|
+
cwd = process.cwd(),
|
|
1016
|
+
appId
|
|
1017
|
+
} = options;
|
|
1018
|
+
if (findExistingConfig(cwd)) return null;
|
|
1019
|
+
const ext = useTypeScript ? "ts" : "js";
|
|
1020
|
+
const configPath = join(cwd, `vocoder.config.${ext}`);
|
|
1021
|
+
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
1022
|
+
const includes = ["**/*.{tsx,jsx,ts,js}"];
|
|
1023
|
+
const includesStr = includes.map((p21) => `'${p21}'`).join(", ");
|
|
1024
|
+
const appIdLine = appId ? ` appId: '${appId}',
|
|
1025
|
+
` : "";
|
|
1026
|
+
const content = `import { defineConfig } from '@vocoder/config'
|
|
1027
|
+
|
|
1028
|
+
export default defineConfig({
|
|
1029
|
+
${appIdLine} targetBranches: [${branchesStr}],
|
|
1030
|
+
include: [${includesStr}],
|
|
1031
|
+
})
|
|
1170
1032
|
`;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
lines.push(info(S_BAR));
|
|
1201
|
-
if (atLimit) {
|
|
1202
|
-
lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
1203
|
-
} else if (added.length === 0 && !isNewDir()) {
|
|
1204
|
-
lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
|
|
1205
|
-
lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
|
|
1206
|
-
} else if (added.length > 0) {
|
|
1207
|
-
lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
1208
|
-
}
|
|
1209
|
-
const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
|
|
1210
|
-
if (this.state === "error") {
|
|
1211
|
-
lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
|
|
1212
|
-
} else {
|
|
1213
|
-
lines.push(barEnd);
|
|
1214
|
-
}
|
|
1215
|
-
lines.push("");
|
|
1216
|
-
return lines.join("\n");
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1033
|
+
try {
|
|
1034
|
+
writeFileSync(configPath, content, "utf-8");
|
|
1035
|
+
return `vocoder.config.${ext}`;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/utils/scaffold.ts
|
|
1042
|
+
function runScaffold(params) {
|
|
1043
|
+
const { targetBranches } = params;
|
|
1044
|
+
const detection = detectLocalEcosystem();
|
|
1045
|
+
if (detection.ecosystem) {
|
|
1046
|
+
const frameworkLabel = detection.framework ?? detection.ecosystem;
|
|
1047
|
+
const pmLabel = detection.packageManager;
|
|
1048
|
+
p5.log.info(`Detected: ${chalk4.bold(frameworkLabel)} (${pmLabel})`);
|
|
1049
|
+
}
|
|
1050
|
+
const { devPackages, runtimePackages } = getPackagesToInstall(detection);
|
|
1051
|
+
const allPackages = [...devPackages, ...runtimePackages];
|
|
1052
|
+
if (allPackages.length > 0) {
|
|
1053
|
+
p5.log.info("");
|
|
1054
|
+
const installSpinner = p5.spinner();
|
|
1055
|
+
installSpinner.start(`Installing ${allPackages.join(", ")}...`);
|
|
1056
|
+
try {
|
|
1057
|
+
if (devPackages.length > 0) {
|
|
1058
|
+
execSync2(
|
|
1059
|
+
buildInstallCommand(detection.packageManager, devPackages, true),
|
|
1060
|
+
{ stdio: "pipe", cwd: process.cwd() }
|
|
1061
|
+
);
|
|
1219
1062
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const cp = key.codePointAt(0) ?? 0;
|
|
1226
|
-
if (cp === 127 || cp === 8) {
|
|
1227
|
-
filter = filter.slice(0, -1);
|
|
1228
|
-
addCursor = false;
|
|
1229
|
-
} else if (cp >= 32 && cp !== 127) {
|
|
1230
|
-
filter += key;
|
|
1231
|
-
cursor = 0;
|
|
1232
|
-
addCursor = false;
|
|
1233
|
-
}
|
|
1234
|
-
});
|
|
1235
|
-
prompt.on("cursor", (action) => {
|
|
1236
|
-
switch (action) {
|
|
1237
|
-
case "up":
|
|
1238
|
-
if (addCursor) {
|
|
1239
|
-
addCursor = false;
|
|
1240
|
-
cursor = Math.max(0, added.length - 1);
|
|
1241
|
-
} else {
|
|
1242
|
-
cursor = Math.max(0, cursor - 1);
|
|
1243
|
-
}
|
|
1244
|
-
break;
|
|
1245
|
-
case "down":
|
|
1246
|
-
if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
|
|
1247
|
-
addCursor = true;
|
|
1248
|
-
} else if (!addCursor) {
|
|
1249
|
-
cursor = Math.min(added.length - 1, cursor + 1);
|
|
1250
|
-
}
|
|
1251
|
-
break;
|
|
1252
|
-
case "space": {
|
|
1253
|
-
if (addCursor || filter.trim().length > 0 && isNewDir()) {
|
|
1254
|
-
if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
|
|
1255
|
-
const trimmed = filter.trim();
|
|
1256
|
-
const err = validateAppDirPath(trimmed, added, opts);
|
|
1257
|
-
if (!err) {
|
|
1258
|
-
added.push(trimmed);
|
|
1259
|
-
filter = "";
|
|
1260
|
-
addCursor = false;
|
|
1261
|
-
cursor = 0;
|
|
1262
|
-
}
|
|
1263
|
-
} else if (added.length > 0 && !isNewDir()) {
|
|
1264
|
-
clampCursor();
|
|
1265
|
-
added.splice(cursor, 1);
|
|
1266
|
-
if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
|
|
1267
|
-
}
|
|
1268
|
-
break;
|
|
1063
|
+
if (runtimePackages.length > 0) {
|
|
1064
|
+
execSync2(
|
|
1065
|
+
buildInstallCommand(detection.packageManager, runtimePackages, false),
|
|
1066
|
+
{ stdio: "pipe", cwd: process.cwd() }
|
|
1067
|
+
);
|
|
1269
1068
|
}
|
|
1069
|
+
installSpinner.stop(`Installed ${allPackages.join(", ")}`);
|
|
1070
|
+
} catch {
|
|
1071
|
+
installSpinner.stop("Package installation failed");
|
|
1072
|
+
const cmds = [
|
|
1073
|
+
devPackages.length > 0 ? buildInstallCommand(detection.packageManager, devPackages, true) : null,
|
|
1074
|
+
runtimePackages.length > 0 ? buildInstallCommand(detection.packageManager, runtimePackages, false) : null
|
|
1075
|
+
].filter(Boolean).join(" && ");
|
|
1076
|
+
p5.log.warn(`Run manually: ${highlight(cmds)}`);
|
|
1270
1077
|
}
|
|
1271
|
-
})
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1078
|
+
} else if (detection.ecosystem) {
|
|
1079
|
+
p5.log.info(`Packages: ${chalk4.green("already installed")}`);
|
|
1080
|
+
}
|
|
1081
|
+
const branchList = targetBranches.length > 0 ? targetBranches.map((b) => highlight(b)).join(" or ") : highlight("your target branch");
|
|
1082
|
+
p5.log.message("");
|
|
1083
|
+
p5.log.success(`Push to ${branchList} to trigger your first translation run.`);
|
|
1084
|
+
p5.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
|
|
1085
|
+
}
|
|
1086
|
+
function writeAppConfigs(apps, targetBranches, useTypeScript, repoRoot) {
|
|
1087
|
+
const base = repoRoot ?? process.cwd();
|
|
1088
|
+
for (const app of apps) {
|
|
1089
|
+
const dir = app.appDir ? resolve2(base, app.appDir) : base;
|
|
1090
|
+
const written = writeVocoderConfig({
|
|
1091
|
+
targetBranches,
|
|
1092
|
+
appId: app.appId,
|
|
1093
|
+
cwd: dir,
|
|
1094
|
+
useTypeScript
|
|
1095
|
+
});
|
|
1096
|
+
if (written) {
|
|
1097
|
+
const displayPath = app.appDir ? `${app.appDir}/${written}` : written;
|
|
1098
|
+
p5.log.success(`Created ${highlight(displayPath)}`);
|
|
1099
|
+
} else if (!findExistingConfig(dir)) {
|
|
1100
|
+
const ext = useTypeScript ? "ts" : "js";
|
|
1101
|
+
p5.log.warn(
|
|
1102
|
+
`Could not write ${app.appDir ? `${app.appDir}/` : ""}vocoder.config.${ext} \u2014 create it manually.`
|
|
1103
|
+
);
|
|
1275
1104
|
}
|
|
1276
|
-
}
|
|
1277
|
-
const result = await prompt.prompt();
|
|
1278
|
-
if (isCancel7(result)) return null;
|
|
1279
|
-
return result;
|
|
1105
|
+
}
|
|
1280
1106
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1107
|
+
|
|
1108
|
+
// src/commands/init.ts
|
|
1109
|
+
import chalk11 from "chalk";
|
|
1110
|
+
import { config as loadEnv } from "dotenv";
|
|
1111
|
+
|
|
1112
|
+
// src/utils/output.ts
|
|
1113
|
+
import * as p6 from "@clack/prompts";
|
|
1114
|
+
import chalk5 from "chalk";
|
|
1115
|
+
import { execSync as execSync3 } from "child_process";
|
|
1116
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1117
|
+
import { join as join2 } from "path";
|
|
1118
|
+
function tryClipboard(text2) {
|
|
1119
|
+
const tools = [
|
|
1120
|
+
{ cmd: "pbcopy" },
|
|
1121
|
+
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
1122
|
+
{ cmd: "xsel", args: ["--clipboard", "--input"] },
|
|
1123
|
+
{ cmd: "wl-copy" },
|
|
1124
|
+
{ cmd: "clip" }
|
|
1125
|
+
];
|
|
1126
|
+
for (const { cmd, args = [] } of tools) {
|
|
1127
|
+
try {
|
|
1128
|
+
execSync3([cmd, ...args].join(" "), {
|
|
1129
|
+
input: text2,
|
|
1130
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
1131
|
+
});
|
|
1132
|
+
return true;
|
|
1133
|
+
} catch {
|
|
1134
|
+
continue;
|
|
1291
1135
|
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1136
|
+
}
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
function printCommand(cmd) {
|
|
1140
|
+
const copied = tryClipboard(cmd);
|
|
1141
|
+
process.stdout.write("\n");
|
|
1142
|
+
process.stdout.write(` ${chalk5.dim("$")} ${chalk5.cyan(cmd)}
|
|
1143
|
+
`);
|
|
1144
|
+
if (copied) process.stdout.write(` ${chalk5.dim("\u2191 copied to clipboard")}
|
|
1145
|
+
`);
|
|
1146
|
+
process.stdout.write("\n");
|
|
1147
|
+
}
|
|
1148
|
+
function printCodeBlock(code) {
|
|
1149
|
+
process.stdout.write("\n");
|
|
1150
|
+
for (const line of code.split("\n")) {
|
|
1151
|
+
process.stdout.write(` ${line}
|
|
1152
|
+
`);
|
|
1153
|
+
}
|
|
1154
|
+
process.stdout.write("\n");
|
|
1155
|
+
}
|
|
1156
|
+
function writeApiKeyToEnv(apiKey, repoRoot) {
|
|
1157
|
+
const envPath = join2(repoRoot ?? process.cwd(), ".env");
|
|
1158
|
+
if (!existsSync3(envPath)) return false;
|
|
1159
|
+
try {
|
|
1160
|
+
const content = readFileSync(envPath, "utf-8");
|
|
1161
|
+
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
1162
|
+
let updated;
|
|
1163
|
+
if (/^VOCODER_API_KEY=/m.test(content)) {
|
|
1164
|
+
updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
|
|
1165
|
+
} else {
|
|
1166
|
+
const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
1167
|
+
updated = `${content}${sep}${keyLine}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
writeFileSync2(envPath, updated);
|
|
1171
|
+
return true;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function printApiKey(apiKey, repoRoot) {
|
|
1177
|
+
const saved = writeApiKeyToEnv(apiKey, repoRoot);
|
|
1178
|
+
if (saved) {
|
|
1179
|
+
p6.log.success("API key saved to .env");
|
|
1180
|
+
} else {
|
|
1181
|
+
p6.log.warn(
|
|
1182
|
+
"Could not write to .env \u2014 find your API key at https://vocoder.app/settings"
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1295
1185
|
}
|
|
1296
1186
|
|
|
1297
|
-
// src/utils/
|
|
1187
|
+
// src/utils/git-identity.ts
|
|
1298
1188
|
import { execSync as execSync4 } from "child_process";
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
var S_SUBMIT2 = "\u25C6";
|
|
1304
|
-
var S_CANCEL2 = "\u25A0";
|
|
1305
|
-
var S_ERROR2 = "\u25B2";
|
|
1306
|
-
function symbol2(state) {
|
|
1307
|
-
switch (state) {
|
|
1308
|
-
case "submit":
|
|
1309
|
-
return grn(S_SUBMIT2);
|
|
1310
|
-
case "cancel":
|
|
1311
|
-
return red(S_CANCEL2);
|
|
1312
|
-
case "error":
|
|
1313
|
-
return ylw(S_ERROR2);
|
|
1314
|
-
default:
|
|
1315
|
-
return active(S_ACTIVE2);
|
|
1189
|
+
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
1190
|
+
function detectCommitSha() {
|
|
1191
|
+
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
1192
|
+
return process.env.VOCODER_COMMIT_SHA;
|
|
1316
1193
|
}
|
|
1194
|
+
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;
|
|
1195
|
+
if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
|
|
1196
|
+
return safeExec("git rev-parse HEAD");
|
|
1317
1197
|
}
|
|
1318
|
-
function
|
|
1319
|
-
const workDir = cwd ?? process.cwd();
|
|
1198
|
+
function safeExec(command) {
|
|
1320
1199
|
try {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
stdio: "pipe"
|
|
1324
|
-
}).
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1200
|
+
const output = execSync4(command, {
|
|
1201
|
+
encoding: "utf-8",
|
|
1202
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1203
|
+
}).trim();
|
|
1204
|
+
return output.length > 0 ? output : null;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function normalizePath(pathname) {
|
|
1210
|
+
const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
|
|
1211
|
+
if (!cleaned || !cleaned.includes("/")) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
return cleaned;
|
|
1215
|
+
}
|
|
1216
|
+
function parseRemoteUrl(remoteUrl) {
|
|
1217
|
+
const trimmed = remoteUrl.trim();
|
|
1218
|
+
if (!trimmed) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
if (!trimmed.includes("://")) {
|
|
1222
|
+
const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
|
|
1223
|
+
if (scpMatch) {
|
|
1224
|
+
const host = (scpMatch[1] || "").toLowerCase();
|
|
1225
|
+
const ownerRepoPath = normalizePath(scpMatch[2] || "");
|
|
1226
|
+
if (!host || !ownerRepoPath) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
return { host, ownerRepoPath };
|
|
1334
1230
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
} catch {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
const parsed = new URL(trimmed);
|
|
1235
|
+
const host = parsed.hostname.toLowerCase();
|
|
1236
|
+
const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
|
|
1237
|
+
if (!host || !ownerRepoPath) {
|
|
1238
|
+
return null;
|
|
1344
1239
|
}
|
|
1345
|
-
return {
|
|
1346
|
-
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
1347
|
-
defaultBranch
|
|
1348
|
-
};
|
|
1240
|
+
return { host, ownerRepoPath };
|
|
1349
1241
|
} catch {
|
|
1350
|
-
return
|
|
1242
|
+
return null;
|
|
1351
1243
|
}
|
|
1352
1244
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (!t) return "Pattern cannot be empty";
|
|
1357
|
-
if (INVALID_CHARS.test(t))
|
|
1358
|
-
return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
1359
|
-
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
1360
|
-
if (t.includes("//")) return "Cannot contain //";
|
|
1361
|
-
return null;
|
|
1362
|
-
}
|
|
1363
|
-
var MAX_VISIBLE = 10;
|
|
1364
|
-
function buildItems(branches, defaultBranch, customPatterns) {
|
|
1365
|
-
const items = branches.map((b) => ({
|
|
1366
|
-
value: b,
|
|
1367
|
-
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
1368
|
-
}));
|
|
1369
|
-
for (const pt of customPatterns) {
|
|
1370
|
-
if (!branches.includes(pt)) {
|
|
1371
|
-
items.push({ value: pt, label: pt, isCustom: true });
|
|
1372
|
-
}
|
|
1245
|
+
function toCanonical(host, ownerRepoPath) {
|
|
1246
|
+
if (host.includes("github.com")) {
|
|
1247
|
+
return `github:${ownerRepoPath.toLowerCase()}`;
|
|
1373
1248
|
}
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
function filterItems(items, query) {
|
|
1377
|
-
if (!query.trim()) return items;
|
|
1378
|
-
const lower = query.toLowerCase();
|
|
1379
|
-
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
1380
|
-
}
|
|
1381
|
-
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
1382
|
-
const lines = [info(S_BAR2)];
|
|
1383
|
-
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
1384
|
-
for (let i = scrollOffset; i < end; i++) {
|
|
1385
|
-
const item = filtered[i];
|
|
1386
|
-
const isCursor = i === cursor && !addCursor;
|
|
1387
|
-
const isChecked = selected.has(item.value);
|
|
1388
|
-
const icon = isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
|
|
1389
|
-
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
1390
|
-
if (isCursor) label = bld(label);
|
|
1391
|
-
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
1249
|
+
if (host.includes("gitlab.com")) {
|
|
1250
|
+
return `gitlab:${ownerRepoPath.toLowerCase()}`;
|
|
1392
1251
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if (isNewPattern) {
|
|
1396
|
-
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
1397
|
-
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
1398
|
-
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
1399
|
-
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
1400
|
-
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
1401
|
-
lines.push(dim(`${S_BAR2} No branches detected`));
|
|
1252
|
+
if (host.includes("bitbucket.org")) {
|
|
1253
|
+
return `bitbucket:${ownerRepoPath.toLowerCase()}`;
|
|
1402
1254
|
}
|
|
1403
|
-
|
|
1404
|
-
if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
|
|
1405
|
-
return lines.join("\n");
|
|
1255
|
+
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
1406
1256
|
}
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
};
|
|
1424
|
-
const clampCursor = (filtered) => {
|
|
1425
|
-
const hasAdd = isNewPattern();
|
|
1426
|
-
const max = filtered.length - 1 + (hasAdd ? 1 : 0);
|
|
1427
|
-
if (cursor > max && !addCursor) cursor = Math.max(0, max);
|
|
1428
|
-
if (!addCursor) {
|
|
1429
|
-
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
1430
|
-
if (cursor >= scrollOffset + MAX_VISIBLE)
|
|
1431
|
-
scrollOffset = cursor - MAX_VISIBLE + 1;
|
|
1432
|
-
if (scrollOffset < 0) scrollOffset = 0;
|
|
1433
|
-
}
|
|
1257
|
+
function resolveGitRepositoryIdentity() {
|
|
1258
|
+
const remoteUrl = safeExec("git config --get remote.origin.url");
|
|
1259
|
+
if (!remoteUrl) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
1263
|
+
if (!parsed) {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
const repoRoot = safeExec("git rev-parse --show-toplevel");
|
|
1267
|
+
if (!repoRoot) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
1272
|
+
repoRoot
|
|
1434
1273
|
};
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
hdr.trimEnd(),
|
|
1469
|
-
`${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
1470
|
-
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
1471
|
-
footer,
|
|
1472
|
-
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
1473
|
-
""
|
|
1474
|
-
].join("\n");
|
|
1475
|
-
default:
|
|
1476
|
-
return [
|
|
1477
|
-
hdr.trimEnd(),
|
|
1478
|
-
`${info(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
1479
|
-
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
1480
|
-
footer,
|
|
1481
|
-
`${info(S_BAR_END2)}`,
|
|
1482
|
-
""
|
|
1483
|
-
].join("\n");
|
|
1484
|
-
}
|
|
1274
|
+
}
|
|
1275
|
+
function resolveGitContext() {
|
|
1276
|
+
const warnings = [];
|
|
1277
|
+
const identity = resolveGitRepositoryIdentity();
|
|
1278
|
+
if (!identity) {
|
|
1279
|
+
warnings.push(
|
|
1280
|
+
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
return { identity, warnings };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/utils/auth-flow.ts
|
|
1287
|
+
import * as p7 from "@clack/prompts";
|
|
1288
|
+
import chalk6 from "chalk";
|
|
1289
|
+
|
|
1290
|
+
// src/utils/local-server.ts
|
|
1291
|
+
import { createServer } from "http";
|
|
1292
|
+
import { URL as URL2 } from "url";
|
|
1293
|
+
function startCallbackServer() {
|
|
1294
|
+
return new Promise((resolve3, reject) => {
|
|
1295
|
+
let settled = false;
|
|
1296
|
+
let callbackResolve = null;
|
|
1297
|
+
let callbackReject = null;
|
|
1298
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
1299
|
+
callbackResolve = res;
|
|
1300
|
+
callbackReject = rej;
|
|
1301
|
+
});
|
|
1302
|
+
const server = createServer((req, res) => {
|
|
1303
|
+
if (!req.url) {
|
|
1304
|
+
res.writeHead(400);
|
|
1305
|
+
res.end();
|
|
1306
|
+
return;
|
|
1485
1307
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
addCursor = false;
|
|
1497
|
-
} else if (cp >= 32 && cp !== 127) {
|
|
1498
|
-
filter += key;
|
|
1499
|
-
cursor = 0;
|
|
1500
|
-
scrollOffset = 0;
|
|
1501
|
-
addCursor = false;
|
|
1502
|
-
}
|
|
1503
|
-
if (isNewPattern()) {
|
|
1504
|
-
addCursor = true;
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
prompt.on("cursor", (action) => {
|
|
1508
|
-
const filtered = getFiltered();
|
|
1509
|
-
const hasAdd = isNewPattern();
|
|
1510
|
-
switch (action) {
|
|
1511
|
-
case "up":
|
|
1512
|
-
if (addCursor) {
|
|
1513
|
-
addCursor = false;
|
|
1514
|
-
cursor = Math.max(0, filtered.length - 1);
|
|
1515
|
-
} else cursor = Math.max(0, cursor - 1);
|
|
1516
|
-
break;
|
|
1517
|
-
case "down":
|
|
1518
|
-
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
1519
|
-
addCursor = true;
|
|
1520
|
-
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1521
|
-
break;
|
|
1522
|
-
case "space": {
|
|
1523
|
-
const t = filter.trim();
|
|
1524
|
-
if (addCursor || t.length > 0 && isNewPattern()) {
|
|
1525
|
-
const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
|
|
1526
|
-
if (!err) {
|
|
1527
|
-
customPatterns.push(t);
|
|
1528
|
-
selected.add(t);
|
|
1529
|
-
filter = "";
|
|
1530
|
-
cursor = 0;
|
|
1531
|
-
scrollOffset = 0;
|
|
1532
|
-
addCursor = false;
|
|
1533
|
-
}
|
|
1534
|
-
} else {
|
|
1535
|
-
const item = filtered[cursor];
|
|
1536
|
-
if (item) {
|
|
1537
|
-
if (selected.has(item.value)) selected.delete(item.value);
|
|
1538
|
-
else selected.add(item.value);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
break;
|
|
1308
|
+
let pathname;
|
|
1309
|
+
let params;
|
|
1310
|
+
try {
|
|
1311
|
+
const parsed = new URL2(req.url, "http://localhost");
|
|
1312
|
+
pathname = parsed.pathname;
|
|
1313
|
+
params = Object.fromEntries(parsed.searchParams.entries());
|
|
1314
|
+
} catch {
|
|
1315
|
+
res.writeHead(400);
|
|
1316
|
+
res.end("Bad request");
|
|
1317
|
+
return;
|
|
1542
1318
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1319
|
+
if (pathname !== "/callback") {
|
|
1320
|
+
res.writeHead(404);
|
|
1321
|
+
res.end("Not found");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1325
|
+
res.end(
|
|
1326
|
+
'<!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>'
|
|
1327
|
+
);
|
|
1328
|
+
if (callbackResolve) {
|
|
1329
|
+
callbackResolve(params);
|
|
1330
|
+
callbackResolve = null;
|
|
1331
|
+
}
|
|
1332
|
+
setImmediate(() => server.close());
|
|
1333
|
+
});
|
|
1334
|
+
server.on("error", (err) => {
|
|
1335
|
+
if (!settled) {
|
|
1336
|
+
settled = true;
|
|
1337
|
+
if (callbackReject) callbackReject(err);
|
|
1338
|
+
reject(err);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1342
|
+
if (settled) return;
|
|
1343
|
+
settled = true;
|
|
1344
|
+
const port = server.address().port;
|
|
1345
|
+
resolve3({
|
|
1346
|
+
port,
|
|
1347
|
+
waitForCallback: () => callbackPromise,
|
|
1348
|
+
close: () => server.close()
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1549
1351
|
});
|
|
1550
|
-
const result = await prompt.prompt();
|
|
1551
|
-
if (isCancel9(result)) return null;
|
|
1552
|
-
return result;
|
|
1553
1352
|
}
|
|
1554
1353
|
|
|
1555
|
-
// src/utils/
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
var S_BAR3 = "\u2502";
|
|
1559
|
-
var S_BAR_END3 = "\u2514";
|
|
1560
|
-
var S_ACTIVE3 = "\u25C6";
|
|
1561
|
-
var S_SUBMIT3 = "\u25C6";
|
|
1562
|
-
var S_CANCEL3 = "\u25A0";
|
|
1563
|
-
var S_ERROR3 = "\u25B2";
|
|
1564
|
-
function symbol3(state) {
|
|
1565
|
-
switch (state) {
|
|
1566
|
-
case "submit":
|
|
1567
|
-
return grn(S_SUBMIT3);
|
|
1568
|
-
case "cancel":
|
|
1569
|
-
return red(S_CANCEL3);
|
|
1570
|
-
case "error":
|
|
1571
|
-
return ylw(S_ERROR3);
|
|
1572
|
-
default:
|
|
1573
|
-
return active(S_ACTIVE3);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
var MAX_VISIBLE2 = 12;
|
|
1577
|
-
function filterLocales(options, query) {
|
|
1578
|
-
if (!query.trim()) return options;
|
|
1579
|
-
const lower = query.toLowerCase();
|
|
1580
|
-
return options.filter(
|
|
1581
|
-
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1582
|
-
);
|
|
1583
|
-
}
|
|
1584
|
-
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
1585
|
-
const isMulti = selected !== null;
|
|
1586
|
-
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
1587
|
-
const visibleLines = [info(S_BAR3)];
|
|
1588
|
-
for (let i = scrollOffset; i < end; i++) {
|
|
1589
|
-
const opt = filtered[i];
|
|
1590
|
-
const isCursor = i === cursor;
|
|
1591
|
-
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
1592
|
-
const icon = isMulti ? isChecked ? active("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
1593
|
-
visibleLines.push(
|
|
1594
|
-
`${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
|
|
1595
|
-
);
|
|
1596
|
-
}
|
|
1597
|
-
const hidden = filtered.length - (end - scrollOffset);
|
|
1598
|
-
if (hidden > 0) visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
|
|
1599
|
-
if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
|
|
1600
|
-
return visibleLines.join("\n");
|
|
1354
|
+
// src/utils/auth-flow.ts
|
|
1355
|
+
async function sleep(ms) {
|
|
1356
|
+
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1601
1357
|
}
|
|
1602
|
-
async function
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
|
|
1610
|
-
if (idx >= 0) cursor = idx;
|
|
1358
|
+
async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
1359
|
+
let server = null;
|
|
1360
|
+
if (!options.ci) {
|
|
1361
|
+
try {
|
|
1362
|
+
server = await startCallbackServer();
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1611
1365
|
}
|
|
1612
|
-
const
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
{
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const val = multi ? Array.from(selected).map((id) => options.find((o) => o.bcp47 === id)?.label ?? id).join(", ") : options.find((o) => o.bcp47 === this.value)?.label ?? "";
|
|
1641
|
-
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
1642
|
-
}
|
|
1643
|
-
case "cancel":
|
|
1644
|
-
return `${hdr}${dim(S_BAR3)}`;
|
|
1645
|
-
case "error":
|
|
1646
|
-
return [
|
|
1647
|
-
hdr.trimEnd(),
|
|
1648
|
-
`${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
1649
|
-
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
1650
|
-
footer,
|
|
1651
|
-
`${ylw(S_BAR_END3)} ${ylw(this.error)}`,
|
|
1652
|
-
""
|
|
1653
|
-
].join("\n");
|
|
1654
|
-
default:
|
|
1655
|
-
return [
|
|
1656
|
-
hdr.trimEnd(),
|
|
1657
|
-
`${info(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
1658
|
-
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
1659
|
-
footer,
|
|
1660
|
-
`${info(S_BAR_END3)}`,
|
|
1661
|
-
""
|
|
1662
|
-
].join("\n");
|
|
1366
|
+
const session = await api.startCliAuthSession(server?.port, repoCanonical);
|
|
1367
|
+
const browserUrl = reauth ? session.verificationUrl : session.installUrl ?? session.verificationUrl;
|
|
1368
|
+
const expiresAt = new Date(session.expiresAt).getTime();
|
|
1369
|
+
if (options.ci) {
|
|
1370
|
+
process.stdout.write(`VOCODER_AUTH_URL: ${browserUrl}
|
|
1371
|
+
`);
|
|
1372
|
+
process.stdout.write(`VOCODER_SESSION_ID: ${session.sessionId}
|
|
1373
|
+
`);
|
|
1374
|
+
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1375
|
+
if (reauth) {
|
|
1376
|
+
if (!options.yes) {
|
|
1377
|
+
const shouldOpen = await p7.confirm({
|
|
1378
|
+
message: "Open your browser to sign in again?"
|
|
1379
|
+
});
|
|
1380
|
+
if (p7.isCancel(shouldOpen)) {
|
|
1381
|
+
server?.close();
|
|
1382
|
+
p7.cancel("Setup cancelled.");
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
if (!shouldOpen) {
|
|
1386
|
+
server?.close();
|
|
1387
|
+
p7.cancel("Setup cancelled.");
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
const opened = await tryOpenBrowser(browserUrl);
|
|
1391
|
+
if (!opened) {
|
|
1392
|
+
p7.note(browserUrl, "Sign In");
|
|
1393
|
+
p7.log.info("Open the URL above manually to continue.");
|
|
1663
1394
|
}
|
|
1395
|
+
} else {
|
|
1396
|
+
await tryOpenBrowser(browserUrl);
|
|
1664
1397
|
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
break;
|
|
1688
|
-
case "down":
|
|
1689
|
-
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
1690
|
-
break;
|
|
1691
|
-
case "space":
|
|
1692
|
-
if (multi) {
|
|
1693
|
-
const opt = filtered[cursor];
|
|
1694
|
-
if (opt) {
|
|
1695
|
-
if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
|
|
1696
|
-
else selected.add(opt.bcp47);
|
|
1697
|
-
}
|
|
1398
|
+
} else {
|
|
1399
|
+
let isLinkFlow = false;
|
|
1400
|
+
if (!options.yes) {
|
|
1401
|
+
const connectChoice = await p7.select({
|
|
1402
|
+
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
1403
|
+
options: [
|
|
1404
|
+
{
|
|
1405
|
+
value: "install",
|
|
1406
|
+
label: "Install GitHub App",
|
|
1407
|
+
hint: "new user"
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
value: "link",
|
|
1411
|
+
label: "Already installed? Link your account",
|
|
1412
|
+
hint: "returning user"
|
|
1413
|
+
}
|
|
1414
|
+
]
|
|
1415
|
+
});
|
|
1416
|
+
if (p7.isCancel(connectChoice)) {
|
|
1417
|
+
server?.close();
|
|
1418
|
+
p7.cancel("Setup cancelled.");
|
|
1419
|
+
return null;
|
|
1698
1420
|
}
|
|
1699
|
-
|
|
1421
|
+
isLinkFlow = connectChoice === "link";
|
|
1422
|
+
}
|
|
1423
|
+
let urlToOpen = browserUrl;
|
|
1424
|
+
if (isLinkFlow) {
|
|
1425
|
+
try {
|
|
1426
|
+
const linkSession = await api.startCliGitHubLinkSession(
|
|
1427
|
+
session.sessionId,
|
|
1428
|
+
server?.port
|
|
1429
|
+
);
|
|
1430
|
+
urlToOpen = linkSession.oauthUrl;
|
|
1431
|
+
} catch {
|
|
1432
|
+
urlToOpen = browserUrl;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const opened = await tryOpenBrowser(urlToOpen);
|
|
1436
|
+
if (!opened) {
|
|
1437
|
+
p7.log.warn("Could not open your browser automatically.");
|
|
1438
|
+
p7.note(urlToOpen, "GitHub");
|
|
1439
|
+
p7.log.info("Open the URL above to continue.");
|
|
1440
|
+
}
|
|
1700
1441
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1442
|
+
}
|
|
1443
|
+
const authSpinner = p7.spinner();
|
|
1444
|
+
authSpinner.start("Waiting for GitHub authorization...");
|
|
1445
|
+
let rawToken = null;
|
|
1446
|
+
let callbackOrganizationId;
|
|
1447
|
+
let callbackDiscoveryReady = false;
|
|
1448
|
+
const deadline = Math.min(expiresAt, Date.now() + 10 * 60 * 1e3);
|
|
1449
|
+
let stopPolling = false;
|
|
1450
|
+
const serverCallback = server ? server.waitForCallback().catch(() => null) : Promise.resolve(null);
|
|
1451
|
+
const sessionPoll = (async () => {
|
|
1452
|
+
while (!stopPolling && Date.now() < expiresAt) {
|
|
1453
|
+
try {
|
|
1454
|
+
const result = await api.pollCliAuthSession(session.sessionId);
|
|
1455
|
+
if (result.status === "complete" || result.status === "failed") {
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
if (!stopPolling) await sleep(2e3);
|
|
1704
1461
|
}
|
|
1462
|
+
return null;
|
|
1463
|
+
})();
|
|
1464
|
+
const winner = await new Promise((resolve3) => {
|
|
1465
|
+
let done = false;
|
|
1466
|
+
serverCallback.then((params) => {
|
|
1467
|
+
if (done || params === null || typeof params.token !== "string") return;
|
|
1468
|
+
done = true;
|
|
1469
|
+
resolve3({ kind: "server", params });
|
|
1470
|
+
}).catch(() => {
|
|
1471
|
+
});
|
|
1472
|
+
sessionPoll.then((result) => {
|
|
1473
|
+
if (done || result === null) return;
|
|
1474
|
+
if (result.status === "complete" || result.status === "failed") {
|
|
1475
|
+
done = true;
|
|
1476
|
+
resolve3({
|
|
1477
|
+
kind: "poll",
|
|
1478
|
+
result
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}).catch(() => {
|
|
1482
|
+
});
|
|
1483
|
+
setTimeout(
|
|
1484
|
+
() => {
|
|
1485
|
+
if (!done) {
|
|
1486
|
+
done = true;
|
|
1487
|
+
resolve3(null);
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
Math.max(0, deadline - Date.now())
|
|
1491
|
+
);
|
|
1705
1492
|
});
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1493
|
+
stopPolling = true;
|
|
1494
|
+
server?.close();
|
|
1495
|
+
if (winner !== null) {
|
|
1496
|
+
if (winner.kind === "server") {
|
|
1497
|
+
rawToken = winner.params.token;
|
|
1498
|
+
if (typeof winner.params.organizationId === "string" && winner.params.organizationId) {
|
|
1499
|
+
callbackOrganizationId = winner.params.organizationId;
|
|
1500
|
+
}
|
|
1501
|
+
if (winner.params.discovery_ready === "1") {
|
|
1502
|
+
callbackDiscoveryReady = true;
|
|
1503
|
+
}
|
|
1504
|
+
} else if (winner.result.status === "complete") {
|
|
1505
|
+
rawToken = winner.result.token;
|
|
1506
|
+
if (winner.result.organizationId) {
|
|
1507
|
+
callbackOrganizationId = winner.result.organizationId;
|
|
1713
1508
|
}
|
|
1509
|
+
} else {
|
|
1510
|
+
authSpinner.stop();
|
|
1511
|
+
p7.log.error(winner.result.reason);
|
|
1512
|
+
return null;
|
|
1714
1513
|
}
|
|
1715
|
-
});
|
|
1716
|
-
const result = await prompt.prompt();
|
|
1717
|
-
if (isCancel10(result)) return null;
|
|
1718
|
-
return result;
|
|
1719
|
-
}
|
|
1720
|
-
async function searchSelectLocale(options, message, initialValue) {
|
|
1721
|
-
const result = await runFilterablePrompt({
|
|
1722
|
-
message,
|
|
1723
|
-
options,
|
|
1724
|
-
multi: false,
|
|
1725
|
-
initialValue
|
|
1726
|
-
});
|
|
1727
|
-
return typeof result === "string" ? result : null;
|
|
1728
|
-
}
|
|
1729
|
-
async function searchMultiSelectLocales(options, message, initialValues) {
|
|
1730
|
-
const result = await runFilterablePrompt({
|
|
1731
|
-
message,
|
|
1732
|
-
options,
|
|
1733
|
-
multi: true,
|
|
1734
|
-
initialValues
|
|
1735
|
-
});
|
|
1736
|
-
if (result === null) return null;
|
|
1737
|
-
const picks = result;
|
|
1738
|
-
if (picks.length === 0) {
|
|
1739
|
-
p10.log.warn(
|
|
1740
|
-
"At least one target language is required. Please select at least one."
|
|
1741
|
-
);
|
|
1742
|
-
return searchMultiSelectLocales(options, message, initialValues);
|
|
1743
1514
|
}
|
|
1744
|
-
|
|
1515
|
+
if (!rawToken) {
|
|
1516
|
+
authSpinner.stop();
|
|
1517
|
+
p7.log.error("The authentication link expired. Run `vocoder init` again.");
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
const userInfo = await api.getCliUserInfo(rawToken);
|
|
1521
|
+
authSpinner.stop(`Authenticated as ${chalk6.bold(userInfo.email)}`);
|
|
1522
|
+
return {
|
|
1523
|
+
token: rawToken,
|
|
1524
|
+
...userInfo,
|
|
1525
|
+
organizationId: callbackOrganizationId,
|
|
1526
|
+
discoveryReady: callbackDiscoveryReady
|
|
1527
|
+
};
|
|
1745
1528
|
}
|
|
1746
1529
|
|
|
1747
|
-
// src/utils/
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1530
|
+
// src/utils/mcp-setup.ts
|
|
1531
|
+
import * as p8 from "@clack/prompts";
|
|
1532
|
+
import chalk7 from "chalk";
|
|
1533
|
+
import { execSync as execSync5 } from "child_process";
|
|
1534
|
+
var MCP_DOCS_URL = "https://vocoder.app/docs/mcp";
|
|
1535
|
+
function mcpServerJson(apiKey) {
|
|
1536
|
+
return JSON.stringify(
|
|
1537
|
+
{
|
|
1538
|
+
mcpServers: {
|
|
1539
|
+
vocoder: {
|
|
1540
|
+
type: "stdio",
|
|
1541
|
+
command: "npx",
|
|
1542
|
+
args: ["-y", "@vocoder/mcp"],
|
|
1543
|
+
env: { VOCODER_API_KEY: apiKey }
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
null,
|
|
1548
|
+
2
|
|
1549
|
+
);
|
|
1753
1550
|
}
|
|
1754
|
-
function
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1551
|
+
async function runMcpSetup(apiKey) {
|
|
1552
|
+
p8.log.message(
|
|
1553
|
+
chalk7.dim(
|
|
1554
|
+
" The Vocoder MCP server lets your AI editor add/remove locales,\n check translation status, and scaffold i18n directly in your project."
|
|
1555
|
+
)
|
|
1556
|
+
);
|
|
1557
|
+
const tool = await p8.select({
|
|
1558
|
+
message: "Which AI editor?",
|
|
1559
|
+
options: [
|
|
1560
|
+
{ value: "claude", label: "Claude Code" },
|
|
1561
|
+
{ value: "cursor", label: "Cursor" },
|
|
1562
|
+
{ value: "windsurf", label: "Windsurf" },
|
|
1563
|
+
{ value: "vscode", label: "VS Code (GitHub Copilot)" },
|
|
1564
|
+
{ value: "other", label: "Other \u2014 show the config JSON" }
|
|
1565
|
+
]
|
|
1566
|
+
});
|
|
1567
|
+
if (p8.isCancel(tool)) return;
|
|
1568
|
+
if (tool === "claude") {
|
|
1569
|
+
try {
|
|
1570
|
+
execSync5(
|
|
1571
|
+
`claude mcp add vocoder --scope user --transport stdio -e VOCODER_API_KEY=${apiKey} -- npx -y @vocoder/mcp`,
|
|
1572
|
+
{ stdio: "pipe" }
|
|
1573
|
+
);
|
|
1574
|
+
p8.log.success("Vocoder MCP server registered in Claude Code.");
|
|
1575
|
+
} catch {
|
|
1576
|
+
p8.log.message(chalk7.dim("(automatic registration failed \u2014 run this command manually:)"));
|
|
1577
|
+
printCommand(
|
|
1578
|
+
`claude mcp add vocoder --scope user --transport stdio -e VOCODER_API_KEY=${apiKey} -- npx -y @vocoder/mcp`
|
|
1579
|
+
);
|
|
1580
|
+
p8.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
1762
1581
|
}
|
|
1582
|
+
return;
|
|
1763
1583
|
}
|
|
1764
|
-
|
|
1584
|
+
const configPaths = {
|
|
1585
|
+
cursor: { path: "~/.cursor/mcp.json", merge: true },
|
|
1586
|
+
windsurf: { path: "~/.codeium/windsurf/mcp_config.json", merge: true },
|
|
1587
|
+
vscode: { path: ".vscode/mcp.json", merge: true },
|
|
1588
|
+
other: { path: ".mcp.json", merge: false }
|
|
1589
|
+
};
|
|
1590
|
+
const { path: configPath, merge } = configPaths[tool];
|
|
1591
|
+
const mergeNote = merge ? chalk7.dim(` Merge into ${configPath} (create if missing):`) : chalk7.dim(` Add to ${configPath}:`);
|
|
1592
|
+
p8.log.message(mergeNote);
|
|
1593
|
+
printCodeBlock(mcpServerJson(apiKey));
|
|
1594
|
+
p8.log.message(info(` Docs: ${MCP_DOCS_URL}`));
|
|
1765
1595
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1596
|
+
|
|
1597
|
+
// src/utils/organization-select.ts
|
|
1598
|
+
import * as p11 from "@clack/prompts";
|
|
1599
|
+
import chalk10 from "chalk";
|
|
1600
|
+
|
|
1601
|
+
// src/utils/github-connect.ts
|
|
1602
|
+
import * as p9 from "@clack/prompts";
|
|
1603
|
+
import chalk8 from "chalk";
|
|
1604
|
+
async function runGitHubInstallFlow(params) {
|
|
1605
|
+
let server = null;
|
|
1771
1606
|
try {
|
|
1772
|
-
|
|
1607
|
+
server = await startCallbackServer();
|
|
1773
1608
|
} catch {
|
|
1774
|
-
p11.log.error(
|
|
1775
|
-
"Failed to fetch supported locales. Check your connection and try again."
|
|
1776
|
-
);
|
|
1777
|
-
return null;
|
|
1778
1609
|
}
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1610
|
+
const { installUrl } = await params.api.startCliGitHubInstall(
|
|
1611
|
+
params.userToken,
|
|
1612
|
+
{
|
|
1613
|
+
organizationId: params.organizationId,
|
|
1614
|
+
callbackPort: server?.port
|
|
1615
|
+
}
|
|
1616
|
+
);
|
|
1617
|
+
p9.log.info("Opening GitHub to install the Vocoder App...");
|
|
1618
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1619
|
+
const shouldOpen = params.yes ? true : await p9.confirm({ message: "Open in your browser?" });
|
|
1620
|
+
if (p9.isCancel(shouldOpen)) {
|
|
1621
|
+
server?.close();
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
if (shouldOpen) {
|
|
1625
|
+
const opened = await tryOpenBrowser(installUrl);
|
|
1626
|
+
if (!opened) {
|
|
1627
|
+
p9.log.info(
|
|
1628
|
+
"Could not open a browser automatically. Use the URL above."
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const connectSpinner = p9.spinner();
|
|
1634
|
+
connectSpinner.start("Waiting for GitHub App installation...");
|
|
1635
|
+
if (server) {
|
|
1636
|
+
try {
|
|
1637
|
+
const params_timeout = 15 * 60 * 1e3;
|
|
1638
|
+
const callbackParams = await Promise.race([
|
|
1639
|
+
server.waitForCallback(),
|
|
1640
|
+
new Promise(
|
|
1641
|
+
(resolve3) => setTimeout(() => resolve3(null), params_timeout)
|
|
1642
|
+
)
|
|
1643
|
+
]);
|
|
1644
|
+
server.close();
|
|
1645
|
+
if (!callbackParams) {
|
|
1646
|
+
connectSpinner.stop("GitHub App installation timed out");
|
|
1647
|
+
p9.log.error(
|
|
1648
|
+
"The installation flow timed out. Run `vocoder init` again."
|
|
1649
|
+
);
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
if (callbackParams.error) {
|
|
1653
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
1654
|
+
p9.log.error(callbackParams.error);
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
1658
|
+
if (!organizationId || !connectionLabel) {
|
|
1659
|
+
connectSpinner.stop("GitHub App installation incomplete");
|
|
1660
|
+
p9.log.error("Missing organization or connection data from callback.");
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
connectSpinner.stop(
|
|
1664
|
+
`Connected to GitHub as ${chalk8.bold(connectionLabel)}`
|
|
1665
|
+
);
|
|
1666
|
+
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
1667
|
+
return {
|
|
1668
|
+
organizationId,
|
|
1669
|
+
organizationName: orgName,
|
|
1670
|
+
connectionLabel
|
|
1671
|
+
};
|
|
1672
|
+
} catch {
|
|
1673
|
+
server.close();
|
|
1674
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1784
1677
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
"
|
|
1788
|
-
params.defaultSourceLocale ?? "en"
|
|
1678
|
+
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
1679
|
+
p9.log.warn(
|
|
1680
|
+
"Complete the installation in your browser, then run `vocoder init` again."
|
|
1789
1681
|
);
|
|
1790
|
-
|
|
1791
|
-
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
async function runGitHubDiscoveryFlow(params) {
|
|
1685
|
+
let server = null;
|
|
1792
1686
|
try {
|
|
1793
|
-
|
|
1687
|
+
server = await startCallbackServer();
|
|
1794
1688
|
} catch {
|
|
1795
|
-
p11.log.error(
|
|
1796
|
-
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
1797
|
-
);
|
|
1798
|
-
return null;
|
|
1799
1689
|
}
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
);
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1690
|
+
const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
|
|
1691
|
+
organizationId: params.organizationId,
|
|
1692
|
+
callbackPort: server?.port
|
|
1693
|
+
});
|
|
1694
|
+
p9.log.info("Opening GitHub to authorize your account...");
|
|
1695
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1696
|
+
const shouldOpen = params.yes ? true : await p9.confirm({ message: "Open in your browser?" });
|
|
1697
|
+
if (p9.isCancel(shouldOpen)) {
|
|
1698
|
+
server?.close();
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
if (shouldOpen) {
|
|
1702
|
+
const opened = await tryOpenBrowser(oauthUrl);
|
|
1703
|
+
if (!opened) {
|
|
1704
|
+
p9.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1813
1707
|
}
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1708
|
+
const oauthSpinner = p9.spinner();
|
|
1709
|
+
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
1710
|
+
if (server) {
|
|
1711
|
+
try {
|
|
1712
|
+
const timeoutMs = 10 * 60 * 1e3;
|
|
1713
|
+
const callbackParams = await Promise.race([
|
|
1714
|
+
server.waitForCallback(),
|
|
1715
|
+
new Promise(
|
|
1716
|
+
(resolve3) => setTimeout(() => resolve3(null), timeoutMs)
|
|
1717
|
+
)
|
|
1718
|
+
]);
|
|
1719
|
+
server.close();
|
|
1720
|
+
if (!callbackParams) {
|
|
1721
|
+
oauthSpinner.stop("GitHub authorization timed out");
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
if (callbackParams.error) {
|
|
1725
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1726
|
+
p9.log.error(callbackParams.error);
|
|
1727
|
+
return null;
|
|
1834
1728
|
}
|
|
1729
|
+
} catch {
|
|
1730
|
+
server.close();
|
|
1731
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1732
|
+
return null;
|
|
1835
1733
|
}
|
|
1836
1734
|
}
|
|
1837
|
-
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1735
|
+
oauthSpinner.stop("GitHub account authorized");
|
|
1736
|
+
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
1737
|
+
params.userToken
|
|
1738
|
+
);
|
|
1739
|
+
return discoveryResult.installations;
|
|
1740
|
+
}
|
|
1741
|
+
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
1742
|
+
const options = installations.map((inst) => ({
|
|
1743
|
+
value: String(inst.installationId),
|
|
1744
|
+
label: inst.accountLogin,
|
|
1745
|
+
hint: [
|
|
1746
|
+
inst.accountType === "Organization" ? "GitHub org" : "personal account",
|
|
1747
|
+
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
1748
|
+
inst.isSuspended ? "suspended" : ""
|
|
1749
|
+
].filter(Boolean).join(" \xB7 ") || void 0
|
|
1750
|
+
}));
|
|
1751
|
+
if (canInstallNew) {
|
|
1752
|
+
options.push({
|
|
1753
|
+
value: "install_new",
|
|
1754
|
+
label: `Install on a new account ${chalk8.dim("(creates a new workspace)")}`
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const selected = await p9.select({
|
|
1758
|
+
message: "Which GitHub account should this workspace connect to?",
|
|
1759
|
+
options
|
|
1846
1760
|
});
|
|
1847
|
-
|
|
1848
|
-
return
|
|
1849
|
-
|
|
1850
|
-
projectName: result.projectName,
|
|
1851
|
-
apiKey: result.apiKey,
|
|
1852
|
-
sourceLocale,
|
|
1853
|
-
targetLocales,
|
|
1854
|
-
targetBranches,
|
|
1855
|
-
repositoryBound: result.repositoryBound,
|
|
1856
|
-
configureUrl: result.configureUrl,
|
|
1857
|
-
apps: result.apps
|
|
1858
|
-
};
|
|
1761
|
+
if (p9.isCancel(selected)) return null;
|
|
1762
|
+
if (selected === "install_new") return "install_new";
|
|
1763
|
+
return Number(selected);
|
|
1859
1764
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1765
|
+
|
|
1766
|
+
// src/utils/organization.ts
|
|
1767
|
+
import * as p10 from "@clack/prompts";
|
|
1768
|
+
import chalk9 from "chalk";
|
|
1769
|
+
async function selectOrganization(result) {
|
|
1770
|
+
const { organizations, canCreateOrganization } = result;
|
|
1771
|
+
if (organizations.length === 0) {
|
|
1772
|
+
return { action: "create" };
|
|
1867
1773
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
);
|
|
1875
|
-
return
|
|
1774
|
+
const options = organizations.map((org) => {
|
|
1775
|
+
const atLimit = org.maxApps !== -1 && org.appCount >= org.maxApps;
|
|
1776
|
+
const hint = [
|
|
1777
|
+
org.projectCount > 0 ? `${org.projectCount} project${org.projectCount !== 1 ? "s" : ""}` : "",
|
|
1778
|
+
org.connectionLabel ? `GitHub: ${org.connectionLabel}` : "",
|
|
1779
|
+
atLimit ? chalk9.yellow(`${org.appCount}/${org.maxApps} apps \u2014 upgrade for more`) : ""
|
|
1780
|
+
].filter(Boolean).join(" \xB7 ") || void 0;
|
|
1781
|
+
return { value: org.id, label: org.name, hint };
|
|
1782
|
+
});
|
|
1783
|
+
if (canCreateOrganization) {
|
|
1784
|
+
options.push({ value: "create", label: "Create new workspace" });
|
|
1876
1785
|
}
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
if (sourceLocale === null) return null;
|
|
1884
|
-
let compatibleTargets;
|
|
1885
|
-
try {
|
|
1886
|
-
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
1887
|
-
} catch {
|
|
1888
|
-
p11.log.error(
|
|
1889
|
-
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
1890
|
-
);
|
|
1891
|
-
return null;
|
|
1786
|
+
const selected = await p10.select({
|
|
1787
|
+
message: "Select workspace",
|
|
1788
|
+
options
|
|
1789
|
+
});
|
|
1790
|
+
if (p10.isCancel(selected)) {
|
|
1791
|
+
return { action: "cancelled" };
|
|
1892
1792
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
);
|
|
1896
|
-
const targetLocales = await searchMultiSelectLocales(
|
|
1897
|
-
targetOptions,
|
|
1898
|
-
"Target languages"
|
|
1899
|
-
);
|
|
1900
|
-
if (targetLocales === null) return null;
|
|
1901
|
-
if (targetLocales.length === 0) {
|
|
1902
|
-
p11.log.warn(
|
|
1903
|
-
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1904
|
-
);
|
|
1793
|
+
if (selected === "create") {
|
|
1794
|
+
return { action: "create" };
|
|
1905
1795
|
}
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
let initial = [detectedApp.defaultBranch];
|
|
1910
|
-
while (appPushBranches.length === 0) {
|
|
1911
|
-
const result2 = await filterableBranchSelect({
|
|
1912
|
-
message: "Which branches should trigger translations?",
|
|
1913
|
-
branches: detectedApp.branches,
|
|
1914
|
-
defaultBranch: detectedApp.defaultBranch,
|
|
1915
|
-
initialValues: initial
|
|
1916
|
-
});
|
|
1917
|
-
if (result2 === null) return null;
|
|
1918
|
-
if (result2.length === 0) {
|
|
1919
|
-
p11.log.warn("At least one branch is required.");
|
|
1920
|
-
initial = [detectedApp.defaultBranch];
|
|
1921
|
-
} else {
|
|
1922
|
-
appPushBranches = result2;
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1796
|
+
const organization = organizations.find((org) => org.id === selected);
|
|
1797
|
+
if (!organization) {
|
|
1798
|
+
return { action: "cancelled" };
|
|
1925
1799
|
}
|
|
1926
|
-
|
|
1927
|
-
const result = await api.createApp(userToken, {
|
|
1928
|
-
projectId,
|
|
1929
|
-
appDir,
|
|
1930
|
-
sourceLocale,
|
|
1931
|
-
targetLocales,
|
|
1932
|
-
targetBranches,
|
|
1933
|
-
repoCanonical: repoCanonical ?? ""
|
|
1934
|
-
});
|
|
1935
|
-
p11.log.success(
|
|
1936
|
-
`App ${chalk10.bold(appDir || "(root)")} added to ${chalk10.bold(projectName)}!`
|
|
1937
|
-
);
|
|
1938
|
-
return {
|
|
1939
|
-
projectId: result.projectId,
|
|
1940
|
-
projectName: result.projectName,
|
|
1941
|
-
appDir: result.appDir,
|
|
1942
|
-
appId: result.appId,
|
|
1943
|
-
sourceLocale,
|
|
1944
|
-
targetLocales,
|
|
1945
|
-
targetBranches
|
|
1946
|
-
};
|
|
1800
|
+
return { action: "use", organization };
|
|
1947
1801
|
}
|
|
1948
1802
|
|
|
1949
|
-
// src/
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1803
|
+
// src/utils/organization-select.ts
|
|
1804
|
+
async function selectOrganizationForInit(params) {
|
|
1805
|
+
const { api, userToken, userEmail, identity, lookup, repoProjectId, options } = params;
|
|
1806
|
+
if (params.authOrganizationId) {
|
|
1807
|
+
const organizationData2 = await api.listOrganizations(userToken);
|
|
1808
|
+
const organization = organizationData2.organizations.find(
|
|
1809
|
+
(o) => o.id === params.authOrganizationId
|
|
1810
|
+
);
|
|
1811
|
+
const organizationName = organization?.name ?? userEmail;
|
|
1812
|
+
p11.log.success(
|
|
1813
|
+
`Connected as ${chalk10.bold(userEmail)} \u2014 workspace: ${chalk10.bold(organizationName)}`
|
|
1814
|
+
);
|
|
1815
|
+
return { organizationId: params.authOrganizationId, organizationName };
|
|
1959
1816
|
}
|
|
1960
|
-
const
|
|
1961
|
-
if (
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
encoding: "utf-8",
|
|
1968
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
1969
|
-
}).trim();
|
|
1970
|
-
return output.length > 0 ? output : null;
|
|
1971
|
-
} catch {
|
|
1972
|
-
return null;
|
|
1817
|
+
const repoOrgContext = identity ? lookup?.organizationContext ?? null : null;
|
|
1818
|
+
if (repoOrgContext && !repoProjectId) {
|
|
1819
|
+
p11.log.success(`Workspace: ${chalk10.bold(repoOrgContext.organizationName)}`);
|
|
1820
|
+
return {
|
|
1821
|
+
organizationId: repoOrgContext.organizationId,
|
|
1822
|
+
organizationName: repoOrgContext.organizationName
|
|
1823
|
+
};
|
|
1973
1824
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1825
|
+
const organizationData = await api.listOrganizations(userToken, {
|
|
1826
|
+
repo: identity?.repoCanonical
|
|
1827
|
+
});
|
|
1828
|
+
const repoCanonical = identity?.repoCanonical ?? null;
|
|
1829
|
+
const covering = repoCanonical ? organizationData.organizations.filter((o) => o.coversRepo === true) : [];
|
|
1830
|
+
const connected = organizationData.organizations.filter(
|
|
1831
|
+
(o) => o.hasGitHubConnection
|
|
1832
|
+
);
|
|
1833
|
+
if (repoCanonical && covering.length === 1) {
|
|
1834
|
+
const organization = covering[0];
|
|
1835
|
+
p11.log.success(`Workspace: ${chalk10.bold(organization.name)}`);
|
|
1836
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
1979
1837
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1838
|
+
if (repoCanonical && covering.length > 1) {
|
|
1839
|
+
const choice = await p11.select({
|
|
1840
|
+
message: "Select workspace for this repo",
|
|
1841
|
+
options: covering.map((o) => ({
|
|
1842
|
+
value: o.id,
|
|
1843
|
+
label: `${o.name} ${chalk10.dim(`(${o.appCount} app${o.appCount !== 1 ? "s" : ""})`)}`
|
|
1844
|
+
}))
|
|
1845
|
+
});
|
|
1846
|
+
if (p11.isCancel(choice)) {
|
|
1847
|
+
p11.cancel("Setup cancelled.");
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
const organization = covering.find((o) => o.id === choice);
|
|
1851
|
+
p11.log.success(`Workspace: ${chalk10.bold(organization.name)}`);
|
|
1852
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
1986
1853
|
}
|
|
1987
|
-
if (
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1854
|
+
if (repoCanonical && covering.length === 0 && connected.length > 0) {
|
|
1855
|
+
const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
|
|
1856
|
+
p11.log.warn(
|
|
1857
|
+
`${chalk10.bold(shortRepo)} isn't accessible from your Vocoder installation.
|
|
1858
|
+
Grant access to this repository or install on the account that owns it.`
|
|
1859
|
+
);
|
|
1860
|
+
const fixOptions = [];
|
|
1861
|
+
for (const organization of connected) {
|
|
1862
|
+
if (organization.installationConfigureUrl) {
|
|
1863
|
+
fixOptions.push({
|
|
1864
|
+
value: `grant:${organization.id}`,
|
|
1865
|
+
label: `Configure ${chalk10.bold(organization.connectionLabel ?? organization.name)}'s GitHub App installation`
|
|
1866
|
+
});
|
|
1994
1867
|
}
|
|
1995
|
-
return { host, ownerRepoPath };
|
|
1996
1868
|
}
|
|
1997
|
-
|
|
1869
|
+
fixOptions.push({
|
|
1870
|
+
value: "install_new",
|
|
1871
|
+
label: `Install on a different GitHub account ${chalk10.dim("(creates a new personal workspace)")}`
|
|
1872
|
+
});
|
|
1873
|
+
fixOptions.push({ value: "cancel", label: "Cancel" });
|
|
1874
|
+
const fix = await p11.select({
|
|
1875
|
+
message: "How would you like to fix this?",
|
|
1876
|
+
options: fixOptions
|
|
1877
|
+
});
|
|
1878
|
+
if (p11.isCancel(fix) || fix === "cancel") {
|
|
1879
|
+
p11.cancel("Setup cancelled.");
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
if (fix.startsWith("grant:")) {
|
|
1883
|
+
const organization = connected.find((o) => `grant:${o.id}` === fix);
|
|
1884
|
+
await tryOpenBrowser(organization.installationConfigureUrl);
|
|
1885
|
+
p11.cancel(
|
|
1886
|
+
`Grant access to ${chalk10.bold(shortRepo)} in your browser,
|
|
1887
|
+
then re-run ${chalk10.bold("vocoder init")}.`
|
|
1888
|
+
);
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
const connectResult = await runGitHubInstallFlow({
|
|
1892
|
+
api,
|
|
1893
|
+
userToken,
|
|
1894
|
+
yes: options.yes
|
|
1895
|
+
});
|
|
1896
|
+
if (!connectResult) {
|
|
1897
|
+
p11.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
p11.log.success(`Workspace: ${chalk10.bold(connectResult.organizationName)}`);
|
|
1901
|
+
return {
|
|
1902
|
+
organizationId: connectResult.organizationId,
|
|
1903
|
+
organizationName: connectResult.organizationName
|
|
1904
|
+
};
|
|
1998
1905
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1906
|
+
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
1907
|
+
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
1908
|
+
if (cachedInstallations.length > 0) {
|
|
1909
|
+
if (repoCanonical) {
|
|
1910
|
+
const repoOwner = repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
1911
|
+
if (repoOwner) {
|
|
1912
|
+
const hasMatchingAccount = cachedInstallations.some(
|
|
1913
|
+
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
1914
|
+
);
|
|
1915
|
+
if (!hasMatchingAccount) {
|
|
1916
|
+
p11.log.warn(
|
|
1917
|
+
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
1918
|
+
The project will be created but translations won't trigger automatically.
|
|
1919
|
+
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
const validInstallations = cachedInstallations.filter(
|
|
1925
|
+
(i) => !i.isSuspended && !i.conflictLabel
|
|
1926
|
+
);
|
|
1927
|
+
let selectedInstallationId2 = null;
|
|
1928
|
+
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
1929
|
+
selectedInstallationId2 = validInstallations[0].installationId;
|
|
1930
|
+
} else {
|
|
1931
|
+
selectedInstallationId2 = await selectGitHubInstallation(
|
|
1932
|
+
cachedInstallations.map((inst) => ({
|
|
1933
|
+
installationId: inst.installationId,
|
|
1934
|
+
accountLogin: inst.accountLogin,
|
|
1935
|
+
accountType: inst.accountType,
|
|
1936
|
+
isSuspended: inst.isSuspended,
|
|
1937
|
+
conflictLabel: inst.conflictLabel
|
|
1938
|
+
})),
|
|
1939
|
+
false
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
if (selectedInstallationId2 === null || selectedInstallationId2 === "install_new") {
|
|
1943
|
+
p11.cancel("Setup cancelled. Re-run `vocoder init` and choose Install GitHub App.");
|
|
2004
1944
|
return null;
|
|
2005
1945
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
1946
|
+
const claimResult2 = await api.claimCliGitHubInstallation(userToken, {
|
|
1947
|
+
installationId: String(selectedInstallationId2),
|
|
1948
|
+
organizationId: null
|
|
1949
|
+
});
|
|
1950
|
+
p11.log.success(`Workspace: ${chalk10.bold(claimResult2.organizationName)}`);
|
|
1951
|
+
return {
|
|
1952
|
+
organizationId: claimResult2.organizationId,
|
|
1953
|
+
organizationName: claimResult2.organizationName
|
|
1954
|
+
};
|
|
2009
1955
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
return
|
|
1956
|
+
if (organizationData.organizations.length === 1 && !organizationData.canCreateOrganization) {
|
|
1957
|
+
const organization = organizationData.organizations[0];
|
|
1958
|
+
p11.log.success(`Workspace: ${chalk10.bold(organization.name)}`);
|
|
1959
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
2014
1960
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
1961
|
+
const organizationResult = await selectOrganization(organizationData);
|
|
1962
|
+
if (organizationResult.action === "cancelled") {
|
|
1963
|
+
p11.cancel("Setup cancelled.");
|
|
1964
|
+
return null;
|
|
2017
1965
|
}
|
|
2018
|
-
if (
|
|
2019
|
-
|
|
1966
|
+
if (organizationResult.action === "use") {
|
|
1967
|
+
const { organization } = organizationResult;
|
|
1968
|
+
p11.log.success(`Workspace: ${chalk10.bold(organization.name)}`);
|
|
1969
|
+
return { organizationId: organization.id, organizationName: organization.name };
|
|
2020
1970
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1971
|
+
const connectChoice = await p11.select({
|
|
1972
|
+
message: "Connect your new workspace to GitHub",
|
|
1973
|
+
options: [
|
|
1974
|
+
{ value: "install", label: "Install the Vocoder GitHub App" },
|
|
1975
|
+
{ value: "link", label: "Link an existing installation" }
|
|
1976
|
+
]
|
|
1977
|
+
});
|
|
1978
|
+
if (p11.isCancel(connectChoice)) {
|
|
1979
|
+
p11.cancel("Setup cancelled.");
|
|
2026
1980
|
return null;
|
|
2027
1981
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
1982
|
+
if (connectChoice === "install") {
|
|
1983
|
+
const connectResult = await runGitHubInstallFlow({
|
|
1984
|
+
api,
|
|
1985
|
+
userToken,
|
|
1986
|
+
yes: options.yes
|
|
1987
|
+
});
|
|
1988
|
+
if (!connectResult) {
|
|
1989
|
+
p11.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
p11.log.success(`Workspace: ${chalk10.bold(connectResult.organizationName)}`);
|
|
1993
|
+
return {
|
|
1994
|
+
organizationId: connectResult.organizationId,
|
|
1995
|
+
organizationName: connectResult.organizationName
|
|
1996
|
+
};
|
|
2031
1997
|
}
|
|
2032
|
-
const
|
|
2033
|
-
|
|
1998
|
+
const installations = await runGitHubDiscoveryFlow({
|
|
1999
|
+
api,
|
|
2000
|
+
userToken,
|
|
2001
|
+
yes: options.yes
|
|
2002
|
+
});
|
|
2003
|
+
if (!installations) return null;
|
|
2004
|
+
if (installations.length === 0) {
|
|
2005
|
+
p11.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
|
|
2006
|
+
const installNow = await p11.confirm({
|
|
2007
|
+
message: "Open GitHub to install the App?"
|
|
2008
|
+
});
|
|
2009
|
+
if (p11.isCancel(installNow) || !installNow) return null;
|
|
2010
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2011
|
+
api,
|
|
2012
|
+
userToken,
|
|
2013
|
+
yes: options.yes
|
|
2014
|
+
});
|
|
2015
|
+
if (!connectResult) return null;
|
|
2016
|
+
p11.log.success(`Workspace: ${chalk10.bold(connectResult.organizationName)}`);
|
|
2017
|
+
return {
|
|
2018
|
+
organizationId: connectResult.organizationId,
|
|
2019
|
+
organizationName: connectResult.organizationName
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
const selectedInstallationId = await selectGitHubInstallation(
|
|
2023
|
+
installations.map((inst) => ({
|
|
2024
|
+
installationId: inst.installationId,
|
|
2025
|
+
accountLogin: inst.accountLogin,
|
|
2026
|
+
accountType: inst.accountType,
|
|
2027
|
+
isSuspended: inst.isSuspended,
|
|
2028
|
+
conflictLabel: inst.conflictLabel
|
|
2029
|
+
})),
|
|
2030
|
+
true
|
|
2031
|
+
);
|
|
2032
|
+
if (selectedInstallationId === null) {
|
|
2033
|
+
p11.cancel("Setup cancelled.");
|
|
2034
2034
|
return null;
|
|
2035
2035
|
}
|
|
2036
|
+
if (selectedInstallationId === "install_new") {
|
|
2037
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2038
|
+
api,
|
|
2039
|
+
userToken,
|
|
2040
|
+
yes: options.yes
|
|
2041
|
+
});
|
|
2042
|
+
if (!connectResult) return null;
|
|
2043
|
+
p11.log.success(`Workspace: ${chalk10.bold(connectResult.organizationName)}`);
|
|
2044
|
+
return {
|
|
2045
|
+
organizationId: connectResult.organizationId,
|
|
2046
|
+
organizationName: connectResult.organizationName
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
const claimResult = await api.claimCliGitHubInstallation(userToken, {
|
|
2050
|
+
installationId: String(selectedInstallationId),
|
|
2051
|
+
organizationId: null
|
|
2052
|
+
});
|
|
2053
|
+
p11.log.success(`Workspace: ${chalk10.bold(claimResult.organizationName)}`);
|
|
2036
2054
|
return {
|
|
2037
|
-
|
|
2038
|
-
|
|
2055
|
+
organizationId: claimResult.organizationId,
|
|
2056
|
+
organizationName: claimResult.organizationName
|
|
2039
2057
|
};
|
|
2040
2058
|
}
|
|
2041
|
-
function resolveGitContext() {
|
|
2042
|
-
const warnings = [];
|
|
2043
|
-
const identity = resolveGitRepositoryIdentity();
|
|
2044
|
-
if (!identity) {
|
|
2045
|
-
warnings.push(
|
|
2046
|
-
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
2047
|
-
);
|
|
2048
|
-
}
|
|
2049
|
-
return { identity, warnings };
|
|
2050
|
-
}
|
|
2051
2059
|
|
|
2052
2060
|
// src/commands/init.ts
|
|
2053
2061
|
loadEnv();
|
|
@@ -2246,7 +2254,7 @@ Translations won't run automatically until you grant access.
|
|
|
2246
2254
|
);
|
|
2247
2255
|
printApiKey(projectResult.apiKey, identity?.repoRoot);
|
|
2248
2256
|
const wantsMcp = await p12.confirm({
|
|
2249
|
-
message: "Set up the Vocoder MCP server
|
|
2257
|
+
message: "Set up the Vocoder MCP server?"
|
|
2250
2258
|
});
|
|
2251
2259
|
if (!p12.isCancel(wantsMcp) && wantsMcp) {
|
|
2252
2260
|
await runMcpSetup(projectResult.apiKey);
|