@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 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,13 +845,37 @@ 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 };
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 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);
@@ -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: "Select a project",
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
- runScaffold({
2375
+ const appResult = await runProjectAppCreate({
2376
+ api,
2377
+ userToken,
2378
+ projectId: chosen.id,
2183
2379
  projectName: chosen.name,
2184
2380
  organizationName: selectedWorkspaceName,
2185
- sourceLocale: chosen.sourceLocale,
2186
- branchTriggers: chosen.branchTriggers ?? [{ pattern: "main", triggers: ["push"] }]
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
  }