attio 0.0.1-experimental.20250402 → 0.0.1-experimental.20250403

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.
Files changed (50) hide show
  1. package/lib/api/api-fetch.js +32 -0
  2. package/lib/api/auth.js +63 -61
  3. package/lib/api/complete-bundle-upload.js +3 -16
  4. package/lib/api/complete-prod-bundle-upload.js +3 -16
  5. package/lib/api/create-dev-version.js +4 -9
  6. package/lib/api/create-version.js +4 -9
  7. package/lib/api/determine-workspace.spinner.js +23 -30
  8. package/lib/api/ensure-authed.js +3 -20
  9. package/lib/api/fetch-app-info.js +14 -20
  10. package/lib/api/fetch-installation.js +5 -16
  11. package/lib/api/fetch-versions.js +9 -17
  12. package/lib/api/fetch-workspaces.js +10 -18
  13. package/lib/api/get-app-info.spinner.js +5 -16
  14. package/lib/api/get-app-slug-from-package-json.js +4 -3
  15. package/lib/api/get-versions.spinner.js +5 -19
  16. package/lib/api/hard-exit.js +6 -0
  17. package/lib/api/keychain.js +29 -7
  18. package/lib/api/start-upload.js +7 -14
  19. package/lib/api/whoami.js +3 -9
  20. package/lib/commands/build/build-javascript.js +0 -2
  21. package/lib/commands/build.js +8 -23
  22. package/lib/commands/dev/boot.js +4 -8
  23. package/lib/commands/dev/build-javascript.js +0 -2
  24. package/lib/commands/dev/bundle-javascript.js +41 -4
  25. package/lib/commands/dev/graphql-code-gen.js +3 -2
  26. package/lib/commands/dev/onboarding.js +2 -2
  27. package/lib/commands/dev/upload.js +8 -40
  28. package/lib/commands/dev/validate-typescript.js +0 -4
  29. package/lib/commands/dev.js +47 -11
  30. package/lib/commands/init/create-project.js +16 -25
  31. package/lib/commands/init.js +4 -7
  32. package/lib/commands/login.js +7 -11
  33. package/lib/commands/logout.js +4 -10
  34. package/lib/commands/version/create.js +26 -51
  35. package/lib/commands/version/list.js +36 -25
  36. package/lib/commands/whoami.js +8 -12
  37. package/lib/util/copy-with-replace.js +3 -2
  38. package/lib/util/create-directory.js +3 -2
  39. package/lib/util/find-available-port.js +2 -1
  40. package/lib/util/load-attio-cli-package-json.js +3 -2
  41. package/lib/util/spinner.js +13 -1
  42. package/lib/util/upload-bundle.js +16 -0
  43. package/package.json +1 -4
  44. package/lib/api/fetch-connections.js +0 -20
  45. package/lib/api/handle-error.js +0 -18
  46. package/lib/api/make-headers.js +0 -7
  47. package/lib/api/remove-connection-definition.js +0 -10
  48. package/lib/commands/init/boot.js +0 -14
  49. package/lib/commands/version/create/boot.js +0 -9
  50. package/lib/util/clear-terminal.js +0 -4
@@ -0,0 +1,32 @@
1
+ import { API } from "../env.js";
2
+ import { ensureAuthed } from "./ensure-authed.js";
3
+ import { hardExit } from "./hard-exit.js";
4
+ export async function apiFetch(verb, path, fetchOptions, schema, allowNull = "error-on-404") {
5
+ const token = await ensureAuthed();
6
+ try {
7
+ const response = await fetch(path.startsWith("https") ? path : `${API}/${path}`, {
8
+ ...fetchOptions,
9
+ headers: {
10
+ "x-attio-platform": "developer-cli",
11
+ "Authorization": `Bearer ${token}`,
12
+ "Content-Type": "application/json",
13
+ },
14
+ });
15
+ if (!response.ok) {
16
+ if (response.status === 404 && allowNull === "null-on-404") {
17
+ return null;
18
+ }
19
+ const errorText = await response.text();
20
+ return hardExit(`Error ${verb}: ${response.status} ${errorText}`);
21
+ }
22
+ const data = await response.json();
23
+ const result = schema.safeParse(data);
24
+ if (!result.success) {
25
+ return hardExit(`Invalid response ${verb}: ${result.error.message}`);
26
+ }
27
+ return result.data;
28
+ }
29
+ catch (error) {
30
+ return hardExit(`Error ${verb}: ${error instanceof Error ? error.message : String(error)}`);
31
+ }
32
+ }
package/lib/api/auth.js CHANGED
@@ -4,8 +4,10 @@ import { randomBytes, createHash } from "crypto";
4
4
  import open from "open";
