@vocoder/cli 0.1.9 → 0.1.10

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