@vocoder/cli 0.1.13 → 0.1.14
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 +202 -47
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -831,8 +831,9 @@ var VocoderAPI = class {
|
|
|
831
831
|
}
|
|
832
832
|
// ── Project lookup ────────────────────────────────────────────────────────────
|
|
833
833
|
/**
|
|
834
|
-
* Look up
|
|
835
|
-
*
|
|
834
|
+
* Look up all project apps for a given repo. Returns info about exact matches,
|
|
835
|
+
* existing apps in other scopes, and whether a whole-repo app exists.
|
|
836
|
+
* No auth required.
|
|
836
837
|
*/
|
|
837
838
|
async lookupProjectByRepo(params) {
|
|
838
839
|
try {
|
|
@@ -844,12 +845,36 @@ var VocoderAPI = class {
|
|
|
844
845
|
scopePath: params.scopePath
|
|
845
846
|
})
|
|
846
847
|
});
|
|
847
|
-
if (response.
|
|
848
|
-
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
|
|
850
|
+
}
|
|
849
851
|
return await response.json();
|
|
850
852
|
} catch {
|
|
851
|
-
return null;
|
|
853
|
+
return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Add a new ProjectApp to an existing project (monorepo: new app directory).
|
|
858
|
+
* Does not check plan limits — no new project is created.
|
|
859
|
+
*/
|
|
860
|
+
async createProjectApp(userToken, params) {
|
|
861
|
+
const response = await fetch(`${this.apiUrl}/api/cli/project/apps`, {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: {
|
|
864
|
+
"Content-Type": "application/json",
|
|
865
|
+
Authorization: `Bearer ${userToken}`
|
|
866
|
+
},
|
|
867
|
+
body: JSON.stringify(params)
|
|
868
|
+
});
|
|
869
|
+
const payload = await readPayload(response);
|
|
870
|
+
if (!response.ok) {
|
|
871
|
+
throw new VocoderAPIError({
|
|
872
|
+
message: extractErrorMessage(payload, `Failed to create project app (${response.status})`),
|
|
873
|
+
status: response.status,
|
|
874
|
+
payload
|
|
875
|
+
});
|
|
852
876
|
}
|
|
877
|
+
return payload;
|
|
853
878
|
}
|
|
854
879
|
};
|
|
855
880
|
|
|
@@ -1499,6 +1524,119 @@ async function runProjectCreate(params) {
|
|
|
1499
1524
|
return null;
|
|
1500
1525
|
}
|
|
1501
1526
|
}
|
|
1527
|
+
async function runProjectAppCreate(params) {
|
|
1528
|
+
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
1529
|
+
const existingScopes = new Set(params.existingApps.map((a) => a.scopePath));
|
|
1530
|
+
let rawLocales;
|
|
1531
|
+
try {
|
|
1532
|
+
rawLocales = await api.listLocales(userToken);
|
|
1533
|
+
} catch {
|
|
1534
|
+
p3.log.error("Failed to fetch supported locales. Check your connection and try again.");
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
const languageOptions = buildLanguageOptions(rawLocales);
|
|
1538
|
+
const localeOptions = buildLocaleOptions(rawLocales);
|
|
1539
|
+
let scopePath;
|
|
1540
|
+
if (params.defaultScopePath && !existingScopes.has(params.defaultScopePath)) {
|
|
1541
|
+
scopePath = params.defaultScopePath;
|
|
1542
|
+
p3.log.success(`App directory: ${chalk4.bold(scopePath)}`);
|
|
1543
|
+
} else {
|
|
1544
|
+
if (params.existingApps.length > 0) {
|
|
1545
|
+
const configuredList = params.existingApps.map((a) => chalk4.dim(a.scopePath || "(entire repo)")).join(", ");
|
|
1546
|
+
p3.log.info(`Already configured: ${configuredList}`);
|
|
1547
|
+
}
|
|
1548
|
+
const hasWholeRepoApp = existingScopes.has("");
|
|
1549
|
+
const rawScope = await p3.text({
|
|
1550
|
+
message: "App directory for this new app",
|
|
1551
|
+
placeholder: "e.g. apps/backend",
|
|
1552
|
+
initialValue: params.defaultScopePath ?? "",
|
|
1553
|
+
validate(value) {
|
|
1554
|
+
const v = value.trim();
|
|
1555
|
+
if (!v && hasWholeRepoApp) return "This project already covers the entire repo.";
|
|
1556
|
+
if (!v) return "App directory is required when other apps already exist.";
|
|
1557
|
+
if (v.startsWith("/")) return "Use a relative path, not an absolute path.";
|
|
1558
|
+
if (v.includes("..")) return 'Path must not contain "..".';
|
|
1559
|
+
if (existingScopes.has(v)) return `"${v}" is already configured. Choose a different directory.`;
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
if (p3.isCancel(rawScope)) return null;
|
|
1563
|
+
scopePath = (rawScope ?? "").trim();
|
|
1564
|
+
}
|
|
1565
|
+
const sourceLocale = await searchSelectLocale(
|
|
1566
|
+
languageOptions,
|
|
1567
|
+
"Source language",
|
|
1568
|
+
"en"
|
|
1569
|
+
);
|
|
1570
|
+
if (sourceLocale === null) return null;
|
|
1571
|
+
const targetOptions = localeOptions.filter((opt) => opt.bcp47 !== sourceLocale);
|
|
1572
|
+
const targetLocales = await searchMultiSelectLocales(
|
|
1573
|
+
targetOptions,
|
|
1574
|
+
"Target languages"
|
|
1575
|
+
);
|
|
1576
|
+
if (targetLocales === null) return null;
|
|
1577
|
+
if (targetLocales.length === 0) {
|
|
1578
|
+
p3.log.warn("No target languages selected \u2014 you can add them later from the dashboard.");
|
|
1579
|
+
}
|
|
1580
|
+
const triggerChoice = await p3.select({
|
|
1581
|
+
message: "When should translations run?",
|
|
1582
|
+
options: [
|
|
1583
|
+
{ value: "push", label: "On push to target branches" },
|
|
1584
|
+
{ value: "pull_request", label: "On pull requests" },
|
|
1585
|
+
{ value: "push_and_pr", label: "On push and pull requests" },
|
|
1586
|
+
{ value: "manual", label: "Manual only", hint: "use vocoder sync or trigger from dashboard" }
|
|
1587
|
+
]
|
|
1588
|
+
});
|
|
1589
|
+
if (p3.isCancel(triggerChoice)) return null;
|
|
1590
|
+
const triggersForBranch = triggerChoice === "push_and_pr" ? ["push", "pull_request"] : [triggerChoice];
|
|
1591
|
+
const detected = detectGitBranches();
|
|
1592
|
+
let selectedBranches = [];
|
|
1593
|
+
{
|
|
1594
|
+
let initial = [detected.defaultBranch];
|
|
1595
|
+
while (selectedBranches.length === 0) {
|
|
1596
|
+
const result = await filterableBranchSelect({
|
|
1597
|
+
message: "Target branches",
|
|
1598
|
+
branches: detected.branches,
|
|
1599
|
+
defaultBranch: detected.defaultBranch,
|
|
1600
|
+
initialValues: initial
|
|
1601
|
+
});
|
|
1602
|
+
if (result === null) return null;
|
|
1603
|
+
if (result.length === 0) {
|
|
1604
|
+
p3.log.warn("At least one branch is required.");
|
|
1605
|
+
initial = [detected.defaultBranch];
|
|
1606
|
+
} else {
|
|
1607
|
+
selectedBranches = result;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const branchTriggers = selectedBranches.map((pattern) => ({
|
|
1612
|
+
pattern,
|
|
1613
|
+
triggers: triggersForBranch
|
|
1614
|
+
}));
|
|
1615
|
+
try {
|
|
1616
|
+
const result = await api.createProjectApp(userToken, {
|
|
1617
|
+
projectId,
|
|
1618
|
+
scopePath,
|
|
1619
|
+
sourceLocale,
|
|
1620
|
+
targetLocales,
|
|
1621
|
+
branchTriggers,
|
|
1622
|
+
repoCanonical: repoCanonical ?? ""
|
|
1623
|
+
});
|
|
1624
|
+
p3.log.success(`App ${chalk4.bold(scopePath)} added to ${chalk4.bold(projectName)}!`);
|
|
1625
|
+
return {
|
|
1626
|
+
projectId: result.projectId,
|
|
1627
|
+
projectName: result.projectName,
|
|
1628
|
+
apiKey: result.apiKey,
|
|
1629
|
+
scopePath: result.scopePath,
|
|
1630
|
+
sourceLocale,
|
|
1631
|
+
targetLocales,
|
|
1632
|
+
branchTriggers
|
|
1633
|
+
};
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1636
|
+
p3.log.error(`Failed to add app: ${message}`);
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1502
1640
|
|
|
1503
1641
|
// src/utils/workspace.ts
|
|
1504
1642
|
import * as p4 from "@clack/prompts";
|
|
@@ -1848,22 +1986,44 @@ async function init(options = {}) {
|
|
|
1848
1986
|
p5.log.warn(warning);
|
|
1849
1987
|
}
|
|
1850
1988
|
}
|
|
1989
|
+
let existingAppsForRepo = [];
|
|
1990
|
+
let repoProjectId = null;
|
|
1991
|
+
let repoProjectName = null;
|
|
1851
1992
|
if (identity) {
|
|
1852
1993
|
const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
1853
|
-
const
|
|
1994
|
+
const lookup = await anonApi.lookupProjectByRepo({
|
|
1854
1995
|
repoCanonical: identity.repoCanonical,
|
|
1855
1996
|
scopePath: identity.repoScopePath
|
|
1856
1997
|
});
|
|
1857
|
-
if (
|
|
1998
|
+
if (lookup.exactMatch) {
|
|
1999
|
+
const { exactMatch } = lookup;
|
|
1858
2000
|
runScaffold({
|
|
1859
|
-
projectName:
|
|
1860
|
-
organizationName:
|
|
1861
|
-
sourceLocale:
|
|
1862
|
-
branchTriggers:
|
|
2001
|
+
projectName: exactMatch.projectName,
|
|
2002
|
+
organizationName: exactMatch.organizationName,
|
|
2003
|
+
sourceLocale: exactMatch.sourceLocale ?? "en",
|
|
2004
|
+
branchTriggers: exactMatch.branchTriggers ?? [{ pattern: "main", triggers: ["push"] }]
|
|
1863
2005
|
});
|
|
1864
2006
|
p5.outro("Vocoder is already set up for this repository.");
|
|
1865
2007
|
return 0;
|
|
1866
2008
|
}
|
|
2009
|
+
if (lookup.hasWholeRepoApp) {
|
|
2010
|
+
const wholeRepo = lookup.existingApps.find((a) => a.scopePath === "");
|
|
2011
|
+
if (wholeRepo) {
|
|
2012
|
+
runScaffold({
|
|
2013
|
+
projectName: wholeRepo.projectName,
|
|
2014
|
+
organizationName: wholeRepo.organizationName,
|
|
2015
|
+
sourceLocale: "en",
|
|
2016
|
+
branchTriggers: [{ pattern: "main", triggers: ["push"] }]
|
|
2017
|
+
});
|
|
2018
|
+
p5.outro("Vocoder is already set up for this repository.");
|
|
2019
|
+
return 0;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (lookup.existingApps.length > 0) {
|
|
2023
|
+
existingAppsForRepo = lookup.existingApps;
|
|
2024
|
+
repoProjectId = lookup.existingApps[0]?.projectId ?? null;
|
|
2025
|
+
repoProjectName = lookup.existingApps[0]?.projectName ?? null;
|
|
2026
|
+
}
|
|
1867
2027
|
}
|
|
1868
2028
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
1869
2029
|
let userToken;
|
|
@@ -2137,6 +2297,34 @@ async function init(options = {}) {
|
|
|
2137
2297
|
}
|
|
2138
2298
|
}
|
|
2139
2299
|
}
|
|
2300
|
+
if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
|
|
2301
|
+
p5.log.info(
|
|
2302
|
+
`${chalk6.bold(repoProjectName)} is already set up for this repo.
|
|
2303
|
+
Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.scopePath || "(entire repo)")).join(", ")}`
|
|
2304
|
+
);
|
|
2305
|
+
const appResult = await runProjectAppCreate({
|
|
2306
|
+
api,
|
|
2307
|
+
userToken,
|
|
2308
|
+
projectId: repoProjectId,
|
|
2309
|
+
projectName: repoProjectName,
|
|
2310
|
+
organizationName: selectedWorkspaceName,
|
|
2311
|
+
repoCanonical: identity?.repoCanonical,
|
|
2312
|
+
defaultScopePath: identity?.repoScopePath,
|
|
2313
|
+
existingApps: existingAppsForRepo
|
|
2314
|
+
});
|
|
2315
|
+
if (!appResult) {
|
|
2316
|
+
p5.log.error("App setup failed. Run `vocoder init` again.");
|
|
2317
|
+
return 1;
|
|
2318
|
+
}
|
|
2319
|
+
runScaffold({
|
|
2320
|
+
projectName: appResult.projectName,
|
|
2321
|
+
organizationName: selectedWorkspaceName,
|
|
2322
|
+
sourceLocale: appResult.sourceLocale,
|
|
2323
|
+
branchTriggers: appResult.branchTriggers
|
|
2324
|
+
});
|
|
2325
|
+
p5.outro("You're all set.");
|
|
2326
|
+
return 0;
|
|
2327
|
+
}
|
|
2140
2328
|
try {
|
|
2141
2329
|
const wsCheck = await api.listWorkspaces(userToken);
|
|
2142
2330
|
const ws = wsCheck.workspaces.find((w) => w.id === selectedWorkspaceId);
|
|
@@ -2148,7 +2336,6 @@ async function init(options = {}) {
|
|
|
2148
2336
|
message: "What would you like to do?",
|
|
2149
2337
|
options: [
|
|
2150
2338
|
{ value: "upgrade", label: "Upgrade plan" },
|
|
2151
|
-
{ value: "existing", label: "Use an existing project in this workspace" },
|
|
2152
2339
|
{ value: "cancel", label: "Cancel" }
|
|
2153
2340
|
]
|
|
2154
2341
|
});
|
|
@@ -2156,41 +2343,9 @@ async function init(options = {}) {
|
|
|
2156
2343
|
p5.cancel("Setup cancelled.");
|
|
2157
2344
|
return 1;
|
|
2158
2345
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
return 1;
|
|
2163
|
-
}
|
|
2164
|
-
const existingProjects = await api.listProjects(userToken, selectedWorkspaceId);
|
|
2165
|
-
if (existingProjects.length === 0) {
|
|
2166
|
-
p5.log.error("No projects found in this workspace.");
|
|
2167
|
-
return 1;
|
|
2168
|
-
}
|
|
2169
|
-
const chosenId = await p5.select({
|
|
2170
|
-
message: "Select a project",
|
|
2171
|
-
options: existingProjects.map((proj) => ({
|
|
2172
|
-
value: proj.id,
|
|
2173
|
-
label: proj.name,
|
|
2174
|
-
hint: `${proj.sourceLocale} \u2192 ${proj.targetLocales.join(", ")}`
|
|
2175
|
-
}))
|
|
2176
|
-
});
|
|
2177
|
-
if (p5.isCancel(chosenId)) {
|
|
2178
|
-
p5.cancel("Setup cancelled.");
|
|
2179
|
-
return 1;
|
|
2180
|
-
}
|
|
2181
|
-
const chosen = existingProjects.find((proj) => proj.id === chosenId);
|
|
2182
|
-
runScaffold({
|
|
2183
|
-
projectName: chosen.name,
|
|
2184
|
-
organizationName: selectedWorkspaceName,
|
|
2185
|
-
sourceLocale: chosen.sourceLocale,
|
|
2186
|
-
branchTriggers: chosen.branchTriggers ?? [{ pattern: "main", triggers: ["push"] }]
|
|
2187
|
-
});
|
|
2188
|
-
p5.log.info(
|
|
2189
|
-
`Get your project API key at:
|
|
2190
|
-
${apiUrl}/dashboard/projects/${chosen.id}/settings`
|
|
2191
|
-
);
|
|
2192
|
-
p5.outro("You're all set.");
|
|
2193
|
-
return 0;
|
|
2346
|
+
await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
|
|
2347
|
+
p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
|
|
2348
|
+
return 1;
|
|
2194
2349
|
}
|
|
2195
2350
|
} catch {
|
|
2196
2351
|
}
|