5
5
  import { APP } from "../env.js";
6
6
  import { findAvailablePort } from "../util/find-available-port.js";
7
- import { loadAuthToken, saveAuthToken } from "./keychain.js";
7
+ import { loadAuthToken, saveAuthToken, deleteAuthToken } from "./keychain.js";
8
8
  import { z } from "zod";
9
+ import { whoami } from "./whoami.js";
10
+ import { hardExit } from "./hard-exit.js";
9
11
  const CLIENT_ID = "f881c6f1-82d7-48a5-a581-649596167845";
10
12
  const tokenResponseSchema = z.object({
11
13
  access_token: z.string(),
@@ -20,23 +22,37 @@ async function exchangeCodeForToken(code, codeVerifier, redirectUri) {
20
22
  params.append("client_id", CLIENT_ID);
21
23
  params.append("redirect_uri", redirectUri);
22
24
  params.append("code_verifier", codeVerifier);
23
- const response = await fetch(tokenUrl, {
24
- method: "POST",
25
- headers: {
26
- "Content-Type": "application/x-www-form-urlencoded",
27
- },
28
- body: params.toString(),
29
- });
30
- if (!response.ok) {
31
- const errorText = await response.text();
32
- throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
25
+ try {
26
+ const response = await fetch(tokenUrl, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/x-www-form-urlencoded",
30
+ },
31
+ body: params.toString(),
32
+ });
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ return hardExit(`Failed to exchange code for token: ${errorText} (status: ${response.status})`);
36
+ }
37
+ const data = await response.json();
38
+ const result = tokenResponseSchema.safeParse(data);
39
+ if (!result.success) {
40
+ return hardExit("Invalid token response");
41
+ }
42
+ return result.data;
43
+ }
44
+ catch (error) {
45
+ return hardExit(`Failed to exchange code for token: ${error instanceof Error ? error.message : String(error)}`);
33
46
  }
34
- return tokenResponseSchema.parse(await response.json());
35
47
  }
