@vocoder/cli 0.14.0 → 0.15.0
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 +1144 -984
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-T4BLNDJ3.mjs → chunk-62KCB6C6.mjs} +277 -260
- package/dist/chunk-62KCB6C6.mjs.map +1 -0
- package/dist/lib.d.mts +43 -21
- package/dist/lib.mjs +1 -1
- package/dist/lib.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-T4BLNDJ3.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
VocoderAPIError,
|
|
7
7
|
buildInstallCommand,
|
|
8
8
|
clearAuthData,
|
|
9
|
+
computeFingerprint,
|
|
9
10
|
detectLocalEcosystem,
|
|
10
11
|
getPackagesToInstall,
|
|
11
12
|
getSetupSnippets,
|
|
@@ -13,15 +14,15 @@ import {
|
|
|
13
14
|
readAuthData,
|
|
14
15
|
verifyStoredAuth,
|
|
15
16
|
writeAuthData
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-62KCB6C6.mjs";
|
|
17
18
|
|
|
18
19
|
// src/bin.ts
|
|
19
20
|
import { Command } from "commander";
|
|
20
21
|
|
|
21
22
|
// src/commands/init.ts
|
|
22
|
-
import * as
|
|
23
|
+
import * as p6 from "@clack/prompts";
|
|
23
24
|
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
24
|
-
import { existsSync as
|
|
25
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
25
26
|
|
|
26
27
|
// src/utils/write-config.ts
|
|
27
28
|
import { existsSync, writeFileSync } from "fs";
|
|
@@ -41,18 +42,21 @@ function writeVocoderConfig(options) {
|
|
|
41
42
|
const {
|
|
42
43
|
targetBranches = ["main"],
|
|
43
44
|
useTypeScript = true,
|
|
44
|
-
cwd = process.cwd()
|
|
45
|
+
cwd = process.cwd(),
|
|
46
|
+
appId
|
|
45
47
|
} = options;
|
|
46
48
|
if (findExistingConfig(cwd)) return null;
|
|
47
49
|
const ext = useTypeScript ? "ts" : "js";
|
|
48
50
|
const configPath = join(cwd, `vocoder.config.${ext}`);
|
|
49
51
|
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
50
52
|
const includes = ["**/*.{tsx,jsx,ts,js}"];
|
|
51
|
-
const includesStr = includes.map((
|
|
53
|
+
const includesStr = includes.map((p15) => `'${p15}'`).join(", ");
|
|
54
|
+
const appIdLine = appId ? ` appId: '${appId}',
|
|
55
|
+
` : "";
|
|
52
56
|
const content = `import { defineConfig } from '@vocoder/config'
|
|
53
57
|
|
|
54
58
|
export default defineConfig({
|
|
55
|
-
targetBranches: [${branchesStr}],
|
|
59
|
+
${appIdLine} targetBranches: [${branchesStr}],
|
|
56
60
|
include: [${includesStr}],
|
|
57
61
|
})
|
|
58
62
|
`;
|
|
@@ -64,311 +68,34 @@ export default defineConfig({
|
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
// src/utils/github-connect.ts
|
|
68
|
-
import { spawn } from "child_process";
|
|
69
|
-
import * as p from "@clack/prompts";
|
|
70
|
-
import chalk from "chalk";
|
|
71
|
-
|
|
72
|
-
// src/utils/local-server.ts
|
|
73
|
-
import { createServer } from "http";
|
|
74
|
-
import { URL as URL2 } from "url";
|
|
75
|
-
function startCallbackServer() {
|
|
76
|
-
return new Promise((resolve2, reject) => {
|
|
77
|
-
let settled = false;
|
|
78
|
-
let callbackResolve = null;
|
|
79
|
-
let callbackReject = null;
|
|
80
|
-
const callbackPromise = new Promise((res, rej) => {
|
|
81
|
-
callbackResolve = res;
|
|
82
|
-
callbackReject = rej;
|
|
83
|
-
});
|
|
84
|
-
const server = createServer((req, res) => {
|
|
85
|
-
if (!req.url) {
|
|
86
|
-
res.writeHead(400);
|
|
87
|
-
res.end();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
let pathname;
|
|
91
|
-
let params;
|
|
92
|
-
try {
|
|
93
|
-
const parsed = new URL2(req.url, "http://localhost");
|
|
94
|
-
pathname = parsed.pathname;
|
|
95
|
-
params = Object.fromEntries(parsed.searchParams.entries());
|
|
96
|
-
} catch {
|
|
97
|
-
res.writeHead(400);
|
|
98
|
-
res.end("Bad request");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (pathname !== "/callback") {
|
|
102
|
-
res.writeHead(404);
|
|
103
|
-
res.end("Not found");
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
107
|
-
res.end(
|
|
108
|
-
'<!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>'
|
|
109
|
-
);
|
|
110
|
-
if (callbackResolve) {
|
|
111
|
-
callbackResolve(params);
|
|
112
|
-
callbackResolve = null;
|
|
113
|
-
}
|
|
114
|
-
setImmediate(() => server.close());
|
|
115
|
-
});
|
|
116
|
-
server.on("error", (err) => {
|
|
117
|
-
if (!settled) {
|
|
118
|
-
settled = true;
|
|
119
|
-
if (callbackReject) callbackReject(err);
|
|
120
|
-
reject(err);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
server.listen(0, "127.0.0.1", () => {
|
|
124
|
-
if (settled) return;
|
|
125
|
-
settled = true;
|
|
126
|
-
const port = server.address().port;
|
|
127
|
-
resolve2({
|
|
128
|
-
port,
|
|
129
|
-
waitForCallback: () => callbackPromise,
|
|
130
|
-
close: () => server.close()
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/utils/github-connect.ts
|
|
137
|
-
async function tryOpenBrowser(url) {
|
|
138
|
-
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
const platform = process.platform;
|
|
142
|
-
let command;
|
|
143
|
-
let args;
|
|
144
|
-
if (platform === "darwin") {
|
|
145
|
-
command = "open";
|
|
146
|
-
args = [url];
|
|
147
|
-
} else if (platform === "win32") {
|
|
148
|
-
command = "rundll32";
|
|
149
|
-
args = ["url.dll,FileProtocolHandler", url];
|
|
150
|
-
} else {
|
|
151
|
-
command = "xdg-open";
|
|
152
|
-
args = [url];
|
|
153
|
-
}
|
|
154
|
-
return new Promise((resolve2) => {
|
|
155
|
-
try {
|
|
156
|
-
const child = spawn(command, args, {
|
|
157
|
-
detached: true,
|
|
158
|
-
stdio: "ignore",
|
|
159
|
-
windowsHide: true
|
|
160
|
-
});
|
|
161
|
-
let settled = false;
|
|
162
|
-
child.once("spawn", () => {
|
|
163
|
-
if (settled) return;
|
|
164
|
-
settled = true;
|
|
165
|
-
child.unref();
|
|
166
|
-
resolve2(true);
|
|
167
|
-
});
|
|
168
|
-
child.once("error", () => {
|
|
169
|
-
if (settled) return;
|
|
170
|
-
settled = true;
|
|
171
|
-
resolve2(false);
|
|
172
|
-
});
|
|
173
|
-
setTimeout(() => {
|
|
174
|
-
if (settled) return;
|
|
175
|
-
settled = true;
|
|
176
|
-
resolve2(false);
|
|
177
|
-
}, 300);
|
|
178
|
-
} catch {
|
|
179
|
-
resolve2(false);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async function runGitHubInstallFlow(params) {
|
|
184
|
-
let server = null;
|
|
185
|
-
try {
|
|
186
|
-
server = await startCallbackServer();
|
|
187
|
-
} catch {
|
|
188
|
-
}
|
|
189
|
-
const { installUrl } = await params.api.startCliGitHubInstall(
|
|
190
|
-
params.userToken,
|
|
191
|
-
{
|
|
192
|
-
organizationId: params.organizationId,
|
|
193
|
-
callbackPort: server?.port
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
p.log.info("Opening GitHub to install the Vocoder App...");
|
|
197
|
-
p.note(installUrl, "Install URL");
|
|
198
|
-
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
199
|
-
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
200
|
-
if (p.isCancel(shouldOpen)) {
|
|
201
|
-
server?.close();
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
if (shouldOpen) {
|
|
205
|
-
const opened = await tryOpenBrowser(installUrl);
|
|
206
|
-
if (!opened) {
|
|
207
|
-
p.log.info(
|
|
208
|
-
"Could not open a browser automatically. Use the URL above."
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const connectSpinner = p.spinner();
|
|
214
|
-
connectSpinner.start("Waiting for GitHub App installation...");
|
|
215
|
-
if (server) {
|
|
216
|
-
try {
|
|
217
|
-
const params_timeout = 15 * 60 * 1e3;
|
|
218
|
-
const callbackParams = await Promise.race([
|
|
219
|
-
server.waitForCallback(),
|
|
220
|
-
new Promise(
|
|
221
|
-
(resolve2) => setTimeout(() => resolve2(null), params_timeout)
|
|
222
|
-
)
|
|
223
|
-
]);
|
|
224
|
-
server.close();
|
|
225
|
-
if (!callbackParams) {
|
|
226
|
-
connectSpinner.stop("GitHub App installation timed out");
|
|
227
|
-
p.log.error(
|
|
228
|
-
"The installation flow timed out. Run `vocoder init` again."
|
|
229
|
-
);
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
if (callbackParams.error) {
|
|
233
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
234
|
-
p.log.error(callbackParams.error);
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
238
|
-
if (!organizationId || !connectionLabel) {
|
|
239
|
-
connectSpinner.stop("GitHub App installation incomplete");
|
|
240
|
-
p.log.error("Missing organization or connection data from callback.");
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
connectSpinner.stop(
|
|
244
|
-
`Connected to GitHub as ${chalk.bold(connectionLabel)}`
|
|
245
|
-
);
|
|
246
|
-
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
247
|
-
return {
|
|
248
|
-
organizationId,
|
|
249
|
-
organizationName: orgName,
|
|
250
|
-
connectionLabel
|
|
251
|
-
};
|
|
252
|
-
} catch {
|
|
253
|
-
server.close();
|
|
254
|
-
connectSpinner.stop("GitHub App installation failed");
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
259
|
-
p.log.warn(
|
|
260
|
-
"Complete the installation in your browser, then run `vocoder init` again."
|
|
261
|
-
);
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
async function runGitHubDiscoveryFlow(params) {
|
|
265
|
-
let server = null;
|
|
266
|
-
try {
|
|
267
|
-
server = await startCallbackServer();
|
|
268
|
-
} catch {
|
|
269
|
-
}
|
|
270
|
-
const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
|
|
271
|
-
organizationId: params.organizationId,
|
|
272
|
-
callbackPort: server?.port
|
|
273
|
-
});
|
|
274
|
-
p.log.info("Opening GitHub to authorize your account...");
|
|
275
|
-
p.note("Complete authorization in your browser.");
|
|
276
|
-
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
277
|
-
const shouldOpen = params.yes ? true : await p.confirm({ message: "Open in your browser?" });
|
|
278
|
-
if (p.isCancel(shouldOpen)) {
|
|
279
|
-
server?.close();
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
if (shouldOpen) {
|
|
283
|
-
const opened = await tryOpenBrowser(oauthUrl);
|
|
284
|
-
if (!opened) {
|
|
285
|
-
p.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const oauthSpinner = p.spinner();
|
|
290
|
-
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
291
|
-
if (server) {
|
|
292
|
-
try {
|
|
293
|
-
const timeoutMs = 10 * 60 * 1e3;
|
|
294
|
-
const callbackParams = await Promise.race([
|
|
295
|
-
server.waitForCallback(),
|
|
296
|
-
new Promise(
|
|
297
|
-
(resolve2) => setTimeout(() => resolve2(null), timeoutMs)
|
|
298
|
-
)
|
|
299
|
-
]);
|
|
300
|
-
server.close();
|
|
301
|
-
if (!callbackParams) {
|
|
302
|
-
oauthSpinner.stop("GitHub authorization timed out");
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
if (callbackParams.error) {
|
|
306
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
307
|
-
p.log.error(callbackParams.error);
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
} catch {
|
|
311
|
-
server.close();
|
|
312
|
-
oauthSpinner.stop("GitHub authorization failed");
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
oauthSpinner.stop("GitHub account authorized");
|
|
317
|
-
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
318
|
-
params.userToken
|
|
319
|
-
);
|
|
320
|
-
return discoveryResult.installations;
|
|
321
|
-
}
|
|
322
|
-
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
323
|
-
const options = installations.map((inst) => ({
|
|
324
|
-
value: String(inst.installationId),
|
|
325
|
-
label: inst.accountLogin,
|
|
326
|
-
hint: [
|
|
327
|
-
inst.accountType === "Organization" ? "organization" : "personal",
|
|
328
|
-
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
329
|
-
inst.isSuspended ? "suspended" : ""
|
|
330
|
-
].filter(Boolean).join(" \xB7 ") || void 0
|
|
331
|
-
}));
|
|
332
|
-
if (canInstallNew) {
|
|
333
|
-
options.push({
|
|
334
|
-
value: "install_new",
|
|
335
|
-
label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
const selected = await p.select({
|
|
339
|
-
message: "Select a GitHub installation",
|
|
340
|
-
options
|
|
341
|
-
});
|
|
342
|
-
if (p.isCancel(selected)) return null;
|
|
343
|
-
if (selected === "install_new") return "install_new";
|
|
344
|
-
return Number(selected);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// src/utils/project-create.ts
|
|
348
|
-
import * as p3 from "@clack/prompts";
|
|
349
|
-
import chalk3 from "chalk";
|
|
350
|
-
|
|
351
|
-
// src/utils/branch-select.ts
|
|
352
|
-
import { execSync } from "child_process";
|
|
353
|
-
import { isCancel as isCancel2, Prompt } from "@clack/core";
|
|
354
|
-
|
|
355
71
|
// src/utils/theme.ts
|
|
356
|
-
import
|
|
72
|
+
import chalk from "chalk";
|
|
357
73
|
var ORANGE = "#FC5206";
|
|
358
74
|
var PINK = "#D51977";
|
|
359
75
|
var BLUE = "#2450A9";
|
|
360
76
|
var noColor = process.env.NO_COLOR === "1" || process.env.FORCE_COLOR === "0";
|
|
361
|
-
var hex = (color) => (s) => noColor ? s :
|
|
362
|
-
var dim = (s) => noColor ? s :
|
|
363
|
-
var bld = (s) => noColor ? s :
|
|
364
|
-
var grn = (s) => noColor ? s :
|
|
365
|
-
var ylw = (s) => noColor ? s :
|
|
366
|
-
var red = (s) => noColor ? s :
|
|
77
|
+
var hex = (color) => (s) => noColor ? s : chalk.hex(color)(s);
|
|
78
|
+
var dim = (s) => noColor ? s : chalk.dim(s);
|
|
79
|
+
var bld = (s) => noColor ? s : chalk.bold(s);
|
|
80
|
+
var grn = (s) => noColor ? s : chalk.green(s);
|
|
81
|
+
var ylw = (s) => noColor ? s : chalk.yellow(s);
|
|
82
|
+
var red = (s) => noColor ? s : chalk.red(s);
|
|
367
83
|
var highlight = hex(PINK);
|
|
368
84
|
var info = hex(BLUE);
|
|
369
85
|
var active = hex(ORANGE);
|
|
370
86
|
|
|
371
|
-
// src/
|
|
87
|
+
// src/commands/init.ts
|
|
88
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
89
|
+
|
|
90
|
+
// src/utils/project-create.ts
|
|
91
|
+
import * as p3 from "@clack/prompts";
|
|
92
|
+
import chalk2 from "chalk";
|
|
93
|
+
|
|
94
|
+
// src/utils/app-dir-select.ts
|
|
95
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
96
|
+
import { resolve } from "path";
|
|
97
|
+
import { isCancel, Prompt } from "@clack/core";
|
|
98
|
+
import * as p from "@clack/prompts";
|
|
372
99
|
var S_BAR = "\u2502";
|
|
373
100
|
var S_BAR_END = "\u2514";
|
|
374
101
|
var S_ACTIVE = "\u25C6";
|
|
@@ -387,6 +114,195 @@ function symbol(state) {
|
|
|
387
114
|
return active(S_ACTIVE);
|
|
388
115
|
}
|
|
389
116
|
}
|
|
117
|
+
function validateAppDirPath(val, existing, opts = {}) {
|
|
118
|
+
if (val.startsWith("/")) return "Must be a relative path (e.g. apps/web)";
|
|
119
|
+
if (val.includes("..")) return "Path traversal not allowed";
|
|
120
|
+
const hasWholeRepo = existing.includes("");
|
|
121
|
+
const hasScoped = existing.some((d) => d !== "");
|
|
122
|
+
if (val === "" && hasScoped) return "Cannot add whole-repo scope to a monorepo project";
|
|
123
|
+
if (val !== "" && hasWholeRepo) return "Cannot add a scoped directory to a whole-repo project";
|
|
124
|
+
if (existing.includes(val)) return `Already added: ${val}`;
|
|
125
|
+
const nested = existing.find(
|
|
126
|
+
(d) => d !== "" && (val.startsWith(d + "/") || d.startsWith(val + "/"))
|
|
127
|
+
);
|
|
128
|
+
if (nested) return `"${val}" overlaps with already-added "${nested}"`;
|
|
129
|
+
if (val !== "") {
|
|
130
|
+
const abs = resolve(opts.cwd ?? process.cwd(), val);
|
|
131
|
+
if (!existsSync2(abs)) return `Directory not found: ${val}`;
|
|
132
|
+
if (!statSync(abs).isDirectory()) return `Not a directory: ${val}`;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
async function collectAppDirs(opts = {}) {
|
|
137
|
+
const added = [];
|
|
138
|
+
let filter = "";
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
let addCursor = false;
|
|
141
|
+
const isNewDir = () => {
|
|
142
|
+
const t = filter.trim();
|
|
143
|
+
return t.length > 0 && !added.includes(t);
|
|
144
|
+
};
|
|
145
|
+
const clampCursor = () => {
|
|
146
|
+
const max = added.length - 1;
|
|
147
|
+
if (cursor > max) cursor = Math.max(0, max);
|
|
148
|
+
};
|
|
149
|
+
const prompt = new Prompt(
|
|
150
|
+
{
|
|
151
|
+
validate() {
|
|
152
|
+
return void 0;
|
|
153
|
+
},
|
|
154
|
+
render() {
|
|
155
|
+
const trimmed = filter.trim();
|
|
156
|
+
const hdr = `${dim(S_BAR)}
|
|
157
|
+
${symbol(this.state)} App directories
|
|
158
|
+
`;
|
|
159
|
+
switch (this.state) {
|
|
160
|
+
case "submit": {
|
|
161
|
+
const summary = added.length > 0 ? bld(added.join(", ")) : dim("none (single-app project)");
|
|
162
|
+
return `${hdr}${dim(S_BAR)} ${summary}`;
|
|
163
|
+
}
|
|
164
|
+
case "cancel":
|
|
165
|
+
return `${hdr}${dim(S_BAR)}`;
|
|
166
|
+
default: {
|
|
167
|
+
const inputHint = filter.length > 0 ? filter : added.length === 0 ? dim("e.g. apps/web") : dim("e.g. apps/api");
|
|
168
|
+
const lines = [
|
|
169
|
+
hdr.trimEnd(),
|
|
170
|
+
`${info(S_BAR)} ${dim("/")} ${inputHint}`,
|
|
171
|
+
info(S_BAR)
|
|
172
|
+
];
|
|
173
|
+
for (let i = 0; i < added.length; i++) {
|
|
174
|
+
const isCursor = i === cursor && !addCursor;
|
|
175
|
+
const icon = isCursor ? active("\u25FC") : info("\u25FC");
|
|
176
|
+
const label = isCursor ? bld(added[i]) : added[i];
|
|
177
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
178
|
+
}
|
|
179
|
+
const atLimit = opts.maxDirs !== void 0 && added.length >= opts.maxDirs;
|
|
180
|
+
if (atLimit) {
|
|
181
|
+
lines.push(`${info(S_BAR)} ${dim(`App limit reached (${added.length}/${opts.maxDirs} on your plan)`)}`);
|
|
182
|
+
} else if (isNewDir()) {
|
|
183
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
184
|
+
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
185
|
+
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}"`;
|
|
186
|
+
lines.push(`${info(S_BAR)} ${icon} ${label}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(info(S_BAR));
|
|
189
|
+
if (atLimit) {
|
|
190
|
+
lines.push(dim(`${S_BAR} \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
191
|
+
} else if (added.length === 0 && !isNewDir()) {
|
|
192
|
+
lines.push(dim(`${S_BAR} Monorepo? Type each app's subdirectory path and press Space.`));
|
|
193
|
+
lines.push(dim(`${S_BAR} Single app? Press Enter to skip this step.`));
|
|
194
|
+
} else if (added.length > 0) {
|
|
195
|
+
lines.push(dim(`${S_BAR} ${added.length} added \xB7 \u2191\u2193 to select, Space to remove \xB7 Enter to confirm`));
|
|
196
|
+
}
|
|
197
|
+
const barEnd = this.state === "error" ? ylw(S_BAR_END) : info(S_BAR_END);
|
|
198
|
+
if (this.state === "error") {
|
|
199
|
+
lines.push(`${ylw(S_BAR_END)} ${ylw(this.error)}`);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(barEnd);
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
false
|
|
210
|
+
);
|
|
211
|
+
prompt.on("key", (key) => {
|
|
212
|
+
if (!key || key === " ") return;
|
|
213
|
+
const cp = key.codePointAt(0) ?? 0;
|
|
214
|
+
if (cp === 127 || cp === 8) {
|
|
215
|
+
filter = filter.slice(0, -1);
|
|
216
|
+
addCursor = false;
|
|
217
|
+
} else if (cp >= 32 && cp !== 127) {
|
|
218
|
+
filter += key;
|
|
219
|
+
cursor = 0;
|
|
220
|
+
addCursor = false;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
prompt.on("cursor", (action) => {
|
|
224
|
+
switch (action) {
|
|
225
|
+
case "up":
|
|
226
|
+
if (addCursor) {
|
|
227
|
+
addCursor = false;
|
|
228
|
+
cursor = Math.max(0, added.length - 1);
|
|
229
|
+
} else {
|
|
230
|
+
cursor = Math.max(0, cursor - 1);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case "down":
|
|
234
|
+
if (!addCursor && cursor >= added.length - 1 && isNewDir()) {
|
|
235
|
+
addCursor = true;
|
|
236
|
+
} else if (!addCursor) {
|
|
237
|
+
cursor = Math.min(added.length - 1, cursor + 1);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
case "space": {
|
|
241
|
+
if (addCursor || filter.trim().length > 0 && isNewDir()) {
|
|
242
|
+
if (opts.maxDirs !== void 0 && added.length >= opts.maxDirs) break;
|
|
243
|
+
const trimmed = filter.trim();
|
|
244
|
+
const err = validateAppDirPath(trimmed, added, opts);
|
|
245
|
+
if (!err) {
|
|
246
|
+
added.push(trimmed);
|
|
247
|
+
filter = "";
|
|
248
|
+
addCursor = false;
|
|
249
|
+
cursor = 0;
|
|
250
|
+
}
|
|
251
|
+
} else if (added.length > 0 && !isNewDir()) {
|
|
252
|
+
clampCursor();
|
|
253
|
+
added.splice(cursor, 1);
|
|
254
|
+
if (cursor >= added.length) cursor = Math.max(0, added.length - 1);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
prompt.on("finalize", () => {
|
|
261
|
+
if (prompt.state === "submit") {
|
|
262
|
+
prompt.value = [...added];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
const result = await prompt.prompt();
|
|
266
|
+
if (isCancel(result)) return null;
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
async function promptSingleAppDir(params) {
|
|
270
|
+
const { existingDirs, cwd } = params;
|
|
271
|
+
const input = await p.text({
|
|
272
|
+
message: "App directory to add",
|
|
273
|
+
placeholder: "apps/web",
|
|
274
|
+
validate(val) {
|
|
275
|
+
const err = validateAppDirPath(val ?? "", existingDirs, { cwd });
|
|
276
|
+
if (err) return err;
|
|
277
|
+
if (!val) return "Directory is required";
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (p.isCancel(input)) return null;
|
|
282
|
+
return input;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/utils/branch-select.ts
|
|
286
|
+
import { execSync } from "child_process";
|
|
287
|
+
import { isCancel as isCancel3, Prompt as Prompt2 } from "@clack/core";
|
|
288
|
+
var S_BAR2 = "\u2502";
|
|
289
|
+
var S_BAR_END2 = "\u2514";
|
|
290
|
+
var S_ACTIVE2 = "\u25C6";
|
|
291
|
+
var S_SUBMIT2 = "\u25C6";
|
|
292
|
+
var S_CANCEL2 = "\u25A0";
|
|
293
|
+
var S_ERROR2 = "\u25B2";
|
|
294
|
+
function symbol2(state) {
|
|
295
|
+
switch (state) {
|
|
296
|
+
case "submit":
|
|
297
|
+
return grn(S_SUBMIT2);
|
|
298
|
+
case "cancel":
|
|
299
|
+
return red(S_CANCEL2);
|
|
300
|
+
case "error":
|
|
301
|
+
return ylw(S_ERROR2);
|
|
302
|
+
default:
|
|
303
|
+
return active(S_ACTIVE2);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
390
306
|
function detectGitBranches(cwd) {
|
|
391
307
|
const workDir = cwd ?? process.cwd();
|
|
392
308
|
try {
|
|
@@ -450,8 +366,8 @@ function filterItems(items, query) {
|
|
|
450
366
|
const lower = query.toLowerCase();
|
|
451
367
|
return items.filter((i) => i.value.toLowerCase().includes(lower));
|
|
452
368
|
}
|
|
453
|
-
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor,
|
|
454
|
-
const lines = [];
|
|
369
|
+
function buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedPatterns = /* @__PURE__ */ new Set()) {
|
|
370
|
+
const lines = [info(S_BAR2)];
|
|
455
371
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE);
|
|
456
372
|
for (let i = scrollOffset; i < end; i++) {
|
|
457
373
|
const item = filtered[i];
|
|
@@ -460,26 +376,20 @@ function buildList(filtered, cursor, scrollOffset, selected, filter, customPatte
|
|
|
460
376
|
const icon = isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB");
|
|
461
377
|
let label = item.isCustom ? `${item.label} ${dim("(custom)")}` : item.label;
|
|
462
378
|
if (isCursor) label = bld(label);
|
|
463
|
-
lines.push(`${info(
|
|
379
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
464
380
|
}
|
|
465
381
|
const trimmed = filter.trim();
|
|
466
|
-
const
|
|
467
|
-
const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
382
|
+
const isNewPattern = trimmed.length > 0 && !filtered.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
|
|
468
383
|
if (isNewPattern) {
|
|
469
384
|
const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
|
|
470
385
|
const icon = addCursor ? active("\u25FB") : dim("\u25FB");
|
|
471
386
|
const label = err ? `${ylw("+")} ${dim(`"${trimmed}" \u2014 ${err}`)}` : `${grn("+")} Add "${trimmed}" as branch pattern`;
|
|
472
|
-
lines.push(`${info(
|
|
387
|
+
lines.push(`${info(S_BAR2)} ${icon} ${label}`);
|
|
473
388
|
} else if (filtered.length === 0 && trimmed.length === 0) {
|
|
474
|
-
lines.push(dim(`${
|
|
389
|
+
lines.push(dim(`${S_BAR2} No branches detected`));
|
|
475
390
|
}
|
|
476
391
|
const hidden = filtered.length - (end - scrollOffset);
|
|
477
|
-
if (hidden > 0) lines.push(dim(`${
|
|
478
|
-
if (selected.size > 0) {
|
|
479
|
-
lines.push(dim(`${S_BAR} ${selected.size} selected \u2014 Enter to confirm`));
|
|
480
|
-
} else if (optional) {
|
|
481
|
-
lines.push(dim(`${S_BAR} Enter to skip`));
|
|
482
|
-
}
|
|
392
|
+
if (hidden > 0) lines.push(dim(`${S_BAR2} ${hidden} more`));
|
|
483
393
|
return lines.join("\n");
|
|
484
394
|
}
|
|
485
395
|
async function filterableBranchSelect(params) {
|
|
@@ -510,7 +420,7 @@ async function filterableBranchSelect(params) {
|
|
|
510
420
|
if (scrollOffset < 0) scrollOffset = 0;
|
|
511
421
|
}
|
|
512
422
|
};
|
|
513
|
-
const prompt = new
|
|
423
|
+
const prompt = new Prompt2(
|
|
514
424
|
{
|
|
515
425
|
validate() {
|
|
516
426
|
if (!optional && selected.size === 0)
|
|
@@ -520,51 +430,34 @@ async function filterableBranchSelect(params) {
|
|
|
520
430
|
render() {
|
|
521
431
|
const filtered = getFiltered();
|
|
522
432
|
clampCursor(filtered);
|
|
523
|
-
const hdr = `${dim(
|
|
524
|
-
${
|
|
433
|
+
const hdr = `${dim(S_BAR2)}
|
|
434
|
+
${symbol2(this.state)} ${message}
|
|
525
435
|
`;
|
|
526
|
-
const
|
|
436
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter \xB7 type a custom pattern to add it");
|
|
437
|
+
const footer = selected.size > 0 ? dim(`${S_BAR2} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : optional ? dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to skip`) : dim(`${S_BAR2} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`);
|
|
527
438
|
switch (this.state) {
|
|
528
439
|
case "submit": {
|
|
529
440
|
const summary = selected.size > 0 ? bld(Array.from(selected).join(", ")) : dim("none");
|
|
530
|
-
return `${hdr}${dim(
|
|
441
|
+
return `${hdr}${dim(S_BAR2)} ${summary}`;
|
|
531
442
|
}
|
|
532
443
|
case "cancel":
|
|
533
|
-
return `${hdr}${dim(
|
|
444
|
+
return `${hdr}${dim(S_BAR2)}`;
|
|
534
445
|
case "error":
|
|
535
446
|
return [
|
|
536
447
|
hdr.trimEnd(),
|
|
537
|
-
`${ylw(
|
|
538
|
-
buildList(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
scrollOffset,
|
|
542
|
-
selected,
|
|
543
|
-
filter,
|
|
544
|
-
customPatterns,
|
|
545
|
-
addCursor,
|
|
546
|
-
optional,
|
|
547
|
-
excludedSet
|
|
548
|
-
),
|
|
549
|
-
`${ylw(S_BAR_END)} ${ylw(this.error)}`,
|
|
448
|
+
`${ylw(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
449
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
450
|
+
footer,
|
|
451
|
+
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
550
452
|
""
|
|
551
453
|
].join("\n");
|
|
552
454
|
default:
|
|
553
455
|
return [
|
|
554
456
|
hdr.trimEnd(),
|
|
555
|
-
`${info(
|
|
556
|
-
buildList(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
scrollOffset,
|
|
560
|
-
selected,
|
|
561
|
-
filter,
|
|
562
|
-
customPatterns,
|
|
563
|
-
addCursor,
|
|
564
|
-
optional,
|
|
565
|
-
excludedSet
|
|
566
|
-
),
|
|
567
|
-
`${info(S_BAR_END)}`,
|
|
457
|
+
`${info(S_BAR2)} ${dim("/")} ${inputHint}`,
|
|
458
|
+
buildList(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, excludedSet),
|
|
459
|
+
footer,
|
|
460
|
+
`${info(S_BAR_END2)}`,
|
|
568
461
|
""
|
|
569
462
|
].join("\n");
|
|
570
463
|
}
|
|
@@ -630,29 +523,29 @@ ${symbol(this.state)} ${message}
|
|
|
630
523
|
}
|
|
631
524
|
});
|
|
632
525
|
const result = await prompt.prompt();
|
|
633
|
-
if (
|
|
526
|
+
if (isCancel3(result)) return null;
|
|
634
527
|
return result;
|
|
635
528
|
}
|
|
636
529
|
|
|
637
530
|
// src/utils/locale-search.ts
|
|
638
|
-
import { isCancel as
|
|
531
|
+
import { isCancel as isCancel4, Prompt as Prompt3 } from "@clack/core";
|
|
639
532
|
import * as p2 from "@clack/prompts";
|
|
640
|
-
var
|
|
641
|
-
var
|
|
642
|
-
var
|
|
643
|
-
var
|
|
644
|
-
var
|
|
645
|
-
var
|
|
646
|
-
function
|
|
533
|
+
var S_BAR3 = "\u2502";
|
|
534
|
+
var S_BAR_END3 = "\u2514";
|
|
535
|
+
var S_ACTIVE3 = "\u25C6";
|
|
536
|
+
var S_SUBMIT3 = "\u25C6";
|
|
537
|
+
var S_CANCEL3 = "\u25A0";
|
|
538
|
+
var S_ERROR3 = "\u25B2";
|
|
539
|
+
function symbol3(state) {
|
|
647
540
|
switch (state) {
|
|
648
541
|
case "submit":
|
|
649
|
-
return grn(
|
|
542
|
+
return grn(S_SUBMIT3);
|
|
650
543
|
case "cancel":
|
|
651
|
-
return red(
|
|
544
|
+
return red(S_CANCEL3);
|
|
652
545
|
case "error":
|
|
653
|
-
return ylw(
|
|
546
|
+
return ylw(S_ERROR3);
|
|
654
547
|
default:
|
|
655
|
-
return active(
|
|
548
|
+
return active(S_ACTIVE3);
|
|
656
549
|
}
|
|
657
550
|
}
|
|
658
551
|
var MAX_VISIBLE2 = 12;
|
|
@@ -666,25 +559,20 @@ function filterLocales(options, query) {
|
|
|
666
559
|
function buildList2(filtered, cursor, scrollOffset, selected) {
|
|
667
560
|
const isMulti = selected !== null;
|
|
668
561
|
const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
|
|
669
|
-
const visibleLines = [];
|
|
562
|
+
const visibleLines = [info(S_BAR3)];
|
|
670
563
|
for (let i = scrollOffset; i < end; i++) {
|
|
671
564
|
const opt = filtered[i];
|
|
672
565
|
const isCursor = i === cursor;
|
|
673
566
|
const isChecked = isMulti && selected.has(opt.bcp47);
|
|
674
567
|
const icon = isMulti ? isChecked ? isCursor ? info("\u25FC") : info("\u25FC") : isCursor ? active("\u25FB") : dim("\u25FB") : isCursor ? active("\u25CF") : dim("\u25CB");
|
|
675
568
|
visibleLines.push(
|
|
676
|
-
`${info(
|
|
569
|
+
`${info(S_BAR3)} ${icon} ${isCursor ? bld(opt.label) : opt.label}`
|
|
677
570
|
);
|
|
678
571
|
}
|
|
679
572
|
const hidden = filtered.length - (end - scrollOffset);
|
|
680
573
|
if (hidden > 0)
|
|
681
|
-
visibleLines.push(dim(`${
|
|
682
|
-
if (filtered.length === 0) visibleLines.push(dim(`${
|
|
683
|
-
if (isMulti && selected.size > 0) {
|
|
684
|
-
visibleLines.push(
|
|
685
|
-
dim(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`)
|
|
686
|
-
);
|
|
687
|
-
}
|
|
574
|
+
visibleLines.push(dim(`${S_BAR3} ${hidden} more \u2014 keep typing to narrow`));
|
|
575
|
+
if (filtered.length === 0) visibleLines.push(dim(`${S_BAR3} No matches`));
|
|
688
576
|
return visibleLines.join("\n");
|
|
689
577
|
}
|
|
690
578
|
async function runFilterablePrompt(opts) {
|
|
@@ -705,7 +593,7 @@ async function runFilterablePrompt(opts) {
|
|
|
705
593
|
scrollOffset = cursor - MAX_VISIBLE2 + 1;
|
|
706
594
|
if (scrollOffset < 0) scrollOffset = 0;
|
|
707
595
|
};
|
|
708
|
-
const prompt = new
|
|
596
|
+
const prompt = new Prompt3(
|
|
709
597
|
{
|
|
710
598
|
initialValue: !multi ? options[cursor]?.bcp47 ?? null : null,
|
|
711
599
|
validate() {
|
|
@@ -718,43 +606,34 @@ async function runFilterablePrompt(opts) {
|
|
|
718
606
|
render() {
|
|
719
607
|
const filtered = getFiltered();
|
|
720
608
|
clampCursor(filtered);
|
|
721
|
-
const hdr = `${dim(
|
|
722
|
-
${
|
|
609
|
+
const hdr = `${dim(S_BAR3)}
|
|
610
|
+
${symbol3(this.state)} ${message}
|
|
723
611
|
`;
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
);
|
|
612
|
+
const inputHint = filter.length > 0 ? filter : dim("type to filter");
|
|
613
|
+
const footer = multi ? selected.size > 0 ? dim(`${S_BAR3} ${selected.size} selected \xB7 \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Space to select \xB7 Enter to confirm`) : dim(`${S_BAR3} \u2191\u2193 navigate \xB7 Enter to confirm`);
|
|
727
614
|
switch (this.state) {
|
|
728
615
|
case "submit": {
|
|
729
616
|
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 ?? "";
|
|
730
|
-
return `${hdr}${dim(
|
|
617
|
+
return `${hdr}${dim(S_BAR3)} ${bld(val || dim("none"))}`;
|
|
731
618
|
}
|
|
732
619
|
case "cancel":
|
|
733
|
-
return `${hdr}${dim(
|
|
620
|
+
return `${hdr}${dim(S_BAR3)}`;
|
|
734
621
|
case "error":
|
|
735
622
|
return [
|
|
736
623
|
hdr.trimEnd(),
|
|
737
|
-
`${ylw(
|
|
738
|
-
buildList2(
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
scrollOffset,
|
|
742
|
-
multi ? selected : null
|
|
743
|
-
),
|
|
744
|
-
`${ylw(S_BAR_END2)} ${ylw(this.error)}`,
|
|
624
|
+
`${ylw(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
625
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
626
|
+
footer,
|
|
627
|
+
`${ylw(S_BAR_END3)} ${ylw(this.error)}`,
|
|
745
628
|
""
|
|
746
629
|
].join("\n");
|
|
747
630
|
default:
|
|
748
631
|
return [
|
|
749
632
|
hdr.trimEnd(),
|
|
750
|
-
`${info(
|
|
751
|
-
buildList2(
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
scrollOffset,
|
|
755
|
-
multi ? selected : null
|
|
756
|
-
),
|
|
757
|
-
`${info(S_BAR_END2)}`,
|
|
633
|
+
`${info(S_BAR3)} ${dim("/")} ${inputHint}`,
|
|
634
|
+
buildList2(filtered, cursor, scrollOffset, multi ? selected : null),
|
|
635
|
+
footer,
|
|
636
|
+
`${info(S_BAR_END3)}`,
|
|
758
637
|
""
|
|
759
638
|
].join("\n");
|
|
760
639
|
}
|
|
@@ -811,7 +690,7 @@ ${symbol2(this.state)} ${message}
|
|
|
811
690
|
}
|
|
812
691
|
});
|
|
813
692
|
const result = await prompt.prompt();
|
|
814
|
-
if (
|
|
693
|
+
if (isCancel4(result)) return null;
|
|
815
694
|
return result;
|
|
816
695
|
}
|
|
817
696
|
async function searchSelectLocale(options, message, initialValue) {
|
|
@@ -858,12 +737,116 @@ function buildLanguageOptions(locales) {
|
|
|
858
737
|
byFamily.set(family, opt);
|
|
859
738
|
}
|
|
860
739
|
}
|
|
861
|
-
return Array.from(byFamily.values());
|
|
740
|
+
return Array.from(byFamily.values());
|
|
741
|
+
}
|
|
742
|
+
async function runProjectCreate(params) {
|
|
743
|
+
const { api, userToken, organizationId, repoCanonical, repoRoot } = params;
|
|
744
|
+
const projectName = (params.defaultName ?? "my-project").trim();
|
|
745
|
+
p3.log.success(`Project: ${chalk2.bold(projectName)}`);
|
|
746
|
+
let sourceLocales;
|
|
747
|
+
try {
|
|
748
|
+
({ sourceLocales } = await api.listLocales(userToken));
|
|
749
|
+
} catch {
|
|
750
|
+
p3.log.error(
|
|
751
|
+
"Failed to fetch supported locales. Check your connection and try again."
|
|
752
|
+
);
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
756
|
+
const appDirs = await collectAppDirs({ cwd: repoRoot, maxDirs: params.maxAppDirs });
|
|
757
|
+
if (appDirs === null) return null;
|
|
758
|
+
if (appDirs.length > 0) {
|
|
759
|
+
p3.log.success(`App directories: ${appDirs.map((d) => chalk2.bold(d)).join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
const sourceLocale = await searchSelectLocale(
|
|
762
|
+
languageOptions,
|
|
763
|
+
"Source language (the language your code is written in)",
|
|
764
|
+
params.defaultSourceLocale ?? "en"
|
|
765
|
+
);
|
|
766
|
+
if (sourceLocale === null) return null;
|
|
767
|
+
let compatibleTargets;
|
|
768
|
+
try {
|
|
769
|
+
compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
|
|
770
|
+
} catch {
|
|
771
|
+
p3.log.error(
|
|
772
|
+
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
773
|
+
);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const localeOptions = buildLocaleOptions(compatibleTargets);
|
|
777
|
+
const targetOptions = localeOptions.filter(
|
|
778
|
+
(opt) => opt.bcp47 !== sourceLocale
|
|
779
|
+
);
|
|
780
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
781
|
+
targetOptions,
|
|
782
|
+
"Target languages (languages to translate into)"
|
|
783
|
+
);
|
|
784
|
+
if (targetLocales === null) return null;
|
|
785
|
+
if (targetLocales.length === 0) {
|
|
786
|
+
p3.log.warn(
|
|
787
|
+
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
const detected = detectGitBranches();
|
|
791
|
+
const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
|
|
792
|
+
let pushBranches = [];
|
|
793
|
+
{
|
|
794
|
+
let initial = initialBranches;
|
|
795
|
+
while (pushBranches.length === 0) {
|
|
796
|
+
const result = await filterableBranchSelect({
|
|
797
|
+
message: "Which branches should trigger translations?",
|
|
798
|
+
branches: detected.branches,
|
|
799
|
+
defaultBranch: detected.defaultBranch,
|
|
800
|
+
initialValues: initial
|
|
801
|
+
});
|
|
802
|
+
if (result === null) return null;
|
|
803
|
+
if (result.length === 0) {
|
|
804
|
+
p3.log.warn(
|
|
805
|
+
"At least one branch is required. Please select at least one."
|
|
806
|
+
);
|
|
807
|
+
initial = [detected.defaultBranch];
|
|
808
|
+
} else {
|
|
809
|
+
pushBranches = result;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const targetBranches = pushBranches;
|
|
814
|
+
try {
|
|
815
|
+
const result = await api.createProject(userToken, {
|
|
816
|
+
organizationId,
|
|
817
|
+
name: projectName,
|
|
818
|
+
sourceLocale,
|
|
819
|
+
targetLocales,
|
|
820
|
+
targetBranches,
|
|
821
|
+
appDirs,
|
|
822
|
+
repoCanonical
|
|
823
|
+
});
|
|
824
|
+
p3.log.success(`Project ${chalk2.bold(result.projectName)} created!`);
|
|
825
|
+
return {
|
|
826
|
+
projectId: result.projectId,
|
|
827
|
+
projectName: result.projectName,
|
|
828
|
+
apiKey: result.apiKey,
|
|
829
|
+
sourceLocale,
|
|
830
|
+
targetLocales,
|
|
831
|
+
targetBranches,
|
|
832
|
+
repositoryBound: result.repositoryBound,
|
|
833
|
+
configureUrl: result.configureUrl,
|
|
834
|
+
apps: result.apps
|
|
835
|
+
};
|
|
836
|
+
} catch (error) {
|
|
837
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
838
|
+
p3.log.error(`Failed to create project: ${message}`);
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
862
841
|
}
|
|
863
|
-
async function
|
|
864
|
-
const { api, userToken,
|
|
865
|
-
const
|
|
866
|
-
|
|
842
|
+
async function runAppCreate(params) {
|
|
843
|
+
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
844
|
+
const existingDirs = params.existingApps.map((a) => a.appDir);
|
|
845
|
+
const appDir = await promptSingleAppDir({ existingDirs });
|
|
846
|
+
if (appDir === null) return null;
|
|
847
|
+
if (appDir) {
|
|
848
|
+
p3.log.success(`App directory: ${chalk2.bold(appDir)}`);
|
|
849
|
+
}
|
|
867
850
|
let sourceLocales;
|
|
868
851
|
try {
|
|
869
852
|
({ sourceLocales } = await api.listLocales(userToken));
|
|
@@ -874,14 +857,10 @@ async function runProjectCreate(params) {
|
|
|
874
857
|
return null;
|
|
875
858
|
}
|
|
876
859
|
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
877
|
-
const appDir = params.defaultAppDir ?? "";
|
|
878
|
-
if (appDir) {
|
|
879
|
-
p3.log.success(`App directory: ${chalk3.bold(appDir)}`);
|
|
880
|
-
}
|
|
881
860
|
const sourceLocale = await searchSelectLocale(
|
|
882
861
|
languageOptions,
|
|
883
|
-
"Source language
|
|
884
|
-
|
|
862
|
+
"Source language",
|
|
863
|
+
"en"
|
|
885
864
|
);
|
|
886
865
|
if (sourceLocale === null) return null;
|
|
887
866
|
let compatibleTargets;
|
|
@@ -893,13 +872,12 @@ async function runProjectCreate(params) {
|
|
|
893
872
|
);
|
|
894
873
|
return null;
|
|
895
874
|
}
|
|
896
|
-
const
|
|
897
|
-
const targetOptions = localeOptions.filter(
|
|
875
|
+
const targetOptions = buildLocaleOptions(compatibleTargets).filter(
|
|
898
876
|
(opt) => opt.bcp47 !== sourceLocale
|
|
899
877
|
);
|
|
900
878
|
const targetLocales = await searchMultiSelectLocales(
|
|
901
879
|
targetOptions,
|
|
902
|
-
"Target languages
|
|
880
|
+
"Target languages"
|
|
903
881
|
);
|
|
904
882
|
if (targetLocales === null) return null;
|
|
905
883
|
if (targetLocales.length === 0) {
|
|
@@ -907,154 +885,340 @@ async function runProjectCreate(params) {
|
|
|
907
885
|
"No target languages selected \u2014 you can add them later from the dashboard."
|
|
908
886
|
);
|
|
909
887
|
}
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
let pushBranches = [];
|
|
888
|
+
const detectedApp = detectGitBranches();
|
|
889
|
+
let appPushBranches = [];
|
|
913
890
|
{
|
|
914
|
-
let initial =
|
|
915
|
-
while (
|
|
891
|
+
let initial = [detectedApp.defaultBranch];
|
|
892
|
+
while (appPushBranches.length === 0) {
|
|
916
893
|
const result = await filterableBranchSelect({
|
|
917
894
|
message: "Which branches should trigger translations?",
|
|
918
|
-
branches:
|
|
919
|
-
defaultBranch:
|
|
895
|
+
branches: detectedApp.branches,
|
|
896
|
+
defaultBranch: detectedApp.defaultBranch,
|
|
920
897
|
initialValues: initial
|
|
921
898
|
});
|
|
922
899
|
if (result === null) return null;
|
|
923
900
|
if (result.length === 0) {
|
|
924
|
-
p3.log.warn(
|
|
925
|
-
|
|
926
|
-
);
|
|
927
|
-
initial = [detected.defaultBranch];
|
|
901
|
+
p3.log.warn("At least one branch is required.");
|
|
902
|
+
initial = [detectedApp.defaultBranch];
|
|
928
903
|
} else {
|
|
929
|
-
|
|
904
|
+
appPushBranches = result;
|
|
930
905
|
}
|
|
931
906
|
}
|
|
932
907
|
}
|
|
933
|
-
const targetBranches =
|
|
908
|
+
const targetBranches = appPushBranches;
|
|
934
909
|
try {
|
|
935
|
-
const result = await api.
|
|
936
|
-
|
|
937
|
-
|
|
910
|
+
const result = await api.createApp(userToken, {
|
|
911
|
+
projectId,
|
|
912
|
+
appDir,
|
|
938
913
|
sourceLocale,
|
|
939
914
|
targetLocales,
|
|
940
915
|
targetBranches,
|
|
941
|
-
|
|
942
|
-
repoCanonical
|
|
916
|
+
repoCanonical: repoCanonical ?? ""
|
|
943
917
|
});
|
|
944
|
-
p3.log.success(
|
|
945
|
-
|
|
918
|
+
p3.log.success(
|
|
919
|
+
`App ${chalk2.bold(appDir || "(root)")} added to ${chalk2.bold(projectName)}!`
|
|
920
|
+
);
|
|
921
|
+
return {
|
|
922
|
+
projectId: result.projectId,
|
|
923
|
+
projectName: result.projectName,
|
|
924
|
+
appDir: result.appDir,
|
|
925
|
+
appId: result.appId,
|
|
926
|
+
sourceLocale,
|
|
927
|
+
targetLocales,
|
|
928
|
+
targetBranches
|
|
929
|
+
};
|
|
946
930
|
} catch (error) {
|
|
947
931
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
948
|
-
p3.log.error(`Failed to
|
|
932
|
+
p3.log.error(`Failed to add app: ${message}`);
|
|
949
933
|
return null;
|
|
950
934
|
}
|
|
951
935
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
936
|
+
|
|
937
|
+
// src/utils/github-connect.ts
|
|
938
|
+
import * as p4 from "@clack/prompts";
|
|
939
|
+
import chalk3 from "chalk";
|
|
940
|
+
import { spawn } from "child_process";
|
|
941
|
+
|
|
942
|
+
// src/utils/local-server.ts
|
|
943
|
+
import { createServer } from "http";
|
|
944
|
+
import { URL as URL2 } from "url";
|
|
945
|
+
function startCallbackServer() {
|
|
946
|
+
return new Promise((resolve3, reject) => {
|
|
947
|
+
let settled = false;
|
|
948
|
+
let callbackResolve = null;
|
|
949
|
+
let callbackReject = null;
|
|
950
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
951
|
+
callbackResolve = res;
|
|
952
|
+
callbackReject = rej;
|
|
953
|
+
});
|
|
954
|
+
const server = createServer((req, res) => {
|
|
955
|
+
if (!req.url) {
|
|
956
|
+
res.writeHead(400);
|
|
957
|
+
res.end();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
let pathname;
|
|
961
|
+
let params;
|
|
962
|
+
try {
|
|
963
|
+
const parsed = new URL2(req.url, "http://localhost");
|
|
964
|
+
pathname = parsed.pathname;
|
|
965
|
+
params = Object.fromEntries(parsed.searchParams.entries());
|
|
966
|
+
} catch {
|
|
967
|
+
res.writeHead(400);
|
|
968
|
+
res.end("Bad request");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (pathname !== "/callback") {
|
|
972
|
+
res.writeHead(404);
|
|
973
|
+
res.end("Not found");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
977
|
+
res.end(
|
|
978
|
+
'<!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>'
|
|
979
|
+
);
|
|
980
|
+
if (callbackResolve) {
|
|
981
|
+
callbackResolve(params);
|
|
982
|
+
callbackResolve = null;
|
|
983
|
+
}
|
|
984
|
+
setImmediate(() => server.close());
|
|
985
|
+
});
|
|
986
|
+
server.on("error", (err) => {
|
|
987
|
+
if (!settled) {
|
|
988
|
+
settled = true;
|
|
989
|
+
if (callbackReject) callbackReject(err);
|
|
990
|
+
reject(err);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
server.listen(0, "127.0.0.1", () => {
|
|
994
|
+
if (settled) return;
|
|
995
|
+
settled = true;
|
|
996
|
+
const port = server.address().port;
|
|
997
|
+
resolve3({
|
|
998
|
+
port,
|
|
999
|
+
waitForCallback: () => callbackPromise,
|
|
1000
|
+
close: () => server.close()
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/utils/github-connect.ts
|
|
1007
|
+
async function tryOpenBrowser(url) {
|
|
1008
|
+
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
const platform = process.platform;
|
|
1012
|
+
let command;
|
|
1013
|
+
let args;
|
|
1014
|
+
if (platform === "darwin") {
|
|
1015
|
+
command = "open";
|
|
1016
|
+
args = [url];
|
|
1017
|
+
} else if (platform === "win32") {
|
|
1018
|
+
command = "rundll32";
|
|
1019
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
1020
|
+
} else {
|
|
1021
|
+
command = "xdg-open";
|
|
1022
|
+
args = [url];
|
|
1023
|
+
}
|
|
1024
|
+
return new Promise((resolve3) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const child = spawn(command, args, {
|
|
1027
|
+
detached: true,
|
|
1028
|
+
stdio: "ignore",
|
|
1029
|
+
windowsHide: true
|
|
1030
|
+
});
|
|
1031
|
+
let settled = false;
|
|
1032
|
+
child.once("spawn", () => {
|
|
1033
|
+
if (settled) return;
|
|
1034
|
+
settled = true;
|
|
1035
|
+
child.unref();
|
|
1036
|
+
resolve3(true);
|
|
1037
|
+
});
|
|
1038
|
+
child.once("error", () => {
|
|
1039
|
+
if (settled) return;
|
|
1040
|
+
settled = true;
|
|
1041
|
+
resolve3(false);
|
|
1042
|
+
});
|
|
1043
|
+
setTimeout(() => {
|
|
1044
|
+
if (settled) return;
|
|
1045
|
+
settled = true;
|
|
1046
|
+
resolve3(false);
|
|
1047
|
+
}, 300);
|
|
1048
|
+
} catch {
|
|
1049
|
+
resolve3(false);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
async function runGitHubInstallFlow(params) {
|
|
1054
|
+
let server = null;
|
|
956
1055
|
try {
|
|
957
|
-
|
|
1056
|
+
server = await startCallbackServer();
|
|
958
1057
|
} catch {
|
|
959
|
-
p3.log.error(
|
|
960
|
-
"Failed to fetch supported locales. Check your connection and try again."
|
|
961
|
-
);
|
|
962
|
-
return null;
|
|
963
1058
|
}
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1059
|
+
const { installUrl } = await params.api.startCliGitHubInstall(
|
|
1060
|
+
params.userToken,
|
|
1061
|
+
{
|
|
1062
|
+
organizationId: params.organizationId,
|
|
1063
|
+
callbackPort: server?.port
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
p4.log.info("Opening GitHub to install the Vocoder App...");
|
|
1067
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1068
|
+
const shouldOpen = params.yes ? true : await p4.confirm({ message: "Open in your browser?" });
|
|
1069
|
+
if (p4.isCancel(shouldOpen)) {
|
|
1070
|
+
server?.close();
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
if (shouldOpen) {
|
|
1074
|
+
const opened = await tryOpenBrowser(installUrl);
|
|
1075
|
+
if (!opened) {
|
|
1076
|
+
p4.log.info(
|
|
1077
|
+
"Could not open a browser automatically. Use the URL above."
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
969
1081
|
}
|
|
970
|
-
|
|
971
|
-
|
|
1082
|
+
const connectSpinner = p4.spinner();
|
|
1083
|
+
connectSpinner.start("Waiting for GitHub App installation...");
|
|
1084
|
+
if (server) {
|
|
1085
|
+
try {
|
|
1086
|
+
const params_timeout = 15 * 60 * 1e3;
|
|
1087
|
+
const callbackParams = await Promise.race([
|
|
1088
|
+
server.waitForCallback(),
|
|
1089
|
+
new Promise(
|
|
1090
|
+
(resolve3) => setTimeout(() => resolve3(null), params_timeout)
|
|
1091
|
+
)
|
|
1092
|
+
]);
|
|
1093
|
+
server.close();
|
|
1094
|
+
if (!callbackParams) {
|
|
1095
|
+
connectSpinner.stop("GitHub App installation timed out");
|
|
1096
|
+
p4.log.error(
|
|
1097
|
+
"The installation flow timed out. Run `vocoder init` again."
|
|
1098
|
+
);
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
if (callbackParams.error) {
|
|
1102
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
1103
|
+
p4.log.error(callbackParams.error);
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const { organizationId, connectionLabel, workspace_created } = callbackParams;
|
|
1107
|
+
if (!organizationId || !connectionLabel) {
|
|
1108
|
+
connectSpinner.stop("GitHub App installation incomplete");
|
|
1109
|
+
p4.log.error("Missing organization or connection data from callback.");
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
connectSpinner.stop(
|
|
1113
|
+
`Connected to GitHub as ${chalk3.bold(connectionLabel)}`
|
|
1114
|
+
);
|
|
1115
|
+
const orgName = workspace_created ? connectionLabel : organizationId;
|
|
1116
|
+
return {
|
|
1117
|
+
organizationId,
|
|
1118
|
+
organizationName: orgName,
|
|
1119
|
+
connectionLabel
|
|
1120
|
+
};
|
|
1121
|
+
} catch {
|
|
1122
|
+
server.close();
|
|
1123
|
+
connectSpinner.stop("GitHub App installation failed");
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
972
1126
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
"
|
|
976
|
-
"en"
|
|
1127
|
+
connectSpinner.stop("Could not detect GitHub App installation automatically");
|
|
1128
|
+
p4.log.warn(
|
|
1129
|
+
"Complete the installation in your browser, then run `vocoder init` again."
|
|
977
1130
|
);
|
|
978
|
-
|
|
979
|
-
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
async function runGitHubDiscoveryFlow(params) {
|
|
1134
|
+
let server = null;
|
|
980
1135
|
try {
|
|
981
|
-
|
|
1136
|
+
server = await startCallbackServer();
|
|
982
1137
|
} catch {
|
|
983
|
-
p3.log.error(
|
|
984
|
-
"Failed to fetch compatible target locales. Check your connection and try again."
|
|
985
|
-
);
|
|
986
|
-
return null;
|
|
987
1138
|
}
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
)
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1139
|
+
const { oauthUrl } = await params.api.startCliGitHubOAuth(params.userToken, {
|
|
1140
|
+
organizationId: params.organizationId,
|
|
1141
|
+
callbackPort: server?.port
|
|
1142
|
+
});
|
|
1143
|
+
p4.log.info("Opening GitHub to authorize your account...");
|
|
1144
|
+
p4.note("Complete authorization in your browser.");
|
|
1145
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1146
|
+
const shouldOpen = params.yes ? true : await p4.confirm({ message: "Open in your browser?" });
|
|
1147
|
+
if (p4.isCancel(shouldOpen)) {
|
|
1148
|
+
server?.close();
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
if (shouldOpen) {
|
|
1152
|
+
const opened = await tryOpenBrowser(oauthUrl);
|
|
1153
|
+
if (!opened) {
|
|
1154
|
+
p4.log.info(`Could not open browser automatically. Visit: ${oauthUrl}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1000
1157
|
}
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
{
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1158
|
+
const oauthSpinner = p4.spinner();
|
|
1159
|
+
oauthSpinner.start("Waiting for GitHub authorization...");
|
|
1160
|
+
if (server) {
|
|
1161
|
+
try {
|
|
1162
|
+
const timeoutMs = 10 * 60 * 1e3;
|
|
1163
|
+
const callbackParams = await Promise.race([
|
|
1164
|
+
server.waitForCallback(),
|
|
1165
|
+
new Promise(
|
|
1166
|
+
(resolve3) => setTimeout(() => resolve3(null), timeoutMs)
|
|
1167
|
+
)
|
|
1168
|
+
]);
|
|
1169
|
+
server.close();
|
|
1170
|
+
if (!callbackParams) {
|
|
1171
|
+
oauthSpinner.stop("GitHub authorization timed out");
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
if (callbackParams.error) {
|
|
1175
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1176
|
+
p4.log.error(callbackParams.error);
|
|
1177
|
+
return null;
|
|
1018
1178
|
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
server.close();
|
|
1181
|
+
oauthSpinner.stop("GitHub authorization failed");
|
|
1182
|
+
return null;
|
|
1019
1183
|
}
|
|
1020
1184
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1185
|
+
oauthSpinner.stop("GitHub account authorized");
|
|
1186
|
+
const discoveryResult = await params.api.getCliGitHubDiscovery(
|
|
1187
|
+
params.userToken
|
|
1188
|
+
);
|
|
1189
|
+
return discoveryResult.installations;
|
|
1190
|
+
}
|
|
1191
|
+
async function selectGitHubInstallation(installations, canInstallNew) {
|
|
1192
|
+
const options = installations.map((inst) => ({
|
|
1193
|
+
value: String(inst.installationId),
|
|
1194
|
+
label: inst.accountLogin,
|
|
1195
|
+
hint: [
|
|
1196
|
+
inst.accountType === "Organization" ? "organization" : "personal",
|
|
1197
|
+
inst.conflictLabel ? `connected to ${inst.conflictLabel}` : "",
|
|
1198
|
+
inst.isSuspended ? "suspended" : ""
|
|
1199
|
+
].filter(Boolean).join(" \xB7 ") || void 0
|
|
1200
|
+
}));
|
|
1201
|
+
if (canInstallNew) {
|
|
1202
|
+
options.push({
|
|
1203
|
+
value: "install_new",
|
|
1204
|
+
label: `Install on a new account ${chalk3.dim("(creates a new personal workspace)")}`
|
|
1030
1205
|
});
|
|
1031
|
-
p3.log.success(
|
|
1032
|
-
`App ${chalk3.bold(appDir)} added to ${chalk3.bold(projectName)}!`
|
|
1033
|
-
);
|
|
1034
|
-
return {
|
|
1035
|
-
projectId: result.projectId,
|
|
1036
|
-
projectName: result.projectName,
|
|
1037
|
-
apiKey: result.apiKey,
|
|
1038
|
-
appDir: result.appDir,
|
|
1039
|
-
sourceLocale,
|
|
1040
|
-
targetLocales,
|
|
1041
|
-
targetBranches
|
|
1042
|
-
};
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1045
|
-
p3.log.error(`Failed to add app: ${message}`);
|
|
1046
|
-
return null;
|
|
1047
1206
|
}
|
|
1207
|
+
const selected = await p4.select({
|
|
1208
|
+
message: "Select a GitHub installation",
|
|
1209
|
+
options
|
|
1210
|
+
});
|
|
1211
|
+
if (p4.isCancel(selected)) return null;
|
|
1212
|
+
if (selected === "install_new") return "install_new";
|
|
1213
|
+
return Number(selected);
|
|
1048
1214
|
}
|
|
1049
1215
|
|
|
1050
1216
|
// src/commands/init.ts
|
|
1051
1217
|
import chalk5 from "chalk";
|
|
1052
|
-
import { join as join2 } from "path";
|
|
1053
1218
|
import { config as loadEnv } from "dotenv";
|
|
1054
1219
|
|
|
1055
1220
|
// src/utils/git-identity.ts
|
|
1056
1221
|
import { execSync as execSync2 } from "child_process";
|
|
1057
|
-
import { relative, resolve } from "path";
|
|
1058
1222
|
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
1059
1223
|
function detectCommitSha() {
|
|
1060
1224
|
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
@@ -1132,21 +1296,13 @@ function resolveGitRepositoryIdentity() {
|
|
|
1132
1296
|
if (!parsed) {
|
|
1133
1297
|
return null;
|
|
1134
1298
|
}
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (repositoryRoot) {
|
|
1139
|
-
const relativePath = relative(
|
|
1140
|
-
resolve(repositoryRoot),
|
|
1141
|
-
resolve(currentDirectory)
|
|
1142
|
-
).replace(/\\/g, "/").trim();
|
|
1143
|
-
if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
|
|
1144
|
-
repoAppDir = relativePath;
|
|
1145
|
-
}
|
|
1299
|
+
const repoRoot = safeExec("git rev-parse --show-toplevel");
|
|
1300
|
+
if (!repoRoot) {
|
|
1301
|
+
return null;
|
|
1146
1302
|
}
|
|
1147
1303
|
return {
|
|
1148
1304
|
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
1149
|
-
|
|
1305
|
+
repoRoot
|
|
1150
1306
|
};
|
|
1151
1307
|
}
|
|
1152
1308
|
function resolveGitContext() {
|
|
@@ -1160,47 +1316,47 @@ function resolveGitContext() {
|
|
|
1160
1316
|
return { identity, warnings };
|
|
1161
1317
|
}
|
|
1162
1318
|
|
|
1163
|
-
// src/utils/
|
|
1164
|
-
import * as
|
|
1319
|
+
// src/utils/organization.ts
|
|
1320
|
+
import * as p5 from "@clack/prompts";
|
|
1165
1321
|
import chalk4 from "chalk";
|
|
1166
|
-
async function
|
|
1167
|
-
const {
|
|
1168
|
-
if (
|
|
1322
|
+
async function selectOrganization(result) {
|
|
1323
|
+
const { organizations, canCreateOrganization } = result;
|
|
1324
|
+
if (organizations.length === 0) {
|
|
1169
1325
|
return { action: "create" };
|
|
1170
1326
|
}
|
|
1171
|
-
const options =
|
|
1172
|
-
value:
|
|
1173
|
-
label:
|
|
1327
|
+
const options = organizations.map((org) => ({
|
|
1328
|
+
value: org.id,
|
|
1329
|
+
label: org.name,
|
|
1174
1330
|
hint: [
|
|
1175
|
-
|
|
1176
|
-
|
|
1331
|
+
org.projectCount > 0 ? `${org.projectCount} project${org.projectCount !== 1 ? "s" : ""}` : "",
|
|
1332
|
+
org.connectionLabel ? `GitHub: ${org.connectionLabel}` : ""
|
|
1177
1333
|
].filter(Boolean).join(" \xB7 ") || void 0
|
|
1178
1334
|
}));
|
|
1179
|
-
if (
|
|
1335
|
+
if (canCreateOrganization) {
|
|
1180
1336
|
options.push({ value: "create", label: "Create new workspace" });
|
|
1181
1337
|
}
|
|
1182
|
-
const selected = await
|
|
1338
|
+
const selected = await p5.select({
|
|
1183
1339
|
message: "Select workspace",
|
|
1184
1340
|
options
|
|
1185
1341
|
});
|
|
1186
|
-
if (
|
|
1342
|
+
if (p5.isCancel(selected)) {
|
|
1187
1343
|
return { action: "cancelled" };
|
|
1188
1344
|
}
|
|
1189
1345
|
if (selected === "create") {
|
|
1190
1346
|
return { action: "create" };
|
|
1191
1347
|
}
|
|
1192
|
-
const
|
|
1193
|
-
if (!
|
|
1348
|
+
const organization = organizations.find((org) => org.id === selected);
|
|
1349
|
+
if (!organization) {
|
|
1194
1350
|
return { action: "cancelled" };
|
|
1195
1351
|
}
|
|
1196
|
-
return { action: "use",
|
|
1352
|
+
return { action: "use", organization };
|
|
1197
1353
|
}
|
|
1198
1354
|
|
|
1199
1355
|
// src/commands/init.ts
|
|
1200
1356
|
loadEnv();
|
|
1201
1357
|
var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
|
|
1202
1358
|
async function sleep(ms) {
|
|
1203
|
-
await new Promise((
|
|
1359
|
+
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1204
1360
|
}
|
|
1205
1361
|
async function tryOpenBrowser2(url) {
|
|
1206
1362
|
if (!process.stdout.isTTY || process.env.CI === "true") {
|
|
@@ -1218,7 +1374,7 @@ async function tryOpenBrowser2(url) {
|
|
|
1218
1374
|
command = "xdg-open";
|
|
1219
1375
|
args = [url];
|
|
1220
1376
|
}
|
|
1221
|
-
return await new Promise((
|
|
1377
|
+
return await new Promise((resolve3) => {
|
|
1222
1378
|
try {
|
|
1223
1379
|
const child = spawn2(command, args, {
|
|
1224
1380
|
detached: true,
|
|
@@ -1230,20 +1386,20 @@ async function tryOpenBrowser2(url) {
|
|
|
1230
1386
|
if (settled) return;
|
|
1231
1387
|
settled = true;
|
|
1232
1388
|
child.unref();
|
|
1233
|
-
|
|
1389
|
+
resolve3(true);
|
|
1234
1390
|
});
|
|
1235
1391
|
child.once("error", () => {
|
|
1236
1392
|
if (settled) return;
|
|
1237
1393
|
settled = true;
|
|
1238
|
-
|
|
1394
|
+
resolve3(false);
|
|
1239
1395
|
});
|
|
1240
1396
|
setTimeout(() => {
|
|
1241
1397
|
if (settled) return;
|
|
1242
1398
|
settled = true;
|
|
1243
|
-
|
|
1399
|
+
resolve3(false);
|
|
1244
1400
|
}, 300);
|
|
1245
1401
|
} catch {
|
|
1246
|
-
|
|
1402
|
+
resolve3(false);
|
|
1247
1403
|
}
|
|
1248
1404
|
});
|
|
1249
1405
|
}
|
|
@@ -1255,24 +1411,24 @@ function getSubscriptionSettingsUrl(apiUrl) {
|
|
|
1255
1411
|
return new URL(SUBSCRIPTION_SETTINGS_PATH, apiUrl).toString();
|
|
1256
1412
|
}
|
|
1257
1413
|
function printPlanLimitMessage(apiUrl, message) {
|
|
1258
|
-
|
|
1414
|
+
p6.log.error(`You are over your plan limits.
|
|
1259
1415
|
${message}`);
|
|
1260
|
-
|
|
1416
|
+
p6.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
1261
1417
|
}
|
|
1262
1418
|
function runScaffold(params) {
|
|
1263
|
-
const { sourceLocale, targetBranches
|
|
1419
|
+
const { sourceLocale, targetBranches } = params;
|
|
1264
1420
|
const detection = detectLocalEcosystem();
|
|
1265
1421
|
const useTypeScript = detection.isTypeScript;
|
|
1266
1422
|
if (detection.ecosystem) {
|
|
1267
1423
|
const frameworkLabel = detection.framework ?? detection.ecosystem;
|
|
1268
1424
|
const pmLabel = detection.packageManager;
|
|
1269
|
-
|
|
1425
|
+
p6.log.info(`Detected: ${chalk5.bold(frameworkLabel)} (${pmLabel})`);
|
|
1270
1426
|
}
|
|
1271
1427
|
const { devPackages, runtimePackages } = getPackagesToInstall(detection);
|
|
1272
1428
|
const allPackages = [...devPackages, ...runtimePackages];
|
|
1273
1429
|
if (allPackages.length > 0) {
|
|
1274
|
-
|
|
1275
|
-
const installSpinner =
|
|
1430
|
+
p6.log.info("");
|
|
1431
|
+
const installSpinner = p6.spinner();
|
|
1276
1432
|
installSpinner.start(`Installing ${allPackages.join(", ")}...`);
|
|
1277
1433
|
try {
|
|
1278
1434
|
if (devPackages.length > 0) {
|
|
@@ -1292,19 +1448,22 @@ function runScaffold(params) {
|
|
|
1292
1448
|
installSpinner.stop("Package installation failed");
|
|
1293
1449
|
const cmds = [
|
|
1294
1450
|
devPackages.length > 0 ? buildInstallCommand(detection.packageManager, devPackages, true) : null,
|
|
1295
|
-
runtimePackages.length > 0 ? buildInstallCommand(
|
|
1451
|
+
runtimePackages.length > 0 ? buildInstallCommand(
|
|
1452
|
+
detection.packageManager,
|
|
1453
|
+
runtimePackages,
|
|
1454
|
+
false
|
|
1455
|
+
) : null
|
|
1296
1456
|
].filter(Boolean).join(" && ");
|
|
1297
|
-
|
|
1457
|
+
p6.log.warn(`Run manually: ${highlight(cmds)}`);
|
|
1298
1458
|
}
|
|
1299
1459
|
} else if (detection.ecosystem) {
|
|
1300
|
-
|
|
1460
|
+
p6.log.info(`Packages: ${chalk5.green("already installed")}`);
|
|
1301
1461
|
}
|
|
1302
1462
|
const snippets = getSetupSnippets({
|
|
1303
1463
|
framework: detection.framework,
|
|
1304
1464
|
ecosystem: detection.ecosystem,
|
|
1305
1465
|
sourceLocale,
|
|
1306
|
-
targetBranches
|
|
1307
|
-
appDir
|
|
1466
|
+
targetBranches
|
|
1308
1467
|
});
|
|
1309
1468
|
const steps = [];
|
|
1310
1469
|
if (snippets.pluginStep) {
|
|
@@ -1326,36 +1485,23 @@ function runScaffold(params) {
|
|
|
1326
1485
|
hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
|
|
1327
1486
|
code: snippets.wrapStep.code
|
|
1328
1487
|
});
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1488
|
+
p6.log.message("");
|
|
1489
|
+
p6.log.message(chalk5.bold("Finish setup in your code"));
|
|
1490
|
+
p6.log.message("");
|
|
1332
1491
|
for (let i = 0; i < steps.length; i++) {
|
|
1333
1492
|
const step = steps[i];
|
|
1334
|
-
|
|
1335
|
-
`${chalk5.bold(step.label)} ${chalk5.dim(`\u2014 ${step.hint}`)}`
|
|
1336
|
-
);
|
|
1493
|
+
p6.log.step(`${chalk5.bold(step.label)} ${chalk5.dim(`\u2014 ${step.hint}`)}`);
|
|
1337
1494
|
printCodeBlock(step.code);
|
|
1338
|
-
if (i < steps.length - 1)
|
|
1339
|
-
}
|
|
1340
|
-
const written = writeVocoderConfig({ targetBranches, useTypeScript });
|
|
1341
|
-
if (written) {
|
|
1342
|
-
p5.log.success(`Created ${highlight(written)}`);
|
|
1343
|
-
} else if (!findExistingConfig(process.cwd())) {
|
|
1344
|
-
const ext = useTypeScript ? "ts" : "js";
|
|
1345
|
-
p5.log.warn(
|
|
1346
|
-
`Could not write vocoder.config.${ext} \u2014 create it manually with your extraction patterns.`
|
|
1347
|
-
);
|
|
1495
|
+
if (i < steps.length - 1) p6.log.message("");
|
|
1348
1496
|
}
|
|
1349
|
-
|
|
1497
|
+
p6.log.message("");
|
|
1350
1498
|
const branchList = targetBranches.length > 0 ? targetBranches.map((b) => highlight(b)).join(" or ") : highlight("your target branch");
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
);
|
|
1354
|
-
p5.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
|
|
1499
|
+
p6.log.success(`Push to ${branchList} to trigger your first translation run.`);
|
|
1500
|
+
p6.log.message(info(" Docs: https://vocoder.app/docs/getting-started"));
|
|
1355
1501
|
}
|
|
1356
|
-
function writeApiKeyToEnv(apiKey) {
|
|
1357
|
-
const envPath = join2(process.cwd(), ".env");
|
|
1358
|
-
if (!
|
|
1502
|
+
function writeApiKeyToEnv(apiKey, repoRoot) {
|
|
1503
|
+
const envPath = join2(repoRoot ?? process.cwd(), ".env");
|
|
1504
|
+
if (!existsSync3(envPath)) return false;
|
|
1359
1505
|
try {
|
|
1360
1506
|
const content = readFileSync(envPath, "utf-8");
|
|
1361
1507
|
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
@@ -1373,15 +1519,36 @@ function writeApiKeyToEnv(apiKey) {
|
|
|
1373
1519
|
return false;
|
|
1374
1520
|
}
|
|
1375
1521
|
}
|
|
1376
|
-
function printApiKey(apiKey) {
|
|
1377
|
-
const saved = writeApiKeyToEnv(apiKey);
|
|
1378
|
-
|
|
1379
|
-
|
|
1522
|
+
function printApiKey(apiKey, repoRoot) {
|
|
1523
|
+
const saved = writeApiKeyToEnv(apiKey, repoRoot);
|
|
1524
|
+
p6.log.message("");
|
|
1525
|
+
p6.log.message(chalk5.bold("Your API Key"));
|
|
1380
1526
|
printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
|
|
1381
1527
|
if (saved) {
|
|
1382
|
-
|
|
1528
|
+
p6.log.success(chalk5.dim("Saved to .env"));
|
|
1383
1529
|
} else {
|
|
1384
|
-
|
|
1530
|
+
p6.log.message(chalk5.dim(" Add the above to your .env file"));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function writeAppConfigs(apps, targetBranches, useTypeScript, repoRoot) {
|
|
1534
|
+
const base = repoRoot ?? process.cwd();
|
|
1535
|
+
for (const app of apps) {
|
|
1536
|
+
const dir = app.appDir ? resolve2(base, app.appDir) : base;
|
|
1537
|
+
const written = writeVocoderConfig({
|
|
1538
|
+
targetBranches,
|
|
1539
|
+
appId: app.appId,
|
|
1540
|
+
cwd: dir,
|
|
1541
|
+
useTypeScript
|
|
1542
|
+
});
|
|
1543
|
+
if (written) {
|
|
1544
|
+
const displayPath = app.appDir ? `${app.appDir}/${written}` : written;
|
|
1545
|
+
p6.log.success(`Created ${highlight(displayPath)}`);
|
|
1546
|
+
} else if (!findExistingConfig(dir)) {
|
|
1547
|
+
const ext = useTypeScript ? "ts" : "js";
|
|
1548
|
+
p6.log.warn(
|
|
1549
|
+
`Could not write ${app.appDir ? `${app.appDir}/` : ""}vocoder.config.${ext} \u2014 create it manually.`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1385
1552
|
}
|
|
1386
1553
|
}
|
|
1387
1554
|
function printCodeBlock(code) {
|
|
@@ -1426,23 +1593,23 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1426
1593
|
} else if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
|
|
1427
1594
|
if (reauth) {
|
|
1428
1595
|
if (!options.yes) {
|
|
1429
|
-
const shouldOpen = await
|
|
1596
|
+
const shouldOpen = await p6.confirm({
|
|
1430
1597
|
message: "Open your browser to sign in again?"
|
|
1431
1598
|
});
|
|
1432
|
-
if (
|
|
1599
|
+
if (p6.isCancel(shouldOpen)) {
|
|
1433
1600
|
server?.close();
|
|
1434
|
-
|
|
1601
|
+
p6.cancel("Setup cancelled.");
|
|
1435
1602
|
return null;
|
|
1436
1603
|
}
|
|
1437
1604
|
if (!shouldOpen) {
|
|
1438
1605
|
server?.close();
|
|
1439
|
-
|
|
1606
|
+
p6.cancel("Setup cancelled.");
|
|
1440
1607
|
return null;
|
|
1441
1608
|
} else {
|
|
1442
1609
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
1443
1610
|
if (!opened) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1611
|
+
p6.note(browserUrl, "Sign In");
|
|
1612
|
+
p6.log.info("Open the URL above manually to continue.");
|
|
1446
1613
|
}
|
|
1447
1614
|
}
|
|
1448
1615
|
} else {
|
|
@@ -1451,20 +1618,24 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1451
1618
|
} else {
|
|
1452
1619
|
let isLinkFlow = false;
|
|
1453
1620
|
if (!options.yes) {
|
|
1454
|
-
const connectChoice = await
|
|
1621
|
+
const connectChoice = await p6.select({
|
|
1455
1622
|
message: "Vocoder needs to be installed on your GitHub account to get started",
|
|
1456
1623
|
options: [
|
|
1457
1624
|
{
|
|
1458
1625
|
value: "install",
|
|
1459
1626
|
label: "Install GitHub App",
|
|
1460
|
-
hint: "
|
|
1627
|
+
hint: "new user"
|
|
1461
1628
|
},
|
|
1462
|
-
{
|
|
1629
|
+
{
|
|
1630
|
+
value: "link",
|
|
1631
|
+
label: "Already installed? Link your account",
|
|
1632
|
+
hint: "returning user"
|
|
1633
|
+
}
|
|
1463
1634
|
]
|
|
1464
1635
|
});
|
|
1465
|
-
if (
|
|
1636
|
+
if (p6.isCancel(connectChoice)) {
|
|
1466
1637
|
server?.close();
|
|
1467
|
-
|
|
1638
|
+
p6.cancel("Setup cancelled.");
|
|
1468
1639
|
return null;
|
|
1469
1640
|
}
|
|
1470
1641
|
isLinkFlow = connectChoice === "link";
|
|
@@ -1483,13 +1654,13 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1483
1654
|
}
|
|
1484
1655
|
const opened = await tryOpenBrowser2(urlToOpen);
|
|
1485
1656
|
if (!opened) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1657
|
+
p6.log.warn("Could not open your browser automatically.");
|
|
1658
|
+
p6.note(urlToOpen, "GitHub");
|
|
1659
|
+
p6.log.info("Open the URL above to continue.");
|
|
1489
1660
|
}
|
|
1490
1661
|
}
|
|
1491
1662
|
}
|
|
1492
|
-
const authSpinner =
|
|
1663
|
+
const authSpinner = p6.spinner();
|
|
1493
1664
|
authSpinner.start("Waiting for GitHub authorization...");
|
|
1494
1665
|
let rawToken = null;
|
|
1495
1666
|
let callbackOrganizationId;
|
|
@@ -1510,19 +1681,19 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1510
1681
|
}
|
|
1511
1682
|
return null;
|
|
1512
1683
|
})();
|
|
1513
|
-
const winner = await new Promise((
|
|
1684
|
+
const winner = await new Promise((resolve3) => {
|
|
1514
1685
|
let done = false;
|
|
1515
1686
|
serverCallback.then((params) => {
|
|
1516
1687
|
if (done || params === null || typeof params.token !== "string") return;
|
|
1517
1688
|
done = true;
|
|
1518
|
-
|
|
1689
|
+
resolve3({ kind: "server", params });
|
|
1519
1690
|
}).catch(() => {
|
|
1520
1691
|
});
|
|
1521
1692
|
sessionPoll.then((result) => {
|
|
1522
1693
|
if (done || result === null) return;
|
|
1523
1694
|
if (result.status === "complete" || result.status === "failed") {
|
|
1524
1695
|
done = true;
|
|
1525
|
-
|
|
1696
|
+
resolve3({
|
|
1526
1697
|
kind: "poll",
|
|
1527
1698
|
result
|
|
1528
1699
|
});
|
|
@@ -1533,7 +1704,7 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1533
1704
|
() => {
|
|
1534
1705
|
if (!done) {
|
|
1535
1706
|
done = true;
|
|
1536
|
-
|
|
1707
|
+
resolve3(null);
|
|
1537
1708
|
}
|
|
1538
1709
|
},
|
|
1539
1710
|
Math.max(0, deadline - Date.now())
|
|
@@ -1557,13 +1728,13 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1557
1728
|
}
|
|
1558
1729
|
} else {
|
|
1559
1730
|
authSpinner.stop();
|
|
1560
|
-
|
|
1731
|
+
p6.log.error(winner.result.reason);
|
|
1561
1732
|
return null;
|
|
1562
1733
|
}
|
|
1563
1734
|
}
|
|
1564
1735
|
if (!rawToken) {
|
|
1565
1736
|
authSpinner.stop();
|
|
1566
|
-
|
|
1737
|
+
p6.log.error("The authentication link expired. Run `vocoder init` again.");
|
|
1567
1738
|
return null;
|
|
1568
1739
|
}
|
|
1569
1740
|
const userInfo = await api.getCliUserInfo(rawToken);
|
|
@@ -1577,34 +1748,44 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1577
1748
|
}
|
|
1578
1749
|
async function init(options = {}) {
|
|
1579
1750
|
const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
|
|
1580
|
-
|
|
1751
|
+
p6.intro(chalk5.bold("Vocoder Setup"));
|
|
1581
1752
|
try {
|
|
1582
1753
|
const gitContext = resolveGitContext();
|
|
1583
1754
|
const identity = gitContext.identity;
|
|
1584
1755
|
if (gitContext.warnings.length > 0) {
|
|
1585
1756
|
for (const warning of gitContext.warnings) {
|
|
1586
|
-
|
|
1757
|
+
p6.log.warn(warning);
|
|
1587
1758
|
}
|
|
1588
1759
|
}
|
|
1589
1760
|
let existingAppsForRepo = [];
|
|
1590
1761
|
let repoProjectId = null;
|
|
1591
1762
|
let repoProjectName = null;
|
|
1763
|
+
let lookup = null;
|
|
1592
1764
|
if (identity) {
|
|
1593
1765
|
const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
1594
|
-
|
|
1766
|
+
lookup = await anonApi.lookupAppByRepo({
|
|
1595
1767
|
repoCanonical: identity.repoCanonical,
|
|
1596
|
-
appDir:
|
|
1768
|
+
appDir: ""
|
|
1597
1769
|
});
|
|
1598
|
-
if (lookup.
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1770
|
+
if (lookup.existingApps.length > 0) {
|
|
1771
|
+
const allApps = lookup.existingApps;
|
|
1772
|
+
const firstApp = allApps[0];
|
|
1773
|
+
p6.log.success(`Project: ${chalk5.bold(firstApp.projectName)}`);
|
|
1774
|
+
p6.log.info(
|
|
1775
|
+
`Configured apps: ${allApps.map((a) => highlight(a.appDir || "(entire repo)")).join(", ")}`
|
|
1603
1776
|
);
|
|
1604
|
-
const
|
|
1605
|
-
message: "
|
|
1777
|
+
const routeAction = await p6.select({
|
|
1778
|
+
message: "This repo is already set up. What would you like to do?",
|
|
1779
|
+
options: [
|
|
1780
|
+
{ value: "key", label: "Get an API key for this project" },
|
|
1781
|
+
{ value: "add", label: "Add a new app directory" }
|
|
1782
|
+
]
|
|
1606
1783
|
});
|
|
1607
|
-
if (
|
|
1784
|
+
if (p6.isCancel(routeAction)) {
|
|
1785
|
+
p6.cancel("Setup cancelled.");
|
|
1786
|
+
return 1;
|
|
1787
|
+
}
|
|
1788
|
+
if (routeAction === "key") {
|
|
1608
1789
|
const anonApi2 = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
1609
1790
|
const authResult = await runAuthFlow(
|
|
1610
1791
|
anonApi2,
|
|
@@ -1613,44 +1794,37 @@ async function init(options = {}) {
|
|
|
1613
1794
|
true
|
|
1614
1795
|
);
|
|
1615
1796
|
if (!authResult) return 1;
|
|
1616
|
-
const spinner7 =
|
|
1617
|
-
spinner7.start("Generating
|
|
1797
|
+
const spinner7 = p6.spinner();
|
|
1798
|
+
spinner7.start("Generating API key...");
|
|
1799
|
+
let apiKey;
|
|
1618
1800
|
try {
|
|
1619
|
-
|
|
1801
|
+
({ apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
1620
1802
|
authResult.token,
|
|
1621
|
-
|
|
1622
|
-
);
|
|
1623
|
-
spinner7.stop("
|
|
1624
|
-
printApiKey(apiKey);
|
|
1803
|
+
firstApp.projectId
|
|
1804
|
+
));
|
|
1805
|
+
spinner7.stop("API key ready");
|
|
1625
1806
|
} catch (err) {
|
|
1626
1807
|
spinner7.stop("Failed to generate key");
|
|
1627
1808
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1628
|
-
|
|
1629
|
-
|
|
1809
|
+
p6.log.error(`Could not generate API key: ${msg}`);
|
|
1810
|
+
p6.log.info("Try again or generate one from the dashboard.");
|
|
1630
1811
|
return 1;
|
|
1631
1812
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
p5.log.success(`Project: ${chalk5.bold(wholeRepo.projectName)}`);
|
|
1643
|
-
const isTs = detectLocalEcosystem().isTypeScript;
|
|
1644
|
-
const written = writeVocoderConfig({ targetBranches: ["main"], useTypeScript: isTs });
|
|
1645
|
-
if (written) p5.log.success(`Created ${highlight(written)}`);
|
|
1646
|
-
p5.outro("Vocoder is already set up for this repository.");
|
|
1813
|
+
printApiKey(apiKey, identity.repoRoot);
|
|
1814
|
+
const detection2 = detectLocalEcosystem();
|
|
1815
|
+
const targetBranches = lookup.exactMatch?.targetBranches ?? ["main"];
|
|
1816
|
+
writeAppConfigs(
|
|
1817
|
+
allApps.map((a) => ({ appDir: a.appDir, appId: a.appId })),
|
|
1818
|
+
targetBranches,
|
|
1819
|
+
detection2.isTypeScript,
|
|
1820
|
+
identity.repoRoot
|
|
1821
|
+
);
|
|
1822
|
+
p6.outro("Vocoder is set up for this repository.");
|
|
1647
1823
|
return 0;
|
|
1648
1824
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
repoProjectId = lookup.existingApps[0]?.projectId ?? null;
|
|
1653
|
-
repoProjectName = lookup.existingApps[0]?.projectName ?? null;
|
|
1825
|
+
existingAppsForRepo = allApps;
|
|
1826
|
+
repoProjectId = firstApp.projectId;
|
|
1827
|
+
repoProjectName = firstApp.projectName;
|
|
1654
1828
|
}
|
|
1655
1829
|
}
|
|
1656
1830
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
@@ -1660,16 +1834,16 @@ async function init(options = {}) {
|
|
|
1660
1834
|
let authOrganizationId;
|
|
1661
1835
|
const storedAuth = await verifyStoredAuth(api);
|
|
1662
1836
|
if (storedAuth.status === "valid") {
|
|
1663
|
-
|
|
1837
|
+
p6.log.success(`Authenticated as ${chalk5.bold(storedAuth.email)}`);
|
|
1664
1838
|
userToken = storedAuth.token;
|
|
1665
1839
|
userEmail = storedAuth.email;
|
|
1666
1840
|
userName = storedAuth.name;
|
|
1667
1841
|
} else {
|
|
1668
1842
|
const reauth = storedAuth.status === "expired";
|
|
1669
1843
|
if (reauth) {
|
|
1670
|
-
|
|
1844
|
+
p6.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1671
1845
|
} else if (storedAuth.status === "gone") {
|
|
1672
|
-
|
|
1846
|
+
p6.log.warn("Account not found \u2014 starting fresh setup");
|
|
1673
1847
|
}
|
|
1674
1848
|
const authResult = await runAuthFlow(
|
|
1675
1849
|
api,
|
|
@@ -1690,101 +1864,57 @@ async function init(options = {}) {
|
|
|
1690
1864
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1691
1865
|
});
|
|
1692
1866
|
}
|
|
1693
|
-
let
|
|
1694
|
-
let
|
|
1867
|
+
let selectedOrganizationId;
|
|
1868
|
+
let selectedOrganizationName;
|
|
1869
|
+
const repoOrgContext = identity ? lookup?.organizationContext ?? null : null;
|
|
1695
1870
|
if (authOrganizationId) {
|
|
1696
|
-
const
|
|
1697
|
-
const ws =
|
|
1871
|
+
const organizationData = await api.listOrganizations(userToken);
|
|
1872
|
+
const ws = organizationData.organizations.find(
|
|
1698
1873
|
(w) => w.id === authOrganizationId
|
|
1699
1874
|
);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
`Connected as ${chalk5.bold(userEmail)} \u2014 workspace: ${chalk5.bold(
|
|
1875
|
+
selectedOrganizationId = authOrganizationId;
|
|
1876
|
+
selectedOrganizationName = ws?.name ?? userEmail;
|
|
1877
|
+
p6.log.success(
|
|
1878
|
+
`Connected as ${chalk5.bold(userEmail)} \u2014 workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
1704
1879
|
);
|
|
1880
|
+
} else if (repoOrgContext && !repoProjectId) {
|
|
1881
|
+
selectedOrganizationId = repoOrgContext.organizationId;
|
|
1882
|
+
selectedOrganizationName = repoOrgContext.organizationName;
|
|
1883
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1705
1884
|
} else {
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
1711
|
-
if (repoOwner) {
|
|
1712
|
-
const hasMatchingAccount = cachedInstallations.some(
|
|
1713
|
-
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
1714
|
-
);
|
|
1715
|
-
if (!hasMatchingAccount) {
|
|
1716
|
-
p5.log.warn(
|
|
1717
|
-
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
1718
|
-
The project will be created but translations won't trigger automatically.
|
|
1719
|
-
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
1720
|
-
);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
const validInstallations = cachedInstallations.filter(
|
|
1725
|
-
(i) => !i.isSuspended && !i.conflictLabel
|
|
1726
|
-
);
|
|
1727
|
-
let selectedInstallationId = null;
|
|
1728
|
-
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
1729
|
-
selectedInstallationId = validInstallations[0].installationId;
|
|
1730
|
-
} else {
|
|
1731
|
-
selectedInstallationId = await selectGitHubInstallation(
|
|
1732
|
-
cachedInstallations.map((inst) => ({
|
|
1733
|
-
installationId: inst.installationId,
|
|
1734
|
-
accountLogin: inst.accountLogin,
|
|
1735
|
-
accountType: inst.accountType,
|
|
1736
|
-
isSuspended: inst.isSuspended,
|
|
1737
|
-
conflictLabel: inst.conflictLabel
|
|
1738
|
-
})),
|
|
1739
|
-
false
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
if (selectedInstallationId === null || selectedInstallationId === "install_new") {
|
|
1743
|
-
p5.cancel(
|
|
1744
|
-
"Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
|
|
1745
|
-
);
|
|
1746
|
-
return 1;
|
|
1747
|
-
}
|
|
1748
|
-
const claimResult = await api.claimCliGitHubInstallation(userToken, {
|
|
1749
|
-
installationId: String(selectedInstallationId),
|
|
1750
|
-
organizationId: null
|
|
1751
|
-
});
|
|
1752
|
-
selectedWorkspaceId = claimResult.organizationId;
|
|
1753
|
-
selectedWorkspaceName = claimResult.organizationName;
|
|
1754
|
-
p5.log.success(`Workspace: ${chalk5.bold(selectedWorkspaceName)}`);
|
|
1755
|
-
} else {
|
|
1756
|
-
const workspaceData = await api.listWorkspaces(userToken, {
|
|
1757
|
-
repo: identity?.repoCanonical
|
|
1758
|
-
});
|
|
1885
|
+
const organizationData = await api.listOrganizations(userToken, {
|
|
1886
|
+
repo: identity?.repoCanonical
|
|
1887
|
+
});
|
|
1888
|
+
{
|
|
1759
1889
|
const repoCanonical = identity?.repoCanonical ?? null;
|
|
1760
|
-
const covering = repoCanonical ?
|
|
1761
|
-
const connected =
|
|
1890
|
+
const covering = repoCanonical ? organizationData.organizations.filter((w) => w.coversRepo === true) : [];
|
|
1891
|
+
const connected = organizationData.organizations.filter(
|
|
1762
1892
|
(w) => w.hasGitHubConnection
|
|
1763
1893
|
);
|
|
1764
1894
|
if (repoCanonical && covering.length === 1) {
|
|
1765
1895
|
const ws = covering[0];
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1896
|
+
selectedOrganizationId = ws.id;
|
|
1897
|
+
selectedOrganizationName = ws.name;
|
|
1898
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1769
1899
|
} else if (repoCanonical && covering.length > 1) {
|
|
1770
|
-
const choice = await
|
|
1900
|
+
const choice = await p6.select({
|
|
1771
1901
|
message: "Select workspace for this repo",
|
|
1772
1902
|
options: covering.map((w) => ({
|
|
1773
1903
|
value: w.id,
|
|
1774
|
-
label: `${w.name} ${chalk5.dim(`(${w.
|
|
1904
|
+
label: `${w.name} ${chalk5.dim(`(${w.appCount} app${w.appCount !== 1 ? "s" : ""})`)}`
|
|
1775
1905
|
}))
|
|
1776
1906
|
});
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
1907
|
+
if (p6.isCancel(choice)) {
|
|
1908
|
+
p6.cancel("Setup cancelled.");
|
|
1779
1909
|
return 1;
|
|
1780
1910
|
}
|
|
1781
1911
|
const ws = covering.find((w) => w.id === choice);
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1912
|
+
selectedOrganizationId = ws.id;
|
|
1913
|
+
selectedOrganizationName = ws.name;
|
|
1914
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1785
1915
|
} else if (repoCanonical && covering.length === 0 && connected.length > 0) {
|
|
1786
1916
|
const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
|
|
1787
|
-
|
|
1917
|
+
p6.log.warn(
|
|
1788
1918
|
`${chalk5.bold(shortRepo)} isn't accessible from your Vocoder installation.
|
|
1789
1919
|
Grant access to this repository or install on the account that owns it.`
|
|
1790
1920
|
);
|
|
@@ -1802,18 +1932,18 @@ async function init(options = {}) {
|
|
|
1802
1932
|
label: `Install on a different GitHub account ${chalk5.dim("(creates a new personal workspace)")}`
|
|
1803
1933
|
});
|
|
1804
1934
|
fixOptions.push({ value: "cancel", label: "Cancel" });
|
|
1805
|
-
const fix = await
|
|
1935
|
+
const fix = await p6.select({
|
|
1806
1936
|
message: "How would you like to fix this?",
|
|
1807
1937
|
options: fixOptions
|
|
1808
1938
|
});
|
|
1809
|
-
if (
|
|
1810
|
-
|
|
1939
|
+
if (p6.isCancel(fix) || fix === "cancel") {
|
|
1940
|
+
p6.cancel("Setup cancelled.");
|
|
1811
1941
|
return 1;
|
|
1812
1942
|
}
|
|
1813
1943
|
if (fix.startsWith("grant:")) {
|
|
1814
1944
|
const ws = connected.find((w) => `grant:${w.id}` === fix);
|
|
1815
1945
|
await tryOpenBrowser2(ws.installationConfigureUrl);
|
|
1816
|
-
|
|
1946
|
+
p6.cancel(
|
|
1817
1947
|
`Grant access to ${chalk5.bold(shortRepo)} in your browser,
|
|
1818
1948
|
then re-run ${chalk5.bold("vocoder init")}.`
|
|
1819
1949
|
);
|
|
@@ -1825,40 +1955,94 @@ async function init(options = {}) {
|
|
|
1825
1955
|
yes: options.yes
|
|
1826
1956
|
});
|
|
1827
1957
|
if (!connectResult) {
|
|
1828
|
-
|
|
1958
|
+
p6.log.error(
|
|
1829
1959
|
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
1830
1960
|
);
|
|
1831
1961
|
return 1;
|
|
1832
1962
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1963
|
+
selectedOrganizationId = connectResult.organizationId;
|
|
1964
|
+
selectedOrganizationName = connectResult.organizationName;
|
|
1965
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1836
1966
|
} else {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1967
|
+
const discoveryResult = await api.getCliGitHubDiscovery(userToken).catch(() => null);
|
|
1968
|
+
const cachedInstallations = discoveryResult?.installations ?? [];
|
|
1969
|
+
if (cachedInstallations.length > 0) {
|
|
1970
|
+
if (identity?.repoCanonical) {
|
|
1971
|
+
const repoOwner = identity.repoCanonical.split(":")[1]?.split("/")[0]?.toLowerCase();
|
|
1972
|
+
if (repoOwner) {
|
|
1973
|
+
const hasMatchingAccount = cachedInstallations.some(
|
|
1974
|
+
(i) => i.accountLogin.toLowerCase() === repoOwner
|
|
1975
|
+
);
|
|
1976
|
+
if (!hasMatchingAccount) {
|
|
1977
|
+
p6.log.warn(
|
|
1978
|
+
`None of your GitHub App installations belong to "${repoOwner}", the account that owns this repository.
|
|
1979
|
+
The project will be created but translations won't trigger automatically.
|
|
1980
|
+
To fix: install the Vocoder GitHub App on "${repoOwner}" instead.`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const validInstallations = cachedInstallations.filter(
|
|
1986
|
+
(i) => !i.isSuspended && !i.conflictLabel
|
|
1987
|
+
);
|
|
1988
|
+
let selectedInstallationId = null;
|
|
1989
|
+
if (validInstallations.length === 1 && cachedInstallations.length === 1) {
|
|
1990
|
+
selectedInstallationId = validInstallations[0].installationId;
|
|
1991
|
+
} else {
|
|
1992
|
+
selectedInstallationId = await selectGitHubInstallation(
|
|
1993
|
+
cachedInstallations.map((inst) => ({
|
|
1994
|
+
installationId: inst.installationId,
|
|
1995
|
+
accountLogin: inst.accountLogin,
|
|
1996
|
+
accountType: inst.accountType,
|
|
1997
|
+
isSuspended: inst.isSuspended,
|
|
1998
|
+
conflictLabel: inst.conflictLabel
|
|
1999
|
+
})),
|
|
2000
|
+
false
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
if (selectedInstallationId === null || selectedInstallationId === "install_new") {
|
|
2004
|
+
p6.cancel(
|
|
2005
|
+
"Setup cancelled. Re-run `vocoder init` and choose Install GitHub App."
|
|
2006
|
+
);
|
|
2007
|
+
return 1;
|
|
2008
|
+
}
|
|
2009
|
+
const claimResult = await api.claimCliGitHubInstallation(
|
|
2010
|
+
userToken,
|
|
2011
|
+
{
|
|
2012
|
+
installationId: String(selectedInstallationId),
|
|
2013
|
+
organizationId: null
|
|
2014
|
+
}
|
|
2015
|
+
);
|
|
2016
|
+
selectedOrganizationId = claimResult.organizationId;
|
|
2017
|
+
selectedOrganizationName = claimResult.organizationName;
|
|
2018
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
2019
|
+
} else if (organizationData.organizations.length === 1 && !organizationData.canCreateOrganization) {
|
|
2020
|
+
const ws = organizationData.organizations[0];
|
|
2021
|
+
selectedOrganizationId = ws.id;
|
|
2022
|
+
selectedOrganizationName = ws.name;
|
|
2023
|
+
p6.log.success(`Workspace: ${chalk5.bold(selectedOrganizationName)}`);
|
|
1842
2024
|
} else {
|
|
1843
|
-
const
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
2025
|
+
const organizationResult = await selectOrganization(organizationData);
|
|
2026
|
+
if (organizationResult.action === "cancelled") {
|
|
2027
|
+
p6.cancel("Setup cancelled.");
|
|
1846
2028
|
return 1;
|
|
1847
2029
|
}
|
|
1848
|
-
if (
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2030
|
+
if (organizationResult.action === "use") {
|
|
2031
|
+
selectedOrganizationId = organizationResult.organization.id;
|
|
2032
|
+
selectedOrganizationName = organizationResult.organization.name;
|
|
2033
|
+
p6.log.success(
|
|
2034
|
+
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
2035
|
+
);
|
|
1852
2036
|
} else {
|
|
1853
|
-
const connectChoice = await
|
|
2037
|
+
const connectChoice = await p6.select({
|
|
1854
2038
|
message: "Connect your new workspace to GitHub",
|
|
1855
2039
|
options: [
|
|
1856
2040
|
{ value: "install", label: "Install the Vocoder GitHub App" },
|
|
1857
2041
|
{ value: "link", label: "Link an existing installation" }
|
|
1858
2042
|
]
|
|
1859
2043
|
});
|
|
1860
|
-
if (
|
|
1861
|
-
|
|
2044
|
+
if (p6.isCancel(connectChoice)) {
|
|
2045
|
+
p6.cancel("Setup cancelled.");
|
|
1862
2046
|
return 1;
|
|
1863
2047
|
}
|
|
1864
2048
|
if (connectChoice === "install") {
|
|
@@ -1868,15 +2052,15 @@ async function init(options = {}) {
|
|
|
1868
2052
|
yes: options.yes
|
|
1869
2053
|
});
|
|
1870
2054
|
if (!connectResult) {
|
|
1871
|
-
|
|
2055
|
+
p6.log.error(
|
|
1872
2056
|
"GitHub App installation did not complete. Run `vocoder init` again."
|
|
1873
2057
|
);
|
|
1874
2058
|
return 1;
|
|
1875
2059
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
`Workspace: ${chalk5.bold(
|
|
2060
|
+
selectedOrganizationId = connectResult.organizationId;
|
|
2061
|
+
selectedOrganizationName = connectResult.organizationName;
|
|
2062
|
+
p6.log.success(
|
|
2063
|
+
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
1880
2064
|
);
|
|
1881
2065
|
} else {
|
|
1882
2066
|
const installations = await runGitHubDiscoveryFlow({
|
|
@@ -1886,21 +2070,21 @@ async function init(options = {}) {
|
|
|
1886
2070
|
});
|
|
1887
2071
|
if (!installations) return 1;
|
|
1888
2072
|
if (installations.length === 0) {
|
|
1889
|
-
|
|
2073
|
+
p6.log.warn(
|
|
1890
2074
|
"No GitHub installations found. Install the Vocoder GitHub App first."
|
|
1891
2075
|
);
|
|
1892
|
-
const installNow = await
|
|
2076
|
+
const installNow = await p6.confirm({
|
|
1893
2077
|
message: "Open GitHub to install the App?"
|
|
1894
2078
|
});
|
|
1895
|
-
if (
|
|
2079
|
+
if (p6.isCancel(installNow) || !installNow) return 1;
|
|
1896
2080
|
const connectResult = await runGitHubInstallFlow({
|
|
1897
2081
|
api,
|
|
1898
2082
|
userToken,
|
|
1899
2083
|
yes: options.yes
|
|
1900
2084
|
});
|
|
1901
2085
|
if (!connectResult) return 1;
|
|
1902
|
-
|
|
1903
|
-
|
|
2086
|
+
selectedOrganizationId = connectResult.organizationId;
|
|
2087
|
+
selectedOrganizationName = connectResult.organizationName;
|
|
1904
2088
|
} else {
|
|
1905
2089
|
const selectedInstallationId = await selectGitHubInstallation(
|
|
1906
2090
|
installations.map((inst) => ({
|
|
@@ -1913,7 +2097,7 @@ async function init(options = {}) {
|
|
|
1913
2097
|
true
|
|
1914
2098
|
);
|
|
1915
2099
|
if (selectedInstallationId === null) {
|
|
1916
|
-
|
|
2100
|
+
p6.cancel("Setup cancelled.");
|
|
1917
2101
|
return 1;
|
|
1918
2102
|
}
|
|
1919
2103
|
if (selectedInstallationId === "install_new") {
|
|
@@ -1923,8 +2107,8 @@ async function init(options = {}) {
|
|
|
1923
2107
|
yes: options.yes
|
|
1924
2108
|
});
|
|
1925
2109
|
if (!connectResult) return 1;
|
|
1926
|
-
|
|
1927
|
-
|
|
2110
|
+
selectedOrganizationId = connectResult.organizationId;
|
|
2111
|
+
selectedOrganizationName = connectResult.organizationName;
|
|
1928
2112
|
} else {
|
|
1929
2113
|
const claimResult = await api.claimCliGitHubInstallation(
|
|
1930
2114
|
userToken,
|
|
@@ -1933,12 +2117,12 @@ async function init(options = {}) {
|
|
|
1933
2117
|
organizationId: null
|
|
1934
2118
|
}
|
|
1935
2119
|
);
|
|
1936
|
-
|
|
1937
|
-
|
|
2120
|
+
selectedOrganizationId = claimResult.organizationId;
|
|
2121
|
+
selectedOrganizationName = claimResult.organizationName;
|
|
1938
2122
|
}
|
|
1939
2123
|
}
|
|
1940
|
-
|
|
1941
|
-
`Workspace: ${chalk5.bold(
|
|
2124
|
+
p6.log.success(
|
|
2125
|
+
`Workspace: ${chalk5.bold(selectedOrganizationName)}`
|
|
1942
2126
|
);
|
|
1943
2127
|
}
|
|
1944
2128
|
}
|
|
@@ -1947,116 +2131,85 @@ async function init(options = {}) {
|
|
|
1947
2131
|
}
|
|
1948
2132
|
}
|
|
1949
2133
|
if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
|
|
1950
|
-
p5.log.info(
|
|
1951
|
-
`${chalk5.bold(repoProjectName)} is already set up for this repo.
|
|
1952
|
-
Configured apps: ${existingAppsForRepo.map((a) => highlight(a.appDir || "(entire repo)")).join(", ")}`
|
|
1953
|
-
);
|
|
1954
2134
|
const appResult = await runAppCreate({
|
|
1955
2135
|
api,
|
|
1956
2136
|
userToken,
|
|
1957
2137
|
projectId: repoProjectId,
|
|
1958
2138
|
projectName: repoProjectName,
|
|
1959
|
-
organizationName:
|
|
2139
|
+
organizationName: selectedOrganizationName,
|
|
1960
2140
|
repoCanonical: identity?.repoCanonical,
|
|
1961
|
-
defaultAppDir: identity?.repoAppDir,
|
|
1962
2141
|
existingApps: existingAppsForRepo
|
|
1963
2142
|
});
|
|
1964
2143
|
if (!appResult) {
|
|
1965
|
-
|
|
2144
|
+
p6.log.error("App setup failed. Run `vocoder init` again.");
|
|
1966
2145
|
return 1;
|
|
1967
2146
|
}
|
|
2147
|
+
const detection2 = detectLocalEcosystem();
|
|
1968
2148
|
runScaffold({
|
|
1969
2149
|
sourceLocale: appResult.sourceLocale,
|
|
1970
|
-
targetBranches: appResult.targetBranches
|
|
1971
|
-
appDir: identity?.repoAppDir
|
|
2150
|
+
targetBranches: appResult.targetBranches
|
|
1972
2151
|
});
|
|
1973
|
-
|
|
2152
|
+
writeAppConfigs(
|
|
2153
|
+
[{ appDir: appResult.appDir, appId: appResult.appId }],
|
|
2154
|
+
appResult.targetBranches,
|
|
2155
|
+
detection2.isTypeScript,
|
|
2156
|
+
identity?.repoRoot
|
|
2157
|
+
);
|
|
2158
|
+
p6.log.info(
|
|
2159
|
+
chalk5.dim("Use the VOCODER_API_KEY already in your root .env")
|
|
2160
|
+
);
|
|
2161
|
+
p6.outro("You're all set.");
|
|
1974
2162
|
return 0;
|
|
1975
2163
|
}
|
|
2164
|
+
let remainingApps;
|
|
1976
2165
|
try {
|
|
1977
|
-
const wsCheck = await api.
|
|
1978
|
-
const ws = wsCheck.
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
)
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2166
|
+
const wsCheck = await api.listOrganizations(userToken);
|
|
2167
|
+
const ws = wsCheck.organizations.find(
|
|
2168
|
+
(w) => w.id === selectedOrganizationId
|
|
2169
|
+
);
|
|
2170
|
+
if (ws) {
|
|
2171
|
+
if (ws.maxApps !== -1 && ws.appCount >= ws.maxApps) {
|
|
2172
|
+
p6.log.warn(
|
|
2173
|
+
`App limit reached \u2014 ${ws.appCount}/${ws.maxApps} on your ${chalk5.bold(ws.planId)} plan.`
|
|
2174
|
+
);
|
|
2175
|
+
const limitAction = await p6.select({
|
|
2176
|
+
message: "What would you like to do?",
|
|
2177
|
+
options: [
|
|
2178
|
+
{ value: "upgrade", label: "Upgrade plan" },
|
|
2179
|
+
{ value: "cancel", label: "Cancel" }
|
|
2180
|
+
]
|
|
1988
2181
|
});
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
message: "What would you like to do?",
|
|
1994
|
-
options: options2
|
|
1995
|
-
});
|
|
1996
|
-
if (p5.isCancel(limitAction) || limitAction === "cancel") {
|
|
1997
|
-
p5.cancel("Setup cancelled.");
|
|
1998
|
-
return 1;
|
|
1999
|
-
}
|
|
2000
|
-
if (limitAction === "upgrade") {
|
|
2182
|
+
if (p6.isCancel(limitAction) || limitAction === "cancel") {
|
|
2183
|
+
p6.cancel("Setup cancelled.");
|
|
2184
|
+
return 1;
|
|
2185
|
+
}
|
|
2001
2186
|
await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
|
|
2002
|
-
|
|
2187
|
+
p6.cancel(
|
|
2003
2188
|
"Upgrade your plan in the browser, then re-run `vocoder init`."
|
|
2004
2189
|
);
|
|
2005
2190
|
return 1;
|
|
2006
2191
|
}
|
|
2007
|
-
|
|
2008
|
-
userToken,
|
|
2009
|
-
selectedWorkspaceId
|
|
2010
|
-
);
|
|
2011
|
-
const chosenProject = existingProjects.find(
|
|
2012
|
-
(proj) => proj.id === repoProjectId
|
|
2013
|
-
);
|
|
2014
|
-
if (!chosenProject) {
|
|
2015
|
-
p5.log.error("Could not find the project. Try again.");
|
|
2016
|
-
return 1;
|
|
2017
|
-
}
|
|
2018
|
-
try {
|
|
2019
|
-
const appResult = await api.createApp(userToken, {
|
|
2020
|
-
projectId: chosenProject.id,
|
|
2021
|
-
appDir: identity?.repoAppDir ?? "",
|
|
2022
|
-
sourceLocale: chosenProject.sourceLocale,
|
|
2023
|
-
targetLocales: chosenProject.targetLocales,
|
|
2024
|
-
targetBranches: chosenProject.targetBranches,
|
|
2025
|
-
repoCanonical: identity?.repoCanonical ?? ""
|
|
2026
|
-
});
|
|
2027
|
-
p5.log.success(`Connected to project: ${chalk5.bold(chosenProject.name)}`);
|
|
2028
|
-
printApiKey(appResult.apiKey);
|
|
2029
|
-
runScaffold({
|
|
2030
|
-
sourceLocale: chosenProject.sourceLocale,
|
|
2031
|
-
targetBranches: chosenProject.targetBranches,
|
|
2032
|
-
appDir: identity?.repoAppDir
|
|
2033
|
-
});
|
|
2034
|
-
} catch (err) {
|
|
2035
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2036
|
-
p5.log.error(`Failed to create app binding: ${msg}`);
|
|
2037
|
-
return 1;
|
|
2038
|
-
}
|
|
2039
|
-
p5.outro("You're all set.");
|
|
2040
|
-
return 0;
|
|
2192
|
+
remainingApps = ws.maxApps === -1 ? void 0 : Math.max(0, ws.maxApps - ws.appCount);
|
|
2041
2193
|
}
|
|
2042
2194
|
} catch {
|
|
2043
2195
|
}
|
|
2044
2196
|
const projectResult = await runProjectCreate({
|
|
2045
2197
|
api,
|
|
2046
2198
|
userToken,
|
|
2047
|
-
organizationId:
|
|
2199
|
+
organizationId: selectedOrganizationId,
|
|
2048
2200
|
defaultName: identity?.repoCanonical ? identity.repoCanonical.split("/").pop() : void 0,
|
|
2049
2201
|
defaultSourceLocale: "en",
|
|
2050
2202
|
repoCanonical: identity?.repoCanonical,
|
|
2203
|
+
repoRoot: identity?.repoRoot,
|
|
2051
2204
|
defaultBranches: ["main"],
|
|
2052
|
-
|
|
2205
|
+
maxAppDirs: remainingApps
|
|
2053
2206
|
});
|
|
2054
2207
|
if (!projectResult) {
|
|
2055
|
-
|
|
2208
|
+
p6.log.error("Project creation failed. Run `vocoder init` again.");
|
|
2056
2209
|
return 1;
|
|
2057
2210
|
}
|
|
2058
2211
|
if (!projectResult.repositoryBound && identity?.repoCanonical) {
|
|
2059
|
-
|
|
2212
|
+
p6.log.warn(
|
|
2060
2213
|
`This repository isn't accessible to your GitHub App installation.
|
|
2061
2214
|
Translations won't run automatically until you grant access.
|
|
2062
2215
|
|
|
@@ -2067,13 +2220,19 @@ Translations won't run automatically until you grant access.
|
|
|
2067
2220
|
` : "")
|
|
2068
2221
|
);
|
|
2069
2222
|
}
|
|
2223
|
+
const detection = detectLocalEcosystem();
|
|
2070
2224
|
runScaffold({
|
|
2071
2225
|
sourceLocale: projectResult.sourceLocale,
|
|
2072
|
-
targetBranches: projectResult.targetBranches
|
|
2073
|
-
appDir: identity?.repoAppDir
|
|
2226
|
+
targetBranches: projectResult.targetBranches
|
|
2074
2227
|
});
|
|
2075
|
-
|
|
2076
|
-
|
|
2228
|
+
writeAppConfigs(
|
|
2229
|
+
projectResult.apps,
|
|
2230
|
+
projectResult.targetBranches,
|
|
2231
|
+
detection.isTypeScript,
|
|
2232
|
+
identity?.repoRoot
|
|
2233
|
+
);
|
|
2234
|
+
printApiKey(projectResult.apiKey, identity?.repoRoot);
|
|
2235
|
+
p6.outro("You're all set.");
|
|
2077
2236
|
return 0;
|
|
2078
2237
|
} catch (error) {
|
|
2079
2238
|
if (error instanceof Error) {
|
|
@@ -2081,24 +2240,25 @@ Translations won't run automatically until you grant access.
|
|
|
2081
2240
|
printPlanLimitMessage(apiUrl, error.message);
|
|
2082
2241
|
return 1;
|
|
2083
2242
|
}
|
|
2084
|
-
|
|
2243
|
+
p6.log.error(`Error: ${error.message}`);
|
|
2085
2244
|
} else {
|
|
2086
|
-
|
|
2245
|
+
p6.log.error("Unknown setup error");
|
|
2087
2246
|
}
|
|
2088
2247
|
return 1;
|
|
2089
2248
|
}
|
|
2090
2249
|
}
|
|
2091
2250
|
|
|
2092
2251
|
// src/commands/locales.ts
|
|
2093
|
-
import * as
|
|
2252
|
+
import * as p9 from "@clack/prompts";
|
|
2094
2253
|
import chalk7 from "chalk";
|
|
2095
2254
|
import { config as loadEnv3 } from "dotenv";
|
|
2255
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2096
2256
|
|
|
2097
2257
|
// src/commands/sync.ts
|
|
2098
|
-
import {
|
|
2099
|
-
import { existsSync as
|
|
2258
|
+
import { randomUUID } from "crypto";
|
|
2259
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2100
2260
|
import { join as join3 } from "path";
|
|
2101
|
-
import * as
|
|
2261
|
+
import * as p8 from "@clack/prompts";
|
|
2102
2262
|
import chalk6 from "chalk";
|
|
2103
2263
|
|
|
2104
2264
|
// src/utils/branch.ts
|
|
@@ -2168,9 +2328,12 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2168
2328
|
}
|
|
2169
2329
|
|
|
2170
2330
|
// src/utils/config.ts
|
|
2171
|
-
import * as
|
|
2331
|
+
import * as p7 from "@clack/prompts";
|
|
2172
2332
|
import { config as loadEnv2 } from "dotenv";
|
|
2173
2333
|
loadEnv2();
|
|
2334
|
+
function extractShortCodeFromApiKey(apiKey) {
|
|
2335
|
+
return apiKey.slice(4, 14);
|
|
2336
|
+
}
|
|
2174
2337
|
function validateLocalConfig(config) {
|
|
2175
2338
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
2176
2339
|
throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
|
|
@@ -2221,7 +2384,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2221
2384
|
};
|
|
2222
2385
|
const fileConfig = loadVocoderConfig(process.cwd());
|
|
2223
2386
|
if (!fileConfig) {
|
|
2224
|
-
|
|
2387
|
+
p7.log.warn(
|
|
2225
2388
|
`No ${highlight("vocoder.config.ts")} found \u2014 run ${highlight("npx @vocoder/cli init")} to generate one.`
|
|
2226
2389
|
);
|
|
2227
2390
|
}
|
|
@@ -2252,7 +2415,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2252
2415
|
excludePattern = fileConfig.exclude;
|
|
2253
2416
|
configSources.excludePattern = "vocoder.config";
|
|
2254
2417
|
} else if (envExcludePattern) {
|
|
2255
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2418
|
+
excludePattern = envExcludePattern.split(",").map((p15) => p15.trim()).filter(Boolean);
|
|
2256
2419
|
configSources.excludePattern = "environment";
|
|
2257
2420
|
} else {
|
|
2258
2421
|
excludePattern = defaults.excludePattern;
|
|
@@ -2309,7 +2472,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2309
2472
|
...maxWaitMs ? [`Max wait: ${highlight(String(configSources.maxWaitMs))}`] : [],
|
|
2310
2473
|
`No fallback: ${highlight(String(configSources.noFallback))}`
|
|
2311
2474
|
];
|
|
2312
|
-
|
|
2475
|
+
p7.note(lines.join("\n"), "Configuration sources");
|
|
2313
2476
|
}
|
|
2314
2477
|
return {
|
|
2315
2478
|
includePattern,
|
|
@@ -2324,10 +2487,6 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2324
2487
|
}
|
|
2325
2488
|
|
|
2326
2489
|
// src/commands/sync.ts
|
|
2327
|
-
function computeFingerprint(shortCode, texts) {
|
|
2328
|
-
const sorted = [...texts].sort();
|
|
2329
|
-
return createHash("sha256").update(`${shortCode}:${sorted.join("\0")}`).digest("hex").slice(0, 12);
|
|
2330
|
-
}
|
|
2331
2490
|
function isRecord(value) {
|
|
2332
2491
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2333
2492
|
}
|
|
@@ -2375,15 +2534,6 @@ function getCacheFilePath(projectRoot, fingerprint) {
|
|
|
2375
2534
|
return join3(projectRoot, "node_modules", ".vocoder", "cache", `${fingerprint}.json`);
|
|
2376
2535
|
}
|
|
2377
2536
|
function buildTranslationData(params) {
|
|
2378
|
-
const textToHash = new Map(params.stringEntries.map((e) => [e.text, e.key]));
|
|
2379
|
-
const hashKeyed = {};
|
|
2380
|
-
for (const [locale, localeMap] of Object.entries(params.translations)) {
|
|
2381
|
-
hashKeyed[locale] = {};
|
|
2382
|
-
for (const [text, translation] of Object.entries(localeMap)) {
|
|
2383
|
-
const hash = textToHash.get(text);
|
|
2384
|
-
if (hash) hashKeyed[locale][hash] = translation;
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
2537
|
const locales = {};
|
|
2388
2538
|
for (const code of [params.sourceLocale, ...params.targetLocales]) {
|
|
2389
2539
|
const meta = params.localeMetadata?.[code];
|
|
@@ -2391,13 +2541,13 @@ function buildTranslationData(params) {
|
|
|
2391
2541
|
}
|
|
2392
2542
|
return {
|
|
2393
2543
|
config: { sourceLocale: params.sourceLocale, targetLocales: params.targetLocales, locales },
|
|
2394
|
-
translations:
|
|
2544
|
+
translations: params.translations,
|
|
2395
2545
|
updatedAt: params.updatedAt
|
|
2396
2546
|
};
|
|
2397
2547
|
}
|
|
2398
2548
|
function readLocalCache(params) {
|
|
2399
2549
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
|
|
2400
|
-
if (!
|
|
2550
|
+
if (!existsSync4(cacheFilePath)) return null;
|
|
2401
2551
|
try {
|
|
2402
2552
|
const raw = readFileSync2(cacheFilePath, "utf-8");
|
|
2403
2553
|
const parsed = JSON.parse(raw);
|
|
@@ -2456,9 +2606,10 @@ function normalizeTranslations(params) {
|
|
|
2456
2606
|
if (!merged[params.sourceLocale]) {
|
|
2457
2607
|
merged[params.sourceLocale] = {};
|
|
2458
2608
|
}
|
|
2459
|
-
for (const
|
|
2460
|
-
if (!
|
|
2461
|
-
|
|
2609
|
+
for (const entry of params.stringEntries) {
|
|
2610
|
+
if (!entry.text) continue;
|
|
2611
|
+
if (!(entry.key in merged[params.sourceLocale])) {
|
|
2612
|
+
merged[params.sourceLocale][entry.key] = entry.text;
|
|
2462
2613
|
}
|
|
2463
2614
|
}
|
|
2464
2615
|
return merged;
|
|
@@ -2534,11 +2685,11 @@ function mergeContext(current, incoming) {
|
|
|
2534
2685
|
return Array.from(merged).join(" | ");
|
|
2535
2686
|
}
|
|
2536
2687
|
function buildStringEntries(extractedStrings) {
|
|
2537
|
-
const
|
|
2688
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2538
2689
|
for (const str of extractedStrings) {
|
|
2539
|
-
const existing =
|
|
2690
|
+
const existing = byKey.get(str.key);
|
|
2540
2691
|
if (!existing) {
|
|
2541
|
-
|
|
2692
|
+
byKey.set(str.key, {
|
|
2542
2693
|
key: str.key,
|
|
2543
2694
|
text: str.text,
|
|
2544
2695
|
...str.context ? { context: str.context } : {},
|
|
@@ -2553,11 +2704,8 @@ function buildStringEntries(extractedStrings) {
|
|
|
2553
2704
|
} else if (existing.formality && str.formality && existing.formality !== str.formality) {
|
|
2554
2705
|
existing.formality = "auto";
|
|
2555
2706
|
}
|
|
2556
|
-
if (str.key < existing.key) {
|
|
2557
|
-
existing.key = str.key;
|
|
2558
|
-
}
|
|
2559
2707
|
}
|
|
2560
|
-
return Array.from(
|
|
2708
|
+
return Array.from(byKey.values());
|
|
2561
2709
|
}
|
|
2562
2710
|
async function fetchApiSnapshot(api, params) {
|
|
2563
2711
|
const snapshot = await api.getTranslationSnapshot({
|
|
@@ -2578,19 +2726,19 @@ async function fetchApiSnapshot(api, params) {
|
|
|
2578
2726
|
async function sync(options = {}) {
|
|
2579
2727
|
const startTime = Date.now();
|
|
2580
2728
|
const projectRoot = process.cwd();
|
|
2581
|
-
|
|
2729
|
+
p8.intro(chalk6.bold("Vocoder Sync"));
|
|
2582
2730
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
2583
2731
|
if (!mergedConfig.apiKey) {
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2732
|
+
p8.log.warn("No API key found. Run init to get started:");
|
|
2733
|
+
p8.log.info(" npx @vocoder/cli init");
|
|
2734
|
+
p8.log.info("");
|
|
2735
|
+
p8.log.info(
|
|
2588
2736
|
" Or add your key to .env: VOCODER_API_KEY=vca_..."
|
|
2589
2737
|
);
|
|
2590
|
-
|
|
2738
|
+
p8.outro("Run `npx @vocoder/cli init` to set up your project.");
|
|
2591
2739
|
return 1;
|
|
2592
2740
|
}
|
|
2593
|
-
const spinner7 =
|
|
2741
|
+
const spinner7 = p8.spinner();
|
|
2594
2742
|
try {
|
|
2595
2743
|
const branch = detectBranch(options.branch);
|
|
2596
2744
|
spinner7.start("Loading project configuration");
|
|
@@ -2619,12 +2767,12 @@ async function sync(options = {}) {
|
|
|
2619
2767
|
};
|
|
2620
2768
|
spinner7.stop(`Branch: ${highlight(branch)}`);
|
|
2621
2769
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
2622
|
-
|
|
2770
|
+
p8.log.warn(
|
|
2623
2771
|
`Skipping translations (${highlight(branch)} is not a target branch)`
|
|
2624
2772
|
);
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2773
|
+
p8.log.info(`Target branches: ${config.targetBranches.map((b) => highlight(b)).join(", ")}`);
|
|
2774
|
+
p8.log.info("Use --force to translate anyway");
|
|
2775
|
+
p8.outro("");
|
|
2628
2776
|
return 0;
|
|
2629
2777
|
}
|
|
2630
2778
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
@@ -2637,10 +2785,10 @@ async function sync(options = {}) {
|
|
|
2637
2785
|
);
|
|
2638
2786
|
if (extractedStrings.length === 0) {
|
|
2639
2787
|
spinner7.stop("No translatable strings found");
|
|
2640
|
-
|
|
2788
|
+
p8.log.warn(
|
|
2641
2789
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
2642
2790
|
);
|
|
2643
|
-
|
|
2791
|
+
p8.outro("");
|
|
2644
2792
|
return 0;
|
|
2645
2793
|
}
|
|
2646
2794
|
spinner7.stop(
|
|
@@ -2651,10 +2799,10 @@ async function sync(options = {}) {
|
|
|
2651
2799
|
if (extractedStrings.length > 5) {
|
|
2652
2800
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
2653
2801
|
}
|
|
2654
|
-
|
|
2802
|
+
p8.note(sampleLines.join("\n"), "Sample strings");
|
|
2655
2803
|
}
|
|
2656
2804
|
if (options.dryRun) {
|
|
2657
|
-
|
|
2805
|
+
p8.note(
|
|
2658
2806
|
[
|
|
2659
2807
|
`Strings: ${extractedStrings.length}`,
|
|
2660
2808
|
`Branch: ${branch}`,
|
|
@@ -2665,36 +2813,36 @@ async function sync(options = {}) {
|
|
|
2665
2813
|
].join("\n"),
|
|
2666
2814
|
"Dry run - would translate"
|
|
2667
2815
|
);
|
|
2668
|
-
|
|
2816
|
+
p8.outro("No API calls made.");
|
|
2669
2817
|
return 0;
|
|
2670
2818
|
}
|
|
2671
2819
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
2672
2820
|
if (!repoIdentity && options.verbose) {
|
|
2673
|
-
|
|
2821
|
+
p8.log.warn(
|
|
2674
2822
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
2675
2823
|
);
|
|
2676
2824
|
}
|
|
2677
2825
|
const commitSha = detectCommitSha() ?? void 0;
|
|
2678
2826
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
2679
|
-
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
2680
2827
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
2681
|
-
|
|
2682
|
-
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique
|
|
2828
|
+
p8.log.info(
|
|
2829
|
+
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique strings`
|
|
2683
2830
|
);
|
|
2684
2831
|
}
|
|
2685
|
-
const
|
|
2832
|
+
const sourceKeys = stringEntries.map((entry) => entry.key);
|
|
2833
|
+
const fingerprint = computeFingerprint(extractShortCodeFromApiKey(localConfig.apiKey), sourceKeys);
|
|
2686
2834
|
if (!options.force) {
|
|
2687
2835
|
const cacheFile = getCacheFilePath(projectRoot, fingerprint);
|
|
2688
|
-
if (
|
|
2836
|
+
if (existsSync4(cacheFile)) {
|
|
2689
2837
|
if (options.verbose) {
|
|
2690
|
-
|
|
2838
|
+
p8.log.info(`Cache hit: ${chalk6.dim(cacheFile)} (fingerprint ${highlight(fingerprint)})`);
|
|
2691
2839
|
}
|
|
2692
2840
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2693
|
-
|
|
2841
|
+
p8.outro(`Up to date (${duration2}s)`);
|
|
2694
2842
|
return 0;
|
|
2695
2843
|
}
|
|
2696
2844
|
if (options.verbose) {
|
|
2697
|
-
|
|
2845
|
+
p8.log.info(`No cache for fingerprint ${highlight(fingerprint)} \u2014 will submit to API`);
|
|
2698
2846
|
}
|
|
2699
2847
|
}
|
|
2700
2848
|
spinner7.start("Submitting strings to Vocoder API");
|
|
@@ -2719,26 +2867,26 @@ async function sync(options = {}) {
|
|
|
2719
2867
|
policy: config.syncPolicy
|
|
2720
2868
|
});
|
|
2721
2869
|
if (options.verbose) {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2870
|
+
p8.log.info(`Batch: ${chalk6.dim(batchResponse.batchId)}`);
|
|
2871
|
+
p8.log.info(`Requested mode: ${requestedMode}`);
|
|
2872
|
+
p8.log.info(`Effective mode: ${effectiveMode}`);
|
|
2873
|
+
p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
2726
2874
|
if (batchResponse.queueStatus) {
|
|
2727
|
-
|
|
2875
|
+
p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
2728
2876
|
}
|
|
2729
2877
|
}
|
|
2730
2878
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
2731
|
-
|
|
2879
|
+
p8.log.success(`Up to date \u2014 ${highlight(batchResponse.totalStrings)} strings, no changes`);
|
|
2732
2880
|
} else if (batchResponse.newStrings === 0) {
|
|
2733
2881
|
const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk6.yellow(batchResponse.deletedStrings)} archived` : "";
|
|
2734
|
-
|
|
2882
|
+
p8.log.success(`No new strings \u2014 ${highlight(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
|
|
2735
2883
|
} else {
|
|
2736
2884
|
const statParts = [`${highlight(batchResponse.newStrings)} new, ${highlight(batchResponse.totalStrings)} total`];
|
|
2737
2885
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
2738
2886
|
statParts.push(`${chalk6.yellow(batchResponse.deletedStrings)} archived`);
|
|
2739
2887
|
}
|
|
2740
2888
|
const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
|
|
2741
|
-
|
|
2889
|
+
p8.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.map((l) => highlight(l)).join(", ")}${estTime}`);
|
|
2742
2890
|
}
|
|
2743
2891
|
let artifacts = null;
|
|
2744
2892
|
if (batchResponse.translations) {
|
|
@@ -2776,7 +2924,7 @@ async function sync(options = {}) {
|
|
|
2776
2924
|
if (effectiveMode === "required") {
|
|
2777
2925
|
throw waitError;
|
|
2778
2926
|
}
|
|
2779
|
-
|
|
2927
|
+
p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
2780
2928
|
}
|
|
2781
2929
|
}
|
|
2782
2930
|
if (!artifacts) {
|
|
@@ -2809,7 +2957,7 @@ async function sync(options = {}) {
|
|
|
2809
2957
|
spinner7.stop("Failed to fetch API snapshot");
|
|
2810
2958
|
if (options.verbose) {
|
|
2811
2959
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
2812
|
-
|
|
2960
|
+
p8.log.warn(`Snapshot fetch error: ${message}`);
|
|
2813
2961
|
}
|
|
2814
2962
|
}
|
|
2815
2963
|
}
|
|
@@ -2827,72 +2975,71 @@ async function sync(options = {}) {
|
|
|
2827
2975
|
const finalTranslations = normalizeTranslations({
|
|
2828
2976
|
sourceLocale: config.sourceLocale,
|
|
2829
2977
|
targetLocales: config.targetLocales,
|
|
2830
|
-
|
|
2978
|
+
stringEntries,
|
|
2831
2979
|
translations: artifacts.translations
|
|
2832
2980
|
});
|
|
2833
2981
|
try {
|
|
2834
2982
|
const data = buildTranslationData({
|
|
2835
2983
|
sourceLocale: config.sourceLocale,
|
|
2836
2984
|
targetLocales: config.targetLocales,
|
|
2837
|
-
stringEntries,
|
|
2838
2985
|
translations: finalTranslations,
|
|
2839
2986
|
localeMetadata: artifacts.localeMetadata,
|
|
2840
2987
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2841
2988
|
});
|
|
2842
2989
|
const cachePath = writeCache({ projectRoot, fingerprint, data });
|
|
2843
2990
|
if (options.verbose) {
|
|
2844
|
-
|
|
2991
|
+
p8.log.info(`Cache written: ${highlight(cachePath)}`);
|
|
2845
2992
|
}
|
|
2846
2993
|
} catch (error) {
|
|
2847
2994
|
if (options.verbose) {
|
|
2848
2995
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
2849
|
-
|
|
2996
|
+
p8.log.warn(`Failed to write cache: ${message}`);
|
|
2850
2997
|
}
|
|
2851
2998
|
}
|
|
2852
2999
|
if (artifacts.source !== "fresh") {
|
|
2853
3000
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
2854
|
-
|
|
3001
|
+
p8.log.warn(
|
|
2855
3002
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
2856
3003
|
);
|
|
2857
3004
|
}
|
|
2858
3005
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2859
|
-
|
|
3006
|
+
p8.outro(`Sync complete! (${duration}s)`);
|
|
2860
3007
|
return 0;
|
|
2861
3008
|
} catch (error) {
|
|
2862
3009
|
spinner7.stop();
|
|
2863
3010
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
2864
|
-
|
|
3011
|
+
p8.log.error(error.syncPolicyError.message);
|
|
2865
3012
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
2866
3013
|
for (const line of guidance) {
|
|
2867
|
-
|
|
3014
|
+
p8.log.info(line);
|
|
2868
3015
|
}
|
|
2869
3016
|
return 1;
|
|
2870
3017
|
}
|
|
2871
3018
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2872
3019
|
const { limitError } = error;
|
|
2873
|
-
|
|
3020
|
+
p8.log.error(limitError.message);
|
|
2874
3021
|
const guidance = getLimitErrorGuidance(limitError);
|
|
2875
3022
|
for (const line of guidance) {
|
|
2876
|
-
|
|
3023
|
+
p8.log.info(line);
|
|
2877
3024
|
}
|
|
2878
3025
|
return 1;
|
|
2879
3026
|
}
|
|
2880
3027
|
if (error instanceof Error) {
|
|
2881
|
-
|
|
3028
|
+
p8.log.error(error.message);
|
|
2882
3029
|
const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
|
|
2883
3030
|
if (isInvalidKey) {
|
|
2884
|
-
|
|
3031
|
+
p8.log.warn(
|
|
2885
3032
|
"API key rejected \u2014 the project may have been deleted or the key revoked."
|
|
2886
3033
|
);
|
|
2887
|
-
|
|
3034
|
+
p8.log.info(
|
|
2888
3035
|
" Run `npx @vocoder/cli init` to create a new project and key."
|
|
2889
3036
|
);
|
|
2890
3037
|
} else if (error.message.includes("git branch")) {
|
|
2891
|
-
|
|
2892
|
-
|
|
3038
|
+
p8.log.warn("Run from a git repository, or use:");
|
|
3039
|
+
p8.log.info(" vocoder sync --branch main");
|
|
2893
3040
|
}
|
|
2894
3041
|
if (options.verbose) {
|
|
2895
|
-
|
|
3042
|
+
p8.log.info(`Full error: ${error.stack ?? error}`);
|
|
2896
3043
|
}
|
|
2897
3044
|
}
|
|
2898
3045
|
return 1;
|
|
@@ -2901,10 +3048,21 @@ async function sync(options = {}) {
|
|
|
2901
3048
|
|
|
2902
3049
|
// src/commands/locales.ts
|
|
2903
3050
|
loadEnv3();
|
|
3051
|
+
function readLocalAppId() {
|
|
3052
|
+
const configPath = findExistingConfig(process.cwd());
|
|
3053
|
+
if (!configPath) return void 0;
|
|
3054
|
+
try {
|
|
3055
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
3056
|
+
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
|
|
3057
|
+
return match?.[1];
|
|
3058
|
+
} catch {
|
|
3059
|
+
return void 0;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
2904
3062
|
function getApiConfig(options) {
|
|
2905
3063
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
2906
3064
|
if (!apiKey) {
|
|
2907
|
-
|
|
3065
|
+
p9.log.error(
|
|
2908
3066
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
2909
3067
|
);
|
|
2910
3068
|
return null;
|
|
@@ -2920,19 +3078,19 @@ async function listProjectLocales(options = {}) {
|
|
|
2920
3078
|
const api = new VocoderAPI(config);
|
|
2921
3079
|
try {
|
|
2922
3080
|
const projectConfig = await api.getAppConfig();
|
|
2923
|
-
|
|
3081
|
+
p9.log.info(
|
|
2924
3082
|
`Source locale: ${highlight(projectConfig.sourceLocale)}`
|
|
2925
3083
|
);
|
|
2926
3084
|
if (projectConfig.targetLocales.length === 0) {
|
|
2927
|
-
|
|
3085
|
+
p9.log.info("Target locales: (none configured)");
|
|
2928
3086
|
} else {
|
|
2929
|
-
|
|
3087
|
+
p9.log.info(
|
|
2930
3088
|
`Target locales: ${projectConfig.targetLocales.map((l) => highlight(l)).join(", ")}`
|
|
2931
3089
|
);
|
|
2932
3090
|
}
|
|
2933
3091
|
return 0;
|
|
2934
3092
|
} catch (error) {
|
|
2935
|
-
|
|
3093
|
+
p9.log.error(
|
|
2936
3094
|
error instanceof Error ? error.message : "Failed to fetch project locales."
|
|
2937
3095
|
);
|
|
2938
3096
|
return 1;
|
|
@@ -2940,19 +3098,20 @@ async function listProjectLocales(options = {}) {
|
|
|
2940
3098
|
}
|
|
2941
3099
|
async function addLocales(locales, options = {}) {
|
|
2942
3100
|
if (locales.length === 0) {
|
|
2943
|
-
|
|
3101
|
+
p9.log.error("No locale codes provided.");
|
|
2944
3102
|
return 1;
|
|
2945
3103
|
}
|
|
2946
3104
|
const config = getApiConfig(options);
|
|
2947
3105
|
if (!config) return 1;
|
|
2948
3106
|
const api = new VocoderAPI(config);
|
|
3107
|
+
const appId = readLocalAppId();
|
|
2949
3108
|
let lastTargetLocales = [];
|
|
2950
3109
|
let hadError = false;
|
|
2951
3110
|
for (const locale of locales) {
|
|
2952
|
-
const spinner7 =
|
|
3111
|
+
const spinner7 = p9.spinner();
|
|
2953
3112
|
spinner7.start(`Adding ${locale}\u2026`);
|
|
2954
3113
|
try {
|
|
2955
|
-
const result = await api.addLocale(locale);
|
|
3114
|
+
const result = await api.addLocale(locale, void 0, appId);
|
|
2956
3115
|
lastTargetLocales = result.targetLocales;
|
|
2957
3116
|
spinner7.stop(`Added ${highlight(locale)}`);
|
|
2958
3117
|
} catch (error) {
|
|
@@ -2960,19 +3119,19 @@ async function addLocales(locales, options = {}) {
|
|
|
2960
3119
|
hadError = true;
|
|
2961
3120
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2962
3121
|
const { limitError } = error;
|
|
2963
|
-
|
|
3122
|
+
p9.log.error(limitError.message);
|
|
2964
3123
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
2965
|
-
|
|
3124
|
+
p9.log.info(line);
|
|
2966
3125
|
}
|
|
2967
3126
|
break;
|
|
2968
3127
|
}
|
|
2969
|
-
|
|
3128
|
+
p9.log.error(
|
|
2970
3129
|
error instanceof Error ? error.message : "Unknown error"
|
|
2971
3130
|
);
|
|
2972
3131
|
}
|
|
2973
3132
|
}
|
|
2974
3133
|
if (lastTargetLocales.length > 0) {
|
|
2975
|
-
|
|
3134
|
+
p9.log.info(
|
|
2976
3135
|
`Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
|
|
2977
3136
|
);
|
|
2978
3137
|
}
|
|
@@ -2980,35 +3139,36 @@ async function addLocales(locales, options = {}) {
|
|
|
2980
3139
|
}
|
|
2981
3140
|
async function removeLocales(locales, options = {}) {
|
|
2982
3141
|
if (locales.length === 0) {
|
|
2983
|
-
|
|
3142
|
+
p9.log.error("No locale codes provided.");
|
|
2984
3143
|
return 1;
|
|
2985
3144
|
}
|
|
2986
3145
|
const config = getApiConfig(options);
|
|
2987
3146
|
if (!config) return 1;
|
|
2988
3147
|
const api = new VocoderAPI(config);
|
|
3148
|
+
const appId = readLocalAppId();
|
|
2989
3149
|
let lastTargetLocales = [];
|
|
2990
3150
|
let hadError = false;
|
|
2991
3151
|
for (const locale of locales) {
|
|
2992
|
-
const spinner7 =
|
|
3152
|
+
const spinner7 = p9.spinner();
|
|
2993
3153
|
spinner7.start(`Removing ${locale}\u2026`);
|
|
2994
3154
|
try {
|
|
2995
|
-
const result = await api.removeLocale(locale);
|
|
3155
|
+
const result = await api.removeLocale(locale, void 0, appId);
|
|
2996
3156
|
lastTargetLocales = result.targetLocales;
|
|
2997
3157
|
spinner7.stop(`Removed ${highlight(locale)}`);
|
|
2998
3158
|
} catch (error) {
|
|
2999
3159
|
spinner7.stop(`Failed to remove ${chalk7.red(locale)}`);
|
|
3000
3160
|
hadError = true;
|
|
3001
|
-
|
|
3161
|
+
p9.log.error(
|
|
3002
3162
|
error instanceof Error ? error.message : "Unknown error"
|
|
3003
3163
|
);
|
|
3004
3164
|
}
|
|
3005
3165
|
}
|
|
3006
3166
|
if (lastTargetLocales.length > 0) {
|
|
3007
|
-
|
|
3167
|
+
p9.log.info(
|
|
3008
3168
|
`Target locales now: ${lastTargetLocales.map((l) => highlight(l)).join(", ")}`
|
|
3009
3169
|
);
|
|
3010
3170
|
} else if (!hadError) {
|
|
3011
|
-
|
|
3171
|
+
p9.log.info("Target locales now: (none configured)");
|
|
3012
3172
|
}
|
|
3013
3173
|
return hadError ? 1 : 0;
|
|
3014
3174
|
}
|
|
@@ -3018,14 +3178,14 @@ async function listSupportedLocales(options = {}) {
|
|
|
3018
3178
|
const api = new VocoderAPI(config);
|
|
3019
3179
|
try {
|
|
3020
3180
|
const result = await api.listLocales(config.apiKey);
|
|
3021
|
-
|
|
3181
|
+
p9.log.info(chalk7.bold("Source locales:"));
|
|
3022
3182
|
printLocaleTable(result.sourceLocales);
|
|
3023
|
-
|
|
3024
|
-
|
|
3183
|
+
p9.log.info("");
|
|
3184
|
+
p9.log.info(chalk7.bold("Target locales:"));
|
|
3025
3185
|
printLocaleTable(result.targetLocales);
|
|
3026
3186
|
return 0;
|
|
3027
3187
|
} catch (error) {
|
|
3028
|
-
|
|
3188
|
+
p9.log.error(
|
|
3029
3189
|
error instanceof Error ? error.message : "Failed to fetch supported locales."
|
|
3030
3190
|
);
|
|
3031
3191
|
return 1;
|
|
@@ -3034,16 +3194,16 @@ async function listSupportedLocales(options = {}) {
|
|
|
3034
3194
|
function printLocaleTable(locales) {
|
|
3035
3195
|
for (const locale of locales) {
|
|
3036
3196
|
const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
|
|
3037
|
-
|
|
3197
|
+
p9.log.info(` ${highlight(locale.code.padEnd(10))} ${locale.name}${native}`);
|
|
3038
3198
|
}
|
|
3039
3199
|
}
|
|
3040
3200
|
|
|
3041
3201
|
// src/commands/logout.ts
|
|
3042
|
-
import * as
|
|
3202
|
+
import * as p10 from "@clack/prompts";
|
|
3043
3203
|
async function logout(options = {}) {
|
|
3044
3204
|
const stored = readAuthData();
|
|
3045
3205
|
if (!stored) {
|
|
3046
|
-
|
|
3206
|
+
p10.log.info("Not currently authenticated.");
|
|
3047
3207
|
return 0;
|
|
3048
3208
|
}
|
|
3049
3209
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
@@ -3053,19 +3213,19 @@ async function logout(options = {}) {
|
|
|
3053
3213
|
} catch {
|
|
3054
3214
|
}
|
|
3055
3215
|
clearAuthData();
|
|
3056
|
-
|
|
3216
|
+
p10.log.success(`Logged out (was ${stored.email})`);
|
|
3057
3217
|
return 0;
|
|
3058
3218
|
}
|
|
3059
3219
|
|
|
3060
3220
|
// src/commands/app-config.ts
|
|
3061
|
-
import * as
|
|
3221
|
+
import * as p11 from "@clack/prompts";
|
|
3062
3222
|
import chalk8 from "chalk";
|
|
3063
3223
|
import { config as loadEnv4 } from "dotenv";
|
|
3064
3224
|
loadEnv4();
|
|
3065
3225
|
async function appConfig(options = {}) {
|
|
3066
3226
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
3067
3227
|
if (!apiKey) {
|
|
3068
|
-
|
|
3228
|
+
p11.log.error(
|
|
3069
3229
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3070
3230
|
);
|
|
3071
3231
|
return 1;
|
|
@@ -3087,10 +3247,10 @@ async function appConfig(options = {}) {
|
|
|
3087
3247
|
` Non-blocking mode: ${highlight(config.syncPolicy.nonBlockingMode)}`,
|
|
3088
3248
|
` Max wait: ${highlight(String(config.syncPolicy.defaultMaxWaitMs))} ms`
|
|
3089
3249
|
];
|
|
3090
|
-
|
|
3250
|
+
p11.note(lines.join("\n"), `${config.projectName} \u2014 app config`);
|
|
3091
3251
|
return 0;
|
|
3092
3252
|
} catch (error) {
|
|
3093
|
-
|
|
3253
|
+
p11.log.error(
|
|
3094
3254
|
error instanceof Error ? error.message : "Failed to fetch project config."
|
|
3095
3255
|
);
|
|
3096
3256
|
return 1;
|
|
@@ -3100,13 +3260,13 @@ async function appConfig(options = {}) {
|
|
|
3100
3260
|
// src/commands/translations.ts
|
|
3101
3261
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3102
3262
|
import { join as join4 } from "path";
|
|
3103
|
-
import * as
|
|
3263
|
+
import * as p12 from "@clack/prompts";
|
|
3104
3264
|
import { config as loadEnv5 } from "dotenv";
|
|
3105
3265
|
loadEnv5();
|
|
3106
3266
|
async function getTranslations(options = {}) {
|
|
3107
3267
|
const apiKey = process.env.VOCODER_API_KEY;
|
|
3108
3268
|
if (!apiKey) {
|
|
3109
|
-
|
|
3269
|
+
p12.log.error(
|
|
3110
3270
|
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3111
3271
|
);
|
|
3112
3272
|
return 1;
|
|
@@ -3117,25 +3277,25 @@ async function getTranslations(options = {}) {
|
|
|
3117
3277
|
try {
|
|
3118
3278
|
branch = detectBranch(options.branch);
|
|
3119
3279
|
} catch (error) {
|
|
3120
|
-
|
|
3280
|
+
p12.log.error(
|
|
3121
3281
|
error instanceof Error ? error.message : "Failed to detect branch."
|
|
3122
3282
|
);
|
|
3123
3283
|
return 1;
|
|
3124
3284
|
}
|
|
3125
|
-
const spinner7 =
|
|
3285
|
+
const spinner7 = p12.spinner();
|
|
3126
3286
|
spinner7.start(`Fetching translations for ${highlight(branch)}\u2026`);
|
|
3127
3287
|
try {
|
|
3128
3288
|
const projectConfig = await api.getAppConfig();
|
|
3129
3289
|
const targetLocales = options.locale ? [options.locale] : projectConfig.targetLocales;
|
|
3130
3290
|
if (targetLocales.length === 0) {
|
|
3131
3291
|
spinner7.stop("No target locales configured.");
|
|
3132
|
-
|
|
3292
|
+
p12.log.info("Add target locales with `vocoder locales add <code>`.");
|
|
3133
3293
|
return 1;
|
|
3134
3294
|
}
|
|
3135
3295
|
const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
|
|
3136
3296
|
spinner7.stop(`Fetched translations for ${highlight(branch)}`);
|
|
3137
3297
|
if (snapshot.status === "NOT_FOUND") {
|
|
3138
|
-
|
|
3298
|
+
p12.log.warn(
|
|
3139
3299
|
`No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
|
|
3140
3300
|
);
|
|
3141
3301
|
return 1;
|
|
@@ -3150,7 +3310,7 @@ async function getTranslations(options = {}) {
|
|
|
3150
3310
|
return 0;
|
|
3151
3311
|
} catch (error) {
|
|
3152
3312
|
spinner7.stop("Failed to fetch translations.");
|
|
3153
|
-
|
|
3313
|
+
p12.log.error(
|
|
3154
3314
|
error instanceof Error ? error.message : "Unknown error."
|
|
3155
3315
|
);
|
|
3156
3316
|
return 1;
|
|
@@ -3161,19 +3321,19 @@ function writeLocaleFiles(translations, outputDir) {
|
|
|
3161
3321
|
for (const [locale, strings] of Object.entries(translations)) {
|
|
3162
3322
|
const filePath = join4(outputDir, `${locale}.json`);
|
|
3163
3323
|
writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
|
|
3164
|
-
|
|
3324
|
+
p12.log.success(`Wrote ${highlight(filePath)}`);
|
|
3165
3325
|
}
|
|
3166
3326
|
}
|
|
3167
3327
|
|
|
3168
3328
|
// src/commands/create-app.ts
|
|
3169
|
-
import * as
|
|
3329
|
+
import * as p13 from "@clack/prompts";
|
|
3170
3330
|
import chalk9 from "chalk";
|
|
3171
3331
|
import { config as loadEnv6 } from "dotenv";
|
|
3172
3332
|
loadEnv6();
|
|
3173
3333
|
async function createApp(options) {
|
|
3174
3334
|
const authData = readAuthData();
|
|
3175
3335
|
if (!authData) {
|
|
3176
|
-
|
|
3336
|
+
p13.log.error(
|
|
3177
3337
|
"Not logged in. Run `npx @vocoder/cli init` to authenticate first."
|
|
3178
3338
|
);
|
|
3179
3339
|
return 1;
|
|
@@ -3192,18 +3352,18 @@ async function createApp(options) {
|
|
|
3192
3352
|
appDir = identity.repoAppDir;
|
|
3193
3353
|
}
|
|
3194
3354
|
} else {
|
|
3195
|
-
|
|
3355
|
+
p13.log.warn(
|
|
3196
3356
|
"Could not detect a git remote. The project will be created without repo binding \u2014 sync-on-push will not function until a repository is connected via the Vocoder dashboard."
|
|
3197
3357
|
);
|
|
3198
3358
|
}
|
|
3199
3359
|
}
|
|
3200
3360
|
const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
|
|
3201
3361
|
const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
|
|
3202
|
-
const spinner7 =
|
|
3362
|
+
const spinner7 = p13.spinner();
|
|
3203
3363
|
spinner7.start(`Creating app "${options.name}"\u2026`);
|
|
3204
3364
|
try {
|
|
3205
3365
|
const result = await api.createProject(authData.token, {
|
|
3206
|
-
organizationId: options.
|
|
3366
|
+
organizationId: options.organization,
|
|
3207
3367
|
name: options.name,
|
|
3208
3368
|
sourceLocale: options.sourceLocale,
|
|
3209
3369
|
targetLocales,
|
|
@@ -3222,9 +3382,9 @@ async function createApp(options) {
|
|
|
3222
3382
|
`Add this to your .env file:`,
|
|
3223
3383
|
` ${chalk9.bold("VOCODER_API_KEY")}=${highlight(result.apiKey)}`
|
|
3224
3384
|
];
|
|
3225
|
-
|
|
3385
|
+
p13.note(lines.join("\n"), "Project created");
|
|
3226
3386
|
if (!result.repositoryBound && repoCanonical) {
|
|
3227
|
-
|
|
3387
|
+
p13.log.warn(
|
|
3228
3388
|
`Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
|
|
3229
3389
|
);
|
|
3230
3390
|
}
|
|
@@ -3233,13 +3393,13 @@ async function createApp(options) {
|
|
|
3233
3393
|
spinner7.stop("Failed to create project.");
|
|
3234
3394
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3235
3395
|
const { limitError } = error;
|
|
3236
|
-
|
|
3396
|
+
p13.log.error(limitError.message);
|
|
3237
3397
|
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3238
|
-
|
|
3398
|
+
p13.log.info(line);
|
|
3239
3399
|
}
|
|
3240
3400
|
return 1;
|
|
3241
3401
|
}
|
|
3242
|
-
|
|
3402
|
+
p13.log.error(
|
|
3243
3403
|
error instanceof Error ? error.message : "Unknown error."
|
|
3244
3404
|
);
|
|
3245
3405
|
return 1;
|
|
@@ -3247,26 +3407,26 @@ async function createApp(options) {
|
|
|
3247
3407
|
}
|
|
3248
3408
|
|
|
3249
3409
|
// src/commands/whoami.ts
|
|
3250
|
-
import * as
|
|
3410
|
+
import * as p14 from "@clack/prompts";
|
|
3251
3411
|
import chalk10 from "chalk";
|
|
3252
3412
|
async function whoami(options = {}) {
|
|
3253
3413
|
const stored = readAuthData();
|
|
3254
3414
|
if (!stored) {
|
|
3255
|
-
|
|
3415
|
+
p14.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
3256
3416
|
return 1;
|
|
3257
3417
|
}
|
|
3258
3418
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3259
3419
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3260
3420
|
try {
|
|
3261
3421
|
const info2 = await api.getCliUserInfo(stored.token);
|
|
3262
|
-
|
|
3422
|
+
p14.log.info(`Logged in as ${chalk10.bold(info2.email)}`);
|
|
3263
3423
|
if (info2.name) {
|
|
3264
|
-
|
|
3424
|
+
p14.log.info(`Name: ${info2.name}`);
|
|
3265
3425
|
}
|
|
3266
|
-
|
|
3426
|
+
p14.log.info(`API: ${apiUrl}`);
|
|
3267
3427
|
return 0;
|
|
3268
3428
|
} catch {
|
|
3269
|
-
|
|
3429
|
+
p14.log.error(
|
|
3270
3430
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3271
3431
|
);
|
|
3272
3432
|
return 1;
|
|
@@ -3308,7 +3468,7 @@ localesCmd.command("remove <codes...>").description("Remove one or more target l
|
|
|
3308
3468
|
localesCmd.command("supported").description("List all locales supported by Vocoder").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listSupportedLocales, options));
|
|
3309
3469
|
program.command("project").description("Show current app configuration").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(appConfig, options));
|
|
3310
3470
|
program.command("translations").description("Download the current translation snapshot").option("--branch <branch>", "Git branch (auto-detected if omitted)").option("--locale <locale>", "Fetch a specific locale only").option("--output <dir>", "Write locale JSON files to this directory").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(getTranslations, options));
|
|
3311
|
-
program.command("create-app").description("Create a new Vocoder app (requires prior `vocoder init`)").requiredOption("--name <name>", "App display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--
|
|
3471
|
+
program.command("create-app").description("Create a new Vocoder app (requires prior `vocoder init`)").requiredOption("--name <name>", "App display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--organization <org-id>", "Organization ID").option(
|
|
3312
3472
|
"--target-locales <codes>",
|
|
3313
3473
|
"Comma-separated target locale codes (e.g. fr,de,pt-BR)"
|
|
3314
3474
|
).option(
|
|
@@ -3327,7 +3487,7 @@ program.command("create-app").description("Create a new Vocoder app (requires pr
|
|
|
3327
3487
|
sourceLocale: options.sourceLocale,
|
|
3328
3488
|
targetLocales: options.targetLocales,
|
|
3329
3489
|
targetBranches: options.targetBranches,
|
|
3330
|
-
|
|
3490
|
+
organization: options.organization
|
|
3331
3491
|
};
|
|
3332
3492
|
return runCommand(createApp, translated);
|
|
3333
3493
|
});
|