@vocoder/cli 0.1.13 → 0.1.15
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 +229 -26
- 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,13 +845,37 @@ 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 };
|
|
852
854
|
}
|
|
853
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
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return payload;
|
|
878
|
+
}
|
|
854
879
|
};
|
|
855
880
|
|
|
856
881
|
// src/commands/init.ts
|
|
@@ -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);
|
|
@@ -2144,13 +2332,19 @@ async function init(options = {}) {
|
|
|
2144
2332
|
p5.log.warn(
|
|
2145
2333
|
`Project limit reached \u2014 ${ws.projectCount}/${ws.maxProjects} on your ${chalk6.bold(ws.planId)} plan.`
|
|
2146
2334
|
);
|
|
2335
|
+
const hasRepoContext = !!identity?.repoCanonical;
|
|
2336
|
+
const options2 = [];
|
|
2337
|
+
if (hasRepoContext) {
|
|
2338
|
+
options2.push({
|
|
2339
|
+
value: "connect",
|
|
2340
|
+
label: "Connect this repo to an existing project"
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
options2.push({ value: "upgrade", label: "Upgrade plan" });
|
|
2344
|
+
options2.push({ value: "cancel", label: "Cancel" });
|
|
2147
2345
|
const limitAction = await p5.select({
|
|
2148
2346
|
message: "What would you like to do?",
|
|
2149
|
-
options:
|
|
2150
|
-
{ value: "upgrade", label: "Upgrade plan" },
|
|
2151
|
-
{ value: "existing", label: "Use an existing project in this workspace" },
|
|
2152
|
-
{ value: "cancel", label: "Cancel" }
|
|
2153
|
-
]
|
|
2347
|
+
options: options2
|
|
2154
2348
|
});
|
|
2155
2349
|
if (p5.isCancel(limitAction) || limitAction === "cancel") {
|
|
2156
2350
|
p5.cancel("Setup cancelled.");
|
|
@@ -2167,11 +2361,10 @@ async function init(options = {}) {
|
|
|
2167
2361
|
return 1;
|
|
2168
2362
|
}
|
|
2169
2363
|
const chosenId = await p5.select({
|
|
2170
|
-
message: "
|
|
2364
|
+
message: "Which project should this repo be connected to?",
|
|
2171
2365
|
options: existingProjects.map((proj) => ({
|
|
2172
2366
|
value: proj.id,
|
|
2173
|
-
label: proj.name
|
|
2174
|
-
hint: `${proj.sourceLocale} \u2192 ${proj.targetLocales.join(", ")}`
|
|
2367
|
+
label: proj.name
|
|
2175
2368
|
}))
|
|
2176
2369
|
});
|
|
2177
2370
|
if (p5.isCancel(chosenId)) {
|
|
@@ -2179,16 +2372,26 @@ async function init(options = {}) {
|
|
|
2179
2372
|
return 1;
|
|
2180
2373
|
}
|
|
2181
2374
|
const chosen = existingProjects.find((proj) => proj.id === chosenId);
|
|
2182
|
-
|
|
2375
|
+
const appResult = await runProjectAppCreate({
|
|
2376
|
+
api,
|
|
2377
|
+
userToken,
|
|
2378
|
+
projectId: chosen.id,
|
|
2183
2379
|
projectName: chosen.name,
|
|
2184
2380
|
organizationName: selectedWorkspaceName,
|
|
2185
|
-
|
|
2186
|
-
|
|
2381
|
+
repoCanonical: identity?.repoCanonical,
|
|
2382
|
+
defaultScopePath: identity?.repoScopePath,
|
|
2383
|
+
existingApps: []
|
|
2384
|
+
});
|
|
2385
|
+
if (!appResult) {
|
|
2386
|
+
p5.log.error("Setup failed. Run `vocoder init` again.");
|
|
2387
|
+
return 1;
|
|
2388
|
+
}
|
|
2389
|
+
runScaffold({
|
|
2390
|
+
projectName: appResult.projectName,
|
|
2391
|
+
organizationName: selectedWorkspaceName,
|
|
2392
|
+
sourceLocale: appResult.sourceLocale,
|
|
2393
|
+
branchTriggers: appResult.branchTriggers
|
|
2187
2394
|
});
|
|
2188
|
-
p5.log.info(
|
|
2189
|
-
`Get your project API key at:
|
|
2190
|
-
${apiUrl}/dashboard/projects/${chosen.id}/settings`
|
|
2191
|
-
);
|
|
2192
2395
|
p5.outro("You're all set.");
|
|
2193
2396
|
return 0;
|
|
2194
2397
|
}
|