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