@vocoder/cli 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +995 -690
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-3QBORM6T.mjs → chunk-OFQLREXF.mjs} +10 -4
- package/dist/chunk-OFQLREXF.mjs.map +1 -0
- package/dist/lib.d.mts +60 -60
- package/dist/lib.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-3QBORM6T.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,
|
|
@@ -423,7 +125,10 @@ var VocoderAPI = class {
|
|
|
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,24 +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?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
|
|
484
|
-
...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {}
|
|
485
|
-
})
|
|
486
|
-
}, "Translation submission failed");
|
|
194
|
+
"Translation submission failed"
|
|
195
|
+
);
|
|
487
196
|
}
|
|
488
197
|
/**
|
|
489
198
|
* Check translation status
|
|
@@ -547,7 +256,10 @@ var VocoderAPI = class {
|
|
|
547
256
|
const payload = await readPayload(response);
|
|
548
257
|
if (!response.ok) {
|
|
549
258
|
throw new VocoderAPIError({
|
|
550
|
-
message: extractErrorMessage(
|
|
259
|
+
message: extractErrorMessage(
|
|
260
|
+
payload,
|
|
261
|
+
`Failed to start init session (${response.status})`
|
|
262
|
+
),
|
|
551
263
|
status: response.status,
|
|
552
264
|
payload
|
|
553
265
|
});
|
|
@@ -566,7 +278,10 @@ var VocoderAPI = class {
|
|
|
566
278
|
const payload = await readPayload(response);
|
|
567
279
|
if (!response.ok) {
|
|
568
280
|
throw new VocoderAPIError({
|
|
569
|
-
message: extractErrorMessage(
|
|
281
|
+
message: extractErrorMessage(
|
|
282
|
+
payload,
|
|
283
|
+
`Failed to get init status (${response.status})`
|
|
284
|
+
),
|
|
570
285
|
status: response.status,
|
|
571
286
|
payload
|
|
572
287
|
});
|
|
@@ -590,7 +305,10 @@ var VocoderAPI = class {
|
|
|
590
305
|
const payload = await readPayload(response);
|
|
591
306
|
if (!response.ok) {
|
|
592
307
|
throw new VocoderAPIError({
|
|
593
|
-
message: extractErrorMessage(
|
|
308
|
+
message: extractErrorMessage(
|
|
309
|
+
payload,
|
|
310
|
+
`Failed to start auth session (${response.status})`
|
|
311
|
+
),
|
|
594
312
|
status: response.status,
|
|
595
313
|
payload
|
|
596
314
|
});
|
|
@@ -619,7 +337,10 @@ var VocoderAPI = class {
|
|
|
619
337
|
if (!response.ok) {
|
|
620
338
|
return {
|
|
621
339
|
status: "failed",
|
|
622
|
-
reason: extractErrorMessage(
|
|
340
|
+
reason: extractErrorMessage(
|
|
341
|
+
payload,
|
|
342
|
+
`Auth session error (${response.status})`
|
|
343
|
+
)
|
|
623
344
|
};
|
|
624
345
|
}
|
|
625
346
|
const result = payload;
|
|
@@ -643,7 +364,10 @@ var VocoderAPI = class {
|
|
|
643
364
|
const payload = await readPayload(response);
|
|
644
365
|
if (!response.ok) {
|
|
645
366
|
throw new VocoderAPIError({
|
|
646
|
-
message: extractErrorMessage(
|
|
367
|
+
message: extractErrorMessage(
|
|
368
|
+
payload,
|
|
369
|
+
`Token validation failed (${response.status})`
|
|
370
|
+
),
|
|
647
371
|
status: response.status,
|
|
648
372
|
payload
|
|
649
373
|
});
|
|
@@ -661,7 +385,10 @@ var VocoderAPI = class {
|
|
|
661
385
|
if (!response.ok) {
|
|
662
386
|
const payload = await readPayload(response);
|
|
663
387
|
throw new VocoderAPIError({
|
|
664
|
-
message: extractErrorMessage(
|
|
388
|
+
message: extractErrorMessage(
|
|
389
|
+
payload,
|
|
390
|
+
`Token revocation failed (${response.status})`
|
|
391
|
+
),
|
|
665
392
|
status: response.status,
|
|
666
393
|
payload
|
|
667
394
|
});
|
|
@@ -677,7 +404,10 @@ var VocoderAPI = class {
|
|
|
677
404
|
const payload = await readPayload(response);
|
|
678
405
|
if (!response.ok) {
|
|
679
406
|
throw new VocoderAPIError({
|
|
680
|
-
message: extractErrorMessage(
|
|
407
|
+
message: extractErrorMessage(
|
|
408
|
+
payload,
|
|
409
|
+
`Failed to list workspaces (${response.status})`
|
|
410
|
+
),
|
|
681
411
|
status: response.status,
|
|
682
412
|
payload
|
|
683
413
|
});
|
|
@@ -693,7 +423,10 @@ var VocoderAPI = class {
|
|
|
693
423
|
const payload = await readPayload(response);
|
|
694
424
|
if (!response.ok) {
|
|
695
425
|
throw new VocoderAPIError({
|
|
696
|
-
message: extractErrorMessage(
|
|
426
|
+
message: extractErrorMessage(
|
|
427
|
+
payload,
|
|
428
|
+
`Failed to list projects (${response.status})`
|
|
429
|
+
),
|
|
697
430
|
status: response.status,
|
|
698
431
|
payload
|
|
699
432
|
});
|
|
@@ -702,18 +435,24 @@ var VocoderAPI = class {
|
|
|
702
435
|
return result.projects;
|
|
703
436
|
}
|
|
704
437
|
async regenerateProjectApiKey(userToken, projectId) {
|
|
705
|
-
const response = await fetch(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
);
|
|
713
449
|
const payload = await readPayload(response);
|
|
714
450
|
if (!response.ok) {
|
|
715
451
|
throw new VocoderAPIError({
|
|
716
|
-
message: extractErrorMessage(
|
|
452
|
+
message: extractErrorMessage(
|
|
453
|
+
payload,
|
|
454
|
+
`Failed to regenerate API key (${response.status})`
|
|
455
|
+
),
|
|
717
456
|
status: response.status,
|
|
718
457
|
payload
|
|
719
458
|
});
|
|
@@ -722,18 +461,24 @@ var VocoderAPI = class {
|
|
|
722
461
|
}
|
|
723
462
|
// ── CLI GitHub endpoints ──────────────────────────────────────────────────────
|
|
724
463
|
async startCliGitHubInstall(userToken, params) {
|
|
725
|
-
const response = await fetch(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
);
|
|
733
475
|
const payload = await readPayload(response);
|
|
734
476
|
if (!response.ok) {
|
|
735
477
|
throw new VocoderAPIError({
|
|
736
|
-
message: extractErrorMessage(
|
|
478
|
+
message: extractErrorMessage(
|
|
479
|
+
payload,
|
|
480
|
+
`Failed to start GitHub install (${response.status})`
|
|
481
|
+
),
|
|
737
482
|
status: response.status,
|
|
738
483
|
payload
|
|
739
484
|
});
|
|
@@ -746,15 +491,24 @@ var VocoderAPI = class {
|
|
|
746
491
|
* account is created from the OAuth code in the callback.
|
|
747
492
|
*/
|
|
748
493
|
async startCliGitHubLinkSession(sessionId, callbackPort) {
|
|
749
|
-
const response = await fetch(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
);
|
|
754
505
|
const payload = await readPayload(response);
|
|
755
506
|
if (!response.ok) {
|
|
756
507
|
throw new VocoderAPIError({
|
|
757
|
-
message: extractErrorMessage(
|
|
508
|
+
message: extractErrorMessage(
|
|
509
|
+
payload,
|
|
510
|
+
`Failed to start GitHub link session (${response.status})`
|
|
511
|
+
),
|
|
758
512
|
status: response.status,
|
|
759
513
|
payload
|
|
760
514
|
});
|
|
@@ -773,7 +527,10 @@ var VocoderAPI = class {
|
|
|
773
527
|
const payload = await readPayload(response);
|
|
774
528
|
if (!response.ok) {
|
|
775
529
|
throw new VocoderAPIError({
|
|
776
|
-
message: extractErrorMessage(
|
|
530
|
+
message: extractErrorMessage(
|
|
531
|
+
payload,
|
|
532
|
+
`Failed to start GitHub OAuth (${response.status})`
|
|
533
|
+
),
|
|
777
534
|
status: response.status,
|
|
778
535
|
payload
|
|
779
536
|
});
|
|
@@ -787,7 +544,10 @@ var VocoderAPI = class {
|
|
|
787
544
|
const payload = await readPayload(response);
|
|
788
545
|
if (!response.ok) {
|
|
789
546
|
throw new VocoderAPIError({
|
|
790
|
-
message: extractErrorMessage(
|
|
547
|
+
message: extractErrorMessage(
|
|
548
|
+
payload,
|
|
549
|
+
`Failed to fetch GitHub discovery (${response.status})`
|
|
550
|
+
),
|
|
791
551
|
status: response.status,
|
|
792
552
|
payload
|
|
793
553
|
});
|
|
@@ -806,7 +566,10 @@ var VocoderAPI = class {
|
|
|
806
566
|
const payload = await readPayload(response);
|
|
807
567
|
if (!response.ok) {
|
|
808
568
|
throw new VocoderAPIError({
|
|
809
|
-
message: extractErrorMessage(
|
|
569
|
+
message: extractErrorMessage(
|
|
570
|
+
payload,
|
|
571
|
+
`Failed to claim GitHub installation (${response.status})`
|
|
572
|
+
),
|
|
810
573
|
status: response.status,
|
|
811
574
|
payload
|
|
812
575
|
});
|
|
@@ -821,7 +584,10 @@ var VocoderAPI = class {
|
|
|
821
584
|
const payload = await readPayload(response);
|
|
822
585
|
if (!response.ok) {
|
|
823
586
|
throw new VocoderAPIError({
|
|
824
|
-
message: extractErrorMessage(
|
|
587
|
+
message: extractErrorMessage(
|
|
588
|
+
payload,
|
|
589
|
+
`Failed to list locales (${response.status})`
|
|
590
|
+
),
|
|
825
591
|
status: response.status,
|
|
826
592
|
payload
|
|
827
593
|
});
|
|
@@ -842,7 +608,10 @@ var VocoderAPI = class {
|
|
|
842
608
|
const payload = await readPayload(response);
|
|
843
609
|
if (!response.ok) {
|
|
844
610
|
throw new VocoderAPIError({
|
|
845
|
-
message: extractErrorMessage(
|
|
611
|
+
message: extractErrorMessage(
|
|
612
|
+
payload,
|
|
613
|
+
`Failed to create project (${response.status})`
|
|
614
|
+
),
|
|
846
615
|
status: response.status,
|
|
847
616
|
payload
|
|
848
617
|
});
|
|
@@ -889,19 +658,60 @@ var VocoderAPI = class {
|
|
|
889
658
|
const payload = await readPayload(response);
|
|
890
659
|
if (!response.ok) {
|
|
891
660
|
throw new VocoderAPIError({
|
|
892
|
-
message: extractErrorMessage(
|
|
661
|
+
message: extractErrorMessage(
|
|
662
|
+
payload,
|
|
663
|
+
`Failed to create project app (${response.status})`
|
|
664
|
+
),
|
|
893
665
|
status: response.status,
|
|
894
666
|
payload
|
|
895
667
|
});
|
|
896
668
|
}
|
|
897
|
-
return payload;
|
|
669
|
+
return payload;
|
|
670
|
+
}
|
|
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;
|
|
898
700
|
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
+
}
|
|
905
715
|
|
|
906
716
|
// src/utils/git-identity.ts
|
|
907
717
|
import { execSync } from "child_process";
|
|
@@ -972,49 +782,332 @@ function toCanonical(host, ownerRepoPath) {
|
|
|
972
782
|
if (host.includes("bitbucket.org")) {
|
|
973
783
|
return `bitbucket:${ownerRepoPath.toLowerCase()}`;
|
|
974
784
|
}
|
|
975
|
-
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
785
|
+
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
786
|
+
}
|
|
787
|
+
function resolveGitRepositoryIdentity() {
|
|
788
|
+
const remoteUrl = safeExec("git config --get remote.origin.url");
|
|
789
|
+
if (!remoteUrl) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
793
|
+
if (!parsed) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
const repositoryRoot = safeExec("git rev-parse --show-toplevel");
|
|
797
|
+
const currentDirectory = process.cwd();
|
|
798
|
+
let repoAppDir = "";
|
|
799
|
+
if (repositoryRoot) {
|
|
800
|
+
const relativePath = relative(
|
|
801
|
+
resolve(repositoryRoot),
|
|
802
|
+
resolve(currentDirectory)
|
|
803
|
+
).replace(/\\/g, "/").trim();
|
|
804
|
+
if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
|
|
805
|
+
repoAppDir = relativePath;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
810
|
+
repoAppDir
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function resolveGitContext() {
|
|
814
|
+
const warnings = [];
|
|
815
|
+
const identity = resolveGitRepositoryIdentity();
|
|
816
|
+
if (!identity) {
|
|
817
|
+
warnings.push(
|
|
818
|
+
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
819
|
+
);
|
|
820
|
+
}
|
|
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;
|
|
976
1020
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1021
|
+
async function runGitHubDiscoveryFlow(params) {
|
|
1022
|
+
let server = null;
|
|
1023
|
+
try {
|
|
1024
|
+
server = await startCallbackServer();
|
|
1025
|
+
} catch {
|
|
981
1026
|
}
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
985
1045
|
}
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
1071
|
}
|
|
994
1072
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1073
|
+
oauthSpinner.stop("GitHub account authorized");
|
|
1074
|
+
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
1075
|
+
params.userToken
|
|
1076
|
+
);
|
|
1077
|
+
return discoveryResult.installations;
|
|
999
1078
|
}
|
|
1000
|
-
function
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
});
|
|
1007
1094
|
}
|
|
1008
|
-
|
|
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);
|
|
1009
1102
|
}
|
|
1010
1103
|
|
|
1011
1104
|
// src/utils/project-create.ts
|
|
1012
1105
|
import * as p3 from "@clack/prompts";
|
|
1013
1106
|
import chalk4 from "chalk";
|
|
1014
1107
|
|
|
1015
|
-
// src/utils/
|
|
1016
|
-
import {
|
|
1017
|
-
import
|
|
1108
|
+
// src/utils/branch-select.ts
|
|
1109
|
+
import { execSync as execSync2 } from "child_process";
|
|
1110
|
+
import { isCancel as isCancel2, Prompt } from "@clack/core";
|
|
1018
1111
|
import chalk2 from "chalk";
|
|
1019
1112
|
var S_BAR = "\u2502";
|
|
1020
1113
|
var S_BAR_END = "\u2514";
|
|
@@ -1041,57 +1134,134 @@ function symbol(state) {
|
|
|
1041
1134
|
return cyan(S_ACTIVE);
|
|
1042
1135
|
}
|
|
1043
1136
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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;
|
|
1047
1197
|
const lower = query.toLowerCase();
|
|
1048
|
-
return
|
|
1049
|
-
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1050
|
-
);
|
|
1198
|
+
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
1051
1199
|
}
|
|
1052
|
-
function buildList(filtered, cursor, scrollOffset, selected) {
|
|
1053
|
-
const
|
|
1200
|
+
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
1201
|
+
const lines = [];
|
|
1054
1202
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
1055
|
-
const visibleLines = [];
|
|
1056
1203
|
for (let i = scrollOffset; i < end; i++) {
|
|
1057
|
-
const
|
|
1058
|
-
const isCursor = i === cursor;
|
|
1059
|
-
const isChecked =
|
|
1060
|
-
const icon =
|
|
1061
|
-
|
|
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`));
|
|
1062
1222
|
}
|
|
1063
1223
|
const hidden = filtered.length - (end - scrollOffset);
|
|
1064
|
-
if (hidden > 0)
|
|
1065
|
-
if (
|
|
1066
|
-
|
|
1067
|
-
|
|
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`));
|
|
1068
1229
|
}
|
|
1069
|
-
return
|
|
1230
|
+
return lines.join("\n");
|
|
1070
1231
|
}
|
|
1071
|
-
async function
|
|
1072
|
-
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 ?? []);
|
|
1073
1236
|
let filter = "";
|
|
1074
1237
|
let cursor = 0;
|
|
1075
1238
|
let scrollOffset = 0;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
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
|
+
};
|
|
1082
1249
|
const clampCursor = (filtered) => {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (cursor
|
|
1086
|
-
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
|
+
}
|
|
1087
1259
|
};
|
|
1088
1260
|
const prompt = new Prompt(
|
|
1089
1261
|
{
|
|
1090
|
-
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
1091
1262
|
validate() {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (!multi && !f[cursor]) return "Please select a language.";
|
|
1263
|
+
if (!optional && selected.size === 0)
|
|
1264
|
+
return "At least one branch is required.";
|
|
1095
1265
|
return void 0;
|
|
1096
1266
|
},
|
|
1097
1267
|
render() {
|
|
@@ -1100,11 +1270,11 @@ async function runFilterablePrompt(opts) {
|
|
|
1100
1270
|
const hdr = `${dim(S_BAR)}
|
|
1101
1271
|
${symbol(this.state)} ${message}
|
|
1102
1272
|
`;
|
|
1103
|
-
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");
|
|
1104
1274
|
switch (this.state) {
|
|
1105
1275
|
case "submit": {
|
|
1106
|
-
const
|
|
1107
|
-
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}`;
|
|
1108
1278
|
}
|
|
1109
1279
|
case "cancel":
|
|
1110
1280
|
return `${hdr}${dim(S_BAR)}`;
|
|
@@ -1112,7 +1282,17 @@ ${symbol(this.state)} ${message}
|
|
|
1112
1282
|
return [
|
|
1113
1283
|
hdr.trimEnd(),
|
|
1114
1284
|
`${ylw(S_BAR)} ${dim("/")} ${hint}`,
|
|
1115
|
-
buildList(
|
|
1285
|
+
buildList(
|
|
1286
|
+
filtered,
|
|
1287
|
+
cursor,
|
|
1288
|
+
scrollOffset,
|
|
1289
|
+
selected,
|
|
1290
|
+
filter,
|
|
1291
|
+
customPatterns,
|
|
1292
|
+
addCursor,
|
|
1293
|
+
optional,
|
|
1294
|
+
excludedSet
|
|
1295
|
+
),
|
|
1116
1296
|
`${ylw(S_BAR_END)} ${ylw(this.error)}`,
|
|
1117
1297
|
""
|
|
1118
1298
|
].join("\n");
|
|
@@ -1120,7 +1300,17 @@ ${symbol(this.state)} ${message}
|
|
|
1120
1300
|
return [
|
|
1121
1301
|
hdr.trimEnd(),
|
|
1122
1302
|
`${cyan(S_BAR)} ${dim("/")} ${hint}`,
|
|
1123
|
-
buildList(
|
|
1303
|
+
buildList(
|
|
1304
|
+
filtered,
|
|
1305
|
+
cursor,
|
|
1306
|
+
scrollOffset,
|
|
1307
|
+
selected,
|
|
1308
|
+
filter,
|
|
1309
|
+
customPatterns,
|
|
1310
|
+
addCursor,
|
|
1311
|
+
optional,
|
|
1312
|
+
excludedSet
|
|
1313
|
+
),
|
|
1124
1314
|
`${cyan(S_BAR_END)}`,
|
|
1125
1315
|
""
|
|
1126
1316
|
].join("\n");
|
|
@@ -1128,7 +1318,6 @@ ${symbol(this.state)} ${message}
|
|
|
1128
1318
|
}
|
|
1129
1319
|
},
|
|
1130
1320
|
false
|
|
1131
|
-
// trackValue=false — we manage value manually
|
|
1132
1321
|
);
|
|
1133
1322
|
prompt.on("key", (key) => {
|
|
1134
1323
|
if (!key || key === " ") return;
|
|
@@ -1137,69 +1326,65 @@ ${symbol(this.state)} ${message}
|
|
|
1137
1326
|
filter = filter.slice(0, -1);
|
|
1138
1327
|
cursor = 0;
|
|
1139
1328
|
scrollOffset = 0;
|
|
1329
|
+
addCursor = false;
|
|
1140
1330
|
} else if (cp >= 32 && cp !== 127) {
|
|
1141
1331
|
filter += key;
|
|
1142
1332
|
cursor = 0;
|
|
1143
1333
|
scrollOffset = 0;
|
|
1334
|
+
addCursor = false;
|
|
1144
1335
|
}
|
|
1145
1336
|
});
|
|
1146
1337
|
prompt.on("cursor", (action) => {
|
|
1147
1338
|
const filtered = getFiltered();
|
|
1339
|
+
const hasAdd = isNewPattern();
|
|
1148
1340
|
switch (action) {
|
|
1149
1341
|
case "up":
|
|
1150
|
-
|
|
1342
|
+
if (addCursor) {
|
|
1343
|
+
addCursor = false;
|
|
1344
|
+
cursor = Math.max(0, filtered.length - 1);
|
|
1345
|
+
} else cursor = Math.max(0, cursor - 1);
|
|
1151
1346
|
break;
|
|
1152
1347
|
case "down":
|
|
1153
|
-
cursor
|
|
1348
|
+
if (!addCursor && cursor >= filtered.length - 1 && hasAdd)
|
|
1349
|
+
addCursor = true;
|
|
1350
|
+
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1154
1351
|
break;
|
|
1155
1352
|
case "space":
|
|
1156
|
-
if (
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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);
|
|
1161
1369
|
}
|
|
1162
1370
|
}
|
|
1163
1371
|
break;
|
|
1164
1372
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
});
|
|
1170
|
-
prompt.on("finalize", () => {
|
|
1171
|
-
if (prompt.state === "submit") {
|
|
1172
|
-
if (multi) {
|
|
1173
|
-
prompt.value = Array.from(selected);
|
|
1174
|
-
} else {
|
|
1175
|
-
const f = getFiltered();
|
|
1176
|
-
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
1177
|
-
}
|
|
1373
|
+
});
|
|
1374
|
+
prompt.on("finalize", () => {
|
|
1375
|
+
if (prompt.state === "submit") {
|
|
1376
|
+
prompt.value = Array.from(selected);
|
|
1178
1377
|
}
|
|
1179
1378
|
});
|
|
1180
1379
|
const result = await prompt.prompt();
|
|
1181
1380
|
if (isCancel2(result)) return null;
|
|
1182
1381
|
return result;
|
|
1183
1382
|
}
|
|
1184
|
-
async function searchSelectLocale(options, message, initialValue) {
|
|
1185
|
-
const result = await runFilterablePrompt({ message, options, multi: false, initialValue });
|
|
1186
|
-
return typeof result === "string" ? result : null;
|
|
1187
|
-
}
|
|
1188
|
-
async function searchMultiSelectLocales(options, message, initialValues) {
|
|
1189
|
-
const result = await runFilterablePrompt({ message, options, multi: true, initialValues });
|
|
1190
|
-
if (result === null) return null;
|
|
1191
|
-
const picks = result;
|
|
1192
|
-
if (picks.length === 0) {
|
|
1193
|
-
p2.log.warn("At least one target language is required. Please select at least one.");
|
|
1194
|
-
return searchMultiSelectLocales(options, message, initialValues);
|
|
1195
|
-
}
|
|
1196
|
-
return picks;
|
|
1197
|
-
}
|
|
1198
1383
|
|
|
1199
|
-
// src/utils/
|
|
1200
|
-
import {
|
|
1384
|
+
// src/utils/locale-search.ts
|
|
1385
|
+
import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
|
|
1386
|
+
import * as p2 from "@clack/prompts";
|
|
1201
1387
|
import chalk3 from "chalk";
|
|
1202
|
-
import { execSync as execSync2 } from "child_process";
|
|
1203
1388
|
var S_BAR2 = "\u2502";
|
|
1204
1389
|
var S_BAR_END2 = "\u2514";
|
|
1205
1390
|
var S_ACTIVE2 = "\u25C6";
|
|
@@ -1225,122 +1410,64 @@ function symbol2(state) {
|
|
|
1225
1410
|
return cyan2(S_ACTIVE2);
|
|
1226
1411
|
}
|
|
1227
1412
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const localOut = execSync2("git branch", { cwd: workDir, stdio: "pipe" }).toString();
|
|
1232
|
-
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
1233
|
-
let remoteBranches = [];
|
|
1234
|
-
try {
|
|
1235
|
-
const remoteOut = execSync2("git branch -r", { cwd: workDir, stdio: "pipe" }).toString();
|
|
1236
|
-
remoteBranches = remoteOut.split("\n").map((b) => b.trim()).filter((b) => b && !b.includes("HEAD")).map((b) => b.replace(/^[^/]+\//, ""));
|
|
1237
|
-
} catch {
|
|
1238
|
-
}
|
|
1239
|
-
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
1240
|
-
let defaultBranch = "main";
|
|
1241
|
-
try {
|
|
1242
|
-
const ref = execSync2("git symbolic-ref refs/remotes/origin/HEAD", { cwd: workDir, stdio: "pipe" }).toString().trim();
|
|
1243
|
-
defaultBranch = ref.split("/").pop() ?? "main";
|
|
1244
|
-
} catch {
|
|
1245
|
-
}
|
|
1246
|
-
return {
|
|
1247
|
-
branches: branches.length > 0 ? branches : [defaultBranch],
|
|
1248
|
-
defaultBranch
|
|
1249
|
-
};
|
|
1250
|
-
} catch {
|
|
1251
|
-
return { branches: ["main"], defaultBranch: "main" };
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
var INVALID_CHARS = /[\s?^~:[\]\\]/;
|
|
1255
|
-
function validateBranchPattern(pattern) {
|
|
1256
|
-
const t = pattern.trim();
|
|
1257
|
-
if (!t) return "Pattern cannot be empty";
|
|
1258
|
-
if (INVALID_CHARS.test(t)) return "Invalid characters \u2014 avoid spaces, ?, ^, ~, :, [, ], \\";
|
|
1259
|
-
if (t.startsWith("/") || t.endsWith("/")) return "Cannot start or end with /";
|
|
1260
|
-
if (t.includes("//")) return "Cannot contain //";
|
|
1261
|
-
return null;
|
|
1262
|
-
}
|
|
1263
|
-
var MAX_VISIBLE2 = 10;
|
|
1264
|
-
function buildItems(branches, defaultBranch, customPatterns) {
|
|
1265
|
-
const items = branches.map((b) => ({
|
|
1266
|
-
value: b,
|
|
1267
|
-
label: b === defaultBranch ? `${b} (default branch)` : b
|
|
1268
|
-
}));
|
|
1269
|
-
for (const pt of customPatterns) {
|
|
1270
|
-
if (!branches.includes(pt)) {
|
|
1271
|
-
items.push({ value: pt, label: pt, isCustom: true });
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return items;
|
|
1275
|
-
}
|
|
1276
|
-
function filterItems(items, query) {
|
|
1277
|
-
if (!query.trim()) return items;
|
|
1413
|
+
var MAX_VISIBLE2 = 12;
|
|
1414
|
+
function filterLocales(options, query) {
|
|
1415
|
+
if (!query.trim()) return options;
|
|
1278
1416
|
const lower = query.toLowerCase();
|
|
1279
|
-
return
|
|
1417
|
+
return options.filter(
|
|
1418
|
+
(o) => o.bcp47.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1419
|
+
);
|
|
1280
1420
|
}
|
|
1281
|
-
function buildList2(filtered, cursor, scrollOffset, selected
|
|
1282
|
-
const
|
|
1421
|
+
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
1422
|
+
const isMulti = selected !== null;
|
|
1283
1423
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
1424
|
+
const visibleLines = [];
|
|
1284
1425
|
for (let i = scrollOffset; i < end; i++) {
|
|
1285
|
-
const
|
|
1286
|
-
const isCursor = i === cursor
|
|
1287
|
-
const isChecked = selected.has(
|
|
1288
|
-
const icon = isChecked ? isCursor ? grn2("\u25FC") : "\u25FC" : isCursor ? grn2("\u25FB") : dim2("\u25FB");
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
const trimmed = filter.trim();
|
|
1294
|
-
const allItems = [...filtered];
|
|
1295
|
-
const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
1296
|
-
if (isNewPattern) {
|
|
1297
|
-
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
1298
|
-
const icon = addCursor ? grn2("\u25FB") : dim2("\u25FB");
|
|
1299
|
-
const label = err ? `${ylw2("+")} ${dim2(`"${trimmed}" \u2014 ${err}`)}` : `${grn2("+")} Add "${trimmed}" as branch pattern`;
|
|
1300
|
-
lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
|
|
1301
|
-
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
1302
|
-
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
|
+
);
|
|
1303
1433
|
}
|
|
1304
1434
|
const hidden = filtered.length - (end - scrollOffset);
|
|
1305
|
-
if (hidden > 0)
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
+
);
|
|
1310
1442
|
}
|
|
1311
|
-
return
|
|
1443
|
+
return visibleLines.join("\n");
|
|
1312
1444
|
}
|
|
1313
|
-
async function
|
|
1314
|
-
const { message,
|
|
1315
|
-
const optional = params.optional ?? false;
|
|
1316
|
-
const excludedSet = new Set(params.excludedPatterns ?? []);
|
|
1445
|
+
async function runFilterablePrompt(opts) {
|
|
1446
|
+
const { message, options, multi } = opts;
|
|
1317
1447
|
let filter = "";
|
|
1318
1448
|
let cursor = 0;
|
|
1319
1449
|
let scrollOffset = 0;
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1326
|
-
const t = filter.trim();
|
|
1327
|
-
if (!t) return false;
|
|
1328
|
-
return !getItems().some((i) => i.value === t) && !customPatterns.includes(t);
|
|
1329
|
-
};
|
|
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);
|
|
1330
1456
|
const clampCursor = (filtered) => {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
if (cursor
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (cursor >= scrollOffset + MAX_VISIBLE2) scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
1337
|
-
if (scrollOffset < 0) scrollOffset = 0;
|
|
1338
|
-
}
|
|
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;
|
|
1339
1462
|
};
|
|
1340
1463
|
const prompt = new Prompt2(
|
|
1341
1464
|
{
|
|
1465
|
+
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
1342
1466
|
validate() {
|
|
1343
|
-
|
|
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.";
|
|
1344
1471
|
return void 0;
|
|
1345
1472
|
},
|
|
1346
1473
|
render() {
|
|
@@ -1349,11 +1476,13 @@ async function filterableBranchSelect(params) {
|
|
|
1349
1476
|
const hdr = `${dim2(S_BAR2)}
|
|
1350
1477
|
${symbol2(this.state)} ${message}
|
|
1351
1478
|
`;
|
|
1352
|
-
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
|
+
);
|
|
1353
1482
|
switch (this.state) {
|
|
1354
1483
|
case "submit": {
|
|
1355
|
-
const
|
|
1356
|
-
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"))}`;
|
|
1357
1486
|
}
|
|
1358
1487
|
case "cancel":
|
|
1359
1488
|
return `${hdr}${dim2(S_BAR2)}`;
|
|
@@ -1361,7 +1490,12 @@ ${symbol2(this.state)} ${message}
|
|
|
1361
1490
|
return [
|
|
1362
1491
|
hdr.trimEnd(),
|
|
1363
1492
|
`${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
|
|
1364
|
-
buildList2(
|
|
1493
|
+
buildList2(
|
|
1494
|
+
filtered,
|
|
1495
|
+
cursor,
|
|
1496
|
+
scrollOffset,
|
|
1497
|
+
multi ? selected : null
|
|
1498
|
+
),
|
|
1365
1499
|
`${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
|
|
1366
1500
|
""
|
|
1367
1501
|
].join("\n");
|
|
@@ -1369,7 +1503,12 @@ ${symbol2(this.state)} ${message}
|
|
|
1369
1503
|
return [
|
|
1370
1504
|
hdr.trimEnd(),
|
|
1371
1505
|
`${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
|
|
1372
|
-
buildList2(
|
|
1506
|
+
buildList2(
|
|
1507
|
+
filtered,
|
|
1508
|
+
cursor,
|
|
1509
|
+
scrollOffset,
|
|
1510
|
+
multi ? selected : null
|
|
1511
|
+
),
|
|
1373
1512
|
`${cyan2(S_BAR_END2)}`,
|
|
1374
1513
|
""
|
|
1375
1514
|
].join("\n");
|
|
@@ -1377,6 +1516,7 @@ ${symbol2(this.state)} ${message}
|
|
|
1377
1516
|
}
|
|
1378
1517
|
},
|
|
1379
1518
|
false
|
|
1519
|
+
// trackValue=false — we manage value manually
|
|
1380
1520
|
);
|
|
1381
1521
|
prompt.on("key", (key) => {
|
|
1382
1522
|
if (!key || key === " ") return;
|
|
@@ -1385,59 +1525,76 @@ ${symbol2(this.state)} ${message}
|
|
|
1385
1525
|
filter = filter.slice(0, -1);
|
|
1386
1526
|
cursor = 0;
|
|
1387
1527
|
scrollOffset = 0;
|
|
1388
|
-
addCursor = false;
|
|
1389
1528
|
} else if (cp >= 32 && cp !== 127) {
|
|
1390
1529
|
filter += key;
|
|
1391
1530
|
cursor = 0;
|
|
1392
1531
|
scrollOffset = 0;
|
|
1393
|
-
addCursor = false;
|
|
1394
1532
|
}
|
|
1395
1533
|
});
|
|
1396
1534
|
prompt.on("cursor", (action) => {
|
|
1397
1535
|
const filtered = getFiltered();
|
|
1398
|
-
const hasAdd = isNewPattern();
|
|
1399
1536
|
switch (action) {
|
|
1400
1537
|
case "up":
|
|
1401
|
-
|
|
1402
|
-
addCursor = false;
|
|
1403
|
-
cursor = Math.max(0, filtered.length - 1);
|
|
1404
|
-
} else cursor = Math.max(0, cursor - 1);
|
|
1538
|
+
cursor = Math.max(0, cursor - 1);
|
|
1405
1539
|
break;
|
|
1406
1540
|
case "down":
|
|
1407
|
-
|
|
1408
|
-
else if (!addCursor) cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
1541
|
+
cursor = Math.min(Math.max(filtered.length - 1, 0), cursor + 1);
|
|
1409
1542
|
break;
|
|
1410
1543
|
case "space":
|
|
1411
|
-
if (
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
selected.add(t);
|
|
1417
|
-
filter = "";
|
|
1418
|
-
cursor = 0;
|
|
1419
|
-
scrollOffset = 0;
|
|
1420
|
-
addCursor = false;
|
|
1421
|
-
}
|
|
1422
|
-
} else {
|
|
1423
|
-
const item = filtered[cursor];
|
|
1424
|
-
if (item) {
|
|
1425
|
-
if (selected.has(item.value)) selected.delete(item.value);
|
|
1426
|
-
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);
|
|
1427
1549
|
}
|
|
1428
1550
|
}
|
|
1429
1551
|
break;
|
|
1430
1552
|
}
|
|
1553
|
+
if (!multi) {
|
|
1554
|
+
const opt = getFiltered()[cursor];
|
|
1555
|
+
prompt.value = opt?.bcp47 ?? null;
|
|
1556
|
+
}
|
|
1431
1557
|
});
|
|
1432
1558
|
prompt.on("finalize", () => {
|
|
1433
1559
|
if (prompt.state === "submit") {
|
|
1434
|
-
|
|
1560
|
+
if (multi) {
|
|
1561
|
+
prompt.value = Array.from(selected);
|
|
1562
|
+
} else {
|
|
1563
|
+
const f = getFiltered();
|
|
1564
|
+
prompt.value = f[cursor]?.bcp47 ?? null;
|
|
1565
|
+
}
|
|
1435
1566
|
}
|
|
1436
1567
|
});
|
|
1437
1568
|
const result = await prompt.prompt();
|
|
1438
1569
|
if (isCancel3(result)) return null;
|
|
1439
1570
|
return result;
|
|
1440
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
|
+
}
|
|
1441
1598
|
|
|
1442
1599
|
// src/utils/project-create.ts
|
|
1443
1600
|
function buildLocaleOptions(locales) {
|
|
@@ -1466,7 +1623,9 @@ async function runProjectCreate(params) {
|
|
|
1466
1623
|
try {
|
|
1467
1624
|
rawLocales = await api.listLocales(userToken);
|
|
1468
1625
|
} catch {
|
|
1469
|
-
p3.log.error(
|
|
1626
|
+
p3.log.error(
|
|
1627
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1628
|
+
);
|
|
1470
1629
|
return null;
|
|
1471
1630
|
}
|
|
1472
1631
|
const languageOptions = buildLanguageOptions(rawLocales);
|
|
@@ -1483,7 +1642,8 @@ async function runProjectCreate(params) {
|
|
|
1483
1642
|
validate(value) {
|
|
1484
1643
|
const v = value.trim();
|
|
1485
1644
|
if (!v) return;
|
|
1486
|
-
if (v.startsWith("/"))
|
|
1645
|
+
if (v.startsWith("/"))
|
|
1646
|
+
return "Use a relative path, not an absolute path";
|
|
1487
1647
|
if (v.includes("..")) return 'Path must not contain ".."';
|
|
1488
1648
|
}
|
|
1489
1649
|
});
|
|
@@ -1496,14 +1656,18 @@ async function runProjectCreate(params) {
|
|
|
1496
1656
|
params.defaultSourceLocale ?? "en"
|
|
1497
1657
|
);
|
|
1498
1658
|
if (sourceLocale === null) return null;
|
|
1499
|
-
const targetOptions = localeOptions.filter(
|
|
1659
|
+
const targetOptions = localeOptions.filter(
|
|
1660
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1661
|
+
);
|
|
1500
1662
|
const targetLocales = await searchMultiSelectLocales(
|
|
1501
1663
|
targetOptions,
|
|
1502
1664
|
"Target languages (languages to translate into)"
|
|
1503
1665
|
);
|
|
1504
1666
|
if (targetLocales === null) return null;
|
|
1505
1667
|
if (targetLocales.length === 0) {
|
|
1506
|
-
p3.log.warn(
|
|
1668
|
+
p3.log.warn(
|
|
1669
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1670
|
+
);
|
|
1507
1671
|
}
|
|
1508
1672
|
const detected = detectGitBranches();
|
|
1509
1673
|
const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
|
|
@@ -1519,7 +1683,9 @@ async function runProjectCreate(params) {
|
|
|
1519
1683
|
});
|
|
1520
1684
|
if (result === null) return null;
|
|
1521
1685
|
if (result.length === 0) {
|
|
1522
|
-
p3.log.warn(
|
|
1686
|
+
p3.log.warn(
|
|
1687
|
+
"At least one branch is required. Please select at least one."
|
|
1688
|
+
);
|
|
1523
1689
|
initial = [detected.defaultBranch];
|
|
1524
1690
|
} else {
|
|
1525
1691
|
pushBranches = result;
|
|
@@ -1552,7 +1718,9 @@ async function runProjectAppCreate(params) {
|
|
|
1552
1718
|
try {
|
|
1553
1719
|
rawLocales = await api.listLocales(userToken);
|
|
1554
1720
|
} catch {
|
|
1555
|
-
p3.log.error(
|
|
1721
|
+
p3.log.error(
|
|
1722
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
1723
|
+
);
|
|
1556
1724
|
return null;
|
|
1557
1725
|
}
|
|
1558
1726
|
const languageOptions = buildLanguageOptions(rawLocales);
|
|
@@ -1573,11 +1741,15 @@ async function runProjectAppCreate(params) {
|
|
|
1573
1741
|
initialValue: params.defaultAppDir ?? "",
|
|
1574
1742
|
validate(value) {
|
|
1575
1743
|
const v = value.trim();
|
|
1576
|
-
if (!v && hasWholeRepoApp)
|
|
1577
|
-
|
|
1578
|
-
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.";
|
|
1579
1750
|
if (v.includes("..")) return 'Path must not contain "..".';
|
|
1580
|
-
if (existingScopes.has(v))
|
|
1751
|
+
if (existingScopes.has(v))
|
|
1752
|
+
return `"${v}" is already configured. Choose a different directory.`;
|
|
1581
1753
|
}
|
|
1582
1754
|
});
|
|
1583
1755
|
if (p3.isCancel(rawScope)) return null;
|
|
@@ -1589,14 +1761,18 @@ async function runProjectAppCreate(params) {
|
|
|
1589
1761
|
"en"
|
|
1590
1762
|
);
|
|
1591
1763
|
if (sourceLocale === null) return null;
|
|
1592
|
-
const targetOptions = localeOptions.filter(
|
|
1764
|
+
const targetOptions = localeOptions.filter(
|
|
1765
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
1766
|
+
);
|
|
1593
1767
|
const targetLocales = await searchMultiSelectLocales(
|
|
1594
1768
|
targetOptions,
|
|
1595
1769
|
"Target languages"
|
|
1596
1770
|
);
|
|
1597
1771
|
if (targetLocales === null) return null;
|
|
1598
1772
|
if (targetLocales.length === 0) {
|
|
1599
|
-
p3.log.warn(
|
|
1773
|
+
p3.log.warn(
|
|
1774
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
1775
|
+
);
|
|
1600
1776
|
}
|
|
1601
1777
|
const detectedApp = detectGitBranches();
|
|
1602
1778
|
let appPushBranches = [];
|
|
@@ -1628,7 +1804,9 @@ async function runProjectAppCreate(params) {
|
|
|
1628
1804
|
targetBranches,
|
|
1629
1805
|
repoCanonical: repoCanonical ?? ""
|
|
1630
1806
|
});
|
|
1631
|
-
p3.log.success(
|
|
1807
|
+
p3.log.success(
|
|
1808
|
+
`App ${chalk4.bold(appDir)} added to ${chalk4.bold(projectName)}!`
|
|
1809
|
+
);
|
|
1632
1810
|
return {
|
|
1633
1811
|
projectId: result.projectId,
|
|
1634
1812
|
projectName: result.projectName,
|
|
@@ -1682,7 +1860,6 @@ async function selectWorkspace(result) {
|
|
|
1682
1860
|
}
|
|
1683
1861
|
|
|
1684
1862
|
// src/commands/init.ts
|
|
1685
|
-
import { spawn as spawn2 } from "child_process";
|
|
1686
1863
|
loadEnv();
|
|
1687
1864
|
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
1688
1865
|
async function sleep(ms) {
|
|
@@ -1755,7 +1932,10 @@ function runScaffold(params) {
|
|
|
1755
1932
|
}
|
|
1756
1933
|
const packagesToInstall = getPackagesToInstall(detection);
|
|
1757
1934
|
if (packagesToInstall.length > 0) {
|
|
1758
|
-
const installCmd = buildInstallCommand(
|
|
1935
|
+
const installCmd = buildInstallCommand(
|
|
1936
|
+
detection.packageManager,
|
|
1937
|
+
packagesToInstall
|
|
1938
|
+
);
|
|
1759
1939
|
p5.log.info("");
|
|
1760
1940
|
const installSpinner = p5.spinner();
|
|
1761
1941
|
installSpinner.start(`Installing ${packagesToInstall.join(", ")}...`);
|
|
@@ -1778,12 +1958,16 @@ function runScaffold(params) {
|
|
|
1778
1958
|
let stepNum = 1;
|
|
1779
1959
|
if (snippets.pluginStep) {
|
|
1780
1960
|
p5.log.message("");
|
|
1781
|
-
p5.log.step(
|
|
1961
|
+
p5.log.step(
|
|
1962
|
+
`${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`
|
|
1963
|
+
);
|
|
1782
1964
|
printCodeBlock(snippets.pluginStep.code);
|
|
1783
1965
|
stepNum++;
|
|
1784
1966
|
}
|
|
1785
1967
|
if (snippets.providerStep) {
|
|
1786
|
-
p5.log.step(
|
|
1968
|
+
p5.log.step(
|
|
1969
|
+
`${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`
|
|
1970
|
+
);
|
|
1787
1971
|
printCodeBlock(snippets.providerStep.code);
|
|
1788
1972
|
stepNum++;
|
|
1789
1973
|
}
|
|
@@ -1805,6 +1989,7 @@ function printMcpSetup(apiKey) {
|
|
|
1805
1989
|
type: "stdio",
|
|
1806
1990
|
command: "npx",
|
|
1807
1991
|
args: ["-y", "@vocoder/mcp"],
|
|
1992
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: MCP config template, not a JS template literal
|
|
1808
1993
|
env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
|
|
1809
1994
|
}
|
|
1810
1995
|
}
|
|
@@ -1818,7 +2003,9 @@ function printMcpSetup(apiKey) {
|
|
|
1818
2003
|
p5.log.message("");
|
|
1819
2004
|
printCodeBlock(addCommand);
|
|
1820
2005
|
p5.log.message("");
|
|
1821
|
-
p5.log.message(
|
|
2006
|
+
p5.log.message(
|
|
2007
|
+
"To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference"
|
|
2008
|
+
);
|
|
1822
2009
|
p5.log.message("so each developer supplies their own key:");
|
|
1823
2010
|
p5.log.message("");
|
|
1824
2011
|
printCodeBlock(teamConfig);
|
|
@@ -1827,19 +2014,26 @@ function printMcpSetup(apiKey) {
|
|
|
1827
2014
|
}
|
|
1828
2015
|
function printCodeBlock(code) {
|
|
1829
2016
|
const lines = code.split("\n");
|
|
1830
|
-
const maxLen = lines.reduce(
|
|
2017
|
+
const maxLen = lines.reduce(
|
|
2018
|
+
(max, line) => Math.max(max, line.length),
|
|
2019
|
+
0
|
|
2020
|
+
);
|
|
1831
2021
|
const bar = chalk6.gray("\u2502");
|
|
1832
2022
|
const pad = (s) => s + " ".repeat(maxLen - s.length);
|
|
1833
2023
|
process.stdout.write(`${chalk6.gray("\u2502")}
|
|
1834
2024
|
`);
|
|
1835
|
-
process.stdout.write(
|
|
1836
|
-
`)
|
|
2025
|
+
process.stdout.write(
|
|
2026
|
+
`${chalk6.gray("\u2502")} ${chalk6.gray(`\u250C${"\u2500".repeat(maxLen + 2)}\u2510`)}
|
|
2027
|
+
`
|
|
2028
|
+
);
|
|
1837
2029
|
for (const line of lines) {
|
|
1838
2030
|
process.stdout.write(`${chalk6.gray("\u2502")} ${bar} ${pad(line)} ${bar}
|
|
1839
2031
|
`);
|
|
1840
2032
|
}
|
|
1841
|
-
process.stdout.write(
|
|
1842
|
-
`)
|
|
2033
|
+
process.stdout.write(
|
|
2034
|
+
`${chalk6.gray("\u2502")} ${chalk6.gray(`\u2514${"\u2500".repeat(maxLen + 2)}\u2518`)}
|
|
2035
|
+
`
|
|
2036
|
+
);
|
|
1843
2037
|
}
|
|
1844
2038
|
async function verifyStoredToken(api, token) {
|
|
1845
2039
|
try {
|
|
@@ -1871,14 +2065,18 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1871
2065
|
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1872
2066
|
if (reauth) {
|
|
1873
2067
|
if (!options.yes) {
|
|
1874
|
-
const shouldOpen = await p5.confirm({
|
|
2068
|
+
const shouldOpen = await p5.confirm({
|
|
2069
|
+
message: "Open your browser to sign in again?"
|
|
2070
|
+
});
|
|
1875
2071
|
if (p5.isCancel(shouldOpen)) {
|
|
1876
2072
|
server?.close();
|
|
1877
2073
|
p5.cancel("Setup cancelled.");
|
|
1878
2074
|
return null;
|
|
1879
2075
|
}
|
|
1880
2076
|
if (!shouldOpen) {
|
|
1881
|
-
p5.log.info(
|
|
2077
|
+
p5.log.info(
|
|
2078
|
+
"Open the URL above manually in your browser to continue."
|
|
2079
|
+
);
|
|
1882
2080
|
} else {
|
|
1883
2081
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
1884
2082
|
if (!opened) {
|
|
@@ -1895,7 +2093,11 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1895
2093
|
const connectChoice = await p5.select({
|
|
1896
2094
|
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
1897
2095
|
options: [
|
|
1898
|
-
{
|
|
2096
|
+
{
|
|
2097
|
+
value: "install",
|
|
2098
|
+
label: "Install GitHub App",
|
|
2099
|
+
hint: "recommended"
|
|
2100
|
+
},
|
|
1899
2101
|
{ value: "link", label: "Already installed? Link your account" }
|
|
1900
2102
|
]
|
|
1901
2103
|
});
|
|
@@ -1937,7 +2139,9 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1937
2139
|
const timeoutMs = deadline - Date.now();
|
|
1938
2140
|
const params = await Promise.race([
|
|
1939
2141
|
server.waitForCallback(),
|
|
1940
|
-
new Promise(
|
|
2142
|
+
new Promise(
|
|
2143
|
+
(resolve2) => setTimeout(() => resolve2(null), timeoutMs)
|
|
2144
|
+
)
|
|
1941
2145
|
]);
|
|
1942
2146
|
if (params && typeof params.token === "string") {
|
|
1943
2147
|
rawToken = params.token;
|
|
@@ -1978,7 +2182,12 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1978
2182
|
}
|
|
1979
2183
|
const userInfo = await api.getCliUserInfo(rawToken);
|
|
1980
2184
|
authSpinner.stop(`Authenticated as ${chalk6.bold(userInfo.email)}`);
|
|
1981
|
-
return {
|
|
2185
|
+
return {
|
|
2186
|
+
token: rawToken,
|
|
2187
|
+
...userInfo,
|
|
2188
|
+
organizationId: callbackOrganizationId,
|
|
2189
|
+
discoveryReady: callbackDiscoveryReady
|
|
2190
|
+
};
|
|
1982
2191
|
}
|
|
1983
2192
|
async function init(options = {}) {
|
|
1984
2193
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
@@ -2003,7 +2212,9 @@ async function init(options = {}) {
|
|
|
2003
2212
|
if (lookup.exactMatch) {
|
|
2004
2213
|
const { exactMatch } = lookup;
|
|
2005
2214
|
p5.log.success(`Project: ${chalk6.bold(exactMatch.projectName)}`);
|
|
2006
|
-
p5.log.info(
|
|
2215
|
+
p5.log.info(
|
|
2216
|
+
`Branches: ${chalk6.cyan((exactMatch.targetBranches ?? ["main"]).join(", "))}`
|
|
2217
|
+
);
|
|
2007
2218
|
const needsKey = await p5.confirm({
|
|
2008
2219
|
message: "Need to regenerate your API key?"
|
|
2009
2220
|
});
|
|
@@ -2019,12 +2230,17 @@ async function init(options = {}) {
|
|
|
2019
2230
|
const spinner4 = p5.spinner();
|
|
2020
2231
|
spinner4.start("Generating new API key...");
|
|
2021
2232
|
try {
|
|
2022
|
-
const { apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
2233
|
+
const { apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
2234
|
+
authResult.token,
|
|
2235
|
+
exactMatch.projectId
|
|
2236
|
+
);
|
|
2023
2237
|
spinner4.stop("New API key generated");
|
|
2024
2238
|
printMcpSetup(apiKey);
|
|
2025
|
-
} catch {
|
|
2239
|
+
} catch (err) {
|
|
2026
2240
|
spinner4.stop("Failed to generate key");
|
|
2027
|
-
|
|
2241
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2242
|
+
p5.log.error(`Could not generate API key: ${msg}`);
|
|
2243
|
+
p5.log.info("Try again or generate one from the dashboard.");
|
|
2028
2244
|
return 1;
|
|
2029
2245
|
}
|
|
2030
2246
|
}
|
|
@@ -2087,7 +2303,12 @@ async function init(options = {}) {
|
|
|
2087
2303
|
});
|
|
2088
2304
|
}
|
|
2089
2305
|
} else {
|
|
2090
|
-
const authResult = await runAuthFlow(
|
|
2306
|
+
const authResult = await runAuthFlow(
|
|
2307
|
+
api,
|
|
2308
|
+
options,
|
|
2309
|
+
false,
|
|
2310
|
+
identity?.repoCanonical
|
|
2311
|
+
);
|
|
2091
2312
|
if (!authResult) return 1;
|
|
2092
2313
|
userToken = authResult.token;
|
|
2093
2314
|
userEmail = authResult.email;
|
|
@@ -2106,10 +2327,14 @@ async function init(options = {}) {
|
|
|
2106
2327
|
let selectedWorkspaceName;
|
|
2107
2328
|
if (authOrganizationId) {
|
|
2108
2329
|
const workspaceData = await api.listWorkspaces(userToken);
|
|
2109
|
-
const ws = workspaceData.workspaces.find(
|
|
2330
|
+
const ws = workspaceData.workspaces.find(
|
|
2331
|
+
(w) => w.id === authOrganizationId
|
|
2332
|
+
);
|
|
2110
2333
|
selectedWorkspaceId = authOrganizationId;
|
|
2111
2334
|
selectedWorkspaceName = ws?.name ?? userEmail;
|
|
2112
|
-
p5.log.success(
|
|
2335
|
+
p5.log.success(
|
|
2336
|
+
`Connected as ${chalk6.bold(userEmail)} \u2014 workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2337
|
+
);
|
|
2113
2338
|
} else {
|
|
2114
2339
|
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
2115
2340
|
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
@@ -2148,7 +2373,9 @@ async function init(options = {}) {
|
|
|
2148
2373
|
);
|
|
2149
2374
|
}
|
|
2150
2375
|
if (selectedInstallationId === null || selectedInstallationId === "install_new") {
|
|
2151
|
-
p5.cancel(
|
|
2376
|
+
p5.cancel(
|
|
2377
|
+
"Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
|
|
2378
|
+
);
|
|
2152
2379
|
return 1;
|
|
2153
2380
|
}
|
|
2154
2381
|
const claimResult = await api.claimCliGitHubInstallation(userToken, {
|
|
@@ -2164,7 +2391,9 @@ async function init(options = {}) {
|
|
|
2164
2391
|
});
|
|
2165
2392
|
const repoCanonical = identity?.repoCanonical ?? null;
|
|
2166
2393
|
const covering = repoCanonical ? workspaceData.workspaces.filter((w) => w.coversRepo === true) : [];
|
|
2167
|
-
const connected = workspaceData.workspaces.filter(
|
|
2394
|
+
const connected = workspaceData.workspaces.filter(
|
|
2395
|
+
(w) => w.hasGitHubConnection
|
|
2396
|
+
);
|
|
2168
2397
|
if (repoCanonical && covering.length === 1) {
|
|
2169
2398
|
const ws = covering[0];
|
|
2170
2399
|
selectedWorkspaceId = ws.id;
|
|
@@ -2223,9 +2452,15 @@ async function init(options = {}) {
|
|
|
2223
2452
|
);
|
|
2224
2453
|
return 1;
|
|
2225
2454
|
}
|
|
2226
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2455
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2456
|
+
api,
|
|
2457
|
+
userToken,
|
|
2458
|
+
yes: options.yes
|
|
2459
|
+
});
|
|
2227
2460
|
if (!connectResult) {
|
|
2228
|
-
p5.log.error(
|
|
2461
|
+
p5.log.error(
|
|
2462
|
+
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
2463
|
+
);
|
|
2229
2464
|
return 1;
|
|
2230
2465
|
}
|
|
2231
2466
|
selectedWorkspaceId = connectResult.organizationId;
|
|
@@ -2260,22 +2495,42 @@ async function init(options = {}) {
|
|
|
2260
2495
|
return 1;
|
|
2261
2496
|
}
|
|
2262
2497
|
if (connectChoice === "install") {
|
|
2263
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2498
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2499
|
+
api,
|
|
2500
|
+
userToken,
|
|
2501
|
+
yes: options.yes
|
|
2502
|
+
});
|
|
2264
2503
|
if (!connectResult) {
|
|
2265
|
-
p5.log.error(
|
|
2504
|
+
p5.log.error(
|
|
2505
|
+
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
2506
|
+
);
|
|
2266
2507
|
return 1;
|
|
2267
2508
|
}
|
|
2268
2509
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2269
2510
|
selectedWorkspaceName = connectResult.organizationName;
|
|
2270
|
-
p5.log.success(
|
|
2511
|
+
p5.log.success(
|
|
2512
|
+
`Workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2513
|
+
);
|
|
2271
2514
|
} else {
|
|
2272
|
-
const installations = await runGitHubDiscoveryFlow({
|
|
2515
|
+
const installations = await runGitHubDiscoveryFlow({
|
|
2516
|
+
api,
|
|
2517
|
+
userToken,
|
|
2518
|
+
yes: options.yes
|
|
2519
|
+
});
|
|
2273
2520
|
if (!installations) return 1;
|
|
2274
2521
|
if (installations.length === 0) {
|
|
2275
|
-
p5.log.warn(
|
|
2276
|
-
|
|
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
|
+
});
|
|
2277
2528
|
if (p5.isCancel(installNow) || !installNow) return 1;
|
|
2278
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2529
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2530
|
+
api,
|
|
2531
|
+
userToken,
|
|
2532
|
+
yes: options.yes
|
|
2533
|
+
});
|
|
2279
2534
|
if (!connectResult) return 1;
|
|
2280
2535
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2281
2536
|
selectedWorkspaceName = connectResult.organizationName;
|
|
@@ -2295,20 +2550,29 @@ async function init(options = {}) {
|
|
|
2295
2550
|
return 1;
|
|
2296
2551
|
}
|
|
2297
2552
|
if (selectedInstallationId === "install_new") {
|
|
2298
|
-
const connectResult = await runGitHubInstallFlow({
|
|
2553
|
+
const connectResult = await runGitHubInstallFlow({
|
|
2554
|
+
api,
|
|
2555
|
+
userToken,
|
|
2556
|
+
yes: options.yes
|
|
2557
|
+
});
|
|
2299
2558
|
if (!connectResult) return 1;
|
|
2300
2559
|
selectedWorkspaceId = connectResult.organizationId;
|
|
2301
2560
|
selectedWorkspaceName = connectResult.organizationName;
|
|
2302
2561
|
} else {
|
|
2303
|
-
const claimResult = await api.claimCliGitHubInstallation(
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2562
|
+
const claimResult = await api.claimCliGitHubInstallation(
|
|
2563
|
+
userToken,
|
|
2564
|
+
{
|
|
2565
|
+
installationId: String(selectedInstallationId),
|
|
2566
|
+
organizationId: null
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2307
2569
|
selectedWorkspaceId = claimResult.organizationId;
|
|
2308
2570
|
selectedWorkspaceName = claimResult.organizationName;
|
|
2309
2571
|
}
|
|
2310
2572
|
}
|
|
2311
|
-
p5.log.success(
|
|
2573
|
+
p5.log.success(
|
|
2574
|
+
`Workspace: ${chalk6.bold(selectedWorkspaceName)}`
|
|
2575
|
+
);
|
|
2312
2576
|
}
|
|
2313
2577
|
}
|
|
2314
2578
|
}
|
|
@@ -2368,10 +2632,15 @@ async function init(options = {}) {
|
|
|
2368
2632
|
}
|
|
2369
2633
|
if (limitAction === "upgrade") {
|
|
2370
2634
|
await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
|
|
2371
|
-
p5.cancel(
|
|
2635
|
+
p5.cancel(
|
|
2636
|
+
"Upgrade your plan in the browser, then re-run `vocoder init`."
|
|
2637
|
+
);
|
|
2372
2638
|
return 1;
|
|
2373
2639
|
}
|
|
2374
|
-
const existingProjects = await api.listProjects(
|
|
2640
|
+
const existingProjects = await api.listProjects(
|
|
2641
|
+
userToken,
|
|
2642
|
+
selectedWorkspaceId
|
|
2643
|
+
);
|
|
2375
2644
|
if (existingProjects.length === 0) {
|
|
2376
2645
|
p5.log.error("No projects found in this workspace.");
|
|
2377
2646
|
return 1;
|
|
@@ -2478,8 +2747,11 @@ async function logout(options = {}) {
|
|
|
2478
2747
|
}
|
|
2479
2748
|
|
|
2480
2749
|
// src/commands/sync.ts
|
|
2481
|
-
import * as p7 from "@clack/prompts";
|
|
2482
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";
|
|
2483
2755
|
|
|
2484
2756
|
// src/utils/branch.ts
|
|
2485
2757
|
import { execSync as execSync4 } from "child_process";
|
|
@@ -2509,7 +2781,7 @@ function detectBranch(override) {
|
|
|
2509
2781
|
stdio: ["pipe", "pipe", "ignore"]
|
|
2510
2782
|
}).trim();
|
|
2511
2783
|
return branch;
|
|
2512
|
-
} catch (
|
|
2784
|
+
} catch (_error) {
|
|
2513
2785
|
throw new Error(
|
|
2514
2786
|
"Failed to detect git branch. Make sure you are in a git repository or set the --branch flag."
|
|
2515
2787
|
);
|
|
@@ -2547,9 +2819,6 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2547
2819
|
return new RegExp(regexSource).test(branch);
|
|
2548
2820
|
}
|
|
2549
2821
|
|
|
2550
|
-
// src/commands/sync.ts
|
|
2551
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2552
|
-
|
|
2553
2822
|
// src/utils/config.ts
|
|
2554
2823
|
import chalk7 from "chalk";
|
|
2555
2824
|
import { config as loadEnv2 } from "dotenv";
|
|
@@ -2560,9 +2829,13 @@ function validateLocalConfig(config) {
|
|
|
2560
2829
|
}
|
|
2561
2830
|
if (!config.apiKey.startsWith("vcp_")) {
|
|
2562
2831
|
if (config.apiKey.startsWith("vco_") || config.apiKey.startsWith("vcu_")) {
|
|
2563
|
-
throw new Error(
|
|
2832
|
+
throw new Error(
|
|
2833
|
+
"VOCODER_API_KEY must be a project-scoped key (starts with vcp_). Got an org or user key."
|
|
2834
|
+
);
|
|
2564
2835
|
}
|
|
2565
|
-
throw new Error(
|
|
2836
|
+
throw new Error(
|
|
2837
|
+
"Invalid API key format. Expected a project API key starting with vcp_."
|
|
2838
|
+
);
|
|
2566
2839
|
}
|
|
2567
2840
|
if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
|
|
2568
2841
|
throw new Error("Invalid API URL");
|
|
@@ -2666,14 +2939,20 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2666
2939
|
noFallback = cliOptions.noFallback;
|
|
2667
2940
|
configSources.noFallback = "CLI flag";
|
|
2668
2941
|
} else if (envSyncNoFallback) {
|
|
2669
|
-
noFallback = ["1", "true", "yes", "on"].includes(
|
|
2942
|
+
noFallback = ["1", "true", "yes", "on"].includes(
|
|
2943
|
+
envSyncNoFallback.toLowerCase()
|
|
2944
|
+
);
|
|
2670
2945
|
configSources.noFallback = "environment";
|
|
2671
2946
|
}
|
|
2672
2947
|
if (verbose) {
|
|
2673
2948
|
console.log(chalk7.dim("\n Configuration sources:"));
|
|
2674
|
-
console.log(
|
|
2949
|
+
console.log(
|
|
2950
|
+
chalk7.dim(` Include patterns: ${configSources.includePattern}`)
|
|
2951
|
+
);
|
|
2675
2952
|
if (excludePattern.length > 0) {
|
|
2676
|
-
console.log(
|
|
2953
|
+
console.log(
|
|
2954
|
+
chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`)
|
|
2955
|
+
);
|
|
2677
2956
|
}
|
|
2678
2957
|
console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
|
|
2679
2958
|
console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
|
|
@@ -2698,8 +2977,6 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2698
2977
|
}
|
|
2699
2978
|
|
|
2700
2979
|
// src/commands/sync.ts
|
|
2701
|
-
import chalk8 from "chalk";
|
|
2702
|
-
import { join as join2 } from "path";
|
|
2703
2980
|
function computeStringsHash(texts) {
|
|
2704
2981
|
const sorted = [...texts].sort();
|
|
2705
2982
|
return createHash("sha256").update(sorted.join("\0")).digest("hex").slice(0, 16);
|
|
@@ -2709,7 +2986,8 @@ function readCachedStringsHash(projectRoot, branch) {
|
|
|
2709
2986
|
if (!existsSync(filePath)) return null;
|
|
2710
2987
|
try {
|
|
2711
2988
|
const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
2712
|
-
if (isRecord(raw) && typeof raw.stringsHash === "string")
|
|
2989
|
+
if (isRecord(raw) && typeof raw.stringsHash === "string")
|
|
2990
|
+
return raw.stringsHash;
|
|
2713
2991
|
} catch {
|
|
2714
2992
|
}
|
|
2715
2993
|
return null;
|
|
@@ -2759,7 +3037,14 @@ function parseTranslations(value) {
|
|
|
2759
3037
|
}
|
|
2760
3038
|
function getCacheFilePath(projectRoot, branch) {
|
|
2761
3039
|
const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
|
|
2762
|
-
return join2(
|
|
3040
|
+
return join2(
|
|
3041
|
+
projectRoot,
|
|
3042
|
+
"node_modules",
|
|
3043
|
+
".vocoder",
|
|
3044
|
+
"cache",
|
|
3045
|
+
"sync",
|
|
3046
|
+
`${branchHash}.json`
|
|
3047
|
+
);
|
|
2763
3048
|
}
|
|
2764
3049
|
function readLocalSnapshotCache(params) {
|
|
2765
3050
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
@@ -2788,16 +3073,18 @@ function readLocalSnapshotCache(params) {
|
|
|
2788
3073
|
cacheBranch: candidateBranch
|
|
2789
3074
|
};
|
|
2790
3075
|
} catch {
|
|
2791
|
-
continue;
|
|
2792
3076
|
}
|
|
2793
3077
|
}
|
|
2794
3078
|
return null;
|
|
2795
3079
|
}
|
|
2796
3080
|
function writeLocalSnapshotCache(params) {
|
|
2797
3081
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
2798
|
-
mkdirSync2(
|
|
2799
|
-
|
|
2800
|
-
|
|
3082
|
+
mkdirSync2(
|
|
3083
|
+
join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
|
|
3084
|
+
{
|
|
3085
|
+
recursive: true
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
2801
3088
|
const payload = {
|
|
2802
3089
|
version: 1,
|
|
2803
3090
|
branch: params.branch,
|
|
@@ -2894,7 +3181,9 @@ function getSyncPolicyErrorGuidance(error) {
|
|
|
2894
3181
|
if (error.branch) {
|
|
2895
3182
|
lines2.push(`Current branch: ${error.branch}`);
|
|
2896
3183
|
}
|
|
2897
|
-
lines2.push(
|
|
3184
|
+
lines2.push(
|
|
3185
|
+
"Update your project target branches in the dashboard if needed."
|
|
3186
|
+
);
|
|
2898
3187
|
return lines2;
|
|
2899
3188
|
}
|
|
2900
3189
|
const lines = ["This project is bound to a different repository."];
|
|
@@ -3010,7 +3299,9 @@ async function sync(options = {}) {
|
|
|
3010
3299
|
);
|
|
3011
3300
|
if (extractedStrings.length === 0) {
|
|
3012
3301
|
spinner4.stop("No translatable strings found");
|
|
3013
|
-
p7.log.warn(
|
|
3302
|
+
p7.log.warn(
|
|
3303
|
+
"Make sure you are wrapping translatable strings with Vocoder"
|
|
3304
|
+
);
|
|
3014
3305
|
p7.outro("");
|
|
3015
3306
|
return 0;
|
|
3016
3307
|
}
|
|
@@ -3074,7 +3365,9 @@ async function sync(options = {}) {
|
|
|
3074
3365
|
},
|
|
3075
3366
|
repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
|
|
3076
3367
|
);
|
|
3077
|
-
spinner4.stop(
|
|
3368
|
+
spinner4.stop(
|
|
3369
|
+
`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`
|
|
3370
|
+
);
|
|
3078
3371
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
3079
3372
|
branch,
|
|
3080
3373
|
requestedMode,
|
|
@@ -3250,11 +3543,15 @@ async function sync(options = {}) {
|
|
|
3250
3543
|
if (error instanceof Error) {
|
|
3251
3544
|
p7.log.error(error.message);
|
|
3252
3545
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
3253
|
-
p7.log.warn(
|
|
3546
|
+
p7.log.warn(
|
|
3547
|
+
"VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
|
|
3548
|
+
);
|
|
3254
3549
|
p7.log.info(" Create one at: https://vocoder.app/dashboard");
|
|
3255
3550
|
p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
|
|
3256
3551
|
p7.log.info("");
|
|
3257
|
-
p7.log.info(
|
|
3552
|
+
p7.log.info(
|
|
3553
|
+
" Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
|
|
3554
|
+
);
|
|
3258
3555
|
p7.log.info(" Translations are fetched automatically at build time.");
|
|
3259
3556
|
} else if (error.message.includes("git branch")) {
|
|
3260
3557
|
p7.log.warn("Run from a git repository, or use:");
|
|
@@ -3288,7 +3585,9 @@ async function whoami(options = {}) {
|
|
|
3288
3585
|
p8.log.info(`API: ${apiUrl}`);
|
|
3289
3586
|
return 0;
|
|
3290
3587
|
} catch {
|
|
3291
|
-
p8.log.error(
|
|
3588
|
+
p8.log.error(
|
|
3589
|
+
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3590
|
+
);
|
|
3292
3591
|
return 1;
|
|
3293
3592
|
}
|
|
3294
3593
|
}
|
|
@@ -3303,7 +3602,13 @@ async function runCommand(command, options) {
|
|
|
3303
3602
|
}
|
|
3304
3603
|
var program = new Command();
|
|
3305
3604
|
program.name("vocoder").description("Vocoder CLI - Project setup and string extraction").version("0.1.5");
|
|
3306
|
-
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));
|
|
3307
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) => {
|
|
3308
3613
|
const translated = { ...options };
|
|
3309
3614
|
if (options.maxWait) translated.maxWaitMs = Number(options.maxWait);
|