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