attio 0.0.1-experimental.20250402.1 → 0.0.1-experimental.20250407

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 (66) hide show
  1. package/lib/api/api.js +98 -0
  2. package/lib/api/fetcher.js +65 -0
  3. package/lib/api/schemas.js +76 -0
  4. package/lib/auth/auth.js +121 -0
  5. package/lib/auth/ensure-authed.js +14 -0
  6. package/lib/auth/keychain.js +52 -0
  7. package/lib/build/client/generate-client-entry.js +14 -4
  8. package/lib/build/server/generate-server-entry.js +24 -15
  9. package/lib/commands/build/build-javascript.js +13 -19
  10. package/lib/commands/build/validate-typescript.js +7 -14
  11. package/lib/commands/build.js +27 -25
  12. package/lib/commands/dev/boot.js +38 -15
  13. package/lib/commands/dev/bundle-javascript.js +65 -27
  14. package/lib/commands/dev/graphql-code-gen.js +3 -2
  15. package/lib/commands/dev/onboarding.js +20 -6
  16. package/lib/commands/dev/prepare-build-contexts.js +163 -32
  17. package/lib/{api → commands/dev}/start-graphql-server.js +2 -2
  18. package/lib/commands/dev/upload.js +40 -44
  19. package/lib/commands/dev/validate-typescript.js +3 -17
  20. package/lib/commands/dev.js +95 -13
  21. package/lib/commands/init/create-project.js +53 -35
  22. package/lib/commands/init.js +30 -9
  23. package/lib/commands/login.js +7 -11
  24. package/lib/commands/logout.js +5 -11
  25. package/lib/commands/version/create/bundle-javascript.js +28 -0
  26. package/lib/commands/version/create.js +89 -64
  27. package/lib/commands/version/list.js +54 -26
  28. package/lib/commands/whoami.js +15 -11
  29. package/lib/spinners/determine-workspace.spinner.js +58 -0
  30. package/lib/spinners/get-app-info.spinner.js +7 -0
  31. package/lib/spinners/get-app-slug-from-package-json.js +46 -0
  32. package/lib/spinners/get-versions.spinner.js +5 -0
  33. package/lib/util/copy-with-replace.js +40 -17
  34. package/lib/util/create-directory.js +21 -13
  35. package/lib/util/find-available-port.js +2 -1
  36. package/lib/util/hard-exit.js +6 -0
  37. package/lib/util/load-attio-cli-version.js +86 -0
  38. package/lib/util/spinner.js +16 -1
  39. package/lib/util/upload-bundle.js +17 -0
  40. package/package.json +2 -1
  41. package/lib/api/auth.js +0 -117
  42. package/lib/api/complete-bundle-upload.js +0 -21
  43. package/lib/api/complete-prod-bundle-upload.js +0 -21
  44. package/lib/api/create-dev-version.js +0 -22
  45. package/lib/api/create-version.js +0 -24
  46. package/lib/api/determine-workspace.spinner.js +0 -39
  47. package/lib/api/ensure-authed.js +0 -31
  48. package/lib/api/fetch-app-info.js +0 -26
  49. package/lib/api/fetch-connections.js +0 -20
  50. package/lib/api/fetch-installation.js +0 -20
  51. package/lib/api/fetch-versions.js +0 -25
  52. package/lib/api/fetch-workspaces.js +0 -33
  53. package/lib/api/get-app-info.spinner.js +0 -18
  54. package/lib/api/get-app-slug-from-package-json.js +0 -26
  55. package/lib/api/get-versions.spinner.js +0 -21
  56. package/lib/api/handle-error.js +0 -18
  57. package/lib/api/keychain.js +0 -30
  58. package/lib/api/make-headers.js +0 -7
  59. package/lib/api/remove-connection-definition.js +0 -10
  60. package/lib/api/start-upload.js +0 -18
  61. package/lib/api/whoami.js +0 -21
  62. package/lib/commands/dev/build-javascript.js +0 -49
  63. package/lib/commands/init/boot.js +0 -14
  64. package/lib/commands/version/create/boot.js +0 -9
  65. package/lib/util/clear-terminal.js +0 -4
  66. package/lib/util/load-attio-cli-package-json.js +0 -26
