@vocoder/cli 0.1.8 → 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
@@ -348,6 +348,10 @@ function parsePayload(raw) {
348
348
  if (raw.length === 0) {
349
349
  return null;
350
350
  }
351
+ const trimmed = raw.trimStart();
352
+ if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
353
+ return { message: "Unexpected response from server (received HTML). Check your network connection or try again." };
354
+ }
351
355
  try {
352
356
  return JSON.parse(raw);
353
357
  } catch {
@@ -659,8 +663,10 @@ var VocoderAPI = class {
659
663
  }
660
664
  }
661
665
  // ── Workspaces ────────────────────────────────────────────────────────────────
662
- async listWorkspaces(userToken) {
663
- 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(), {
664
670
  headers: { Authorization: `Bearer ${userToken}` }
665
671
  });
666
672
  const payload = await readPayload(response);
@@ -673,6 +679,23 @@ var VocoderAPI = class {
673
679
  }
674
680
  return payload;
675
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
+ }
676
699
  // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
677
700
  async startCliGitHubInstall(userToken, params) {
678
701
  const response = await fetch(`${this.apiUrl}/api/cli/github/install/start`, {
@@ -1926,105 +1949,219 @@ async function init(options = {}) {
1926
1949
  selectedWorkspaceName = claimResult.organizationName;
1927
1950
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1928
1951
  } else {
1929
- const workspaceData = await api.listWorkspaces(userToken);
1930
- if (workspaceData.workspaces.length === 1 && !workspaceData.canCreateWorkspace) {
1931
- 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];
1932
1960
  selectedWorkspaceId = ws.id;
1933
1961
  selectedWorkspaceName = ws.name;
1934
1962
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1935
- } else {
1936
- const workspaceResult = await selectWorkspace(workspaceData);
1937
- 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)) {
1972
+ p5.cancel("Setup cancelled.");
1973
+ return 1;
1974
+ }
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") {
1938
2004
  p5.cancel("Setup cancelled.");
1939
2005
  return 1;
1940
2006
  }
1941
- if (workspaceResult.action === "use") {
1942
- selectedWorkspaceId = workspaceResult.workspace.id;
1943
- selectedWorkspaceName = workspaceResult.workspace.name;
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;
1944
2029
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1945
2030
  } else {
1946
- const connectChoice = await p5.select({
1947
- message: "Connect your new workspace to GitHub",
1948
- options: [
1949
- { value: "install", label: "Install the Vocoder GitHub App" },
1950
- { value: "link", label: "Link an existing installation" }
1951
- ]
1952
- });
1953
- if (p5.isCancel(connectChoice)) {
2031
+ const workspaceResult = await selectWorkspace(workspaceData);
2032
+ if (workspaceResult.action === "cancelled") {
1954
2033
  p5.cancel("Setup cancelled.");
1955
2034
  return 1;
1956
2035
  }
1957
- if (connectChoice === "install") {
1958
- const connectResult = await runGitHubInstallFlow({
1959
- api,
1960
- userToken,
1961
- yes: options.yes
1962
- });
1963
- if (!connectResult) {
1964
- p5.log.error("GitHub App installation did not complete. Run `vocoder init` again.");
1965
- return 1;
1966
- }
1967
- selectedWorkspaceId = connectResult.organizationId;
1968
- selectedWorkspaceName = connectResult.organizationName;
2036
+ if (workspaceResult.action === "use") {
2037
+ selectedWorkspaceId = workspaceResult.workspace.id;
2038
+ selectedWorkspaceName = workspaceResult.workspace.name;
1969
2039
  p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1970
2040
  } else {
1971
- const installations = await runGitHubDiscoveryFlow({
1972
- api,
1973
- userToken,
1974
- 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
+ ]
1975
2047
  });
1976
- if (!installations) return 1;
1977
- if (installations.length === 0) {
1978
- p5.log.warn("No GitHub installations found. Install the Vocoder GitHub App first.");
1979
- const installNow = await p5.confirm({ message: "Open GitHub to install the App?" });
1980
- if (p5.isCancel(installNow) || !installNow) return 1;
1981
- const connectResult = await runGitHubInstallFlow({
1982
- api,
1983
- userToken,
1984
- yes: options.yes
1985
- });
1986
- 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
+ }
1987
2058
  selectedWorkspaceId = connectResult.organizationId;
1988
2059
  selectedWorkspaceName = connectResult.organizationName;
2060
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
1989
2061
  } else {
1990
- const selectedInstallationId = await selectGitHubInstallation(
1991
- installations.map((inst) => ({
1992
- installationId: inst.installationId,
1993
- accountLogin: inst.accountLogin,
1994
- accountType: inst.accountType,
1995
- isSuspended: inst.isSuspended,
1996
- conflictLabel: inst.conflictLabel
1997
- })),
1998
- true
1999
- );
2000
- if (selectedInstallationId === null) {
2001
- p5.cancel("Setup cancelled.");
2002
- return 1;
2003
- }
2004
- if (selectedInstallationId === "install_new") {
2005
- const connectResult = await runGitHubInstallFlow({
2006
- api,
2007
- userToken,
2008
- yes: options.yes
2009
- });
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 });
2010
2069
  if (!connectResult) return 1;
2011
2070
  selectedWorkspaceId = connectResult.organizationId;
2012
2071
  selectedWorkspaceName = connectResult.organizationName;
2013
2072
  } else {
2014
- const claimResult = await api.claimCliGitHubInstallation(userToken, {
2015
- installationId: String(selectedInstallationId),
2016
- organizationId: null
2017
- });
2018
- selectedWorkspaceId = claimResult.organizationId;
2019
- 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
+ }
2020
2100
  }
2101
+ p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2021
2102
  }
2022
- p5.log.success(`Workspace: ${chalk6.bold(selectedWorkspaceName)}`);
2023
2103
  }
2024
2104
  }
2025
2105
  }
2026
2106
  }
2027
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
+ }
2028
2165
  const projectResult = await runProjectCreate({
2029
2166
  api,
2030
2167
  userToken,