@vocoder/cli 0.1.9 → 0.1.11

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
@@ -305,7 +305,10 @@ async function selectGitHubInstallation(installations, canInstallNew) {
305
305
  ].filter(Boolean).join(" \xB7 ") || void 0
306
306
  }));
307
307
  if (canInstallNew) {
308
- options.push({ value: "install_new", label: "Install on a new account" });
308
+ options.push({
309
+ value: "install_new",
310
+ label: `Install on a new account ${chalk.dim("(creates a new personal workspace)")}`
311
+ });
309
312
  }
310
313
  const selected = await p.select({
311
314
  message: "Select a GitHub installation",
@@ -663,8 +666,10 @@ var VocoderAPI = class {
663
666
  }
664
667
  }
665
668
  // ── Workspaces ────────────────────────────────────────────────────────────────
666
- async listWorkspaces(userToken) {
667
- const response = await fetch(`${this.apiUrl}/api/cli/workspaces`, {
669
+ async listWorkspaces(userToken, params) {
670
+ const url = new URL(`${this.apiUrl}/api/cli/workspaces`);
671
+ if (params?.repo) url.searchParams.set("repo", params.repo);
672
+ const response = await fetch(url.toString(), {
668
673
  headers: { Authorization: `Bearer ${userToken}` }
669
674
  });
670
675
  const payload = await readPayload(response);
@@ -677,6 +682,23 @@ var VocoderAPI = class {
677
682
  }
678
683
  return payload;
679
684
  }
685
+ async listProjects(userToken, organizationId) {
686
+ const url = new URL(`${this.apiUrl}/api/cli/projects`);
687
+ url.searchParams.set("organizationId", organizationId);
688
+ const response = await fetch(url.toString(), {
689
+ headers: { Authorization: `Bearer ${userToken}` }
690
+ });
691
+ const payload = await readPayload(response);
692
+ if (!response.ok) {
693
+ throw new VocoderAPIError({
694
+ message: extractErrorMessage(payload, `Failed to list projects (${response.status})`),
695
+ status: response.status,
696
+ payload
697
+ });
698
+ }
699
+ const result = payload;
700
+ return result.projects;
701
+ }
680
702
  // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
681
703
  async startCliGitHubInstall(userToken, params) {
682
704
  const response = await fetch(`${this.apiUrl}/api/cli/github/install/start`, {
@@ -1930,105 +1952,219 @@ async function init(options = {}) {
1930
1952
  selectedWorkspaceName = claimResult.organizationName;
1931
1953
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1932
1954
  } else {
1933
- const workspaceData = await api.listWorkspaces(userToken);
1934
- if (workspaceData.workspaces.length === 1 && !workspaceData.canCreateWorkspace) {
1935
- const ws = workspaceData.workspaces[0];
1955
+ const workspaceData = await api.listWorkspaces(userToken, {
1956
+ repo: identity?.repoCanonical
1957
+ });
1958
+ const repoCanonical = identity?.repoCanonical ?? null;
1959
+ const covering = repoCanonical ? workspaceData.workspaces.filter((w) => w.coversRepo === true) : [];
1960
+ const connected = workspaceData.workspaces.filter((w) => w.hasGitHubConnection);
1961
+ if (repoCanonical && covering.length === 1) {
1962
+ const ws = covering[0];
1936
1963
  selectedWorkspaceId = ws.id;
1937
1964
  selectedWorkspaceName = ws.name;
1938
1965
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1939
- } else {
1940
- const workspaceResult = await selectWorkspace(workspaceData);
1941
- if (workspaceResult.action === "cancelled") {
1966
+ } else if (repoCanonical && covering.length > 1) {
1967
+ const choice = await p5.select({
1968
+ message: "Select workspace for this repo",
1969
+ options: covering.map((w) => ({
1970
+ value: w.id,
1971
+ label: `${w.name} ${chalk6.dim(`(${w.projectCount} project${w.projectCount !== 1 ? "s" : ""})`)}`
1972
+ }))
1973
+ });
1974
+ if (p5.isCancel(choice)) {
1975
+ p5.cancel("Setup cancelled.");
1976
+ return 1;
1977
+ }
1978
+ const ws = covering.find((w) => w.id === choice);
1979
+ selectedWorkspaceId = ws.id;
1980
+ selectedWorkspaceName = ws.name;
1981
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1982
+ } else if (repoCanonical && covering.length === 0 && connected.length > 0) {
1983
+ const shortRepo = repoCanonical.split(":")[1] ?? repoCanonical;
1984
+ p5.log.warn(
1985
+ `${chalk6.bold(shortRepo)} isn't accessible from your Vocoder installation.
1986
+ Grant access to this repository or install on the account that owns it.`
1987
+ );
1988
+ const fixOptions = [];
1989
+ for (const ws of connected) {
1990
+ if (ws.installationConfigureUrl) {
1991
+ fixOptions.push({
1992
+ value: `grant:${ws.id}`,
1993
+ label: `Configure ${chalk6.bold(ws.connectionLabel ?? ws.name)}'s GitHub App installation`
1994
+ });
1995
+ }
1996
+ }
1997
+ fixOptions.push({
1998
+ value: "install_new",
1999
+ label: `Install on a different GitHub account ${chalk6.dim("(creates a new personal workspace)")}`
2000
+ });
2001
+ fixOptions.push({ value: "cancel", label: "Cancel" });
2002
+ const fix = await p5.select({
2003
+ message: "How would you like to fix this?",
2004
+ options: fixOptions
2005
+ });
2006
+ if (p5.isCancel(fix) || fix === "cancel") {
1942
2007
  p5.cancel("Setup cancelled.");
1943
2008
  return 1;
1944
2009
  }
1945
- if (workspaceResult.action === "use") {
1946
- selectedWorkspaceId = workspaceResult.workspace.id;
1947
- selectedWorkspaceName = workspaceResult.workspace.name;
2010
+ if (fix.startsWith("grant:")) {
2011
+ const ws = connected.find((w) => `grant:${w.id}` === fix);
2012
+ await tryOpenBrowser2(ws.installationConfigureUrl);
2013
+ p5.cancel(
2014
+ `Grant access to ${chalk6.bold(shortRepo)} in your browser,
2015
+ then re-run ${chalk6.bold("vocoder init")}.`
2016
+ );
2017
+ return 1;
2018
+ }
2019
+ const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2020
+ if (!connectResult) {
2021
+ p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
2022
+ return 1;
2023
+ }
2024
+ selectedWorkspaceId = connectResult.organizationId;
2025
+ selectedWorkspaceName = connectResult.organizationName;
2026
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2027
+ } else {
2028
+ if (workspaceData.workspaces.length === 1 && !workspaceData.canCreateWorkspace) {
2029
+ const ws = workspaceData.workspaces[0];
2030
+ selectedWorkspaceId = ws.id;
2031
+ selectedWorkspaceName = ws.name;
1948
2032
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1949
2033
  } else {
1950
- const connectChoice = await p5.select({
1951
- message: "Connect your new workspace to GitHub",
1952
- options: [
1953
- { value: "install", label: "Install the Vocoder GitHub App" },
1954
- { value: "link", label: "Link an existing installation" }
1955
- ]
1956
- });
1957
- if (p5.isCancel(connectChoice)) {
2034
+ const workspaceResult = await selectWorkspace(workspaceData);
2035
+ if (workspaceResult.action === "cancelled") {
1958
2036
  p5.cancel("Setup cancelled.");
1959
2037
  return 1;
1960
2038
  }
1961
- if (connectChoice === "install") {
1962
- const connectResult = await runGitHubInstallFlow({
1963
- api,
1964
- userToken,
1965
- yes: options.yes
1966
- });
1967
- if (!connectResult) {
1968
- p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
1969
- return 1;
1970
- }
1971
- selectedWorkspaceId = connectResult.organizationId;
1972
- selectedWorkspaceName = connectResult.organizationName;
2039
+ if (workspaceResult.action === "use") {
2040
+ selectedWorkspaceId = workspaceResult.workspace.id;
2041
+ selectedWorkspaceName = workspaceResult.workspace.name;
1973
2042
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1974
2043
  } else {
1975
- const installations = await runGitHubDiscoveryFlow({
1976
- api,
1977
- userToken,
1978
- yes: options.yes
2044
+ const connectChoice = await p5.select({
2045
+ message: "Connect your new workspace to GitHub",
2046
+ options: [
2047
+ { value: "install", label: "Install the Vocoder GitHub App" },
2048
+ { value: "link", label: "Link an existing installation" }
2049
+ ]
1979
2050
  });
1980
- if (!installations) return 1;
1981
- if (installations.length === 0) {
1982
- p5.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
1983
- const installNow = await p5.confirm({ message: "Open GitHub to install the App?" });
1984
- if (p5.isCancel(installNow) || !installNow) return 1;
1985
- const connectResult = await runGitHubInstallFlow({
1986
- api,
1987
- userToken,
1988
- yes: options.yes
1989
- });
1990
- if (!connectResult) return 1;
2051
+ if (p5.isCancel(connectChoice)) {
2052
+ p5.cancel("Setup cancelled.");
2053
+ return 1;
2054
+ }
2055
+ if (connectChoice === "install") {
2056
+ const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2057
+ if (!connectResult) {
2058
+ p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
2059
+ return 1;
2060
+ }
1991
2061
  selectedWorkspaceId = connectResult.organizationId;
1992
2062
  selectedWorkspaceName = connectResult.organizationName;
2063
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1993
2064
  } else {
1994
- const selectedInstallationId = await selectGitHubInstallation(
1995
- installations.map((inst) => ({
1996
- installationId: inst.installationId,
1997
- accountLogin: inst.accountLogin,
1998
- accountType: inst.accountType,
1999
- isSuspended: inst.isSuspended,
2000
- conflictLabel: inst.conflictLabel
2001
- })),
2002
- true
2003
- );
2004
- if (selectedInstallationId === null) {
2005
- p5.cancel("Setup cancelled.");
2006
- return 1;
2007
- }
2008
- if (selectedInstallationId === "install_new") {
2009
- const connectResult = await runGitHubInstallFlow({
2010
- api,
2011
- userToken,
2012
- yes: options.yes
2013
- });
2065
+ const installations = await runGitHubDiscoveryFlow({ api, userToken, yes: options.yes });
2066
+ if (!installations) return 1;
2067
+ if (installations.length === 0) {
2068
+ p5.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
2069
+ const installNow = await p5.confirm({ message: "Open GitHub to install the App?" });
2070
+ if (p5.isCancel(installNow) || !installNow) return 1;
2071
+ const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2014
2072
  if (!connectResult) return 1;
2015
2073
  selectedWorkspaceId = connectResult.organizationId;
2016
2074
  selectedWorkspaceName = connectResult.organizationName;
2017
2075
  } else {
2018
- const claimResult = await api.claimCliGitHubInstallation(userToken, {
2019
- installationId: String(selectedInstallationId),
2020
- organizationId: null
2021
- });
2022
- selectedWorkspaceId = claimResult.organizationId;
2023
- selectedWorkspaceName = claimResult.organizationName;
2076
+ const selectedInstallationId = await selectGitHubInstallation(
2077
+ installations.map((inst) => ({
2078
+ installationId: inst.installationId,
2079
+ accountLogin: inst.accountLogin,
2080
+ accountType: inst.accountType,
2081
+ isSuspended: inst.isSuspended,
2082
+ conflictLabel: inst.conflictLabel
2083
+ })),
2084
+ true
2085
+ );
2086
+ if (selectedInstallationId === null) {
2087
+ p5.cancel("Setup cancelled.");
2088
+ return 1;
2089
+ }
2090
+ if (selectedInstallationId === "install_new") {
2091
+ const connectResult = await runGitHubInstallFlow({ api, userToken, yes: options.yes });
2092
+ if (!connectResult) return 1;
2093
+ selectedWorkspaceId = connectResult.organizationId;
2094
+ selectedWorkspaceName = connectResult.organizationName;
2095
+ } else {
2096
+ const claimResult = await api.claimCliGitHubInstallation(userToken, {
2097
+ installationId: String(selectedInstallationId),
2098
+ organizationId: null
2099
+ });
2100
+ selectedWorkspaceId = claimResult.organizationId;
2101
+ selectedWorkspaceName = claimResult.organizationName;
2102
+ }
2024
2103
  }
2104
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2025
2105
  }
2026
- p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2027
2106
  }
2028
2107
  }
2029
2108
  }
2030
2109
  }
2031
2110
  }
2111
+ try {
2112
+ const wsCheck = await api.listWorkspaces(userToken);
2113
+ const ws = wsCheck.workspaces.find((w) => w.id === selectedWorkspaceId);
2114
+ if (ws && ws.maxProjects !== -1 && ws.projectCount >= ws.maxProjects) {
2115
+ p5.log.warn(
2116
+ `Project limit reached \u2014 ${ws.projectCount}/${ws.maxProjects} on your ${chalk6.bold(ws.planId)} plan.`
2117
+ );
2118
+ const limitAction = await p5.select({
2119
+ message: "What would you like to do?",
2120
+ options: [
2121
+ { value: "upgrade", label: "Upgrade plan" },
2122
+ { value: "existing", label: "Use an existing project in this workspace" },
2123
+ { value: "cancel", label: "Cancel" }
2124
+ ]
2125
+ });
2126
+ if (p5.isCancel(limitAction) || limitAction === "cancel") {
2127
+ p5.cancel("Setup cancelled.");
2128
+ return 1;
2129
+ }
2130
+ if (limitAction === "upgrade") {
2131
+ await tryOpenBrowser2(`${apiUrl}/dashboard/billing`);
2132
+ p5.cancel("Upgrade your plan in the browser, then re-run `vocoder init`.");
2133
+ return 1;
2134
+ }
2135
+ const existingProjects = await api.listProjects(userToken, selectedWorkspaceId);
2136
+ if (existingProjects.length === 0) {
2137
+ p5.log.error("No projects found in this workspace.");
2138
+ return 1;
2139
+ }
2140
+ const chosenId = await p5.select({
2141
+ message: "Select a project",
2142
+ options: existingProjects.map((proj) => ({
2143
+ value: proj.id,
2144
+ label: proj.name,
2145
+ hint: `${proj.sourceLocale} \u2192 ${proj.targetLocales.join(", ")}`
2146
+ }))
2147
+ });
2148
+ if (p5.isCancel(chosenId)) {
2149
+ p5.cancel("Setup cancelled.");
2150
+ return 1;
2151
+ }
2152
+ const chosen = existingProjects.find((proj) => proj.id === chosenId);
2153
+ runScaffold({
2154
+ projectName: chosen.name,
2155
+ organizationName: selectedWorkspaceName,
2156
+ sourceLocale: chosen.sourceLocale,
2157
+ translationTriggers: chosen.translationTriggers
2158
+ });
2159
+ p5.log.info(
2160
+ `Get your project API key at:
2161
+ ${apiUrl}/dashboard/projects/${chosen.id}/settings`
2162
+ );
2163
+ p5.outro("You're all set.");
2164
+ return 0;
2165
+ }
2166
+ } catch {
2167
+ }
2032
2168
  const projectResult = await runProjectCreate({
2033
2169
  api,
2034
2170
  userToken,