@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 CHANGED
@@ -831,8 +831,9 @@ var VocoderAPI = class {
831
831
  }
832
832
  // ── Project lookup ────────────────────────────────────────────────────────────
833
833
  /**
834
- * Look up whether a project already exists for a given repo + scope.
835
- * Returns { projectId, projectName, organizationName } or null if not found.
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.status === 404) return null;
848
- if (!response.ok) return null;
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 existing = await anonApi.lookupProjectByRepo({
1994
+ const lookup = await anonApi.lookupProjectByRepo({
1854
1995
  repoCanonical: identity.repoCanonical,
1855
1996
  scopePath: identity.repoScopePath
1856
1997
  });
1857
- if (existing) {
1998
+ if (lookup.exactMatch) {
1999
+ const { exactMatch } = lookup;
1858
2000
  runScaffold({
1859
- projectName: existing.projectName,
1860
- organizationName: existing.organizationName,
1861
- sourceLocale: existing.sourceLocale ?? "en",
1862
- branchTriggers: existing.branchTriggers ?? [{ pattern: "main", triggers: ["push"] }]
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
- if (limitAction === "upgrade") {
2160
- await tryOpenBrowser2(`${apiUrl}${SUBSCRIPTION_SETTINGS_PATH}`);
2161
- p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
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
  }