package/lib/api/api.js ADDED
@@ -0,0 +1,98 @@
1
+ import { Fetcher } from "./fetcher.js";
2
+ import { APP } from "../env.js";
3
+ import { isErrored, complete, errored } from "@attio/fetchable";
4
+ import { whoamiSchema, listDevWorkspacesResponseSchema, createVersionSchema, appInfoSchema, completeBundleUploadSchema, startUploadSchema, createDevVersionSchema, installationSchema, versionsSchema, TEST_WORKSPACES, TEST_APP_INFO, } from "./schemas.js";
5
+ class CliApi {
6
+ _fetcher;
7
+ constructor() {
8
+ this._fetcher = new Fetcher();
9
+ }
10
+ async whoami() {
11
+ const result = await this._fetcher.get(`${APP}/api/auth/whoami`, whoamiSchema);
12
+ if (isErrored(result)) {
13
+ return errored({ code: "WHOAMI_ERROR", fetcherError: result.error });
14
+ }
15
+ return complete(result.value.user);
16
+ }
17
+ async fetchWorkspaces() {
18
+ if (process.env.NODE_ENV === "test") {
19
+ return complete(TEST_WORKSPACES);
20
+ }
21
+ const result = await this._fetcher.get("/dev-workspaces", listDevWorkspacesResponseSchema);
22
+ if (isErrored(result)) {
23
+ return errored({ code: "FETCH_WORKSPACES_ERROR", fetcherError: result.error });
24
+ }
25
+ return complete(result.value.workspaces);
26
+ }
27
+ async createVersion({ appId, major, cliVersion, }) {
28
+ const result = await this._fetcher.post(`/apps/${appId}/prod-versions`, {
29
+ major,
30
+ cli_version: cliVersion,
31
+ }, createVersionSchema);
32
+ if (isErrored(result)) {
33
+ return errored({ code: "CREATE_VERSION_ERROR", fetcherError: result.error });
34
+ }
35
+ return result;
36
+ }
37
+ async fetchAppInfo(appSlug) {
38
+ if (process.env.NODE_ENV === "test") {
39
+ return complete(TEST_APP_INFO.app);
40
+ }
41
+ const result = await this._fetcher.get(`/apps/by-slug/${appSlug}`, appInfoSchema);
42
+ if (isErrored(result)) {
43
+ return errored({ code: "FETCH_APP_INFO_ERROR", fetcherError: result.error });
44
+ }
45
+ return complete(result.value.app);
46
+ }
47
+ async completeBundleUpload({ appId, devVersionId, bundleId, }) {
48
+ const result = await this._fetcher.post(`/apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`, {}, completeBundleUploadSchema);
49
+ if (isErrored(result)) {
50
+ return errored({ code: "COMPLETE_BUNDLE_UPLOAD_ERROR", fetcherError: result.error });
51
+ }
52
+ return complete(undefined);
53
+ }
54
+ async completeProdBundleUpload({ appId, bundleId, major, minor, }) {
55
+ const result = await this._fetcher.post(`/apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`, {}, completeBundleUploadSchema);
56
+ if (isErrored(result)) {
57
+ return errored({ code: "COMPLETE_PROD_BUNDLE_UPLOAD_ERROR", fetcherError: result.error });
58
+ }
59
+ return complete(undefined);
60
+ }
61
+ async startUpload({ appId, devVersionId, }) {
62
+ const result = await this._fetcher.post(`/apps/${appId}/dev-versions/${devVersionId}/bundles`, {}, startUploadSchema);
63
+ if (isErrored(result)) {
64
+ return errored({ code: "START_UPLOAD_ERROR", fetcherError: result.error });
65
+ }
66
+ return result;
67
+ }
68
+ async createDevVersion({ appId, cliVersion, targetWorkspaceId, environmentVariables, }) {
69
+ const result = await this._fetcher.post(`/apps/${appId}/dev-versions`, {
70
+ major: 1,
71
+ target_workspace_id: targetWorkspaceId,
72
+ environment_variables: environmentVariables,
73
+ cli_version: cliVersion,
74
+ }, createDevVersionSchema);
75
+ if (isErrored(result)) {
76
+ return errored({ code: "CREATE_DEV_VERSION_ERROR", fetcherError: result.error });
77
+ }
78
+ return result;
79
+ }
80
+ async fetchInstallation({ appId, workspaceId, }) {
81
+ const result = await this._fetcher.get(`/apps/${appId}/workspace/${workspaceId}/dev-installation`, installationSchema);
82
+ if (isErrored(result)) {
83
+ if (result.error.code === "HTTP_ERROR" && result.error.status === 404) {
84
+ return complete(null);
85
+ }
86
+ return errored({ code: "FETCH_INSTALLATION_ERROR", fetcherError: result.error });
87
+ }
88
+ return result;
89
+ }
90
+ async fetchVersions(appId) {
91
+ const result = await this._fetcher.get(`/apps/${appId}/prod-versions`, versionsSchema);
92
+ if (isErrored(result)) {
93
+ return errored({ code: "FETCH_VERSIONS_ERROR", fetcherError: result.error });
94
+ }
95
+ return complete(result.value.app_prod_versions);
96
+ }
97
+ }
98
+ export const API = new CliApi();
@@ -0,0 +1,65 @@
1
+ import { complete, errored } from "@attio/fetchable";
2
+ import { API } from "../env.js";
3
+ import { ensureAuthed } from "../auth/ensure-authed.js";
4
+ import chalk from "chalk";
5
+ export class Fetcher {
6
+ async _fetch(path, fetchOptions, schema) {
7
+ const token = await ensureAuthed();
8
+ try {
9
+ const response = await fetch(path.startsWith("https") ? path : `${API}${path}`, {
10
+ ...fetchOptions,
11
+ headers: {
12
+ "x-attio-platform": "developer-cli",
13
+ "Authorization": `Bearer ${token}`,
14
+ "Content-Type": "application/json",
15
+ },
16
+ });
17
+ if (!response.ok) {
18
+ const errorText = await response.text();
19
+ return errored({
20
+ code: "HTTP_ERROR",
21
+ status: response.status,
22
+ message: errorText,
23
+ });
24
+ }
25
+ const data = await response.json();
26
+ const result = schema.safeParse(data);
27
+ if (!result.success) {
28
+ return errored({
29
+ code: "INVALID_RESPONSE",
30
+ message: result.error.message,
31
+ });
32
+ }
33
+ return complete(result.data);
34
+ }
35
+ catch (error) {
36
+ return errored({
37
+ code: "NETWORK_ERROR",
38
+ message: error instanceof Error ? error.message : String(error),
39
+ });
40
+ }
41
+ }
42
+ async get(path, schema) {
43
+ return this._fetch(path, { method: "GET" }, schema);
44
+ }
45
+ async post(path, body, schema) {
46
+ return this._fetch(path, {
47
+ method: "POST",
48
+ body: JSON.stringify(body),
49
+ }, schema);
50
+ }
51
+ }
52
+ export function printFetcherError(message, error) {
53
+ process.stderr.write(`${chalk.red("✖ ")}${message}\n`);
54
+ switch (error.code) {
55
+ case "HTTP_ERROR":
56
+ process.stderr.write(`HTTP Error (${error.status}): ${error.message}\n`);
57
+ break;
58
+ case "INVALID_RESPONSE":
59
+ process.stderr.write(`Invalid response: ${error.message}\n`);
60
+ break;
61
+ case "NETWORK_ERROR":
62
+ process.stderr.write(`Network error: ${error.message}\n`);
63
+ break;
64
+ }
65
+ }
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ export const whoamiSchema = z.object({
3
+ user: z.object({
4
+ id: z.string(),
5
+ email_address: z.string(),
6
+ name: z.object({
7
+ full: z.string(),
8
+ }),
9
+ }),
10
+ });
11
+ export const workspaceResponseSchema = z.object({
12
+ workspace_id: z.string().uuid(),
13
+ slug: z.string(),
14
+ name: z.string(),
15
+ logo_url: z.string().nullable(),
16
+ });
17
+ export const TEST_WORKSPACES = [
18
+ workspaceResponseSchema.parse({
19
+ workspace_id: "a85e3bcf-a9a2-4df9-9e92-e708bf98d238",
20
+ slug: "test-slug",
21
+ name: "Test Workspace",
22
+ logo_url: null,
23
+ }),
24
+ ];
25
+ export const listDevWorkspacesResponseSchema = z.object({
26
+ workspaces: z.array(workspaceResponseSchema),
27
+ });
28
+ export const createVersionSchema = z.object({
29
+ app_id: z.string().uuid(),
30
+ major: z.number(),
31
+ minor: z.number(),
32
+ app_prod_version_bundle_id: z.string(),
33
+ client_bundle_upload_url: z.string(),
34
+ server_bundle_upload_url: z.string(),
35
+ });
36
+ export const appInfoSchema = z.object({
37
+ app: z.object({
38
+ app_id: z.string().uuid(),
39
+ title: z.string(),
40
+ }),
41
+ });
42
+ export const TEST_APP_INFO = appInfoSchema.parse({
43
+ app: {
44
+ app_id: "0cbafb09-2ef7-473c-a048-08a5d190a395",
45
+ title: "Test App",
46
+ },
47
+ });
48
+ export const completeBundleUploadSchema = z.object({
49
+ success: z.literal(true),
50
+ });
51
+ export const startUploadSchema = z.object({
52
+ app_dev_version_id: z.string().uuid(),
53
+ app_dev_version_bundle_id: z.string().uuid(),
54
+ client_bundle_upload_url: z.string().url(),
55
+ server_bundle_upload_url: z.string().url(),
56
+ });
57
+ export const createDevVersionSchema = z.object({
58
+ app_id: z.string().uuid(),
59
+ app_dev_version_id: z.string(),
60
+ });
61
+ export const installationSchema = z.object({
62
+ app_id: z.string().uuid(),
63
+ workspace_id: z.string(),
64
+ });
65
+ export const versionSchema = z.object({
66
+ major: z.number(),
67
+ minor: z.number(),
68
+ created_at: z.string(),
69
+ released_at: z.string().nullable().optional(),
70
+ is_published: z.boolean(),
71
+ num_installations: z.number(),
72
+ publication_status: z.enum(["private", "in_review", "published", "rejected"]),
73
+ });
74
+ export const versionsSchema = z.object({
75
+ app_prod_versions: z.array(versionSchema),
76
+ });
@@ -0,0 +1,121 @@
1
+ import { Hono } from "hono";
2
+ import { serve } from "@hono/node-server";
3
+ import { randomBytes, createHash } from "crypto";
4
+ import open from "open";
5
+ import { APP } from "../env.js";
6
+ import { findAvailablePort } from "../util/find-available-port.js";
7
+ import { loadAuthToken, saveAuthToken, deleteAuthToken } from "./keychain.js";
8
+ import { z } from "zod";
9
+ import { hardExit } from "../util/hard-exit.js";
10
+ import { API } from "../api/api.js";
11
+ import { isComplete } from "@attio/fetchable";
12
+ const CLIENT_ID = "f881c6f1-82d7-48a5-a581-649596167845";
13
+ const tokenResponseSchema = z.object({
14
+ access_token: z.string(),
15
+ token_type: z.string(),
16
+ expires_in: z.number(),
17
+ });
18
+ async function exchangeCodeForToken(code, codeVerifier, redirectUri) {
19
+ const tokenUrl = `${APP}/oidc/token`;
20
+ const params = new URLSearchParams();
21
+ params.append("grant_type", "authorization_code");
22
+ params.append("code", code);
23
+ params.append("client_id", CLIENT_ID);
24
+ params.append("redirect_uri", redirectUri);
25
+ params.append("code_verifier", codeVerifier);
26
+ try {
27
+ const response = await fetch(tokenUrl, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/x-www-form-urlencoded",
31
+ },
32
+ body: params.toString(),
33
+ });
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ return hardExit(`Failed to exchange code for token: ${errorText} (status: ${response.status})`);
37
+ }
38
+ const data = await response.json();
39
+ const result = tokenResponseSchema.safeParse(data);
40
+ if (!result.success) {
41
+ return hardExit("Invalid token response");
42
+ }
43
+ return result.data;
44
+ }
45
+ catch (error) {
46
+ return hardExit(`Failed to exchange code for token: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ }
49
+ export async function auth() {
50
+ const existingTokenResult = await loadAuthToken();
51
+ if (existingTokenResult !== null) {
52
+ const userResult = await API.whoami();
53
+ const user = isComplete(userResult) ? userResult.value : null;
54
+ if (user !== null) {
55
+ return existingTokenResult;
56
+ }
57
+ await deleteAuthToken();
58
+ }
59
+ const verifier = randomBytes(32);
60
+ const verifierString = verifier.toString("base64url");
61
+ const hash = createHash("sha256");
62
+ hash.update(verifier);
63
+ const challenge = hash.digest();
64
+ const challengeString = challenge.toString("base64url");
65
+ const state = randomBytes(32).toString("base64url");
66
+ const port = await findAvailablePort(3000, 65000);
67
+ const redirectUri = `http://localhost:${port}`;
68
+ let resolveToken;
69
+ const tokenPromise = new Promise((resolve) => {
70
+ resolveToken = resolve;
71
+ });
72
+ const app = new Hono();
73
+ let serverRef;
74
+ app.get("/", async (c) => {
75
+ const query = c.req.query();
76
+ const receivedCode = query.authorization_code;
77
+ const receivedState = query.state;
78
+ if (receivedState !== state) {
79
+ hardExit("State mismatch - possible CSRF attack");
80
+ }
81
+ if (!receivedCode) {
82
+ hardExit("No authorization code received");
83
+ }
84
+ const tokenResult = await exchangeCodeForToken(receivedCode, verifierString, redirectUri);
85
+ setTimeout(() => {
86
+ serverRef.close();
87
+ }, 1000);
88
+ resolveToken(tokenResult.access_token);
89
+ return c.html(`
90
+ <html>
91
+ <body>
92
+ <script>window.location.href = '${APP}/authorized';</script>
93
+ </body>
94
+ </html>
95
+ `);
96
+ });
97
+ serverRef = serve({
98
+ fetch: app.fetch,
99
+ port,
100
+ });
101
+ try {
102
+ const authUrl = new URL(`${APP}/oidc/authorize`);
103
+ authUrl.searchParams.append("scope", "openid");
104
+ authUrl.searchParams.append("response_type", "code");
105
+ authUrl.searchParams.append("client_id", CLIENT_ID);
106
+ authUrl.searchParams.append("redirect_uri", redirectUri);
107
+ authUrl.searchParams.append("state", state);
108
+ authUrl.searchParams.append("code_challenge", challengeString);
109
+ authUrl.searchParams.append("code_challenge_method", "S256");
110
+ await open(authUrl.toString());
111
+ const token = await tokenPromise;
112
+ if (token) {
113
+ process.stdout.write("🔑 Saving new token to keychain\n");
114
+ await saveAuthToken(token);
115
+ }
116
+ return token;
117
+ }
118
+ finally {
119
+ serverRef.close();
120
+ }
121
+ }
@@ -0,0 +1,14 @@
1
+ import { auth as authApi } from "./auth.js";
2
+ import { deleteAuthToken, loadAuthToken } from "./keychain.js";
3
+ async function auth() {
4
+ await deleteAuthToken();
5
+ if (process.env.NODE_ENV !== "test") {
6
+ process.stdout.write("You need to log in with Attio. Press Enter to continue...\n\n");
7
+ await new Promise((resolve) => process.stdin.once("data", resolve));
8
+ }
9
+ return await authApi();
10
+ }
11
+ export async function ensureAuthed() {
12
+ const token = await loadAuthToken();
13
+ return token ?? (await auth());
14
+ }
@@ -0,0 +1,52 @@
1
+ import { hardExit } from "../util/hard-exit.js";
2
+ const TEST_TOKEN = "TEST";
3
+ let keytar = null;
4
+ if (process.env.NODE_ENV !== "test") {
5
+ try {
6
+ keytar = (await import("keytar")).default;
7
+ }
8
+ catch (e) {
9
+ console.warn("Keychain functionality not available");
10
+ }
11
+ }
12
+ const SERVICE_NAME = "attio-cli";
13
+ const ACCOUNT_NAME = "developer";
14
+ export async function saveAuthToken(token) {
15
+ if (!keytar) {
16
+ return hardExit("Keychain functionality not available");
17
+ }
18
+ try {
19
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
20
+ }
21
+ catch (error) {
22
+ return hardExit(error instanceof Error ? error.message : "Failed to save token to keychain");
23
+ }
24
+ }
25
+ export async function loadAuthToken() {
26
+ if (process.env.NODE_ENV === "test") {
27
+ return TEST_TOKEN;
28
+ }
29
+ if (!keytar) {
30
+ return hardExit("Keychain functionality not available");
31
+ }
32
+ try {
33
+ return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
34
+ }
35
+ catch (error) {
36
+ return hardExit(error instanceof Error ? error.message : "Failed to load token from keychain");
37
+ }
38
+ }
39
+ export async function deleteAuthToken() {
40
+ if (!keytar) {
41
+ if (process.env.NODE_ENV === "test") {
42
+ return;
43
+ }
44
+ return hardExit("Keychain functionality not available");
45
+ }
46
+ try {
47
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
48
+ }
49
+ catch (error) {
50
+ return hardExit(error instanceof Error ? error.message : "Failed to delete token from keychain");
51
+ }
52
+ }
@@ -1,9 +1,19 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { findSurfaceExports } from "../../util/find-surface-exports/find-surface-exports.js";
3
+ import { findSurfaceExports, } from "../../util/find-surface-exports/find-surface-exports.js";
4
+ import { errored, complete } from "@attio/fetchable";
4
5
  const ASSET_FILE_EXTENSIONS = ["png"];
5
6
  export async function generateClientEntry({ srcDirAbsolute, assetsDirAbsolute, }) {
6
- const surfaceExports = await findSurfaceExports(srcDirAbsolute);
7
+ let surfaceExports;
8
+ try {
9
+ surfaceExports = await findSurfaceExports(srcDirAbsolute);
10
+ }
11
+ catch (error) {
12
+ return errored({
13
+ code: "ERROR_FINDING_SURFACE_EXPORTS",
14
+ error,
15
+ });
16
+ }
7
17
  let assetFiles;
8
18
  try {
9
19
  assetFiles = await fs.readdir(assetsDirAbsolute, { recursive: true });
@@ -47,7 +57,7 @@ const assets = [];
47
57
 
48
58
  registerAssets(assets);
49
59
  `;
50
- return `
60
+ return complete(`
51
61
  ${importSurfacesJS}
52
62
 
53
63
  ${importAssetsJS}
@@ -55,5 +65,5 @@ ${importAssetsJS}
55
65
  ${registerSurfacesJS}
56
66
 
57
67
  ${registerAssetsJS}
58
- `;
68
+ `);
59
69
  }
@@ -1,20 +1,29 @@
1
1
  import { glob } from "glob";
2
2
  import path from "path";
3
+ import { errored, complete, isErrored, combineAsync } from "@attio/fetchable";
4
+ async function findPaths(dir, pattern) {
5
+ try {
6
+ return complete(await glob(`**/*.${pattern}.(js|ts)`, { nodir: true, cwd: dir }));
7
+ }
8
+ catch (error) {
9
+ return errored({
10
+ code: "ERROR_FINDING_SERVER_FUNCTION_MODULES",
11
+ dir,
12
+ pattern,
13
+ error,
14
+ });
15
+ }
16
+ }
3
17
  export async function generateServerEntry({ srcDirAbsolute, webhooksDirAbsolute, eventDirAbsolute, log, }) {
4
- const [serverFunctionConcretePaths, webhookConcretePaths, eventConcretePaths] = await Promise.all([
5
- Promise.all([
6
- glob("**/*.server.ts", { nodir: true, cwd: srcDirAbsolute }),
7
- glob("**/*.server.js", { nodir: true, cwd: srcDirAbsolute }),
8
- ]).then((paths) => paths.flat()),
9
- Promise.all([
10
- glob("**/*.webhook.ts", { nodir: true, cwd: webhooksDirAbsolute }),
11
- glob("**/*.webhook.js", { nodir: true, cwd: webhooksDirAbsolute }),
12
- ]).then((paths) => paths.flat()),
13
- Promise.all([
14
- glob("*.event.ts", { nodir: true, cwd: eventDirAbsolute }),
15
- glob("*.event.js", { nodir: true, cwd: eventDirAbsolute }),
16
- ]).then((paths) => paths.flat()),
18
+ const pathsResult = await combineAsync([
19
+ findPaths(srcDirAbsolute, "server"),
20
+ findPaths(webhooksDirAbsolute, "webhook"),
21
+ findPaths(eventDirAbsolute, "event"),
17
22
  ]);
23
+ if (isErrored(pathsResult)) {
24
+ return pathsResult;
25
+ }
26
+ const [serverFunctionConcretePaths, webhookConcretePaths, eventConcretePaths] = pathsResult.value;
18
27
  const serverFunctionModules = serverFunctionConcretePaths.map((path) => ({
19
28
  path,
20
29
  hash: path.replace(/\.(js|ts)$/, ""),
@@ -30,7 +39,7 @@ export async function generateServerEntry({ srcDirAbsolute, webhooksDirAbsolute,
30
39
  for (const module of serverFunctionModules) {
31
40
  log?.(`🔎 Found server module "${module.path}"`);
32
41
  }
33
- return `
42
+ return complete(`
34
43
 
35
44
  const modules = new Map()
36
45
  const webhookModules = new Map()
@@ -118,5 +127,5 @@ export async function generateServerEntry({ srcDirAbsolute, webhooksDirAbsolute,
118
127
 
119
128
  main()
120
129
 
121
- `;
130
+ `);
122
131
  }
@@ -1,24 +1,18 @@
1
- import { clearTerminal } from "../../util/clear-terminal.js";
2
- import { prepareBuildContexts } from "../dev/prepare-build-contexts.js";
3
- import { printJsError } from "../../util/typescript.js";
4
- import { errorsAndWarningsSchema } from "../../build.js";
1
+ import { prepareBuildContexts, } from "../dev/prepare-build-contexts.js";
2
+ import { combineAsync, complete, isErrored } from "@attio/fetchable";
5
3
  export async function buildJavaScript() {
6
- let buildContexts;
7
- try {
8
- buildContexts = await prepareBuildContexts("write-to-disk");
9
- await Promise.all(buildContexts.map(async (context) => context.rebuild()));
10
- return true;
4
+ const buildContextsResult = await prepareBuildContexts("write-to-disk");
5
+ if (isErrored(buildContextsResult)) {
6
+ return buildContextsResult;
11
7
  }
12
- catch (error) {
13
- const errors = errorsAndWarningsSchema.parse(error);
14
- clearTerminal();
15
- errors.errors?.forEach((error) => printJsError(error, "error"));
16
- errors.warnings?.forEach((warning) => printJsError(warning, "warning"));
17
- return false;
8
+ const buildContexts = buildContextsResult.value;
9
+ const buildResult = await combineAsync(buildContexts.map(async (context) => context.rebuild()));
10
+ const disposeResult = await combineAsync(buildContexts.map(async (context) => context.dispose()));
11
+ if (isErrored(disposeResult)) {
12
+ return disposeResult;
18
13
  }
19
- finally {
20
- if (buildContexts) {
21
- await Promise.all(buildContexts.map(async (context) => context.dispose()));
22
- }
14
+ if (isErrored(buildResult)) {
15
+ return buildResult;
23
16
  }
17
+ return complete(true);
24
18
  }
@@ -1,30 +1,23 @@
1
1
  import path from "path";
2
- import { getDiagnostics, readConfig, typeScriptErrorSchema, printTsError, } from "../../util/typescript.js";
2
+ import { getDiagnostics, readConfig, typeScriptErrorSchema, } from "../../util/typescript.js";
3
+ import { complete, errored } from "@attio/fetchable";
3
4
  export async function validateTypeScript() {
4
5
  try {
5
6
  const program = await readConfig(path.resolve("tsconfig.json"));
6
7
  if (program === "Not a TypeScript project") {
7
- return true;
8
+ return complete(true);
8
9
  }
9
10
  const errors = await getDiagnostics(program);
10
11
  if (errors.length) {
11
- errors.forEach(printTsError);
12
- return false;
12
+ return errored({ code: "VALIDATE_TYPE_SCRIPT_ERROR", errors });
13
13
  }
14
- return true;
14
+ return complete(true);
15
15
  }
16
16
  catch (error) {
17
17
  if (error instanceof Error) {
18
18
  const tsError = typeScriptErrorSchema.parse({ text: error.message });
19
- printTsError(tsError);
20
- return false;
19
+ return errored({ code: "VALIDATE_TYPE_SCRIPT_ERROR", errors: [tsError] });
21
20
  }
22
- if (typeof error === "object" &&
23
- error !== null &&
24
- "code" in error &&
25
- (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) {
26
- throw error;
27
- }
28
- return false;
21
+ return errored({ code: "FILE_SYSTEM_ERROR", error });
29
22
  }
30
23
  }
@@ -1,36 +1,38 @@
1
1
  import { Command } from "commander";
2
2
  import { validateTypeScript } from "./build/validate-typescript.js";
3
3
  import { buildJavaScript } from "./build/build-javascript.js";
4
- import { Spinner } from "../util/spinner.js";
4
+ import { spinnerify } from "../util/spinner.js";
5
+ import { isErrored } from "@attio/fetchable";
6
+ import { printJsError, printTsError } from "../util/typescript.js";
7
+ import { printBuildContextError } from "./dev/prepare-build-contexts.js";
5
8
  export const build = new Command("build")
6
9
  .description("Build your Attio extension locally")
7
- .action(async (unparsedOptions) => {
8
- const spinner = new Spinner();
9
- spinner.start("Validating TypeScript...");
10
- try {
11
- const tsValid = await validateTypeScript();
12
- if (!tsValid) {
13
- spinner.error("TypeScript validation failed");
14
- process.exit(1);
10
+ .action(async () => {
11
+ const tsResult = await spinnerify("Validating TypeScript...", "TypeScript validation passed", validateTypeScript);
12
+ if (isErrored(tsResult)) {
13
+ switch (tsResult.error.code) {
14
+ case "VALIDATE_TYPE_SCRIPT_ERROR":
15
+ tsResult.error.errors.forEach(printTsError);
16
+ process.stderr.write("TypeScript validation failed");
17
+ process.exit(1);
18
+ case "FILE_SYSTEM_ERROR":
19
+ process.stderr.write(`TypeScript validation failed due to a file system error: ${tsResult.error.error}`);
20
+ process.exit(1);
15
21
  }
16
- spinner.success("TypeScript validation passed");
17
- }
18
- catch (error) {
19
- spinner.error(`TypeScript validation failed: ${error}`);
20
- process.exit(1);
21
22
  }
22
- spinner.start("Building JavaScript...");
23
- try {
24
- const jsBuilt = await buildJavaScript();
25
- if (!jsBuilt) {
26
- spinner.error("JavaScript build failed");
23
+ const jsResult = await spinnerify("Building JavaScript...", "JavaScript build completed successfully", buildJavaScript);
24
+ if (isErrored(jsResult)) {
25
+ if (jsResult.error.code === "BUILD_JAVASCRIPT_ERROR") {
26
+ const { errors, warnings } = jsResult.error;
27
+ errors.forEach((error) => printJsError(error, "error"));
28
+ warnings.forEach((warning) => printJsError(warning, "warning"));
29
+ process.stderr.write("JavaScript build failed");
30
+ process.exit(1);
31
+ }
32
+ else {
33
+ printBuildContextError(jsResult.error);
34
+ process.stderr.write("Build context error");
27
35
  process.exit(1);
28
36
  }
29
- spinner.success("Build completed successfully");
30
- process.exit(0);
31
- }
32
- catch (error) {
33
- spinner.error(`JavaScript build failed: ${error}`);
34
- process.exit(1);
35
37
  }
36
38
  });