@vocoder/cli 0.1.16 → 0.1.18
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/LICENSE +21 -0
- package/dist/bin.mjs +1139 -833
- package/dist/bin.mjs.map +1 -1
- package/dist/chunk-OFQLREXF.mjs +267 -0
- package/dist/chunk-OFQLREXF.mjs.map +1 -0
- package/dist/lib.d.mts +18 -73
- package/dist/lib.mjs +1 -1
- package/package.json +14 -20
- package/dist/chunk-KPIT5ETY.mjs +0 -547
- package/dist/chunk-KPIT5ETY.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -5,319 +5,16 @@ import {
|
|
|
5
5
|
detectLocalEcosystem,
|
|
6
6
|
getPackagesToInstall,
|
|
7
7
|
getSetupSnippets
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OFQLREXF.mjs";
|
|
9
9
|
|
|
10
10
|
// src/bin.ts
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
13
13
|
// src/commands/init.ts
|
|
14
|
+
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
14
15
|
import * as p5 from "@clack/prompts";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
18
|
-
import { homedir } from "os";
|
|
19
|
-
import { dirname, join } from "path";
|
|
20
|
-
function getAuthFilePath() {
|
|
21
|
-
return join(homedir(), ".config", "vocoder", "auth.json");
|
|
22
|
-
}
|
|
23
|
-
function readAuthData() {
|
|
24
|
-
const filePath = getAuthFilePath();
|
|
25
|
-
try {
|
|
26
|
-
const raw = readFileSync(filePath, "utf8");
|
|
27
|
-
const parsed = JSON.parse(raw);
|
|
28
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
29
|
-
const data = parsed;
|
|
30
|
-
if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
token: data.token,
|
|
35
|
-
apiUrl: data.apiUrl,
|
|
36
|
-
userId: data.userId,
|
|
37
|
-
email: data.email,
|
|
38
|
-
name: typeof data.name === "string" ? data.name : null,
|
|
39
|
-
createdAt: data.createdAt
|
|
40
|
-
};
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function writeAuthData(data) {
|
|
46
|
-
const filePath = getAuthFilePath();
|
|
47
|
-
const dir = dirname(filePath);
|
|
48
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
49
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
50
|
-
}
|
|
51
|
-
function clearAuthData() {
|
|
52
|
-
const filePath = getAuthFilePath();
|
|
53
|
-
try {
|
|
54
|
-
unlinkSync(filePath);
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/utils/github-connect.ts
|
|
60
|
-
import * as p from "@clack/prompts";
|
|
61
|
-
import chalk from "chalk";
|
|
62
|
-
import { spawn } from "child_process";
|
|
63
|
-
|
|
64
|
-
// src/utils/local-server.ts
|
|
65
|
-
import { createServer } from "http";
|
|
66
|
-
import { URL as URL2 } from "url";
|
|
67
|
-
function startCallbackServer() {
|
|
68
|
-
return new Promise((resolve2, reject) => {
|
|
69
|
-
let settled = false;
|
|
70
|
-
let callbackResolve = null;
|
|
71
|
-
let callbackReject = null;
|
|
72
|
-
const callbackPromise = new Promise((res, rej) => {
|
|
73
|
-
callbackResolve = res;
|
|
74
|
-
callbackReject = rej;
|
|
75
|
-
});
|
|
76
|
-
const server = createServer((req, res) => {
|
|
77
|
-
if (!req.url) {
|
|
78
|
-
res.writeHead(400);
|
|
79
|
-
res.end();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
let pathname;
|
|
83
|
-
let params;
|
|
84
|
-
try {
|
|
85
|
-
const parsed = new URL2(req.url, "http://localhost");
|
|
86
|
-
pathname = parsed.pathname;
|
|
87
|
-
params = Object.fromEntries(parsed.searchParams.entries());
|
|
88
|
-
} catch {
|
|
89
|
-
res.writeHead(400);
|
|
90
|
-
res.end("Bad request");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (pathname !== "/callback") {
|
|
94
|
-
res.writeHead(404);
|
|
95
|
-
res.end("Not found");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
99
|
-
res.end(
|
|
100
|
-
'<!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>'
|
|
101
|
-
);
|
|
102
|
-
if (callbackResolve) {
|
|
103
|
-
callbackResolve(params);
|
|
104
|
-
callbackResolve = null;
|
|
105
|
-
}
|
|
106
|
-
setImmediate(() => server.close());
|
|
107
|
-
});
|
|
108
|
-
server.on("error", (err) => {
|
|
109
|
-
if (!settled) {
|
|
110
|
-
settled = true;
|
|
111
|
-
if (callbackReject) callbackReject(err);
|
|
112
|
-
reject(err);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
server.listen(0, "127.0.0.1", () => {
|
|
116
|
-
if (settled) return;
|
|
117
|
-
settled = true;
|
|
118
|
-
const port = server.address().port;
|
|
119
|
-
resolve2({
|
|
120
|
-
port,
|
|
121
|
-
waitForCallback: () => callbackPromise,
|
|
122
|
-
close: () => server.close()
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/utils/github-connect.ts
|
|
129
|
-
async function tryOpenBrowser(url) {
|
|
130
|
-
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
const platform = process.platform;
|
|
134
|
-
let command;
|
|
135
|
-
let args;
|
|
136
|
-
if (platform === "darwin") {
|
|
137
|
-
command = "open";
|
|
138
|
-
args = [url];
|
|
139
|
-
} else if (platform === "win32") {
|
|
140
|
-
command = "rundll32";
|
|
141
|
-
args = ["url.dll,FileProtocolHandler", url];
|
|
142
|
-
} else {
|
|
143
|
-
command = "xdg-open";
|
|
144
|
-
args = [url];
|
|
145
|
-
}
|
|
146
|
-
return new Promise((resolve2) => {
|
|
147
|
-
try {
|
|
148
|
-
const child = spawn(command, args, {
|
|
149
|
-
detached: true,
|
|
150
|
-
stdio: "ignore",
|
|
151
|
-
windowsHide: true
|
|
152
|
-
});
|
|
153
|
-
let settled = false;
|
|
154
|
-
child.once("spawn", () => {
|
|
155
|
-
if (settled) return;
|
|
156
|
-
settled = true;
|
|
157
|
-
child.unref();
|
|
158
|
-
resolve2(true);
|
|
159
|
-
});
|
|
160
|
-
child.once("error", () => {
|
|
161
|
-
if (settled) return;
|
|
162
|
-
settled = true;
|
|
163
|
-
resolve2(false);
|
|
164
|
-
});
|
|
165
|
-
setTimeout(() => {
|
|
166
|
-
if (settled) return;
|
|
167
|
-
settled = true;
|
|
168
|
-
resolve2(false);
|
|
169
|
-
}, 300);
|
|
170
|
-
} catch {
|
|
171
|
-
resolve2(false);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
async function runGitHubInstallFlow(params) {
|
|
176
|
-
let server = null;
|
|
177
|
-
try {
|
|
178
|
-
server = await startCallbackServer();
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
const { installUrl } = await params.api.startCliGitHubInstall(params.userToken, {
|
|
182
|
-
organizationId: params.organizationId,
|
|
183
|
-
callbackPort: server?.port
|
|
184
|
-
});
|
|
185
|
-
p.log.info("Opening GitHub to install the Vocoder App...");
|
|
186
|
-
p.note(installUrl, "Install URL");
|
|
187
|
-
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
188
|
-
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
189
|
-
if (p.isCancel(shouldOpen)) {
|
|
190
|
-
server?.close();
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
if (shouldOpen) {
|
|
194
|
-
const opened = await tryOpenBrowser(installUrl);
|
|
195
|
-
if (!opened) {
|
|
196
|
-
p.log.info("Could not open a browser automatically. Use the URL above.");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const connectSpinner = p.spinner();
|
|
201
|
-
connectSpinner.start("Waiting for GitHub App installation...");
|
|
202
|
-
if (server) {
|
|
203
|
-
try {
|
|
204
|
-
const params_timeout = 15 * 60 * 1e3;
|
|
205
|
-
const callbackParams = await Promise.race([
|
|
206
|
-
server.waitForCallback(),
|
|
207
|
-
new Promise((resolve2) => setTimeout(() => resolve2(null), params_timeout))
|
|
208
|
-
]);
|
|
209
|
-
server.close();
|
|
210
|
-
if (!callbackParams) {
|
|
211
|
-
connectSpinner.stop("GitHub App installation timed out");
|
|
212
|
-
p.log.error("The installation flow timed out. Run `vocoder init` again.");
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
if (callbackParams.error) {
|
|
216
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
217
|
-
p.log.error(callbackParams.error);
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
221
|
-
if (!organizationId || !connectionLabel) {
|
|
222
|
-
connectSpinner.stop("GitHub App installation incomplete");
|
|
223
|
-
p.log.error("Missing organization or connection data from callback.");
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
connectSpinner.stop(`Connected to GitHub as ${chalk.bold(connectionLabel)}`);
|
|
227
|
-
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
228
|
-
return {
|
|
229
|
-
organizationId,
|
|
230
|
-
organizationName: orgName,
|
|
231
|
-
connectionLabel
|
|
232
|
-
};
|
|
233
|
-
} catch {
|
|
234
|
-
server.close();
|
|
235
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
240
|
-
p.log.warn("Complete the installation in your browser, then run `vocoder init` again.");
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
async function runGitHubDiscoveryFlow(params) {
|
|
244
|
-
let server = null;
|
|
245
|
-
try {
|
|
246
|
-
server = await startCallbackServer();
|
|
247
|
-
} catch {
|
|
248
|
-
}
|
|
249
|
-
const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
|
|
250
|
-
organizationId: params.organizationId,
|
|
251
|
-
callbackPort: server?.port
|
|
252
|
-
});
|
|
253
|
-
p.log.info("Opening GitHub to authorize your account...");
|
|
254
|
-
p.note("Complete authorization in your browser.");
|
|
255
|
-
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
256
|
-
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
257
|
-
if (p.isCancel(shouldOpen)) {
|
|
258
|
-
server?.close();
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
if (shouldOpen) {
|
|
262
|
-
const opened = await tryOpenBrowser(oauthUrl);
|
|
263
|
-
if (!opened) {
|
|
264
|
-
p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const oauthSpinner = p.spinner();
|
|
269
|
-
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
270
|
-
if (server) {
|
|
271
|
-
try {
|
|
272
|
-
const timeoutMs = 10 * 60 * 1e3;
|
|
273
|
-
const callbackParams = await Promise.race([
|
|
274
|
-
server.waitForCallback(),
|
|
275
|
-
new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
|
|
276
|
-
]);
|
|
277
|
-
server.close();
|
|
278
|
-
if (!callbackParams) {
|
|
279
|
-
oauthSpinner.stop("GitHub authorization timed out");
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
if (callbackParams.error) {
|
|
283
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
284
|
-
p.log.error(callbackParams.error);
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
server.close();
|
|
289
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
oauthSpinner.stop("GitHub account authorized");
|
|
294
|
-
const discoveryResult = await params.api.getCliGitHubDiscovery(params.userToken);
|
|
295
|
-
return discoveryResult.installations;
|
|
296
|
-
}
|
|
297
|
-
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
298
|
-
const options = installations.map((inst) => ({
|
|
299
|
-
value: String(inst.installationId),
|
|
300
|
-
label: inst.accountLogin,
|
|
301
|
-
hint: [
|
|
302
|
-
inst.accountType === "Organization" ? "organization" : "personal",
|
|
303
|
-
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
304
|
-
inst.isSuspended ? "suspended" : ""
|
|
305
|
-
].filter(Boolean).join(" \xB7 ") || void 0
|
|
306
|
-
}));
|
|
307
|
-
if (canInstallNew) {
|
|
308
|
-
options.push({
|
|
309
|
-
value: "install_new",
|
|
310
|
-
label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
const selected = await p.select({
|
|
314
|
-
message: "Select a GitHub installation",
|
|
315
|
-
options
|
|
316
|
-
});
|
|
317
|
-
if (p.isCancel(selected)) return null;
|
|
318
|
-
if (selected === "install_new") return "install_new";
|
|
319
|
-
return Number(selected);
|
|
320
|
-
}
|
|
16
|
+
import chalk6 from "chalk";
|
|
17
|
+
import { config as loadEnv } from "dotenv";
|
|
321
18
|
|
|
322
19
|
// src/utils/api.ts
|
|
323
20
|
function isLimitErrorResponse(value) {
|
|
@@ -353,7 +50,9 @@ function parsePayload(raw) {
|
|
|
353
50
|
}
|
|
354
51
|
const trimmed = raw.trimStart();
|
|
355
52
|
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
|
356
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
message: "Unexpected response from server (received HTML). Check your network connection or try again."
|
|
55
|
+
};
|
|
357
56
|
}
|
|
358
57
|
try {
|
|
359
58
|
return JSON.parse(raw);
|
|
@@ -398,7 +97,10 @@ var VocoderAPI = class {
|
|
|
398
97
|
if (!response.ok) {
|
|
399
98
|
const limitError = isLimitErrorResponse(payload) ? payload : null;
|
|
400
99
|
const syncPolicyError = isSyncPolicyErrorResponse(payload) ? payload : null;
|
|
401
|
-
const baseMessage = extractErrorMessage(
|
|
100
|
+
const baseMessage = extractErrorMessage(
|
|
101
|
+
payload,
|
|
102
|
+
`Request failed with status ${response.status}`
|
|
103
|
+
);
|
|
402
104
|
throw new VocoderAPIError({
|
|
403
105
|
message: errorPrefix ? `${errorPrefix}: ${baseMessage}` : baseMessage,
|
|
404
106
|
status: response.status,
|
|
@@ -420,10 +122,13 @@ var VocoderAPI = class {
|
|
|
420
122
|
organizationName: data.organizationName,
|
|
421
123
|
sourceLocale: data.sourceLocale,
|
|
422
124
|
targetLocales: data.targetLocales,
|
|
423
|
-
|
|
125
|
+
targetBranches: data.targetBranches ?? ["main"],
|
|
424
126
|
primaryBranch: data.primaryBranch,
|
|
425
127
|
syncPolicy: {
|
|
426
|
-
blockingBranches: data.syncPolicy?.blockingBranches ?? [
|
|
128
|
+
blockingBranches: data.syncPolicy?.blockingBranches ?? [
|
|
129
|
+
"main",
|
|
130
|
+
"master"
|
|
131
|
+
],
|
|
427
132
|
blockingMode: data.syncPolicy?.blockingMode ?? "required",
|
|
428
133
|
nonBlockingMode: data.syncPolicy?.nonBlockingMode ?? "best-effort",
|
|
429
134
|
defaultMaxWaitMs: data.syncPolicy?.defaultMaxWaitMs ?? 6e4
|
|
@@ -466,23 +171,28 @@ var VocoderAPI = class {
|
|
|
466
171
|
const crypto = await import("crypto");
|
|
467
172
|
const sortedStrings = [...strings].sort();
|
|
468
173
|
const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
|
|
469
|
-
return this.request(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
174
|
+
return this.request(
|
|
175
|
+
"/api/cli/sync",
|
|
176
|
+
{
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json"
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
branch,
|
|
183
|
+
stringEntries,
|
|
184
|
+
targetLocales,
|
|
185
|
+
stringsHash,
|
|
186
|
+
...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
|
|
187
|
+
...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
|
|
188
|
+
...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
|
|
189
|
+
...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
|
|
190
|
+
...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
|
|
191
|
+
...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {}
|
|
192
|
+
})
|
|
473
193
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
stringEntries,
|
|
477
|
-
targetLocales,
|
|
478
|
-
stringsHash,
|
|
479
|
-
...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
|
|
480
|
-
...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
|
|
481
|
-
...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
|
|
482
|
-
...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
|
|
483
|
-
...repoIdentity?.repoScopePath !== void 0 ? { repoScopePath: repoIdentity.repoScopePath } : {}
|
|
484
|
-
})
|
|
485
|
-
}, "Translation submission failed");
|
|
194
|
+
"Translation submission failed"
|
|
195
|
+
);
|
|
486
196
|
}
|
|
487
197
|
/**
|
|
488
198
|
* Check translation status
|
|
@@ -546,7 +256,10 @@ var VocoderAPI = class {
|
|
|
546
256
|
const payload = await readPayload(response);
|
|
547
257
|
if (!response.ok) {
|
|
548
258
|
throw new VocoderAPIError({
|
|
549
|
-
message: extractErrorMessage(
|
|
259
|
+
message: extractErrorMessage(
|
|
260
|
+
payload,
|
|
261
|
+
`Failed to start init session (${response.status})`
|
|
262
|
+
),
|
|
550
263
|
status: response.status,
|
|
551
264
|
payload
|
|
552
265
|
});
|
|
@@ -565,7 +278,10 @@ var VocoderAPI = class {
|
|
|
565
278
|
const payload = await readPayload(response);
|
|
566
279
|
if (!response.ok) {
|
|
567
280
|
throw new VocoderAPIError({
|
|
568
|
-
message: extractErrorMessage(
|
|
281
|
+
message: extractErrorMessage(
|
|
282
|
+
payload,
|
|
283
|
+
`Failed to get init status (${response.status})`
|
|
284
|
+
),
|
|
569
285
|
status: response.status,
|
|
570
286
|
payload
|
|
571
287
|
});
|
|
@@ -589,7 +305,10 @@ var VocoderAPI = class {
|
|
|
589
305
|
const payload = await readPayload(response);
|
|
590
306
|
if (!response.ok) {
|
|
591
307
|
throw new VocoderAPIError({
|
|
592
|
-
message: extractErrorMessage(
|
|
308
|
+
message: extractErrorMessage(
|
|
309
|
+
payload,
|
|
310
|
+
`Failed to start auth session (${response.status})`
|
|
311
|
+
),
|
|
593
312
|
status: response.status,
|
|
594
313
|
payload
|
|
595
314
|
});
|
|
@@ -618,7 +337,10 @@ var VocoderAPI = class {
|
|
|
618
337
|
if (!response.ok) {
|
|
619
338
|
return {
|
|
620
339
|
status: "failed",
|
|
621
|
-
reason: extractErrorMessage(
|
|
340
|
+
reason: extractErrorMessage(
|
|
341
|
+
payload,
|
|
342
|
+
`Auth session error (${response.status})`
|
|
343
|
+
)
|
|
622
344
|
};
|
|
623
345
|
}
|
|
624
346
|
const result = payload;
|
|
@@ -642,7 +364,10 @@ var VocoderAPI = class {
|
|
|
642
364
|
const payload = await readPayload(response);
|
|
643
365
|
if (!response.ok) {
|
|
644
366
|
throw new VocoderAPIError({
|
|
645
|
-
message: extractErrorMessage(
|
|
367
|
+
message: extractErrorMessage(
|
|
368
|
+
payload,
|
|
369
|
+
`Token validation failed (${response.status})`
|
|
370
|
+
),
|
|
646
371
|
status: response.status,
|
|
647
372
|
payload
|
|
648
373
|
});
|
|
@@ -660,7 +385,10 @@ var VocoderAPI = class {
|
|
|
660
385
|
if (!response.ok) {
|
|
661
386
|
const payload = await readPayload(response);
|
|
662
387
|
throw new VocoderAPIError({
|
|
663
|
-
message: extractErrorMessage(
|
|
388
|
+
message: extractErrorMessage(
|
|
389
|
+
payload,
|
|
390
|
+
`Token revocation failed (${response.status})`
|
|
391
|
+
),
|
|
664
392
|
status: response.status,
|
|
665
393
|
payload
|
|
666
394
|
});
|
|
@@ -676,7 +404,10 @@ var VocoderAPI = class {
|
|
|
676
404
|
const payload = await readPayload(response);
|
|
677
405
|
if (!response.ok) {
|
|
678
406
|
throw new VocoderAPIError({
|
|
679
|
-
message: extractErrorMessage(
|
|
407
|
+
message: extractErrorMessage(
|
|
408
|
+
payload,
|
|
409
|
+
`Failed to list workspaces (${response.status})`
|
|
410
|
+
),
|
|
680
411
|
status: response.status,
|
|
681
412
|
payload
|
|
682
413
|
});
|
|
@@ -692,7 +423,10 @@ var VocoderAPI = class {
|
|
|
692
423
|
const payload = await readPayload(response);
|
|
693
424
|
if (!response.ok) {
|
|
694
425
|
throw new VocoderAPIError({
|
|
695
|
-
message: extractErrorMessage(
|
|
426
|
+
message: extractErrorMessage(
|
|
427
|
+
payload,
|
|
428
|
+
`Failed to list projects (${response.status})`
|
|
429
|
+
),
|
|
696
430
|
status: response.status,
|
|
697
431
|
payload
|
|
698
432
|
});
|
|
@@ -700,20 +434,51 @@ var VocoderAPI = class {
|
|
|
700
434
|
const result = payload;
|
|
701
435
|
return result.projects;
|
|
702
436
|
}
|
|
437
|
+
async regenerateProjectApiKey(userToken, projectId) {
|
|
438
|
+
const response = await fetch(
|
|
439
|
+
`${this.apiUrl}/api/cli/project/regenerate-key`,
|
|
440
|
+
{
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: {
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
Authorization: `Bearer ${userToken}`
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify({ projectId })
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
const payload = await readPayload(response);
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new VocoderAPIError({
|
|
452
|
+
message: extractErrorMessage(
|
|
453
|
+
payload,
|
|
454
|
+
`Failed to regenerate API key (${response.status})`
|
|
455
|
+
),
|
|
456
|
+
status: response.status,
|
|
457
|
+
payload
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return payload;
|
|
461
|
+
}
|
|
703
462
|
// ── CLI GitHub endpoints ──────────────────────────────────────────────────────
|
|
704
463
|
async startCliGitHubInstall(userToken, params) {
|
|
705
|
-
const response = await fetch(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
464
|
+
const response = await fetch(
|
|
465
|
+
`${this.apiUrl}/api/cli/github/install/start`,
|
|
466
|
+
{
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: {
|
|
469
|
+
Authorization: `Bearer ${userToken}`,
|
|
470
|
+
"Content-Type": "application/json"
|
|
471
|
+
},
|
|
472
|
+
body: JSON.stringify(params)
|
|
473
|
+
}
|
|
474
|
+
);
|
|
713
475
|
const payload = await readPayload(response);
|
|
714
476
|
if (!response.ok) {
|
|
715
477
|
throw new VocoderAPIError({
|
|
716
|
-
message: extractErrorMessage(
|
|
478
|
+
message: extractErrorMessage(
|
|
479
|
+
payload,
|
|
480
|
+
`Failed to start GitHub install (${response.status})`
|
|
481
|
+
),
|
|
717
482
|
status: response.status,
|
|
718
483
|
payload
|
|
719
484
|
});
|
|
@@ -726,15 +491,24 @@ var VocoderAPI = class {
|
|
|
726
491
|
* account is created from the OAuth code in the callback.
|
|
727
492
|
*/
|
|
728
493
|
async startCliGitHubLinkSession(sessionId, callbackPort) {
|
|
729
|
-
const response = await fetch(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
494
|
+
const response = await fetch(
|
|
495
|
+
`${this.apiUrl}/api/cli/github/oauth/link-start`,
|
|
496
|
+
{
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: { "Content-Type": "application/json" },
|
|
499
|
+
body: JSON.stringify({
|
|
500
|
+
sessionId,
|
|
501
|
+
...callbackPort != null ? { callbackPort } : {}
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
);
|
|
734
505
|
const payload = await readPayload(response);
|
|
735
506
|
if (!response.ok) {
|
|
736
507
|
throw new VocoderAPIError({
|
|
737
|
-
message: extractErrorMessage(
|
|
508
|
+
message: extractErrorMessage(
|
|
509
|
+
payload,
|
|
510
|
+
`Failed to start GitHub link session (${response.status})`
|
|
511
|
+
),
|
|
738
512
|
status: response.status,
|
|
739
513
|
payload
|
|
740
514
|
});
|
|
@@ -753,7 +527,10 @@ var VocoderAPI = class {
|
|
|
753
527
|
const payload = await readPayload(response);
|
|
754
528
|
if (!response.ok) {
|
|
755
529
|
throw new VocoderAPIError({
|
|
756
|
-
message: extractErrorMessage(
|
|
530
|
+
message: extractErrorMessage(
|
|
531
|
+
payload,
|
|
532
|
+
`Failed to start GitHub OAuth (${response.status})`
|
|
533
|
+
),
|
|
757
534
|
status: response.status,
|
|
758
535
|
payload
|
|
759
536
|
});
|
|
@@ -767,7 +544,10 @@ var VocoderAPI = class {
|
|
|
767
544
|
const payload = await readPayload(response);
|
|
768
545
|
if (!response.ok) {
|
|
769
546
|
throw new VocoderAPIError({
|
|
770
|
-
message: extractErrorMessage(
|
|
547
|
+
message: extractErrorMessage(
|
|
548
|
+
payload,
|
|
549
|
+
`Failed to fetch GitHub discovery (${response.status})`
|
|
550
|
+
),
|
|
771
551
|
status: response.status,
|
|
772
552
|
payload
|
|
773
553
|
});
|
|
@@ -786,7 +566,10 @@ var VocoderAPI = class {
|
|
|
786
566
|
const payload = await readPayload(response);
|
|
787
567
|
if (!response.ok) {
|
|
788
568
|
throw new VocoderAPIError({
|
|
789
|
-
message: extractErrorMessage(
|
|
569
|
+
message: extractErrorMessage(
|
|
570
|
+
payload,
|
|
571
|
+
`Failed to claim GitHub installation (${response.status})`
|
|
572
|
+
),
|
|
790
573
|
status: response.status,
|
|
791
574
|
payload
|
|
792
575
|
});
|
|
@@ -801,7 +584,10 @@ var VocoderAPI = class {
|
|
|
801
584
|
const payload = await readPayload(response);
|
|
802
585
|
if (!response.ok) {
|
|
803
586
|
throw new VocoderAPIError({
|
|
804
|
-
message: extractErrorMessage(
|
|
587
|
+
message: extractErrorMessage(
|
|
588
|
+
payload,
|
|
589
|
+
`Failed to list locales (${response.status})`
|
|
590
|
+
),
|
|
805
591
|
status: response.status,
|
|
806
592
|
payload
|
|
807
593
|
});
|
|
@@ -822,7 +608,10 @@ var VocoderAPI = class {
|
|
|
822
608
|
const payload = await readPayload(response);
|
|
823
609
|
if (!response.ok) {
|
|
824
610
|
throw new VocoderAPIError({
|
|
825
|
-
message: extractErrorMessage(
|
|
611
|
+
message: extractErrorMessage(
|
|
612
|
+
payload,
|
|
613
|
+
`Failed to create project (${response.status})`
|
|
614
|
+
),
|
|
826
615
|
status: response.status,
|
|
827
616
|
payload
|
|
828
617
|
});
|
|
@@ -842,7 +631,7 @@ var VocoderAPI = class {
|
|
|
842
631
|
headers: { "Content-Type": "application/json" },
|
|
843
632
|
body: JSON.stringify({
|
|
844
633
|
repo: params.repoCanonical,
|
|
845
|
-
|
|
634
|
+
appDir: params.appDir
|
|
846
635
|
})
|
|
847
636
|
});
|
|
848
637
|
if (!response.ok) {
|
|
@@ -869,23 +658,73 @@ var VocoderAPI = class {
|
|
|
869
658
|
const payload = await readPayload(response);
|
|
870
659
|
if (!response.ok) {
|
|
871
660
|
throw new VocoderAPIError({
|
|
872
|
-
message: extractErrorMessage(
|
|
661
|
+
message: extractErrorMessage(
|
|
662
|
+
payload,
|
|
663
|
+
`Failed to create project app (${response.status})`
|
|
664
|
+
),
|
|
873
665
|
status: response.status,
|
|
874
666
|
payload
|
|
875
667
|
});
|
|
876
668
|
}
|
|
877
669
|
return payload;
|
|
878
670
|
}
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
// src/
|
|
882
|
-
import
|
|
883
|
-
import {
|
|
884
|
-
import {
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// src/utils/auth-store.ts
|
|
674
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
675
|
+
import { homedir } from "os";
|
|
676
|
+
import { dirname, join } from "path";
|
|
677
|
+
function getAuthFilePath() {
|
|
678
|
+
return join(homedir(), ".config", "vocoder", "auth.json");
|
|
679
|
+
}
|
|
680
|
+
function readAuthData() {
|
|
681
|
+
const filePath = getAuthFilePath();
|
|
682
|
+
try {
|
|
683
|
+
const raw = readFileSync(filePath, "utf8");
|
|
684
|
+
const parsed = JSON.parse(raw);
|
|
685
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
686
|
+
const data = parsed;
|
|
687
|
+
if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
token: data.token,
|
|
692
|
+
apiUrl: data.apiUrl,
|
|
693
|
+
userId: data.userId,
|
|
694
|
+
email: data.email,
|
|
695
|
+
name: typeof data.name === "string" ? data.name : null,
|
|
696
|
+
createdAt: data.createdAt
|
|
697
|
+
};
|
|
698
|
+
} catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function writeAuthData(data) {
|
|
703
|
+
const filePath = getAuthFilePath();
|
|
704
|
+
const dir = dirname(filePath);
|
|
705
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
706
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
707
|
+
}
|
|
708
|
+
function clearAuthData() {
|
|
709
|
+
const filePath = getAuthFilePath();
|
|
710
|
+
try {
|
|
711
|
+
unlinkSync(filePath);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
885
715
|
|
|
886
716
|
// src/utils/git-identity.ts
|
|
887
717
|
import { execSync } from "child_process";
|
|
888
718
|
import { relative, resolve } from "path";
|
|
719
|
+
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
720
|
+
function detectCommitSha() {
|
|
721
|
+
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
722
|
+
return process.env.VOCODER_COMMIT_SHA;
|
|
723
|
+
}
|
|
724
|
+
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;
|
|
725
|
+
if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
|
|
726
|
+
return safeExec("git rev-parse HEAD");
|
|
727
|
+
}
|
|
889
728
|
function safeExec(command) {
|
|
890
729
|
try {
|
|
891
730
|
const output = execSync(command, {
|
|
@@ -956,16 +795,19 @@ function resolveGitRepositoryIdentity() {
|
|
|
956
795
|
}
|
|
957
796
|
const repositoryRoot = safeExec("git rev-parse --show-toplevel");
|
|
958
797
|
const currentDirectory = process.cwd();
|
|
959
|
-
let
|
|
798
|
+
let repoAppDir = "";
|
|
960
799
|
if (repositoryRoot) {
|
|
961
|
-
const relativePath = relative(
|
|
800
|
+
const relativePath = relative(
|
|
801
|
+
resolve(repositoryRoot),
|
|
802
|
+
resolve(currentDirectory)
|
|
803
|
+
).replace(/\\/g, "/").trim();
|
|
962
804
|
if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
|
|
963
|
-
|
|
805
|
+
repoAppDir = relativePath;
|
|
964
806
|
}
|
|
965
807
|
}
|
|
966
808
|
return {
|
|
967
809
|
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
968
|
-
|
|
810
|
+
repoAppDir
|
|
969
811
|
};
|
|
970
812
|
}
|
|
971
813
|
function resolveGitContext() {
|
|
@@ -976,16 +818,296 @@ function resolveGitContext() {
|
|
|
976
818
|
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
977
819
|
);
|
|
978
820
|
}
|
|
979
|
-
return { identity, warnings };
|
|
821
|
+
return { identity, warnings };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/utils/github-connect.ts
|
|
825
|
+
import { spawn } from "child_process";
|
|
826
|
+
import * as p from "@clack/prompts";
|
|
827
|
+
import chalk from "chalk";
|
|
828
|
+
|
|
829
|
+
// src/utils/local-server.ts
|
|
830
|
+
import { createServer } from "http";
|
|
831
|
+
import { URL as URL2 } from "url";
|
|
832
|
+
function startCallbackServer() {
|
|
833
|
+
return new Promise((resolve2, reject) => {
|
|
834
|
+
let settled = false;
|
|
835
|
+
let callbackResolve = null;
|
|
836
|
+
let callbackReject = null;
|
|
837
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
838
|
+
callbackResolve = res;
|
|
839
|
+
callbackReject = rej;
|
|
840
|
+
});
|
|
841
|
+
const server = createServer((req, res) => {
|
|
842
|
+
if (!req.url) {
|
|
843
|
+
res.writeHead(400);
|
|
844
|
+
res.end();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
let pathname;
|
|
848
|
+
let params;
|
|
849
|
+
try {
|
|
850
|
+
const parsed = new URL2(req.url, "http://localhost");
|
|
851
|
+
pathname = parsed.pathname;
|
|
852
|
+
params = Object.fromEntries(parsed.searchParams.entries());
|
|
853
|
+
} catch {
|
|
854
|
+
res.writeHead(400);
|
|
855
|
+
res.end("Bad request");
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (pathname !== "/callback") {
|
|
859
|
+
res.writeHead(404);
|
|
860
|
+
res.end("Not found");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
864
|
+
res.end(
|
|
865
|
+
'<!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>'
|
|
866
|
+
);
|
|
867
|
+
if (callbackResolve) {
|
|
868
|
+
callbackResolve(params);
|
|
869
|
+
callbackResolve = null;
|
|
870
|
+
}
|
|
871
|
+
setImmediate(() => server.close());
|
|
872
|
+
});
|
|
873
|
+
server.on("error", (err) => {
|
|
874
|
+
if (!settled) {
|
|
875
|
+
settled = true;
|
|
876
|
+
if (callbackReject) callbackReject(err);
|
|
877
|
+
reject(err);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
server.listen(0, "127.0.0.1", () => {
|
|
881
|
+
if (settled) return;
|
|
882
|
+
settled = true;
|
|
883
|
+
const port = server.address().port;
|
|
884
|
+
resolve2({
|
|
885
|
+
port,
|
|
886
|
+
waitForCallback: () => callbackPromise,
|
|
887
|
+
close: () => server.close()
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/utils/github-connect.ts
|
|
894
|
+
async function tryOpenBrowser(url) {
|
|
895
|
+
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
const platform = process.platform;
|
|
899
|
+
let command;
|
|
900
|
+
let args;
|
|
901
|
+
if (platform === "darwin") {
|
|
902
|
+
command = "open";
|
|
903
|
+
args = [url];
|
|
904
|
+
} else if (platform === "win32") {
|
|
905
|
+
command = "rundll32";
|
|
906
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
907
|
+
} else {
|
|
908
|
+
command = "xdg-open";
|
|
909
|
+
args = [url];
|
|
910
|
+
}
|
|
911
|
+
return new Promise((resolve2) => {
|
|
912
|
+
try {
|
|
913
|
+
const child = spawn(command, args, {
|
|
914
|
+
detached: true,
|
|
915
|
+
stdio: "ignore",
|
|
916
|
+
windowsHide: true
|
|
917
|
+
});
|
|
918
|
+
let settled = false;
|
|
919
|
+
child.once("spawn", () => {
|
|
920
|
+
if (settled) return;
|
|
921
|
+
settled = true;
|
|
922
|
+
child.unref();
|
|
923
|
+
resolve2(true);
|
|
924
|
+
});
|
|
925
|
+
child.once("error", () => {
|
|
926
|
+
if (settled) return;
|
|
927
|
+
settled = true;
|
|
928
|
+
resolve2(false);
|
|
929
|
+
});
|
|
930
|
+
setTimeout(() => {
|
|
931
|
+
if (settled) return;
|
|
932
|
+
settled = true;
|
|
933
|
+
resolve2(false);
|
|
934
|
+
}, 300);
|
|
935
|
+
} catch {
|
|
936
|
+
resolve2(false);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
async function runGitHubInstallFlow(params) {
|
|
941
|
+
let server = null;
|
|
942
|
+
try {
|
|
943
|
+
server = await startCallbackServer();
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
const { installUrl } = await params.api.startCliGitHubInstall(
|
|
947
|
+
params.userToken,
|
|
948
|
+
{
|
|
949
|
+
organizationId: params.organizationId,
|
|
950
|
+
callbackPort: server?.port
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
p.log.info("Opening GitHub to install the Vocoder App...");
|
|
954
|
+
p.note(installUrl, "Install URL");
|
|
955
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
956
|
+
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
957
|
+
if (p.isCancel(shouldOpen)) {
|
|
958
|
+
server?.close();
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
if (shouldOpen) {
|
|
962
|
+
const opened = await tryOpenBrowser(installUrl);
|
|
963
|
+
if (!opened) {
|
|
964
|
+
p.log.info(
|
|
965
|
+
"Could not open a browser automatically. Use the URL above."
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const connectSpinner = p.spinner();
|
|
971
|
+
connectSpinner.start("Waiting for GitHub App installation...");
|
|
972
|
+
if (server) {
|
|
973
|
+
try {
|
|
974
|
+
const params_timeout = 15 * 60 * 1e3;
|
|
975
|
+
const callbackParams = await Promise.race([
|
|
976
|
+
server.waitForCallback(),
|
|
977
|
+
new Promise(
|
|
978
|
+
(resolve2) => setTimeout(() => resolve2(null), params_timeout)
|
|
979
|
+
)
|
|
980
|
+
]);
|
|
981
|
+
server.close();
|
|
982
|
+
if (!callbackParams) {
|
|
983
|
+
connectSpinner.stop("GitHub App installation timed out");
|
|
984
|
+
p.log.error(
|
|
985
|
+
"The installation flow timed out. Run `vocoder init` again."
|
|
986
|
+
);
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
if (callbackParams.error) {
|
|
990
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
991
|
+
p.log.error(callbackParams.error);
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
995
|
+
if (!organizationId || !connectionLabel) {
|
|
996
|
+
connectSpinner.stop("GitHub App installation incomplete");
|
|
997
|
+
p.log.error("Missing organization or connection data from callback.");
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
connectSpinner.stop(
|
|
1001
|
+
`Connected to GitHub as ${chalk.bold(connectionLabel)}`
|
|
1002
|
+
);
|
|
1003
|
+
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
1004
|
+
return {
|
|
1005
|
+
organizationId,
|
|
1006
|
+
organizationName: orgName,
|
|
1007
|
+
connectionLabel
|
|
1008
|
+
};
|
|
1009
|
+
} catch {
|
|
1010
|
+
server.close();
|
|
1011
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
1016
|
+
p.log.warn(
|
|
1017
|
+
"Complete the installation in your browser, then run `vocoder init` again."
|
|
1018
|
+
);
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
async function runGitHubDiscoveryFlow(params) {
|
|
1022
|
+
let server = null;
|
|
1023
|
+
try {
|
|
1024
|
+
server = await startCallbackServer();
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
|
|
1028
|
+
organizationId: params.organizationId,
|
|
1029
|
+
callbackPort: server?.port
|
|
1030
|
+
});
|
|
1031
|
+
p.log.info("Opening GitHub to authorize your account...");
|
|
1032
|
+
p.note("Complete authorization in your browser.");
|
|
1033
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1034
|
+
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
1035
|
+
if (p.isCancel(shouldOpen)) {
|
|
1036
|
+
server?.close();
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
if (shouldOpen) {
|
|
1040
|
+
const opened = await tryOpenBrowser(oauthUrl);
|
|
1041
|
+
if (!opened) {
|
|
1042
|
+
p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const oauthSpinner = p.spinner();
|
|
1047
|
+
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
1048
|
+
if (server) {
|
|
1049
|
+
try {
|
|
1050
|
+
const timeoutMs = 10 * 60 * 1e3;
|
|
1051
|
+
const callbackParams = await Promise.race([
|
|
1052
|
+
server.waitForCallback(),
|
|
1053
|
+
new Promise(
|
|
1054
|
+
(resolve2) => setTimeout(() => resolve2(null), timeoutMs)
|
|
1055
|
+
)
|
|
1056
|
+
]);
|
|
1057
|
+
server.close();
|
|
1058
|
+
if (!callbackParams) {
|
|
1059
|
+
oauthSpinner.stop("GitHub authorization timed out");
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
if (callbackParams.error) {
|
|
1063
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1064
|
+
p.log.error(callbackParams.error);
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
server.close();
|
|
1069
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
oauthSpinner.stop("GitHub account authorized");
|
|
1074
|
+
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
1075
|
+
params.userToken
|
|
1076
|
+
);
|
|
1077
|
+
return discoveryResult.installations;
|
|
1078
|
+
}
|
|
1079
|
+
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
1080
|
+
const options = installations.map((inst) => ({
|
|
1081
|
+
value: String(inst.installationId),
|
|
1082
|
+
label: inst.accountLogin,
|
|
1083
|
+
hint: [
|
|
1084
|
+
inst.accountType === "Organization" ? "organization" : "personal",
|
|
1085
|
+
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
1086
|
+
inst.isSuspended ? "suspended" : ""
|
|
1087
|
+
].filter(Boolean).join(" \xB7 ") || void 0
|
|
1088
|
+
}));
|
|
1089
|
+
if (canInstallNew) {
|
|
1090
|
+
options.push({
|
|
1091
|
+
value: "install_new",
|
|
1092
|
+
label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const selected = await p.select({
|
|
1096
|
+
message: "Select a GitHub installation",
|
|
1097
|
+
options
|
|
1098
|
+
});
|
|
1099
|
+
if (p.isCancel(selected)) return null;
|
|
1100
|
+
if (selected === "install_new") return "install_new";
|
|
1101
|
+
return Number(selected);
|
|
980
1102
|
}
|
|
981
1103
|
|
|
982
1104
|
// src/utils/project-create.ts
|
|
983
1105
|
import * as p3 from "@clack/prompts";
|
|
984
1106
|
import chalk4 from "chalk";
|
|
985
1107
|
|
|
986
|
-
// src/utils/
|
|
987
|
-
import {
|
|
988
|
-
import
|
|
1108
|
+
// src/utils/branch-select.ts
|
|
1109
|
+
import { execSync as execSync2 } from "child_process";
|
|
1110
|
+
import { isCancel as isCancel2, Prompt } from "@clack/core";
|
|
989
1111
|
import chalk2 from "chalk";
|
|
990
1112
|
var S_BAR = "\u2502";
|
|
991
1113
|
var S_BAR_END = "\u2514";
|
|
@@ -1012,57 +1134,134 @@ function symbol(state) {
|
|
|
1012
1134
|
return cyan(S_ACTIVE);
|
|
1013
1135
|
}
|
|
1014
1136
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1137
|
+
function detectGitBranches(cwd) {
|
|
1138
|
+
const workDir = cwd ?? process.cwd();
|
|
1139
|
+
try {
|
|
1140
|
+
const localOut = execSync2("git branch", {
|
|
1141
|
+
cwd: workDir,
|
|
1142
|
+
stdio: "pipe"
|
|
1143
|
+
}).toString();
|
|
1144
|
+
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
1145
|
+
let remoteBranches = [];
|
|
1146
|
+
try {
|
|
1147
|
+
const remoteOut = execSync2("git branch -r", {
|
|
1148
|
+
cwd: workDir,
|
|
1149
|
+
stdio: "pipe"
|
|
1150
|
+
}).toString();
|
|
1151
|
+
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
1155
|
+
let defaultBranch = "main";
|
|
1156
|
+
try {
|
|
1157
|
+
const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
1158
|
+
cwd: workDir,
|
|
1159
|
+
stdio: "pipe"
|
|
1160
|
+
}).toString().trim();
|
|
1161
|
+
defaultBranch = ref.split("/").pop() ?? "main";
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
1166
|
+
defaultBranch
|
|
1167
|
+
};
|
|
1168
|
+
} catch {
|
|
1169
|
+
return { branches: ["main"], defaultBranch: "main" };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
var INVALID_CHARS = /[\s?^~:[\]\\]/;
|
|
1173
|
+
function validateBranchPattern(pattern) {
|
|
1174
|
+
const t = pattern.trim();
|
|
1175
|
+
if (!t) return "Pattern cannot be empty";
|
|
1176
|
+
if (INVALID_CHARS.test(t))
|
|
1177
|
+
return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
1178
|
+
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
1179
|
+
if (t.includes("//")) return "Cannot contain //";
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
var MAX_VISIBLE = 10;
|
|
1183
|
+
function buildItems(branches, defaultBranch, customPatterns) {
|
|
1184
|
+
const items = branches.map((b) => ({
|
|
1185
|
+
value: b,
|
|
1186
|
+
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
1187
|
+
}));
|
|
1188
|
+
for (const pt of customPatterns) {
|
|
1189
|
+
if (!branches.includes(pt)) {
|
|
1190
|
+
items.push({ value: pt, label: pt, isCustom: true });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return items;
|
|
1194
|
+
}
|
|
1195
|
+
function filterItems(items, query) {
|
|
1196
|
+
if (!query.trim()) return items;
|
|
1018
1197
|
const lower = query.toLowerCase();
|
|
1019
|
-
return
|
|
1020
|
-
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1021
|
-
);
|
|
1198
|
+
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
1022
1199
|
}
|
|
1023
|
-
function buildList(filtered, cursor, scrollOffset, selected) {
|
|
1024
|
-
const
|
|
1200
|
+
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
1201
|
+
const lines = [];
|
|
1025
1202
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
1026
|
-
const visibleLines = [];
|
|
1027
1203
|
for (let i = scrollOffset; i < end; i++) {
|
|
1028
|
-
const
|
|
1029
|
-
const isCursor = i === cursor;
|
|
1030
|
-
const isChecked =
|
|
1031
|
-
const icon =
|
|
1032
|
-
|
|
1204
|
+
const item = filtered[i];
|
|
1205
|
+
const isCursor = i === cursor && !addCursor;
|
|
1206
|
+
const isChecked = selected.has(item.value);
|
|
1207
|
+
const icon = isChecked ? isCursor ? grn("\u25FC") : "\u25FC" : isCursor ? grn("\u25FB") : dim("\u25FB");
|
|
1208
|
+
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
1209
|
+
if (isCursor) label = bld(label);
|
|
1210
|
+
lines.push(`${cyan(S_BAR)} ${icon} ${label}`);
|
|
1211
|
+
}
|
|
1212
|
+
const trimmed = filter.trim();
|
|
1213
|
+
const allItems = [...filtered];
|
|
1214
|
+
const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
1215
|
+
if (isNewPattern) {
|
|
1216
|
+
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
1217
|
+
const icon = addCursor ? grn("\u25FB") : dim("\u25FB");
|
|
1218
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
1219
|
+
lines.push(`${cyan(S_BAR)} ${icon} ${label}`);
|
|
1220
|
+
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
1221
|
+
lines.push(dim(`${S_BAR} No branches detected`));
|
|
1033
1222
|
}
|
|
1034
1223
|
const hidden = filtered.length - (end - scrollOffset);
|
|
1035
|
-
if (hidden > 0)
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
1224
|
+
if (hidden > 0) lines.push(dim(`${S_BAR} ${hidden} more`));
|
|
1225
|
+
if (selected.size > 0) {
|
|
1226
|
+
lines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
|
|
1227
|
+
} else if (optional) {
|
|
1228
|
+
lines.push(dim(`${S_BAR} Enter to skip`));
|
|
1039
1229
|
}
|
|
1040
|
-
return
|
|
1230
|
+
return lines.join("\n");
|
|
1041
1231
|
}
|
|
1042
|
-
async function
|
|
1043
|
-
const { message,
|
|
1232
|
+
async function filterableBranchSelect(params) {
|
|
1233
|
+
const { message, branches, defaultBranch } = params;
|
|
1234
|
+
const optional = params.optional ?? false;
|
|
1235
|
+
const excludedSet = new Set(params.excludedPatterns ?? []);
|
|
1044
1236
|
let filter = "";
|
|
1045
1237
|
let cursor = 0;
|
|
1046
1238
|
let scrollOffset = 0;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1239
|
+
let addCursor = false;
|
|
1240
|
+
const customPatterns = [];
|
|
1241
|
+
const selected = new Set(params.initialValues ?? [defaultBranch]);
|
|
1242
|
+
const getItems = () => buildItems(branches, defaultBranch, customPatterns);
|
|
1243
|
+
const getFiltered = () => filterItems(getItems(), filter);
|
|
1244
|
+
const isNewPattern = () => {
|
|
1245
|
+
const t = filter.trim();
|
|
1246
|
+
if (!t) return false;
|
|
1247
|
+
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
1248
|
+
};
|
|
1053
1249
|
const clampCursor = (filtered) => {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (cursor
|
|
1057
|
-
if (
|
|
1250
|
+
const hasAdd = isNewPattern();
|
|
1251
|
+
const max = filtered.length - 1 + (hasAdd ? 1 : 0);
|
|
1252
|
+
if (cursor > max && !addCursor) cursor = Math.max(0, max);
|
|
1253
|
+
if (!addCursor) {
|
|
1254
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
1255
|
+
if (cursor >= scrollOffset + MAX_VISIBLE)
|
|
1256
|
+
scrollOffset = cursor - MAX_VISIBLE + 1;
|
|
1257
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
1258
|
+
}
|
|
1058
1259
|
};
|
|
1059
1260
|
const prompt = new Prompt(
|
|
1060
1261
|
{
|
|
1061
|
-
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
1062
1262
|
validate() {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (!multi && !f[cursor]) return "Please select a language.";
|
|
1263
|
+
if (!optional && selected.size === 0)
|
|
1264
|
+
return "At least one branch is required.";
|
|
1066
1265
|
return void 0;
|
|
1067
1266
|
},
|
|
1068
1267
|
render() {
|
|
@@ -1071,11 +1270,11 @@ async function runFilterablePrompt(opts) {
|
|
|
1071
1270
|
const hdr = `${dim(S_BAR)}
|
|
1072
1271
|
${symbol(this.state)} ${message}
|
|
1073
1272
|
`;
|
|
1074
|
-
const hint = filter.length > 0 ? filter : dim("type to filter, \u2191\u2193 navigate
|
|
1273
|
+
const hint = filter.length > 0 ? filter : dim("type to filter or add pattern, \u2191\u2193 navigate, space select");
|
|
1075
1274
|
switch (this.state) {
|
|
1076
1275
|
case "submit": {
|
|
1077
|
-
const
|
|
1078
|
-
return `${hdr}${dim(S_BAR)} ${
|
|
1276
|
+
const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
|
|
1277
|
+
return `${hdr}${dim(S_BAR)} ${summary}`;
|
|
1079
1278
|
}
|
|
1080
1279
|
case "cancel":
|
|
1081
1280
|
return `${hdr}${dim(S_BAR)}`;
|
|
@@ -1083,7 +1282,17 @@ ${symbol(this.state)} ${message}
|
|
|
1083
1282
|
return [
|
|
1084
1283
|
hdr.trimEnd(),
|
|
1085
1284
|
`${ylw(S_BAR)} ${dim("/")} ${hint}`,
|
|
1086
|
-
buildList(
|
|
1285
|
+
buildList(
|
|
1286
|
+
filtered,
|
|
1287
|
+
cursor,
|
|
1288
|
+
scrollOffset,
|
|
1289
|
+
selected,
|
|
1290
|
+
filter,
|
|
1291
|
+
customPatterns,
|
|
1292
|
+
addCursor,
|
|
1293
|
+
optional,
|
|
1294
|
+
excludedSet
|
|
1295
|
+
),
|
|
1087
1296
|
`${ylw(S_BAR_END)} ${ylw(this.error)}`,
|
|
1088
1297
|
""
|
|
1089
1298
|
].join("\n");
|
|
@@ -1091,7 +1300,17 @@ ${symbol(this.state)} ${message}
|
|
|
1091
1300
|
return [
|
|
1092
1301
|
hdr.trimEnd(),
|
|
1093
1302
|
`${cyan(S_BAR)} ${dim("/")} ${hint}`,
|
|
1094
|
-
buildList(
|
|
1303
|
+
buildList(
|
|
1304
|
+
filtered,
|
|
1305
|
+
cursor,
|
|
1306
|
+
scrollOffset,
|
|
1307
|
+
selected,
|
|
1308
|
+
filter,
|
|
1309
|
+
customPatterns,
|
|
1310
|
+
addCursor,
|
|
1311
|
+
optional,
|
|
1312
|
+
excludedSet
|
|
1313
|
+
),
|
|
1095
1314
|
`${cyan(S_BAR_END)}`,
|
|
1096
1315
|
""
|
|
1097
1316
|
].join("\n");
|
|
@@ -1099,7 +1318,6 @@ ${symbol(this.state)} ${message}
|
|
|
1099
1318
|
}
|
|
1100
1319
|
},
|
|
1101
1320
|
false
|
|
1102
|
-
// trackValue=false — we manage value manually
|
|
1103
1321
|
);
|
|
1104
1322
|
prompt.on("key", (key) => {
|
|
1105
1323
|
if (!key || key === " ") return;
|
|
@@ -1108,209 +1326,148 @@ ${symbol(this.state)} ${message}
|
|
|
1108
1326
|
filter = filter.slice(0, -1);
|
|
1109
1327
|
cursor = 0;
|
|
1110
1328
|
scrollOffset = 0;
|
|
1329
|
+
addCursor = false;
|
|
1111
1330
|
} else if (cp >= 32 && cp !== 127) {
|
|
1112
1331
|
filter += key;
|
|
1113
1332
|
cursor = 0;
|
|
1114
1333
|
scrollOffset = 0;
|
|
1334
|
+
addCursor = false;
|
|
1115
1335
|
}
|
|
1116
1336
|
});
|
|
1117
1337
|
prompt.on("cursor", (action) => {
|
|
1118
1338
|
const filtered = getFiltered();
|
|
1339
|
+
const hasAdd = isNewPattern();
|
|
1119
1340
|
switch (action) {
|
|
1120
1341
|
case "up":
|
|
1121
|
-
|
|
1342
|
+
if (addCursor) {
|
|
1343
|
+
addCursor = false;
|
|
1344
|
+
cursor = Math.max(0, filtered.length - 1);
|
|
1345
|
+
} else cursor = Math.max(0, cursor - 1);
|
|
1122
1346
|
break;
|
|
1123
1347
|
case "down":
|
|
1124
|
-
cursor
|
|
1348
|
+
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
1349
|
+
addCursor = true;
|
|
1350
|
+
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1125
1351
|
break;
|
|
1126
1352
|
case "space":
|
|
1127
|
-
if (
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1353
|
+
if (addCursor) {
|
|
1354
|
+
const t = filter.trim();
|
|
1355
|
+
const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
|
|
1356
|
+
if (!err) {
|
|
1357
|
+
customPatterns.push(t);
|
|
1358
|
+
selected.add(t);
|
|
1359
|
+
filter = "";
|
|
1360
|
+
cursor = 0;
|
|
1361
|
+
scrollOffset = 0;
|
|
1362
|
+
addCursor = false;
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
const item = filtered[cursor];
|
|
1366
|
+
if (item) {
|
|
1367
|
+
if (selected.has(item.value)) selected.delete(item.value);
|
|
1368
|
+
else selected.add(item.value);
|
|
1132
1369
|
}
|
|
1133
1370
|
}
|
|
1134
1371
|
break;
|
|
1135
1372
|
}
|
|
1136
|
-
if (!multi) {
|
|
1137
|
-
const opt = getFiltered()[cursor];
|
|
1138
|
-
prompt.value = opt?.bcp47 ?? null;
|
|
1139
|
-
}
|
|
1140
1373
|
});
|
|
1141
1374
|
prompt.on("finalize", () => {
|
|
1142
1375
|
if (prompt.state === "submit") {
|
|
1143
|
-
|
|
1144
|
-
prompt.value = Array.from(selected);
|
|
1145
|
-
} else {
|
|
1146
|
-
const f = getFiltered();
|
|
1147
|
-
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
1148
|
-
}
|
|
1376
|
+
prompt.value = Array.from(selected);
|
|
1149
1377
|
}
|
|
1150
1378
|
});
|
|
1151
1379
|
const result = await prompt.prompt();
|
|
1152
1380
|
if (isCancel2(result)) return null;
|
|
1153
1381
|
return result;
|
|
1154
1382
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
var grn2 = (s) => noColor2 ? s : chalk3.green(s);
|
|
1184
|
-
var ylw2 = (s) => noColor2 ? s : chalk3.yellow(s);
|
|
1185
|
-
var red2 = (s) => noColor2 ? s : chalk3.red(s);
|
|
1186
|
-
var bld2 = (s) => noColor2 ? s : chalk3.bold(s);
|
|
1187
|
-
function symbol2(state) {
|
|
1188
|
-
switch (state) {
|
|
1189
|
-
case "submit":
|
|
1190
|
-
return grn2(S_SUBMIT2);
|
|
1191
|
-
case "cancel":
|
|
1192
|
-
return red2(S_CANCEL2);
|
|
1193
|
-
case "error":
|
|
1194
|
-
return ylw2(S_ERROR2);
|
|
1195
|
-
default:
|
|
1196
|
-
return cyan2(S_ACTIVE2);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function detectGitBranches(cwd) {
|
|
1200
|
-
const workDir = cwd ?? process.cwd();
|
|
1201
|
-
try {
|
|
1202
|
-
const localOut = execSync2("git branch", { cwd: workDir, stdio: "pipe" }).toString();
|
|
1203
|
-
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
1204
|
-
let remoteBranches = [];
|
|
1205
|
-
try {
|
|
1206
|
-
const remoteOut = execSync2("git branch -r", { cwd: workDir, stdio: "pipe" }).toString();
|
|
1207
|
-
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
1208
|
-
} catch {
|
|
1209
|
-
}
|
|
1210
|
-
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
1211
|
-
let defaultBranch = "main";
|
|
1212
|
-
try {
|
|
1213
|
-
const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", { cwd: workDir, stdio: "pipe" }).toString().trim();
|
|
1214
|
-
defaultBranch = ref.split("/").pop() ?? "main";
|
|
1215
|
-
} catch {
|
|
1216
|
-
}
|
|
1217
|
-
return {
|
|
1218
|
-
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
1219
|
-
defaultBranch
|
|
1220
|
-
};
|
|
1221
|
-
} catch {
|
|
1222
|
-
return { branches: ["main"], defaultBranch: "main" };
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
var INVALID_CHARS = /[\s?^~:[\]\\]/;
|
|
1226
|
-
function validateBranchPattern(pattern) {
|
|
1227
|
-
const t = pattern.trim();
|
|
1228
|
-
if (!t) return "Pattern cannot be empty";
|
|
1229
|
-
if (INVALID_CHARS.test(t)) return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
1230
|
-
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
1231
|
-
if (t.includes("//")) return "Cannot contain //";
|
|
1232
|
-
return null;
|
|
1233
|
-
}
|
|
1234
|
-
var MAX_VISIBLE2 = 10;
|
|
1235
|
-
function buildItems(branches, defaultBranch, customPatterns) {
|
|
1236
|
-
const items = branches.map((b) => ({
|
|
1237
|
-
value: b,
|
|
1238
|
-
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
1239
|
-
}));
|
|
1240
|
-
for (const pt of customPatterns) {
|
|
1241
|
-
if (!branches.includes(pt)) {
|
|
1242
|
-
items.push({ value: pt, label: pt, isCustom: true });
|
|
1243
|
-
}
|
|
1383
|
+
|
|
1384
|
+
// src/utils/locale-search.ts
|
|
1385
|
+
import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
|
|
1386
|
+
import * as p2 from "@clack/prompts";
|
|
1387
|
+
import chalk3 from "chalk";
|
|
1388
|
+
var S_BAR2 = "\u2502";
|
|
1389
|
+
var S_BAR_END2 = "\u2514";
|
|
1390
|
+
var S_ACTIVE2 = "\u25C6";
|
|
1391
|
+
var S_SUBMIT2 = "\u25C6";
|
|
1392
|
+
var S_CANCEL2 = "\u25A0";
|
|
1393
|
+
var S_ERROR2 = "\u25B2";
|
|
1394
|
+
var noColor2 = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
|
|
1395
|
+
var dim2 = (s) => noColor2 ? s : chalk3.gray(s);
|
|
1396
|
+
var cyan2 = (s) => noColor2 ? s : chalk3.cyan(s);
|
|
1397
|
+
var grn2 = (s) => noColor2 ? s : chalk3.green(s);
|
|
1398
|
+
var ylw2 = (s) => noColor2 ? s : chalk3.yellow(s);
|
|
1399
|
+
var red2 = (s) => noColor2 ? s : chalk3.red(s);
|
|
1400
|
+
var bld2 = (s) => noColor2 ? s : chalk3.bold(s);
|
|
1401
|
+
function symbol2(state) {
|
|
1402
|
+
switch (state) {
|
|
1403
|
+
case "submit":
|
|
1404
|
+
return grn2(S_SUBMIT2);
|
|
1405
|
+
case "cancel":
|
|
1406
|
+
return red2(S_CANCEL2);
|
|
1407
|
+
case "error":
|
|
1408
|
+
return ylw2(S_ERROR2);
|
|
1409
|
+
default:
|
|
1410
|
+
return cyan2(S_ACTIVE2);
|
|
1244
1411
|
}
|
|
1245
|
-
return items;
|
|
1246
1412
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1413
|
+
var MAX_VISIBLE2 = 12;
|
|
1414
|
+
function filterLocales(options, query) {
|
|
1415
|
+
if (!query.trim()) return options;
|
|
1249
1416
|
const lower = query.toLowerCase();
|
|
1250
|
-
return
|
|
1417
|
+
return options.filter(
|
|
1418
|
+
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1419
|
+
);
|
|
1251
1420
|
}
|
|
1252
|
-
function buildList2(filtered, cursor, scrollOffset, selected
|
|
1253
|
-
const
|
|
1421
|
+
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
1422
|
+
const isMulti = selected !== null;
|
|
1254
1423
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
1424
|
+
const visibleLines = [];
|
|
1255
1425
|
for (let i = scrollOffset; i < end; i++) {
|
|
1256
|
-
const
|
|
1257
|
-
const isCursor = i === cursor
|
|
1258
|
-
const isChecked = selected.has(
|
|
1259
|
-
const icon = isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB");
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
}
|
|
1264
|
-
const trimmed = filter.trim();
|
|
1265
|
-
const allItems = [...filtered];
|
|
1266
|
-
const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
1267
|
-
if (isNewPattern) {
|
|
1268
|
-
const err = validateBranchPattern(trimmed);
|
|
1269
|
-
const icon = addCursor ? grn2("\u25FB") : dim2("\u25FB");
|
|
1270
|
-
const label = err ? `${ylw2("+")} ${dim2(`"${trimmed}" \u2014 ${err}`)}` : `${grn2("+")} Add "${trimmed}" as branch pattern`;
|
|
1271
|
-
lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
|
|
1272
|
-
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
1273
|
-
lines.push(dim2(`${S_BAR2} No branches detected`));
|
|
1426
|
+
const opt = filtered[i];
|
|
1427
|
+
const isCursor = i === cursor;
|
|
1428
|
+
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
1429
|
+
const icon = isMulti ? isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB") : isCursor ? grn2("\u25CF") : dim2("\u25CB");
|
|
1430
|
+
visibleLines.push(
|
|
1431
|
+
`${cyan2(S_BAR2)} ${icon} ${isCursor ? bld2(opt.label) : opt.label}`
|
|
1432
|
+
);
|
|
1274
1433
|
}
|
|
1275
1434
|
const hidden = filtered.length - (end - scrollOffset);
|
|
1276
|
-
if (hidden > 0)
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1435
|
+
if (hidden > 0)
|
|
1436
|
+
visibleLines.push(dim2(`${S_BAR2} ${hidden} more \u2014 keep typing to narrow`));
|
|
1437
|
+
if (filtered.length === 0) visibleLines.push(dim2(`${S_BAR2} No matches`));
|
|
1438
|
+
if (isMulti && selected.size > 0) {
|
|
1439
|
+
visibleLines.push(
|
|
1440
|
+
dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`)
|
|
1441
|
+
);
|
|
1281
1442
|
}
|
|
1282
|
-
return
|
|
1443
|
+
return visibleLines.join("\n");
|
|
1283
1444
|
}
|
|
1284
|
-
async function
|
|
1285
|
-
const { message,
|
|
1286
|
-
const optional = params.optional ?? false;
|
|
1445
|
+
async function runFilterablePrompt(opts) {
|
|
1446
|
+
const { message, options, multi } = opts;
|
|
1287
1447
|
let filter = "";
|
|
1288
1448
|
let cursor = 0;
|
|
1289
1449
|
let scrollOffset = 0;
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
const t = filter.trim();
|
|
1297
|
-
if (!t) return false;
|
|
1298
|
-
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
1299
|
-
};
|
|
1450
|
+
const selected = new Set(multi ? opts.initialValues ?? [] : []);
|
|
1451
|
+
if (!multi && opts.initialValue) {
|
|
1452
|
+
const idx = options.findIndex((o) => o.bcp47 === opts.initialValue);
|
|
1453
|
+
if (idx >= 0) cursor = idx;
|
|
1454
|
+
}
|
|
1455
|
+
const getFiltered = () => filterLocales(options, filter);
|
|
1300
1456
|
const clampCursor = (filtered) => {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
if (cursor
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
if (cursor >= scrollOffset + MAX_VISIBLE2) scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
1307
|
-
if (scrollOffset < 0) scrollOffset = 0;
|
|
1308
|
-
}
|
|
1457
|
+
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1);
|
|
1458
|
+
if (cursor < scrollOffset) scrollOffset = cursor;
|
|
1459
|
+
if (cursor >= scrollOffset + MAX_VISIBLE2)
|
|
1460
|
+
scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
1461
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
1309
1462
|
};
|
|
1310
1463
|
const prompt = new Prompt2(
|
|
1311
1464
|
{
|
|
1465
|
+
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
1312
1466
|
validate() {
|
|
1313
|
-
|
|
1467
|
+
const f = getFiltered();
|
|
1468
|
+
if (multi && selected.size === 0)
|
|
1469
|
+
return "At least one target language is required.";
|
|
1470
|
+
if (!multi && !f[cursor]) return "Please select a language.";
|
|
1314
1471
|
return void 0;
|
|
1315
1472
|
},
|
|
1316
1473
|
render() {
|
|
@@ -1319,11 +1476,13 @@ async function filterableBranchSelect(params) {
|
|
|
1319
1476
|
const hdr = `${dim2(S_BAR2)}
|
|
1320
1477
|
${symbol2(this.state)} ${message}
|
|
1321
1478
|
`;
|
|
1322
|
-
const hint = filter.length > 0 ? filter : dim2(
|
|
1479
|
+
const hint = filter.length > 0 ? filter : dim2(
|
|
1480
|
+
`type to filter, \u2191\u2193 navigate${multi ? ", space select" : ""}`
|
|
1481
|
+
);
|
|
1323
1482
|
switch (this.state) {
|
|
1324
1483
|
case "submit": {
|
|
1325
|
-
const
|
|
1326
|
-
return `${hdr}${dim2(S_BAR2)} ${
|
|
1484
|
+
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 ?? "";
|
|
1485
|
+
return `${hdr}${dim2(S_BAR2)} ${bld2(val || dim2("none"))}`;
|
|
1327
1486
|
}
|
|
1328
1487
|
case "cancel":
|
|
1329
1488
|
return `${hdr}${dim2(S_BAR2)}`;
|
|
@@ -1331,7 +1490,12 @@ ${symbol2(this.state)} ${message}
|
|
|
1331
1490
|
return [
|
|
1332
1491
|
hdr.trimEnd(),
|
|
1333
1492
|
`${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
|
|
1334
|
-
buildList2(
|
|
1493
|
+
buildList2(
|
|
1494
|
+
filtered,
|
|
1495
|
+
cursor,
|
|
1496
|
+
scrollOffset,
|
|
1497
|
+
multi ? selected : null
|
|
1498
|
+
),
|
|
1335
1499
|
`${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
|
|
1336
1500
|
""
|
|
1337
1501
|
].join("\n");
|
|
@@ -1339,7 +1503,12 @@ ${symbol2(this.state)} ${message}
|
|
|
1339
1503
|
return [
|
|
1340
1504
|
hdr.trimEnd(),
|
|
1341
1505
|
`${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
|
|
1342
|
-
buildList2(
|
|
1506
|
+
buildList2(
|
|
1507
|
+
filtered,
|
|
1508
|
+
cursor,
|
|
1509
|
+
scrollOffset,
|
|
1510
|
+
multi ? selected : null
|
|
1511
|
+
),
|
|
1343
1512
|
`${cyan2(S_BAR_END2)}`,
|
|
1344
1513
|
""
|
|
1345
1514
|
].join("\n");
|
|
@@ -1347,6 +1516,7 @@ ${symbol2(this.state)} ${message}
|
|
|
1347
1516
|
}
|
|
1348
1517
|
},
|
|
1349
1518
|
false
|
|
1519
|
+
// trackValue=false — we manage value manually
|
|
1350
1520
|
);
|
|
1351
1521
|
prompt.on("key", (key) => {
|
|
1352
1522
|
if (!key || key === " ") return;
|
|
@@ -1355,59 +1525,76 @@ ${symbol2(this.state)} ${message}
|
|
|
1355
1525
|
filter = filter.slice(0, -1);
|
|
1356
1526
|
cursor = 0;
|
|
1357
1527
|
scrollOffset = 0;
|
|
1358
|
-
addCursor = false;
|
|
1359
1528
|
} else if (cp >= 32 && cp !== 127) {
|
|
1360
1529
|
filter += key;
|
|
1361
1530
|
cursor = 0;
|
|
1362
1531
|
scrollOffset = 0;
|
|
1363
|
-
addCursor = false;
|
|
1364
1532
|
}
|
|
1365
1533
|
});
|
|
1366
1534
|
prompt.on("cursor", (action) => {
|
|
1367
1535
|
const filtered = getFiltered();
|
|
1368
|
-
const hasAdd = isNewPattern();
|
|
1369
1536
|
switch (action) {
|
|
1370
1537
|
case "up":
|
|
1371
|
-
|
|
1372
|
-
addCursor = false;
|
|
1373
|
-
cursor = Math.max(0, filtered.length - 1);
|
|
1374
|
-
} else cursor = Math.max(0, cursor - 1);
|
|
1538
|
+
cursor = Math.max(0, cursor - 1);
|
|
1375
1539
|
break;
|
|
1376
1540
|
case "down":
|
|
1377
|
-
|
|
1378
|
-
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1541
|
+
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
1379
1542
|
break;
|
|
1380
1543
|
case "space":
|
|
1381
|
-
if (
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
selected.add(t);
|
|
1387
|
-
filter = "";
|
|
1388
|
-
cursor = 0;
|
|
1389
|
-
scrollOffset = 0;
|
|
1390
|
-
addCursor = false;
|
|
1391
|
-
}
|
|
1392
|
-
} else {
|
|
1393
|
-
const item = filtered[cursor];
|
|
1394
|
-
if (item) {
|
|
1395
|
-
if (selected.has(item.value)) selected.delete(item.value);
|
|
1396
|
-
else selected.add(item.value);
|
|
1544
|
+
if (multi) {
|
|
1545
|
+
const opt = filtered[cursor];
|
|
1546
|
+
if (opt) {
|
|
1547
|
+
if (selected.has(opt.bcp47)) selected.delete(opt.bcp47);
|
|
1548
|
+
else selected.add(opt.bcp47);
|
|
1397
1549
|
}
|
|
1398
1550
|
}
|
|
1399
1551
|
break;
|
|
1400
1552
|
}
|
|
1553
|
+
if (!multi) {
|
|
1554
|
+
const opt = getFiltered()[cursor];
|
|
1555
|
+
prompt.value = opt?.bcp47 ?? null;
|
|
1556
|
+
}
|
|
1401
1557
|
});
|
|
1402
1558
|
prompt.on("finalize", () => {
|
|
1403
1559
|
if (prompt.state === "submit") {
|
|
1404
|
-
|
|
1560
|
+
if (multi) {
|
|
1561
|
+
prompt.value = Array.from(selected);
|
|
1562
|
+
} else {
|
|
1563
|
+
const f = getFiltered();
|
|
1564
|
+
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
1565
|
+
}
|
|
1405
1566
|
}
|
|
1406
1567
|
});
|
|
1407
1568
|
const result = await prompt.prompt();
|
|
1408
1569
|
if (isCancel3(result)) return null;
|
|
1409
1570
|
return result;
|
|
1410
1571
|
}
|
|
1572
|
+
async function searchSelectLocale(options, message, initialValue) {
|
|
1573
|
+
const result = await runFilterablePrompt({
|
|
1574
|
+
message,
|
|
1575
|
+
options,
|
|
1576
|
+
multi: false,
|
|
1577
|
+
initialValue
|
|
1578
|
+
});
|
|
1579
|
+
return typeof result === "string" ? result : null;
|
|
1580
|
+
}
|
|
1581
|
+
async function searchMultiSelectLocales(options, message, initialValues) {
|
|
1582
|
+
const result = await runFilterablePrompt({
|
|
1583
|
+
message,
|
|
1584
|
+
options,
|
|
1585
|
+
multi: true,
|
|
1586
|
+
initialValues
|
|
1587
|
+
});
|
|
1588
|
+
if (result === null) return null;
|
|
1589
|
+
const picks = result;
|
|
1590
|
+
if (picks.length === 0) {
|
|
1591
|
+
p2.log.warn(
|
|
1592
|
+
"At least one target language is required. Please select at least one."
|
|
1593
|
+
);
|
|
1594
|
+
return searchMultiSelectLocales(options, message, initialValues);
|
|
1595
|
+
}
|
|
1596
|
+
return picks;
|
|
1597
|
+
}
|
|
1411
1598
|
|
|
1412
1599
|
// src/utils/project-create.ts
|
|
1413
1600
|
function buildLocaleOptions(locales) {
|
|
@@ -1436,15 +1623,17 @@ async function runProjectCreate(params) {
|
|
|
1436
1623
|
try {
|
|
1437
1624
|
rawLocales = await api.listLocales(userToken);
|
|
1438
1625
|
} catch {
|
|
1439
|
-
p3.log.error(
|
|
1626
|
+
p3.log.error(
|
|
1627
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1628
|
+
);
|
|
1440
1629
|
return null;
|
|
1441
1630
|
}
|
|
1442
1631
|
const languageOptions = buildLanguageOptions(rawLocales);
|
|
1443
1632
|
const localeOptions = buildLocaleOptions(rawLocales);
|
|
1444
|
-
let
|
|
1445
|
-
if (params.
|
|
1446
|
-
|
|
1447
|
-
p3.log.success(`App directory: ${chalk4.bold(
|
|
1633
|
+
let appDir;
|
|
1634
|
+
if (params.defaultAppDir) {
|
|
1635
|
+
appDir = params.defaultAppDir;
|
|
1636
|
+
p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
|
|
1448
1637
|
} else {
|
|
1449
1638
|
const rawScope = await p3.text({
|
|
1450
1639
|
message: "App directory (leave blank for the entire repo)",
|
|
@@ -1453,12 +1642,13 @@ async function runProjectCreate(params) {
|
|
|
1453
1642
|
validate(value) {
|
|
1454
1643
|
const v = value.trim();
|
|
1455
1644
|
if (!v) return;
|
|
1456
|
-
if (v.startsWith("/"))
|
|
1645
|
+
if (v.startsWith("/"))
|
|
1646
|
+
return "Use a relative path, not an absolute path";
|
|
1457
1647
|
if (v.includes("..")) return 'Path must not contain ".."';
|
|
1458
1648
|
}
|
|
1459
1649
|
});
|
|
1460
1650
|
if (p3.isCancel(rawScope)) return null;
|
|
1461
|
-
|
|
1651
|
+
appDir = (rawScope ?? "").trim();
|
|
1462
1652
|
}
|
|
1463
1653
|
const sourceLocale = await searchSelectLocale(
|
|
1464
1654
|
languageOptions,
|
|
@@ -1466,14 +1656,18 @@ async function runProjectCreate(params) {
|
|
|
1466
1656
|
params.defaultSourceLocale ?? "en"
|
|
1467
1657
|
);
|
|
1468
1658
|
if (sourceLocale === null) return null;
|
|
1469
|
-
const targetOptions = localeOptions.filter(
|
|
1659
|
+
const targetOptions = localeOptions.filter(
|
|
1660
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1661
|
+
);
|
|
1470
1662
|
const targetLocales = await searchMultiSelectLocales(
|
|
1471
1663
|
targetOptions,
|
|
1472
1664
|
"Target languages (languages to translate into)"
|
|
1473
1665
|
);
|
|
1474
1666
|
if (targetLocales === null) return null;
|
|
1475
1667
|
if (targetLocales.length === 0) {
|
|
1476
|
-
p3.log.warn(
|
|
1668
|
+
p3.log.warn(
|
|
1669
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1670
|
+
);
|
|
1477
1671
|
}
|
|
1478
1672
|
const detected = detectGitBranches();
|
|
1479
1673
|
const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
|
|
@@ -1482,73 +1676,31 @@ async function runProjectCreate(params) {
|
|
|
1482
1676
|
let initial = initialBranches;
|
|
1483
1677
|
while (pushBranches.length === 0) {
|
|
1484
1678
|
const result = await filterableBranchSelect({
|
|
1485
|
-
message: "
|
|
1679
|
+
message: "Which branches should trigger translations?",
|
|
1486
1680
|
branches: detected.branches,
|
|
1487
1681
|
defaultBranch: detected.defaultBranch,
|
|
1488
1682
|
initialValues: initial
|
|
1489
1683
|
});
|
|
1490
1684
|
if (result === null) return null;
|
|
1491
1685
|
if (result.length === 0) {
|
|
1492
|
-
p3.log.warn(
|
|
1686
|
+
p3.log.warn(
|
|
1687
|
+
"At least one branch is required. Please select at least one."
|
|
1688
|
+
);
|
|
1493
1689
|
initial = [detected.defaultBranch];
|
|
1494
1690
|
} else {
|
|
1495
1691
|
pushBranches = result;
|
|
1496
1692
|
}
|
|
1497
1693
|
}
|
|
1498
1694
|
}
|
|
1499
|
-
const
|
|
1500
|
-
message: "Translate on pull requests \u2014 which branches? (optional)",
|
|
1501
|
-
branches: detected.branches,
|
|
1502
|
-
defaultBranch: detected.defaultBranch,
|
|
1503
|
-
initialValues: [],
|
|
1504
|
-
optional: true
|
|
1505
|
-
});
|
|
1506
|
-
if (prResult === null) return null;
|
|
1507
|
-
const prBranches = prResult;
|
|
1508
|
-
const autoSet = /* @__PURE__ */ new Set([...pushBranches, ...prBranches]);
|
|
1509
|
-
const manualResult = await filterableBranchSelect({
|
|
1510
|
-
message: `Manual-only branches \u2014 translate via \`vocoder sync\` only (optional)`,
|
|
1511
|
-
branches: detected.branches.filter((b) => !autoSet.has(b)),
|
|
1512
|
-
defaultBranch: detected.defaultBranch,
|
|
1513
|
-
initialValues: [],
|
|
1514
|
-
optional: true
|
|
1515
|
-
});
|
|
1516
|
-
if (manualResult === null) return null;
|
|
1517
|
-
const manualBranches = manualResult.filter((b) => {
|
|
1518
|
-
if (autoSet.has(b)) {
|
|
1519
|
-
p3.log.warn(`"${b}" is already configured for automatic translation \u2014 skipping from manual.`);
|
|
1520
|
-
return false;
|
|
1521
|
-
}
|
|
1522
|
-
return true;
|
|
1523
|
-
});
|
|
1524
|
-
if (pushBranches.length === 0 && prBranches.length === 0 && manualBranches.length === 0) {
|
|
1525
|
-
p3.log.error("At least one branch must be configured.");
|
|
1526
|
-
return null;
|
|
1527
|
-
}
|
|
1528
|
-
const triggerMap = /* @__PURE__ */ new Map();
|
|
1529
|
-
for (const b of pushBranches) {
|
|
1530
|
-
if (!triggerMap.has(b)) triggerMap.set(b, /* @__PURE__ */ new Set());
|
|
1531
|
-
triggerMap.get(b).add("push");
|
|
1532
|
-
}
|
|
1533
|
-
for (const b of prBranches) {
|
|
1534
|
-
if (!triggerMap.has(b)) triggerMap.set(b, /* @__PURE__ */ new Set());
|
|
1535
|
-
triggerMap.get(b).add("pull_request");
|
|
1536
|
-
}
|
|
1537
|
-
for (const b of manualBranches) {
|
|
1538
|
-
triggerMap.set(b, /* @__PURE__ */ new Set(["manual"]));
|
|
1539
|
-
}
|
|
1540
|
-
const branchTriggers = Array.from(triggerMap.entries()).map(([pattern, triggers]) => ({
|
|
1541
|
-
pattern,
|
|
1542
|
-
triggers: Array.from(triggers)
|
|
1543
|
-
}));
|
|
1695
|
+
const targetBranches = pushBranches;
|
|
1544
1696
|
try {
|
|
1545
1697
|
const result = await api.createProject(userToken, {
|
|
1546
1698
|
organizationId,
|
|
1547
1699
|
name: projectName,
|
|
1548
1700
|
sourceLocale,
|
|
1549
1701
|
targetLocales,
|
|
1550
|
-
|
|
1551
|
-
|
|
1702
|
+
targetBranches,
|
|
1703
|
+
appDirs: appDir ? [appDir] : [],
|
|
1552
1704
|
repoCanonical
|
|
1553
1705
|
});
|
|
1554
1706
|
p3.log.success(`Project ${chalk4.bold(result.projectName)} created!`);
|
|
@@ -1561,41 +1713,47 @@ async function runProjectCreate(params) {
|
|
|
1561
1713
|
}
|
|
1562
1714
|
async function runProjectAppCreate(params) {
|
|
1563
1715
|
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
1564
|
-
const existingScopes = new Set(params.existingApps.map((a) => a.
|
|
1716
|
+
const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
|
|
1565
1717
|
let rawLocales;
|
|
1566
1718
|
try {
|
|
1567
1719
|
rawLocales = await api.listLocales(userToken);
|
|
1568
1720
|
} catch {
|
|
1569
|
-
p3.log.error(
|
|
1721
|
+
p3.log.error(
|
|
1722
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1723
|
+
);
|
|
1570
1724
|
return null;
|
|
1571
1725
|
}
|
|
1572
1726
|
const languageOptions = buildLanguageOptions(rawLocales);
|
|
1573
1727
|
const localeOptions = buildLocaleOptions(rawLocales);
|
|
1574
|
-
let
|
|
1575
|
-
if (params.
|
|
1576
|
-
|
|
1577
|
-
p3.log.success(`App directory: ${chalk4.bold(
|
|
1728
|
+
let appDir;
|
|
1729
|
+
if (params.defaultAppDir && !existingScopes.has(params.defaultAppDir)) {
|
|
1730
|
+
appDir = params.defaultAppDir;
|
|
1731
|
+
p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
|
|
1578
1732
|
} else {
|
|
1579
1733
|
if (params.existingApps.length > 0) {
|
|
1580
|
-
const configuredList = params.existingApps.map((a) => chalk4.dim(a.
|
|
1734
|
+
const configuredList = params.existingApps.map((a) => chalk4.dim(a.appDir || "(entire repo)")).join(", ");
|
|
1581
1735
|
p3.log.info(`Already configured: ${configuredList}`);
|
|
1582
1736
|
}
|
|
1583
1737
|
const hasWholeRepoApp = existingScopes.has("");
|
|
1584
1738
|
const rawScope = await p3.text({
|
|
1585
1739
|
message: "App directory for this new app",
|
|
1586
1740
|
placeholder: "e.g. apps/backend",
|
|
1587
|
-
initialValue: params.
|
|
1741
|
+
initialValue: params.defaultAppDir ?? "",
|
|
1588
1742
|
validate(value) {
|
|
1589
1743
|
const v = value.trim();
|
|
1590
|
-
if (!v && hasWholeRepoApp)
|
|
1591
|
-
|
|
1592
|
-
if (v
|
|
1744
|
+
if (!v && hasWholeRepoApp)
|
|
1745
|
+
return "This project already covers the entire repo.";
|
|
1746
|
+
if (!v)
|
|
1747
|
+
return "App directory is required when other apps already exist.";
|
|
1748
|
+
if (v.startsWith("/"))
|
|
1749
|
+
return "Use a relative path, not an absolute path.";
|
|
1593
1750
|
if (v.includes("..")) return 'Path must not contain "..".';
|
|
1594
|
-
if (existingScopes.has(v))
|
|
1751
|
+
if (existingScopes.has(v))
|
|
1752
|
+
return `"${v}" is already configured. Choose a different directory.`;
|
|
1595
1753
|
}
|
|
1596
1754
|
});
|
|
1597
1755
|
if (p3.isCancel(rawScope)) return null;
|
|
1598
|
-
|
|
1756
|
+
appDir = (rawScope ?? "").trim();
|
|
1599
1757
|
}
|
|
1600
1758
|
const sourceLocale = await searchSelectLocale(
|
|
1601
1759
|
languageOptions,
|
|
@@ -1603,14 +1761,18 @@ async function runProjectAppCreate(params) {
|
|
|
1603
1761
|
"en"
|
|
1604
1762
|
);
|
|
1605
1763
|
if (sourceLocale === null) return null;
|
|
1606
|
-
const targetOptions = localeOptions.filter(
|
|
1764
|
+
const targetOptions = localeOptions.filter(
|
|
1765
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1766
|
+
);
|
|
1607
1767
|
const targetLocales = await searchMultiSelectLocales(
|
|
1608
1768
|
targetOptions,
|
|
1609
1769
|
"Target languages"
|
|
1610
1770
|
);
|
|
1611
1771
|
if (targetLocales === null) return null;
|
|
1612
1772
|
if (targetLocales.length === 0) {
|
|
1613
|
-
p3.log.warn(
|
|
1773
|
+
p3.log.warn(
|
|
1774
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1775
|
+
);
|
|
1614
1776
|
}
|
|
1615
1777
|
const detectedApp = detectGitBranches();
|
|
1616
1778
|
let appPushBranches = [];
|
|
@@ -1618,7 +1780,7 @@ async function runProjectAppCreate(params) {
|
|
|
1618
1780
|
let initial = [detectedApp.defaultBranch];
|
|
1619
1781
|
while (appPushBranches.length === 0) {
|
|
1620
1782
|
const result = await filterableBranchSelect({
|
|
1621
|
-
message: "
|
|
1783
|
+
message: "Which branches should trigger translations?",
|
|
1622
1784
|
branches: detectedApp.branches,
|
|
1623
1785
|
defaultBranch: detectedApp.defaultBranch,
|
|
1624
1786
|
initialValues: initial
|
|
@@ -1632,64 +1794,27 @@ async function runProjectAppCreate(params) {
|
|
|
1632
1794
|
}
|
|
1633
1795
|
}
|
|
1634
1796
|
}
|
|
1635
|
-
const
|
|
1636
|
-
message: "Translate on pull requests \u2014 which branches? (optional)",
|
|
1637
|
-
branches: detectedApp.branches,
|
|
1638
|
-
defaultBranch: detectedApp.defaultBranch,
|
|
1639
|
-
initialValues: [],
|
|
1640
|
-
optional: true
|
|
1641
|
-
});
|
|
1642
|
-
if (appPrResult === null) return null;
|
|
1643
|
-
const appAutoSet = /* @__PURE__ */ new Set([...appPushBranches, ...appPrResult]);
|
|
1644
|
-
const appManualResult = await filterableBranchSelect({
|
|
1645
|
-
message: "Manual-only branches (optional)",
|
|
1646
|
-
branches: detectedApp.branches.filter((b) => !appAutoSet.has(b)),
|
|
1647
|
-
defaultBranch: detectedApp.defaultBranch,
|
|
1648
|
-
initialValues: [],
|
|
1649
|
-
optional: true
|
|
1650
|
-
});
|
|
1651
|
-
if (appManualResult === null) return null;
|
|
1652
|
-
const appManualBranches = appManualResult.filter((b) => {
|
|
1653
|
-
if (appAutoSet.has(b)) {
|
|
1654
|
-
p3.log.warn(`"${b}" is already configured for automatic translation \u2014 skipping from manual.`);
|
|
1655
|
-
return false;
|
|
1656
|
-
}
|
|
1657
|
-
return true;
|
|
1658
|
-
});
|
|
1659
|
-
const appTriggerMap = /* @__PURE__ */ new Map();
|
|
1660
|
-
for (const b of appPushBranches) {
|
|
1661
|
-
if (!appTriggerMap.has(b)) appTriggerMap.set(b, /* @__PURE__ */ new Set());
|
|
1662
|
-
appTriggerMap.get(b).add("push");
|
|
1663
|
-
}
|
|
1664
|
-
for (const b of appPrResult) {
|
|
1665
|
-
if (!appTriggerMap.has(b)) appTriggerMap.set(b, /* @__PURE__ */ new Set());
|
|
1666
|
-
appTriggerMap.get(b).add("pull_request");
|
|
1667
|
-
}
|
|
1668
|
-
for (const b of appManualBranches) {
|
|
1669
|
-
appTriggerMap.set(b, /* @__PURE__ */ new Set(["manual"]));
|
|
1670
|
-
}
|
|
1671
|
-
const branchTriggers = Array.from(appTriggerMap.entries()).map(([pattern, triggers]) => ({
|
|
1672
|
-
pattern,
|
|
1673
|
-
triggers: Array.from(triggers)
|
|
1674
|
-
}));
|
|
1797
|
+
const targetBranches = appPushBranches;
|
|
1675
1798
|
try {
|
|
1676
1799
|
const result = await api.createProjectApp(userToken, {
|
|
1677
1800
|
projectId,
|
|
1678
|
-
|
|
1801
|
+
appDir,
|
|
1679
1802
|
sourceLocale,
|
|
1680
1803
|
targetLocales,
|
|
1681
|
-
|
|
1804
|
+
targetBranches,
|
|
1682
1805
|
repoCanonical: repoCanonical ?? ""
|
|
1683
1806
|
});
|
|
1684
|
-
p3.log.success(
|
|
1807
|
+
p3.log.success(
|
|
1808
|
+
`App ${chalk4.bold(appDir)} added to ${chalk4.bold(projectName)}!`
|
|
1809
|
+
);
|
|
1685
1810
|
return {
|
|
1686
1811
|
projectId: result.projectId,
|
|
1687
1812
|
projectName: result.projectName,
|
|
1688
1813
|
apiKey: result.apiKey,
|
|
1689
|
-
|
|
1814
|
+
appDir: result.appDir,
|
|
1690
1815
|
sourceLocale,
|
|
1691
1816
|
targetLocales,
|
|
1692
|
-
|
|
1817
|
+
targetBranches
|
|
1693
1818
|
};
|
|
1694
1819
|
} catch (error) {
|
|
1695
1820
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -1735,7 +1860,6 @@ async function selectWorkspace(result) {
|
|
|
1735
1860
|
}
|
|
1736
1861
|
|
|
1737
1862
|
// src/commands/init.ts
|
|
1738
|
-
import { spawn as spawn2 } from "child_process";
|
|
1739
1863
|
loadEnv();
|
|
1740
1864
|
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
1741
1865
|
async function sleep(ms) {
|
|
@@ -1799,9 +1923,7 @@ function printPlanLimitMessage(apiUrl, message) {
|
|
|
1799
1923
|
p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
1800
1924
|
}
|
|
1801
1925
|
function runScaffold(params) {
|
|
1802
|
-
const {
|
|
1803
|
-
p5.log.info(`Project: ${chalk6.bold(projectName)}`);
|
|
1804
|
-
p5.log.info(`Workspace: ${chalk6.bold(organizationName)}`);
|
|
1926
|
+
const { sourceLocale, targetBranches } = params;
|
|
1805
1927
|
const detection = detectLocalEcosystem();
|
|
1806
1928
|
if (detection.ecosystem) {
|
|
1807
1929
|
const frameworkLabel = detection.framework ?? detection.ecosystem;
|
|
@@ -1810,7 +1932,10 @@ function runScaffold(params) {
|
|
|
1810
1932
|
}
|
|
1811
1933
|
const packagesToInstall = getPackagesToInstall(detection);
|
|
1812
1934
|
if (packagesToInstall.length > 0) {
|
|
1813
|
-
const installCmd = buildInstallCommand(
|
|
1935
|
+
const installCmd = buildInstallCommand(
|
|
1936
|
+
detection.packageManager,
|
|
1937
|
+
packagesToInstall
|
|
1938
|
+
);
|
|
1814
1939
|
p5.log.info("");
|
|
1815
1940
|
const installSpinner = p5.spinner();
|
|
1816
1941
|
installSpinner.start(`Installing ${packagesToInstall.join(", ")}...`);
|
|
@@ -1828,17 +1953,21 @@ function runScaffold(params) {
|
|
|
1828
1953
|
framework: detection.framework,
|
|
1829
1954
|
ecosystem: detection.ecosystem,
|
|
1830
1955
|
sourceLocale,
|
|
1831
|
-
|
|
1956
|
+
targetBranches
|
|
1832
1957
|
});
|
|
1833
1958
|
let stepNum = 1;
|
|
1834
1959
|
if (snippets.pluginStep) {
|
|
1835
1960
|
p5.log.message("");
|
|
1836
|
-
p5.log.step(
|
|
1961
|
+
p5.log.step(
|
|
1962
|
+
`${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`
|
|
1963
|
+
);
|
|
1837
1964
|
printCodeBlock(snippets.pluginStep.code);
|
|
1838
1965
|
stepNum++;
|
|
1839
1966
|
}
|
|
1840
1967
|
if (snippets.providerStep) {
|
|
1841
|
-
p5.log.step(
|
|
1968
|
+
p5.log.step(
|
|
1969
|
+
`${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`
|
|
1970
|
+
);
|
|
1842
1971
|
printCodeBlock(snippets.providerStep.code);
|
|
1843
1972
|
stepNum++;
|
|
1844
1973
|
}
|
|
@@ -1860,6 +1989,7 @@ function printMcpSetup(apiKey) {
|
|
|
1860
1989
|
type: "stdio",
|
|
1861
1990
|
command: "npx",
|
|
1862
1991
|
args: ["-y", "@vocoder/mcp"],
|
|
1992
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: MCP config template, not a JS template literal
|
|
1863
1993
|
env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
|
|
1864
1994
|
}
|
|
1865
1995
|
}
|
|
@@ -1873,7 +2003,9 @@ function printMcpSetup(apiKey) {
|
|
|
1873
2003
|
p5.log.message("");
|
|
1874
2004
|
printCodeBlock(addCommand);
|
|
1875
2005
|
p5.log.message("");
|
|
1876
|
-
p5.log.message(
|
|
2006
|
+
p5.log.message(
|
|
2007
|
+
"To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference"
|
|
2008
|
+
);
|
|
1877
2009
|
p5.log.message("so each developer supplies their own key:");
|
|
1878
2010
|
p5.log.message("");
|
|
1879
2011
|
printCodeBlock(teamConfig);
|
|
@@ -1882,19 +2014,26 @@ function printMcpSetup(apiKey) {
|
|
|
1882
2014
|
}
|
|
1883
2015
|
function printCodeBlock(code) {
|
|
1884
2016
|
const lines = code.split("\n");
|
|
1885
|
-
const maxLen = lines.reduce(
|
|
2017
|
+
const maxLen = lines.reduce(
|
|
2018
|
+
(max, line) => Math.max(max, line.length),
|
|
2019
|
+
0
|
|
2020
|
+
);
|
|
1886
2021
|
const bar = chalk6.gray("\u2502");
|
|
1887
2022
|
const pad = (s) => s + " ".repeat(maxLen - s.length);
|
|
1888
2023
|
process.stdout.write(`${chalk6.gray("\u2502")}
|
|
1889
2024
|
`);
|
|
1890
|
-
process.stdout.write(
|
|
1891
|
-
`)
|
|
2025
|
+
process.stdout.write(
|
|
2026
|
+
`${chalk6.gray("\u2502")} ${chalk6.gray(`\u250C${"\u2500".repeat(maxLen + 2)}\u2510`)}
|
|
2027
|
+
`
|
|
2028
|
+
);
|
|
1892
2029
|
for (const line of lines) {
|
|
1893
2030
|
process.stdout.write(`${chalk6.gray("\u2502")} ${bar} ${pad(line)} ${bar}
|
|
1894
2031
|
`);
|
|
1895
2032
|
}
|
|
1896
|
-
process.stdout.write(
|
|
1897
|
-
`)
|
|
2033
|
+
process.stdout.write(
|
|
2034
|
+
`${chalk6.gray("\u2502")} ${chalk6.gray(`\u2514${"\u2500".repeat(maxLen + 2)}\u2518`)}
|
|
2035
|
+
`
|
|
2036
|
+
);
|
|
1898
2037
|
}
|
|
1899
2038
|
async function verifyStoredToken(api, token) {
|
|
1900
2039
|
try {
|
|
@@ -1926,14 +2065,18 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1926
2065
|
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1927
2066
|
if (reauth) {
|
|
1928
2067
|
if (!options.yes) {
|
|
1929
|
-
const shouldOpen = await p5.confirm({
|
|
2068
|
+
const shouldOpen = await p5.confirm({
|
|
2069
|
+
message: "Open your browser to sign in again?"
|
|
2070
|
+
});
|
|
1930
2071
|
if (p5.isCancel(shouldOpen)) {
|
|
1931
2072
|
server?.close();
|
|
1932
2073
|
p5.cancel("Setup cancelled.");
|
|
1933
2074
|
return null;
|
|
1934
2075
|
}
|
|
1935
2076
|
if (!shouldOpen) {
|
|
1936
|
-
p5.log.info(
|
|
2077
|
+
p5.log.info(
|
|
2078
|
+
"Open the URL above manually in your browser to continue."
|
|
2079
|
+
);
|
|
1937
2080
|
} else {
|
|
1938
2081
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
1939
2082
|
if (!opened) {
|
|
@@ -1950,7 +2093,11 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1950
2093
|
const connectChoice = await p5.select({
|
|
1951
2094
|
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
1952
2095
|
options: [
|
|
1953
|
-
{
|
|
2096
|
+
{
|
|
2097
|
+
value: "install",
|
|
2098
|
+
label: "Install GitHub App",
|
|
2099
|
+
hint: "recommended"
|
|
2100
|
+
},
|
|
1954
2101
|
{ value: "link", label: "Already installed? Link your account" }
|
|
1955
2102
|
]
|
|
1956
2103
|
});
|
|
@@ -1992,7 +2139,9 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1992
2139
|
const timeoutMs = deadline - Date.now();
|
|
1993
2140
|
const params = await Promise.race([
|
|
1994
2141
|
server.waitForCallback(),
|
|
1995
|
-
new Promise(
|
|
2142
|
+
new Promise(
|
|
2143
|
+
(resolve2) => setTimeout(() => resolve2(null), timeoutMs)
|
|
2144
|
+
)
|
|
1996
2145
|
]);
|
|
1997
2146
|
if (params && typeof params.token === "string") {
|
|
1998
2147
|
rawToken = params.token;
|
|
@@ -2033,11 +2182,16 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
2033
2182
|
}
|
|
2034
2183
|
const userInfo = await api.getCliUserInfo(rawToken);
|
|
2035
2184
|
authSpinner.stop(`Authenticated as ${chalk6.bold(userInfo.email)}`);
|
|
2036
|
-
return {
|
|
2185
|
+
return {
|
|
2186
|
+
token: rawToken,
|
|
2187
|
+
...userInfo,
|
|
2188
|
+
organizationId: callbackOrganizationId,
|
|
2189
|
+
discoveryReady: callbackDiscoveryReady
|
|
2190
|
+
};
|
|
2037
2191
|
}
|
|
2038
2192
|
async function init(options = {}) {
|
|
2039
2193
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
2040
|
-
p5.intro("Vocoder Setup");
|
|
2194
|
+
p5.intro(chalk6.bold("Vocoder Setup"));
|
|
2041
2195
|
try {
|
|
2042
2196
|
const gitContext = resolveGitContext();
|
|
2043
2197
|
const identity = gitContext.identity;
|
|
@@ -2053,28 +2207,50 @@ async function init(options = {}) {
|
|
|
2053
2207
|
const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
2054
2208
|
const lookup = await anonApi.lookupProjectByRepo({
|
|
2055
2209
|
repoCanonical: identity.repoCanonical,
|
|
2056
|
-
|
|
2210
|
+
appDir: identity.repoAppDir
|
|
2057
2211
|
});
|
|
2058
2212
|
if (lookup.exactMatch) {
|
|
2059
2213
|
const { exactMatch } = lookup;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2214
|
+
p5.log.success(`Project: ${chalk6.bold(exactMatch.projectName)}`);
|
|
2215
|
+
p5.log.info(
|
|
2216
|
+
`Branches: ${chalk6.cyan((exactMatch.targetBranches ?? ["main"]).join(", "))}`
|
|
2217
|
+
);
|
|
2218
|
+
const needsKey = await p5.confirm({
|
|
2219
|
+
message: "Need to regenerate your API key?"
|
|
2065
2220
|
});
|
|
2221
|
+
if (!p5.isCancel(needsKey) && needsKey) {
|
|
2222
|
+
const anonApi2 = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
2223
|
+
const authResult = await runAuthFlow(
|
|
2224
|
+
anonApi2,
|
|
2225
|
+
options,
|
|
2226
|
+
/* reauth */
|
|
2227
|
+
true
|
|
2228
|
+
);
|
|
2229
|
+
if (!authResult) return 1;
|
|
2230
|
+
const spinner4 = p5.spinner();
|
|
2231
|
+
spinner4.start("Generating new API key...");
|
|
2232
|
+
try {
|
|
2233
|
+
const { apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
2234
|
+
authResult.token,
|
|
2235
|
+
exactMatch.projectId
|
|
2236
|
+
);
|
|
2237
|
+
spinner4.stop("New API key generated");
|
|
2238
|
+
printMcpSetup(apiKey);
|
|
2239
|
+
} catch {
|
|
2240
|
+
spinner4.stop("Failed to generate key");
|
|
2241
|
+
p5.log.error(
|
|
2242
|
+
"Could not generate API key. Try again or generate one from the dashboard."
|
|
2243
|
+
);
|
|
2244
|
+
return 1;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2066
2247
|
p5.outro("Vocoder is already set up for this repository.");
|
|
2067
2248
|
return 0;
|
|
2068
2249
|
}
|
|
2069
2250
|
if (lookup.hasWholeRepoApp) {
|
|
2070
|
-
const wholeRepo = lookup.existingApps.find((a) => a.
|
|
2251
|
+
const wholeRepo = lookup.existingApps.find((a) => a.appDir === "");
|
|
2071
2252
|
if (wholeRepo) {
|
|
2072
|
-
|
|
2073
|
-
projectName: wholeRepo.projectName,
|
|
2074
|
-
organizationName: wholeRepo.organizationName,
|
|
2075
|
-
sourceLocale: "en",
|
|
2076
|
-
branchTriggers: [{ pattern: "main", triggers: ["push"] }]
|
|
2077
|
-
});
|
|
2253
|
+
p5.log.success(`Project: ${chalk6.bold(wholeRepo.projectName)}`);
|
|
2078
2254
|
p5.outro("Vocoder is already set up for this repository.");
|
|
2079
2255
|
return 0;
|
|
2080
2256
|
}
|
|
@@ -2090,7 +2266,6 @@ async function init(options = {}) {
|
|
|
2090
2266
|
let userEmail;
|
|
2091
2267
|
let userName;
|
|
2092
2268
|
let authOrganizationId;
|
|
2093
|
-
let authDiscoveryReady = false;
|
|
2094
2269
|
const stored = readAuthData();
|
|
2095
2270
|
if (stored && stored.apiUrl === apiUrl) {
|
|
2096
2271
|
const verified = await verifyStoredToken(api, stored.token);
|
|
@@ -2118,7 +2293,6 @@ async function init(options = {}) {
|
|
|
2118
2293
|
userEmail = authResult.email;
|
|
2119
2294
|
userName = authResult.name;
|
|
2120
2295
|
authOrganizationId = authResult.organizationId;
|
|
2121
|
-
authDiscoveryReady = authResult.discoveryReady ?? false;
|
|
2122
2296
|
writeAuthData({
|
|
2123
2297
|
token: userToken,
|
|
2124
2298
|
apiUrl,
|
|
@@ -2129,7 +2303,12 @@ async function init(options = {}) {
|
|
|
2129
2303
|
});
|
|
2130
2304
|
}
|
|
2131
2305
|
} else {
|
|
2132
|
-
const authResult = await runAuthFlow(
|
|
2306
|
+
const authResult = await runAuthFlow(
|
|
2307
|
+
api,
|
|
2308
|
+
options,
|
|
2309
|
+
false,
|
|
2310
|
+
identity?.repoCanonical
|
|
2311
|
+
);
|
|
2133
2312
|
if (!authResult) return 1;
|
|
2134
2313
|
userToken = authResult.token;
|
|
2135
2314
|
userEmail = authResult.email;
|
|
@@ -2148,10 +2327,14 @@ async function init(options = {}) {
|
|
|
2148
2327
|
let selectedWorkspaceName;
|
|
2149
2328
|
if (authOrganizationId) {
|
|
2150
2329
|
const workspaceData = await api.listWorkspaces(userToken);
|
|
2151
|
-
const ws = workspaceData.workspaces.find(
|
|
2330
|
+
const ws = workspaceData.workspaces.find(
|
|
2331
|
+
(w) => w.id === authOrganizationId
|
|
2332
|
+
);
|
|
2152
2333
|
selectedWorkspaceId = authOrganizationId;
|
|
2153
2334
|
selectedWorkspaceName = ws?.name ?? userEmail;
|
|
2154
|
-
p5.log.success(
|
|
2335
|
+
p5.log.success(
|
|
2336
|
+
`Connected as ${chalk6.bold(userEmail)} \u2014 workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2337
|
+
);
|
|
2155
2338
|
} else {
|
|
2156
2339
|
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
2157
2340
|
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
@@ -2190,7 +2373,9 @@ async function init(options = {}) {
|
|
|
2190
2373
|
);
|
|
2191
2374
|
}
|
|
2192
2375
|
if (selectedInstallationId === null || selectedInstallationId === "install_new") {
|
|
2193
|
-
p5.cancel(
|
|
2376
|
+
p5.cancel(
|
|
2377
|
+
"Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
|
|
2378
|
+
);
|
|
2194
2379
|
return 1;
|
|
2195
2380
|
}
|
|
2196
2381
|
const claimResult = await api.claimCliGitHubInstallation(userToken, {
|
|
@@ -2206,7 +2391,9 @@ async function init(options = {}) {
|
|
|
2206
2391
|
});
|
|
2207
2392
|
const repoCanonical = identity?.repoCanonical ?? null;
|
|
2208
2393
|
const covering = repoCanonical ? workspaceData.workspaces.filter((w) => w.coversRepo === true) : [];
|
|
2209
|
-
const connected = workspaceData.workspaces.filter(
|
|
2394
|
+
const connected = workspaceData.workspaces.filter(
|
|
2395
|
+
(w) => w.hasGitHubConnection
|
|
2396
|
+
);
|
|
2210
2397
|
if (repoCanonical && covering.length === 1) {
|
|
2211
2398
|
const ws = covering[0];
|
|
2212
2399
|
selectedWorkspaceId = ws.id;
|
|
@@ -2265,9 +2452,15 @@ async function init(options = {}) {
|
|
|
2265
2452
|
);
|
|
2266
2453
|
return 1;
|
|
2267
2454
|
}
|
|
2268
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2455
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2456
|
+
api,
|
|
2457
|
+
userToken,
|
|
2458
|
+
yes: options.yes
|
|
2459
|
+
});
|
|
2269
2460
|
if (!connectResult) {
|
|
2270
|
-
p5.log.error(
|
|
2461
|
+
p5.log.error(
|
|
2462
|
+
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
2463
|
+
);
|
|
2271
2464
|
return 1;
|
|
2272
2465
|
}
|
|
2273
2466
|
selectedWorkspaceId = connectResult.organizationId;
|
|
@@ -2302,22 +2495,42 @@ async function init(options = {}) {
|
|
|
2302
2495
|
return 1;
|
|
2303
2496
|
}
|
|
2304
2497
|
if (connectChoice === "install") {
|
|
2305
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2498
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2499
|
+
api,
|
|
2500
|
+
userToken,
|
|
2501
|
+
yes: options.yes
|
|
2502
|
+
});
|
|
2306
2503
|
if (!connectResult) {
|
|
2307
|
-
p5.log.error(
|
|
2504
|
+
p5.log.error(
|
|
2505
|
+
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
2506
|
+
);
|
|
2308
2507
|
return 1;
|
|
2309
2508
|
}
|
|
2310
2509
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2311
2510
|
selectedWorkspaceName = connectResult.organizationName;
|
|
2312
|
-
p5.log.success(
|
|
2511
|
+
p5.log.success(
|
|
2512
|
+
`Workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2513
|
+
);
|
|
2313
2514
|
} else {
|
|
2314
|
-
const installations = await runGitHubDiscoveryFlow({
|
|
2515
|
+
const installations = await runGitHubDiscoveryFlow({
|
|
2516
|
+
api,
|
|
2517
|
+
userToken,
|
|
2518
|
+
yes: options.yes
|
|
2519
|
+
});
|
|
2315
2520
|
if (!installations) return 1;
|
|
2316
2521
|
if (installations.length === 0) {
|
|
2317
|
-
p5.log.warn(
|
|
2318
|
-
|
|
2522
|
+
p5.log.warn(
|
|
2523
|
+
"No GitHub installations found. Install the Vocoder GitHub App first."
|
|
2524
|
+
);
|
|
2525
|
+
const installNow = await p5.confirm({
|
|
2526
|
+
message: "Open GitHub to install the App?"
|
|
2527
|
+
});
|
|
2319
2528
|
if (p5.isCancel(installNow) || !installNow) return 1;
|
|
2320
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2529
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2530
|
+
api,
|
|
2531
|
+
userToken,
|
|
2532
|
+
yes: options.yes
|
|
2533
|
+
});
|
|
2321
2534
|
if (!connectResult) return 1;
|
|
2322
2535
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2323
2536
|
selectedWorkspaceName = connectResult.organizationName;
|
|
@@ -2337,20 +2550,29 @@ async function init(options = {}) {
|
|
|
2337
2550
|
return 1;
|
|
2338
2551
|
}
|
|
2339
2552
|
if (selectedInstallationId === "install_new") {
|
|
2340
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2553
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2554
|
+
api,
|
|
2555
|
+
userToken,
|
|
2556
|
+
yes: options.yes
|
|
2557
|
+
});
|
|
2341
2558
|
if (!connectResult) return 1;
|
|
2342
2559
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2343
2560
|
selectedWorkspaceName = connectResult.organizationName;
|
|
2344
2561
|
} else {
|
|
2345
|
-
const claimResult = await api.claimCliGitHubInstallation(
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2562
|
+
const claimResult = await api.claimCliGitHubInstallation(
|
|
2563
|
+
userToken,
|
|
2564
|
+
{
|
|
2565
|
+
installationId: String(selectedInstallationId),
|
|
2566
|
+
organizationId: null
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2349
2569
|
selectedWorkspaceId = claimResult.organizationId;
|
|
2350
2570
|
selectedWorkspaceName = claimResult.organizationName;
|
|
2351
2571
|
}
|
|
2352
2572
|
}
|
|
2353
|
-
p5.log.success(
|
|
2573
|
+
p5.log.success(
|
|
2574
|
+
`Workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2575
|
+
);
|
|
2354
2576
|
}
|
|
2355
2577
|
}
|
|
2356
2578
|
}
|
|
@@ -2360,7 +2582,7 @@ async function init(options = {}) {
|
|
|
2360
2582
|
if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
|
|
2361
2583
|
p5.log.info(
|
|
2362
2584
|
`${chalk6.bold(repoProjectName)} is already set up for this repo.
|
|
2363
|
-
Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.
|
|
2585
|
+
Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.appDir || "(entire repo)")).join(", ")}`
|
|
2364
2586
|
);
|
|
2365
2587
|
const appResult = await runProjectAppCreate({
|
|
2366
2588
|
api,
|
|
@@ -2369,7 +2591,7 @@ async function init(options = {}) {
|
|
|
2369
2591
|
projectName: repoProjectName,
|
|
2370
2592
|
organizationName: selectedWorkspaceName,
|
|
2371
2593
|
repoCanonical: identity?.repoCanonical,
|
|
2372
|
-
|
|
2594
|
+
defaultAppDir: identity?.repoAppDir,
|
|
2373
2595
|
existingApps: existingAppsForRepo
|
|
2374
2596
|
});
|
|
2375
2597
|
if (!appResult) {
|
|
@@ -2377,10 +2599,8 @@ async function init(options = {}) {
|
|
|
2377
2599
|
return 1;
|
|
2378
2600
|
}
|
|
2379
2601
|
runScaffold({
|
|
2380
|
-
projectName: appResult.projectName,
|
|
2381
|
-
organizationName: selectedWorkspaceName,
|
|
2382
2602
|
sourceLocale: appResult.sourceLocale,
|
|
2383
|
-
|
|
2603
|
+
targetBranches: appResult.targetBranches
|
|
2384
2604
|
});
|
|
2385
2605
|
p5.outro("You're all set.");
|
|
2386
2606
|
return 0;
|
|
@@ -2412,10 +2632,15 @@ async function init(options = {}) {
|
|
|
2412
2632
|
}
|
|
2413
2633
|
if (limitAction === "upgrade") {
|
|
2414
2634
|
await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
|
|
2415
|
-
p5.cancel(
|
|
2635
|
+
p5.cancel(
|
|
2636
|
+
"Upgrade your plan in the browser, then re-run `vocoder init`."
|
|
2637
|
+
);
|
|
2416
2638
|
return 1;
|
|
2417
2639
|
}
|
|
2418
|
-
const existingProjects = await api.listProjects(
|
|
2640
|
+
const existingProjects = await api.listProjects(
|
|
2641
|
+
userToken,
|
|
2642
|
+
selectedWorkspaceId
|
|
2643
|
+
);
|
|
2419
2644
|
if (existingProjects.length === 0) {
|
|
2420
2645
|
p5.log.error("No projects found in this workspace.");
|
|
2421
2646
|
return 1;
|
|
@@ -2439,7 +2664,7 @@ async function init(options = {}) {
|
|
|
2439
2664
|
projectName: chosen.name,
|
|
2440
2665
|
organizationName: selectedWorkspaceName,
|
|
2441
2666
|
repoCanonical: identity?.repoCanonical,
|
|
2442
|
-
|
|
2667
|
+
defaultAppDir: identity?.repoAppDir,
|
|
2443
2668
|
existingApps: []
|
|
2444
2669
|
});
|
|
2445
2670
|
if (!appResult) {
|
|
@@ -2447,10 +2672,8 @@ async function init(options = {}) {
|
|
|
2447
2672
|
return 1;
|
|
2448
2673
|
}
|
|
2449
2674
|
runScaffold({
|
|
2450
|
-
projectName: appResult.projectName,
|
|
2451
|
-
organizationName: selectedWorkspaceName,
|
|
2452
2675
|
sourceLocale: appResult.sourceLocale,
|
|
2453
|
-
|
|
2676
|
+
targetBranches: appResult.targetBranches
|
|
2454
2677
|
});
|
|
2455
2678
|
p5.outro("You're all set.");
|
|
2456
2679
|
return 0;
|
|
@@ -2465,7 +2688,7 @@ async function init(options = {}) {
|
|
|
2465
2688
|
defaultSourceLocale: "en",
|
|
2466
2689
|
repoCanonical: identity?.repoCanonical,
|
|
2467
2690
|
defaultBranches: ["main"],
|
|
2468
|
-
|
|
2691
|
+
defaultAppDir: identity?.repoAppDir
|
|
2469
2692
|
});
|
|
2470
2693
|
if (!projectResult) {
|
|
2471
2694
|
p5.log.error("Project creation failed. Run `vocoder init` again.");
|
|
@@ -2484,10 +2707,8 @@ Translations won't run automatically until you grant access.
|
|
|
2484
2707
|
);
|
|
2485
2708
|
}
|
|
2486
2709
|
runScaffold({
|
|
2487
|
-
projectName: projectResult.projectName,
|
|
2488
|
-
organizationName: selectedWorkspaceName,
|
|
2489
2710
|
sourceLocale: projectResult.sourceLocale,
|
|
2490
|
-
|
|
2711
|
+
targetBranches: projectResult.targetBranches
|
|
2491
2712
|
});
|
|
2492
2713
|
printMcpSetup(projectResult.apiKey);
|
|
2493
2714
|
p5.outro("You're all set.");
|
|
@@ -2526,8 +2747,11 @@ async function logout(options = {}) {
|
|
|
2526
2747
|
}
|
|
2527
2748
|
|
|
2528
2749
|
// src/commands/sync.ts
|
|
2529
|
-
import * as p7 from "@clack/prompts";
|
|
2530
2750
|
import { createHash, randomUUID } from "crypto";
|
|
2751
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2752
|
+
import { join as join2 } from "path";
|
|
2753
|
+
import * as p7 from "@clack/prompts";
|
|
2754
|
+
import chalk8 from "chalk";
|
|
2531
2755
|
|
|
2532
2756
|
// src/utils/branch.ts
|
|
2533
2757
|
import { execSync as execSync4 } from "child_process";
|
|
@@ -2557,7 +2781,7 @@ function detectBranch(override) {
|
|
|
2557
2781
|
stdio: ["pipe", "pipe", "ignore"]
|
|
2558
2782
|
}).trim();
|
|
2559
2783
|
return branch;
|
|
2560
|
-
} catch (
|
|
2784
|
+
} catch (_error) {
|
|
2561
2785
|
throw new Error(
|
|
2562
2786
|
"Failed to detect git branch. Make sure you are in a git repository or set the --branch flag."
|
|
2563
2787
|
);
|
|
@@ -2595,9 +2819,6 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2595
2819
|
return new RegExp(regexSource).test(branch);
|
|
2596
2820
|
}
|
|
2597
2821
|
|
|
2598
|
-
// src/commands/sync.ts
|
|
2599
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2600
|
-
|
|
2601
2822
|
// src/utils/config.ts
|
|
2602
2823
|
import chalk7 from "chalk";
|
|
2603
2824
|
import { config as loadEnv2 } from "dotenv";
|
|
@@ -2606,8 +2827,15 @@ function validateLocalConfig(config) {
|
|
|
2606
2827
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
2607
2828
|
throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
|
|
2608
2829
|
}
|
|
2609
|
-
if (!config.apiKey.startsWith("
|
|
2610
|
-
|
|
2830
|
+
if (!config.apiKey.startsWith("vcp_")) {
|
|
2831
|
+
if (config.apiKey.startsWith("vco_") || config.apiKey.startsWith("vcu_")) {
|
|
2832
|
+
throw new Error(
|
|
2833
|
+
"VOCODER_API_KEY must be a project-scoped key (starts with vcp_). Got an org or user key."
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
throw new Error(
|
|
2837
|
+
"Invalid API key format. Expected a project API key starting with vcp_."
|
|
2838
|
+
);
|
|
2611
2839
|
}
|
|
2612
2840
|
if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
|
|
2613
2841
|
throw new Error("Invalid API URL");
|
|
@@ -2615,7 +2843,7 @@ function validateLocalConfig(config) {
|
|
|
2615
2843
|
}
|
|
2616
2844
|
async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
2617
2845
|
const configSources = {
|
|
2618
|
-
|
|
2846
|
+
includePattern: "default",
|
|
2619
2847
|
excludePattern: "default",
|
|
2620
2848
|
apiKey: "environment",
|
|
2621
2849
|
apiUrl: "default",
|
|
@@ -2624,29 +2852,53 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2624
2852
|
noFallback: "default"
|
|
2625
2853
|
};
|
|
2626
2854
|
const defaults = {
|
|
2627
|
-
|
|
2628
|
-
excludePattern: [
|
|
2855
|
+
includePattern: ["**/*.{tsx,jsx,ts,js}"],
|
|
2856
|
+
excludePattern: [
|
|
2857
|
+
"**/node_modules/**",
|
|
2858
|
+
"**/.next/**",
|
|
2859
|
+
"**/.nuxt/**",
|
|
2860
|
+
"**/.svelte-kit/**",
|
|
2861
|
+
"**/.output/**",
|
|
2862
|
+
"**/dist/**",
|
|
2863
|
+
"**/build/**",
|
|
2864
|
+
"**/out/**",
|
|
2865
|
+
"**/.vite/**",
|
|
2866
|
+
"**/.turbo/**",
|
|
2867
|
+
"**/coverage/**",
|
|
2868
|
+
"**/.cache/**",
|
|
2869
|
+
"**/*.min.js",
|
|
2870
|
+
"**/*.min.ts",
|
|
2871
|
+
"**/__generated__/**",
|
|
2872
|
+
"**/*.test.*",
|
|
2873
|
+
"**/*.spec.*",
|
|
2874
|
+
"**/*.stories.*",
|
|
2875
|
+
"**/__tests__/**"
|
|
2876
|
+
],
|
|
2629
2877
|
apiUrl: "https://vocoder.app"
|
|
2630
2878
|
};
|
|
2631
|
-
const envExtractionPattern = process.env.
|
|
2879
|
+
const envExtractionPattern = process.env.VOCODER_INCLUDE_PATTERN;
|
|
2880
|
+
const envExcludePattern = process.env.VOCODER_EXCLUDE_PATTERN;
|
|
2632
2881
|
const envApiUrl = process.env.VOCODER_API_URL;
|
|
2633
2882
|
const envSyncMode = process.env.VOCODER_SYNC_MODE;
|
|
2634
2883
|
const envSyncMaxWaitMs = process.env.VOCODER_SYNC_MAX_WAIT_MS;
|
|
2635
2884
|
const envSyncNoFallback = process.env.VOCODER_SYNC_NO_FALLBACK;
|
|
2636
|
-
let
|
|
2885
|
+
let includePattern;
|
|
2637
2886
|
if (cliOptions.include && cliOptions.include.length > 0) {
|
|
2638
|
-
|
|
2639
|
-
configSources.
|
|
2887
|
+
includePattern = cliOptions.include;
|
|
2888
|
+
configSources.includePattern = "CLI flag";
|
|
2640
2889
|
} else if (envExtractionPattern) {
|
|
2641
|
-
|
|
2642
|
-
configSources.
|
|
2890
|
+
includePattern = [envExtractionPattern];
|
|
2891
|
+
configSources.includePattern = "environment";
|
|
2643
2892
|
} else {
|
|
2644
|
-
|
|
2893
|
+
includePattern = defaults.includePattern;
|
|
2645
2894
|
}
|
|
2646
2895
|
let excludePattern;
|
|
2647
2896
|
if (cliOptions.exclude && cliOptions.exclude.length > 0) {
|
|
2648
2897
|
excludePattern = cliOptions.exclude;
|
|
2649
2898
|
configSources.excludePattern = "CLI flag";
|
|
2899
|
+
} else if (envExcludePattern) {
|
|
2900
|
+
excludePattern = envExcludePattern.split(",").map((p9) => p9.trim()).filter(Boolean);
|
|
2901
|
+
configSources.excludePattern = "environment";
|
|
2650
2902
|
} else {
|
|
2651
2903
|
excludePattern = defaults.excludePattern;
|
|
2652
2904
|
}
|
|
@@ -2687,14 +2939,20 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2687
2939
|
noFallback = cliOptions.noFallback;
|
|
2688
2940
|
configSources.noFallback = "CLI flag";
|
|
2689
2941
|
} else if (envSyncNoFallback) {
|
|
2690
|
-
noFallback = ["1", "true", "yes", "on"].includes(
|
|
2942
|
+
noFallback = ["1", "true", "yes", "on"].includes(
|
|
2943
|
+
envSyncNoFallback.toLowerCase()
|
|
2944
|
+
);
|
|
2691
2945
|
configSources.noFallback = "environment";
|
|
2692
2946
|
}
|
|
2693
2947
|
if (verbose) {
|
|
2694
2948
|
console.log(chalk7.dim("\n Configuration sources:"));
|
|
2695
|
-
console.log(
|
|
2949
|
+
console.log(
|
|
2950
|
+
chalk7.dim(` Include patterns: ${configSources.includePattern}`)
|
|
2951
|
+
);
|
|
2696
2952
|
if (excludePattern.length > 0) {
|
|
2697
|
-
console.log(
|
|
2953
|
+
console.log(
|
|
2954
|
+
chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`)
|
|
2955
|
+
);
|
|
2698
2956
|
}
|
|
2699
2957
|
console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
|
|
2700
2958
|
console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
|
|
@@ -2707,7 +2965,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2707
2965
|
`));
|
|
2708
2966
|
}
|
|
2709
2967
|
return {
|
|
2710
|
-
|
|
2968
|
+
includePattern,
|
|
2711
2969
|
excludePattern,
|
|
2712
2970
|
apiKey,
|
|
2713
2971
|
apiUrl,
|
|
@@ -2719,8 +2977,21 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2719
2977
|
}
|
|
2720
2978
|
|
|
2721
2979
|
// src/commands/sync.ts
|
|
2722
|
-
|
|
2723
|
-
|
|
2980
|
+
function computeStringsHash(texts) {
|
|
2981
|
+
const sorted = [...texts].sort();
|
|
2982
|
+
return createHash("sha256").update(sorted.join("\0")).digest("hex").slice(0, 16);
|
|
2983
|
+
}
|
|
2984
|
+
function readCachedStringsHash(projectRoot, branch) {
|
|
2985
|
+
const filePath = getCacheFilePath(projectRoot, branch);
|
|
2986
|
+
if (!existsSync(filePath)) return null;
|
|
2987
|
+
try {
|
|
2988
|
+
const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
2989
|
+
if (isRecord(raw) && typeof raw.stringsHash === "string")
|
|
2990
|
+
return raw.stringsHash;
|
|
2991
|
+
} catch {
|
|
2992
|
+
}
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2724
2995
|
function isRecord(value) {
|
|
2725
2996
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2726
2997
|
}
|
|
@@ -2765,10 +3036,15 @@ function parseTranslations(value) {
|
|
|
2765
3036
|
return Object.keys(translations).length > 0 ? translations : null;
|
|
2766
3037
|
}
|
|
2767
3038
|
function getCacheFilePath(projectRoot, branch) {
|
|
2768
|
-
const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
2769
3039
|
const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
|
|
2770
|
-
|
|
2771
|
-
|
|
3040
|
+
return join2(
|
|
3041
|
+
projectRoot,
|
|
3042
|
+
"node_modules",
|
|
3043
|
+
".vocoder",
|
|
3044
|
+
"cache",
|
|
3045
|
+
"sync",
|
|
3046
|
+
`${branchHash}.json`
|
|
3047
|
+
);
|
|
2772
3048
|
}
|
|
2773
3049
|
function readLocalSnapshotCache(params) {
|
|
2774
3050
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
@@ -2797,22 +3073,25 @@ function readLocalSnapshotCache(params) {
|
|
|
2797
3073
|
cacheBranch: candidateBranch
|
|
2798
3074
|
};
|
|
2799
3075
|
} catch {
|
|
2800
|
-
continue;
|
|
2801
3076
|
}
|
|
2802
3077
|
}
|
|
2803
3078
|
return null;
|
|
2804
3079
|
}
|
|
2805
3080
|
function writeLocalSnapshotCache(params) {
|
|
2806
3081
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
2807
|
-
mkdirSync2(
|
|
2808
|
-
|
|
2809
|
-
|
|
3082
|
+
mkdirSync2(
|
|
3083
|
+
join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
|
|
3084
|
+
{
|
|
3085
|
+
recursive: true
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
2810
3088
|
const payload = {
|
|
2811
3089
|
version: 1,
|
|
2812
3090
|
branch: params.branch,
|
|
2813
3091
|
sourceLocale: params.sourceLocale,
|
|
2814
3092
|
targetLocales: params.targetLocales,
|
|
2815
3093
|
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3094
|
+
...params.stringsHash ? { stringsHash: params.stringsHash } : {},
|
|
2816
3095
|
...params.snapshotBatchId ? { snapshotBatchId: params.snapshotBatchId } : {},
|
|
2817
3096
|
...params.completedAt ? { completedAt: params.completedAt } : {},
|
|
2818
3097
|
...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
|
|
@@ -2902,7 +3181,9 @@ function getSyncPolicyErrorGuidance(error) {
|
|
|
2902
3181
|
if (error.branch) {
|
|
2903
3182
|
lines2.push(`Current branch: ${error.branch}`);
|
|
2904
3183
|
}
|
|
2905
|
-
lines2.push(
|
|
3184
|
+
lines2.push(
|
|
3185
|
+
"Update your project target branches in the dashboard if needed."
|
|
3186
|
+
);
|
|
2906
3187
|
return lines2;
|
|
2907
3188
|
}
|
|
2908
3189
|
const lines = ["This project is bound to a different repository."];
|
|
@@ -2994,7 +3275,7 @@ async function sync(options = {}) {
|
|
|
2994
3275
|
const config = {
|
|
2995
3276
|
...localConfig,
|
|
2996
3277
|
...apiConfig,
|
|
2997
|
-
|
|
3278
|
+
includePattern: mergedConfig.includePattern,
|
|
2998
3279
|
excludePattern: mergedConfig.excludePattern,
|
|
2999
3280
|
timeout: waitTimeoutMs
|
|
3000
3281
|
};
|
|
@@ -3008,17 +3289,19 @@ async function sync(options = {}) {
|
|
|
3008
3289
|
p7.outro("");
|
|
3009
3290
|
return 0;
|
|
3010
3291
|
}
|
|
3011
|
-
const patternsDisplay = Array.isArray(config.
|
|
3292
|
+
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
3012
3293
|
spinner4.start(`Extracting strings from ${patternsDisplay}`);
|
|
3013
3294
|
const extractor = new StringExtractor();
|
|
3014
3295
|
const extractedStrings = await extractor.extractFromProject(
|
|
3015
|
-
config.
|
|
3296
|
+
config.includePattern,
|
|
3016
3297
|
projectRoot,
|
|
3017
3298
|
config.excludePattern
|
|
3018
3299
|
);
|
|
3019
3300
|
if (extractedStrings.length === 0) {
|
|
3020
3301
|
spinner4.stop("No translatable strings found");
|
|
3021
|
-
p7.log.warn(
|
|
3302
|
+
p7.log.warn(
|
|
3303
|
+
"Make sure you are wrapping translatable strings with Vocoder"
|
|
3304
|
+
);
|
|
3022
3305
|
p7.outro("");
|
|
3023
3306
|
return 0;
|
|
3024
3307
|
}
|
|
@@ -3053,6 +3336,7 @@ async function sync(options = {}) {
|
|
|
3053
3336
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
3054
3337
|
);
|
|
3055
3338
|
}
|
|
3339
|
+
const commitSha = detectCommitSha() ?? void 0;
|
|
3056
3340
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
3057
3341
|
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
3058
3342
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
@@ -3060,6 +3344,15 @@ async function sync(options = {}) {
|
|
|
3060
3344
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
3061
3345
|
);
|
|
3062
3346
|
}
|
|
3347
|
+
const currentHash = computeStringsHash(sourceStrings);
|
|
3348
|
+
if (!options.force) {
|
|
3349
|
+
const cachedHash = readCachedStringsHash(projectRoot, branch);
|
|
3350
|
+
if (cachedHash && cachedHash === currentHash) {
|
|
3351
|
+
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3352
|
+
p7.outro(`Up to date (${duration2}s)`);
|
|
3353
|
+
return 0;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3063
3356
|
spinner4.start("Submitting strings to Vocoder API");
|
|
3064
3357
|
const batchResponse = await api.submitTranslation(
|
|
3065
3358
|
branch,
|
|
@@ -3070,9 +3363,11 @@ async function sync(options = {}) {
|
|
|
3070
3363
|
requestedMaxWaitMs: waitTimeoutMs,
|
|
3071
3364
|
clientRunId: randomUUID()
|
|
3072
3365
|
},
|
|
3073
|
-
repoIdentity
|
|
3366
|
+
repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
|
|
3367
|
+
);
|
|
3368
|
+
spinner4.stop(
|
|
3369
|
+
`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`
|
|
3074
3370
|
);
|
|
3075
|
-
spinner4.stop(`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`);
|
|
3076
3371
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
3077
3372
|
branch,
|
|
3078
3373
|
requestedMode,
|
|
@@ -3204,6 +3499,7 @@ async function sync(options = {}) {
|
|
|
3204
3499
|
targetLocales: config.targetLocales,
|
|
3205
3500
|
translations: finalTranslations,
|
|
3206
3501
|
localeMetadata: artifacts.localeMetadata,
|
|
3502
|
+
stringsHash: currentHash,
|
|
3207
3503
|
snapshotBatchId: artifacts.snapshotBatchId ?? (artifacts.source === "fresh" ? batchResponse.batchId : batchResponse.latestCompletedBatchId),
|
|
3208
3504
|
completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
|
|
3209
3505
|
});
|
|
@@ -3224,8 +3520,6 @@ async function sync(options = {}) {
|
|
|
3224
3520
|
}
|
|
3225
3521
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3226
3522
|
p7.outro(`Sync complete! (${duration}s)`);
|
|
3227
|
-
p7.log.info("Translations will be injected at build time by @vocoder/unplugin.");
|
|
3228
|
-
p7.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
|
|
3229
3523
|
return 0;
|
|
3230
3524
|
} catch (error) {
|
|
3231
3525
|
spinner4.stop();
|
|
@@ -3249,11 +3543,15 @@ async function sync(options = {}) {
|
|
|
3249
3543
|
if (error instanceof Error) {
|
|
3250
3544
|
p7.log.error(error.message);
|
|
3251
3545
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
3252
|
-
p7.log.warn(
|
|
3546
|
+
p7.log.warn(
|
|
3547
|
+
"VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
|
|
3548
|
+
);
|
|
3253
3549
|
p7.log.info(" Create one at: https://vocoder.app/dashboard");
|
|
3254
3550
|
p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
|
|
3255
3551
|
p7.log.info("");
|
|
3256
|
-
p7.log.info(
|
|
3552
|
+
p7.log.info(
|
|
3553
|
+
" Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
|
|
3554
|
+
);
|
|
3257
3555
|
p7.log.info(" Translations are fetched automatically at build time.");
|
|
3258
3556
|
} else if (error.message.includes("git branch")) {
|
|
3259
3557
|
p7.log.warn("Run from a git repository, or use:");
|
|
@@ -3287,7 +3585,9 @@ async function whoami(options = {}) {
|
|
|
3287
3585
|
p8.log.info(`API: ${apiUrl}`);
|
|
3288
3586
|
return 0;
|
|
3289
3587
|
} catch {
|
|
3290
|
-
p8.log.error(
|
|
3588
|
+
p8.log.error(
|
|
3589
|
+
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3590
|
+
);
|
|
3291
3591
|
return 1;
|
|
3292
3592
|
}
|
|
3293
3593
|
}
|
|
@@ -3302,7 +3602,13 @@ async function runCommand(command, options) {
|
|
|
3302
3602
|
}
|
|
3303
3603
|
var program = new Command();
|
|
3304
3604
|
program.name("vocoder").description("Vocoder CLI - Project setup and string extraction").version("0.1.5");
|
|
3305
|
-
program.command("init").description("Authenticate and provision Vocoder for this project").option("--api-url <url>", "Override Vocoder API URL").option("--yes", "Allow overwriting existing local config values").option(
|
|
3605
|
+
program.command("init").description("Authenticate and provision Vocoder for this project").option("--api-url <url>", "Override Vocoder API URL").option("--yes", "Allow overwriting existing local config values").option(
|
|
3606
|
+
"--ci",
|
|
3607
|
+
"Non-interactive mode: print auth URL to stdout, skip browser open"
|
|
3608
|
+
).option("--project-name <name>", "Starter project name to create").option("--source-locale <locale>", "Source locale for the starter project").option(
|
|
3609
|
+
"--target-locales <list>",
|
|
3610
|
+
"Comma-separated target locales (e.g. es,fr,de)"
|
|
3611
|
+
).action((options) => runCommand(init, options));
|
|
3306
3612
|
program.command("sync").description("Extract strings and sync translations").option("--branch <branch>", "Override detected branch").option("--mode <mode>", "Sync mode: auto, required, best-effort", "auto").option("--max-wait <ms>", "Max wait for translations (ms)").option("--force", "Force re-extraction even if no changes").option("--dry-run", "Preview without syncing").option("--no-fallback", "Disable fallback to cached translations").option("--include <pattern>", "Include glob pattern", collect, []).option("--exclude <pattern>", "Exclude glob pattern", collect, []).option("--verbose", "Detailed output").action((options) => {
|
|
3307
3613
|
const translated = { ...options };
|
|
3308
3614
|
if (options.maxWait) translated.maxWaitMs = Number(options.maxWait);
|