36
48
  export async function auth() {
37
- const existingToken = await loadAuthToken();
38
- if (existingToken) {
39
- return existingToken;
49
+ const existingTokenResult = await loadAuthToken();
50
+ if (existingTokenResult !== null) {
51
+ const user = await whoami();
52
+ if (user !== null) {
53
+ return existingTokenResult;
54
+ }
55
+ await deleteAuthToken();
40
56
  }
41
57
  const verifier = randomBytes(32);
42
58
  const verifierString = verifier.toString("base64url");
@@ -48,70 +64,56 @@ export async function auth() {
48
64
  const port = await findAvailablePort(3000, 65000);
49
65
  const redirectUri = `http://localhost:${port}`;
50
66
  let resolveToken;
51
- let rejectToken;
52
- const tokenPromise = new Promise((resolve, reject) => {
67
+ const tokenPromise = new Promise((resolve) => {
53
68
  resolveToken = resolve;
54
- rejectToken = reject;
55
69
  });
56
70
  const app = new Hono();
57
71
  let serverRef;
58
72
  app.get("/", async (c) => {
59
- try {
60
- const query = c.req.query();
61
- const receivedCode = query.authorization_code;
62
- const receivedState = query.state;
63
- if (receivedState !== state) {
64
- throw new Error("State mismatch - possible CSRF attack");
65
- }
66
- if (!receivedCode) {
67
- throw new Error("No authorization code received");
68
- }
69
- const tokenResponse = await exchangeCodeForToken(receivedCode, verifierString, redirectUri);
70
- setTimeout(() => {
71
- serverRef.close();
72
- }, 1000);
73
- resolveToken(tokenResponse.access_token);
74
- return c.html(`
75
- <html>
76
- <body>
77
- <script>window.location.href = '${APP}/authorized';</script>
78
- </body>
79
- </html>
80
- `);
73
+ const query = c.req.query();
74
+ const receivedCode = query.authorization_code;
75
+ const receivedState = query.state;
76
+ if (receivedState !== state) {
77
+ hardExit("State mismatch - possible CSRF attack");
78
+ }
79
+ if (!receivedCode) {
80
+ hardExit("No authorization code received");
81
81
  }
82
- catch (error) {
83
- rejectToken(error instanceof Error ? error : new Error(String(error)));
84
- return c.html(`
82
+ const tokenResult = await exchangeCodeForToken(receivedCode, verifierString, redirectUri);
83
+ setTimeout(() => {
84
+ serverRef.close();
85
+ }, 1000);
86
+ resolveToken(tokenResult.access_token);
87
+ return c.html(`
85
88
  <html>
86
89
  <body>
87
- <h1>Authentication Failed</h1>
88
- <p>Error: ${error instanceof Error ? error.message : String(error)}</p>
89
- <p>Please close this window and try again.</p>
90
+ <script>window.location.href = '${APP}/authorized';</script>
90
91
  </body>
91
92
  </html>
92
93
  `);
93
- }
94
94
  });
95
95
  serverRef = serve({
96
96
  fetch: app.fetch,
97
97
  port,
98
98
  });
99
- const authUrl = new URL(`${APP}/oidc/authorize`);
100
- authUrl.searchParams.append("scope", "openid");
101
- authUrl.searchParams.append("response_type", "code");
102
- authUrl.searchParams.append("client_id", CLIENT_ID);
103
- authUrl.searchParams.append("redirect_uri", redirectUri);
104
- authUrl.searchParams.append("state", state);
105
- authUrl.searchParams.append("code_challenge", challengeString);
106
- authUrl.searchParams.append("code_challenge_method", "S256");
107
- await open(authUrl.toString());
108
99
  try {
109
- const accessToken = await tokenPromise;
110
- await saveAuthToken(accessToken);
111
- return accessToken;
100
+ const authUrl = new URL(`${APP}/oidc/authorize`);
101
+ authUrl.searchParams.append("scope", "openid");
102
+ authUrl.searchParams.append("response_type", "code");
103
+ authUrl.searchParams.append("client_id", CLIENT_ID);
104
+ authUrl.searchParams.append("redirect_uri", redirectUri);
105
+ authUrl.searchParams.append("state", state);
106
+ authUrl.searchParams.append("code_challenge", challengeString);
107
+ authUrl.searchParams.append("code_challenge_method", "S256");
108
+ await open(authUrl.toString());
109
+ const token = await tokenPromise;
110
+ if (token) {
111
+ process.stdout.write("🔑 Saving new token to keychain\n");
112
+ await saveAuthToken(token);
113
+ }
114
+ return token;
112
115
  }
113
- catch (error) {
116
+ finally {
114
117
  serverRef.close();
115
- throw error;
116
118
  }
117
119
  }
@@ -1,21 +1,8 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const completeBundleUploadSchema = z.object({
6
4
  success: z.literal(true),
7
5
  });
8
- export async function completeBundleUpload({ token, appId, devVersionId, bundleId, }) {
9
- const response = await fetch(`${API}/apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`, {
10
- method: "POST",
11
- headers: makeHeaders(token),
12
- });
13
- await handleError(response);
14
- const json = await response.json();
15
- try {
16
- return completeBundleUploadSchema.parse(json).success;
17
- }
18
- catch {
19
- throw new Error(JSON.stringify(json));
20
- }
6
+ export async function completeBundleUpload({ appId, devVersionId, bundleId, }) {
7
+ return (await apiFetch("completing bundle upload", `apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`, { method: "POST" }, completeBundleUploadSchema)).success;
21
8
  }
@@ -1,21 +1,8 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const completeBundleUploadSchema = z.object({
6
4
  success: z.literal(true),
7
5
  });
8
- export async function completeProdBundleUpload({ token, appId, bundleId, major, minor, }) {
9
- const response = await fetch(`${API}/apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`, {
10
- method: "POST",
11
- headers: makeHeaders(token),
12
- });
13
- await handleError(response);
14
- const json = await response.json();
15
- try {
16
- return completeBundleUploadSchema.parse(json).success;
17
- }
18
- catch {
19
- throw new Error(JSON.stringify(json));
20
- }
6
+ export async function completeProdBundleUpload({ appId, bundleId, major, minor, }) {
7
+ return (await apiFetch("completing production bundle upload", `apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`, { method: "POST" }, completeBundleUploadSchema)).success;
21
8
  }
@@ -1,22 +1,17 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const createDevVersionSchema = z.object({
6
4
  app_id: z.string(),
7
5
  app_dev_version_id: z.string(),
8
6
  });
9
- export async function createDevVersion({ token, appId, targetWorkspaceId, environmentVariables, cliVersion, }) {
10
- const response = await fetch(`${API}/apps/${appId}/dev-versions`, {
7
+ export async function createDevVersion({ appId, cliVersion, targetWorkspaceId, environmentVariables, }) {
8
+ return await apiFetch("creating dev version", `apps/${appId}/dev-versions`, {
11
9
  method: "POST",
12
- headers: makeHeaders(token),
13
10
  body: JSON.stringify({
14
11
  major: 1,
15
12
  target_workspace_id: targetWorkspaceId,
16
13
  environment_variables: environmentVariables,
17
14
  cli_version: cliVersion,
18
15
  }),
19
- });
20
- await handleError(response);
21
- return createDevVersionSchema.parse(await response.json());
16
+ }, createDevVersionSchema);
22
17
  }
@@ -1,7 +1,5 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const createVersionSchema = z.object({
6
4
  app_id: z.string(),
7
5
  major: z.number(),
@@ -10,15 +8,12 @@ const createVersionSchema = z.object({
10
8
  client_bundle_upload_url: z.string(),
11
9
  server_bundle_upload_url: z.string(),
12
10
  });
13
- export async function createVersion({ token, appId, major, cliVersion, }) {
14
- const response = await fetch(`${API}/apps/${appId}/prod-versions`, {
11
+ export async function createVersion({ appId, major, cliVersion, }) {
12
+ return await apiFetch("creating version", `apps/${appId}/prod-versions`, {
15
13
  method: "POST",
16
- headers: makeHeaders(token),
17
14
  body: JSON.stringify({
18
15
  major,
19
16
  cli_version: cliVersion,
20
17
  }),
21
- });
22
- await handleError(response);
23
- return createVersionSchema.parse(await response.json());
18
+ }, createVersionSchema);
24
19
  }
@@ -1,39 +1,32 @@
1
1
  import { select } from "@inquirer/prompts";
2
2
  import { fetchWorkspaces } from "./fetch-workspaces.js";
3
3
  import { APP } from "../env.js";
4
- import { Spinner } from "../util/spinner.js";
5
- export async function determineWorkspace({ token, workspaceSlug, }) {
6
- const spinner = new Spinner();
7
- spinner.start("Loading workspaces...");
8
- try {
9
- const workspaces = await fetchWorkspaces({ token });
10
- spinner.success("Workspaces loaded");
11
- const workspace = workspaces.find((workspace) => workspace.slug === workspaceSlug);
12
- if (workspace) {
13
- spinner.success(`Using workspace: ${workspace.name}`);
14
- return workspace;
15
- }
16
- if (workspaces.length === 0) {
17
- throw new Error(`You are not the admin of any workspaces. Either request permission from an existing workspace or create your own.
4
+ import { spinnerify } from "../util/spinner.js";
5
+ import { hardExit } from "./hard-exit.js";
6
+ export async function determineWorkspace(workspaceSlug) {
7
+ const workspaces = await spinnerify("Loading workspaces...", "Workspaces loaded", fetchWorkspaces);
8
+ const workspace = workspaces.find((workspace) => workspace.slug === workspaceSlug);
9
+ if (workspace) {
10
+ process.stdout.write(`Using workspace: ${workspace.name}`);
11
+ return workspace;
12
+ }
13
+ if (workspaces.length === 0) {
14
+ hardExit(`You are not the admin of any workspaces. Either request permission from an existing workspace or create your own.
18
15
 
19
16
  ${APP}/welcome/workspace-details
20
17
  `);
21
- }
22
- if (workspaces.length === 1) {
23
- spinner.success(`Using workspace: ${workspaces[0].name}`);
24
- return workspaces[0];
25
- }
26
- const choice = await select({
27
- message: "Choose a workspace",
28
- choices: workspaces.map((workspace) => ({
29
- name: workspace.name,
30
- value: workspace,
31
- })),
32
- });
33
- spinner.success(`Using workspace: ${choice.name}`);
34
- return choice;
35
18
  }
36
- finally {
37
- spinner.stop();
19
+ if (workspaces.length === 1) {
20
+ process.stdout.write(`Using workspace: ${workspaces[0].name}`);
21
+ return workspaces[0];
38
22
  }
23
+ const choice = await select({
24
+ message: "Choose a workspace",
25
+ choices: workspaces.map((workspace) => ({
26
+ name: workspace.name,
27
+ value: workspace,
28
+ })),
29
+ });
30
+ process.stdout.write(`Using workspace: ${choice.name}`);
31
+ return choice;
39
32
  }
@@ -1,31 +1,14 @@
1
1
  import { auth as authApi } from "./auth.js";
2
2
  import { deleteAuthToken, loadAuthToken } from "./keychain.js";
3
- import { whoami } from "./whoami.js";
4
3
  async function auth() {
5
4
  await deleteAuthToken();
6
5
  if (process.env.NODE_ENV !== "test") {
7
6
  process.stdout.write("You need to log in with Attio. Press Enter to continue...\n\n");
8
- await new Promise((resolve) => {
9
- process.stdin.once("data", () => {
10
- resolve();
11
- });
12
- });
7
+ await new Promise((resolve) => process.stdin.once("data", resolve));
13
8
  }
14
9
  return await authApi();
15
10
  }
16
11
  export async function ensureAuthed() {
17
- let token = await loadAuthToken();
18
- if (!token) {
19
- token = await auth();
20
- }
21
- try {
22
- const user = await whoami({ token });
23
- if (!user) {
24
- token = await auth();
25
- }
26
- }
27
- catch (error) {
28
- return await auth();
29
- }
30
- return token;
12
+ const token = await loadAuthToken();
13
+ return token ?? (await auth());
31
14
  }
@@ -1,26 +1,20 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
5
- const isTest = process.env.NODE_ENV === "test";
6
- const appSchema = z.object({
7
- app: z
8
- .object({
2
+ import { apiFetch } from "./api-fetch.js";
3
+ const appInfoSchema = z.object({
4
+ app: z.object({
9
5
  app_id: z.string().uuid(),
10
6
  title: z.string(),
11
- })
12
- .nullable(),
7
+ }),
13
8
  });
14
- export async function fetchAppInfo({ token, appSlug, }) {
15
- if (isTest) {
16
- return {
17
- app_id: "test-id",
18
- title: "Test App",
19
- };
9
+ const TEST_APP_INFO = appInfoSchema.parse({
10
+ app: {
11
+ app_id: "0cbafb09-2ef7-473c-a048-08a5d190a395",
12
+ title: "Test App",
13
+ },
14
+ });
15
+ export async function fetchAppInfo(appSlug) {
16
+ if (process.env.NODE_ENV === "test") {
17
+ return TEST_APP_INFO.app;
20
18
  }
21
- const response = await fetch(`${API}/apps/by-slug/${appSlug}`, {
22
- headers: makeHeaders(token),
23
- });
24
- await handleError(response);
25
- return appSchema.parse(await response.json()).app;
19
+ return (await apiFetch("fetching app info", `apps/by-slug/${appSlug}`, { method: "GET" }, appInfoSchema)).app;
26
20
  }
@@ -1,20 +1,9 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const installationSchema = z.object({
6
- workspace_slug: z.string(),
7
- workspace_id: z.string().uuid(),
8
- app_id: z.string().uuid(),
9
- installation_id: z.string().uuid(),
4
+ app_id: z.string(),
5
+ workspace_id: z.string(),
10
6
  });
11
- export async function fetchInstallation({ token, appId, workspaceId, }) {
12
- const response = await fetch(`${API}/apps/${appId}/workspace/${workspaceId}/dev-installation`, {
13
- method: "GET",
14
- headers: makeHeaders(token),
15
- });
16
- if (response.status === 404)
17
- return null;
18
- await handleError(response);
19
- return installationSchema.parse(await response.json());
7
+ export async function fetchInstallation({ appId, workspaceId, }) {
8
+ return await apiFetch("fetching installation", `apps/${appId}/workspace/${workspaceId}/dev-installation`, { method: "GET" }, installationSchema, "null-on-404");
20
9
  }
@@ -1,25 +1,17 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
5
- const prodVersionSchema = z.object({
6
- app_id: z.string(),
2
+ import { apiFetch } from "./api-fetch.js";
3
+ const versionSchema = z.object({
7
4
  major: z.number(),
8
5
  minor: z.number(),
6
+ created_at: z.string(),
7
+ released_at: z.string().nullable().optional(),
9
8
  is_published: z.boolean(),
10
9
  num_installations: z.number(),
11
- created_at: z.string(),
12
- updated_at: z.string(),
13
- accurate_at: z.string(),
10
+ publication_status: z.enum(["private", "in_review", "published", "rejected"]),
14
11
  });
15
- const fetchVersionsSchema = z.object({
16
- app_prod_versions: z.array(prodVersionSchema),
12
+ const versionsSchema = z.object({
13
+ app_prod_versions: z.array(versionSchema),
17
14
  });
18
- export async function fetchVersions({ token, appId, }) {
19
- const response = await fetch(`${API}/apps/${appId}/prod-versions`, {
20
- method: "GET",
21
- headers: makeHeaders(token),
22
- });
23
- await handleError(response);
24
- return fetchVersionsSchema.parse(await response.json()).app_prod_versions;
15
+ export async function fetchVersions(appId) {
16
+ return (await apiFetch("fetching versions", `apps/${appId}/prod-versions`, { method: "GET" }, versionsSchema)).app_prod_versions;
25
17
  }
@@ -1,7 +1,5 @@
1
1
  import { z } from "zod";
2
- import { API } from "../env.js";
3
- import { handleError } from "./handle-error.js";
4
- import { makeHeaders } from "./make-headers.js";
2
+ import { apiFetch } from "./api-fetch.js";
5
3
  const isTest = process.env.NODE_ENV === "test";
6
4
  const workspaceResponseSchema = z.object({
7
5
  workspace_id: z.string().uuid(),
@@ -9,25 +7,19 @@ const workspaceResponseSchema = z.object({
9
7
  name: z.string(),
10
8
  logo_url: z.string().nullable(),
11
9
  });
10
+ const TEST_WORKSPACE = workspaceResponseSchema.parse({
11
+ workspace_id: "a85e3bcf-a9a2-4df9-9e92-e708bf98d238",
12
+ slug: "test-slug",
13
+ name: "Test Workspace",
14
+ logo_url: null,
15
+ });
12
16
  const listDevWorkspacesResponseSchema = z.object({
13
17
  workspaces: z.array(workspaceResponseSchema),
14
18
  accurate_at: z.string().datetime(),
15
19
  });
16
- export async function fetchWorkspaces({ token }) {
20
+ export async function fetchWorkspaces() {
17
21
  if (isTest) {
18
- return [
19
- {
20
- workspace_id: "test-id",
21
- slug: "test-slug",
22
- name: "Test Workspace",
23
- logo_url: null,
24
- },
25
- ];
22
+ return [TEST_WORKSPACE];
26
23
  }
27
- const response = await fetch(`${API}/dev-workspaces`, {
28
- method: "GET",
29
- headers: makeHeaders(token),
30
- });
31
- await handleError(response);
32
- return listDevWorkspacesResponseSchema.parse(await response.json()).workspaces;
24
+ return (await apiFetch("fetching workspaces", "dev-workspaces", { method: "GET" }, listDevWorkspacesResponseSchema)).workspaces;
33
25
  }
@@ -1,18 +1,7 @@
1
1
  import { fetchAppInfo } from "./fetch-app-info.js";
2
- import { Spinner } from "../util/spinner.js";
3
- export async function getAppInfo({ token, appSlug, }) {
4
- const spinner = new Spinner();
5
- spinner.start("Loading app information...");
6
- try {
7
- const appInfo = await fetchAppInfo({ token, appSlug });
8
- if (appInfo === null) {
9
- spinner.error("App not found");
10
- throw new Error("App not found");
11
- }
12
- spinner.success(`App found: ${appInfo.title}`);
13
- return appInfo;
14
- }
15
- finally {
16
- spinner.stop();
17
- }
2
+ import { spinnerify } from "../util/spinner.js";
3
+ export async function getAppInfo(appSlug) {
4
+ return await spinnerify("Loading app information...", (app) => `App found: ${app.title}`, async () => {
5
+ return await fetchAppInfo(appSlug);
6
+ });
18
7
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { z } from "zod";
4
+ import { hardExit } from "./hard-exit.js";
4
5
  const packageJsonSchema = z.object({
5
6
  name: z.string({
6
7
  required_error: "No name field found in package.json",
@@ -13,14 +14,14 @@ export async function getAppSlugFromPackageJson() {
13
14
  const packageJsonRaw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
14
15
  const result = packageJsonSchema.safeParse(packageJsonRaw);
15
16
  if (!result.success) {
16
- throw new Error(result.error.issues[0]?.message || "Malformed package.json");
17
+ return hardExit(result.error.issues[0]?.message || "Malformed package.json");
17
18
  }
18
19
  return result.data.name;
19
20
  }
20
21
  catch (error) {
21
22
  if (error instanceof SyntaxError) {
22
- throw new Error("Invalid JSON in package.json");
23
+ return hardExit("Invalid JSON in package.json");
23
24
  }
24
- throw new Error("Failed to read package.json");
25
+ return hardExit("Failed to read package.json");
25
26
  }
26
27
  }
@@ -1,21 +1,7 @@
1
1
  import { fetchVersions } from "./fetch-versions.js";
2
- import { Spinner } from "../util/spinner.js";
3
- export async function getVersions({ token, appInfo, }) {
4
- const spinner = new Spinner();
5
- try {
6
- spinner.start("Loading versions...");
7
- const versions = await fetchVersions({
8
- token,
9
- appId: appInfo.app_id,
10
- });
11
- spinner.success("Versions loaded");
12
- return versions;
13
- }
14
- catch (error) {
15
- spinner.error("Error loading versions");
16
- throw error;
17
- }
18
- finally {
19
- spinner.stop();
20
- }
2
+ import { spinnerify } from "../util/spinner.js";
3
+ export async function getVersions(appInfo) {
4
+ return await spinnerify("Loading versions...", "Versions loaded", async () => {
5
+ return await fetchVersions(appInfo.app_id);
6
+ });
21
7
  }
@@ -0,0 +1,6 @@
1
+ import chalk from "chalk";
2
+ import process from "process";
3
+ export function hardExit(message) {
4
+ process.stderr.write(chalk.red("✖ " + message) + "\n");
5
+ process.exit(1);
6
+ }