attio 0.0.1-experimental.20250403 → 0.0.1-experimental.20250408

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 (55) 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/{api → auth}/auth.js +5 -3
  5. package/lib/{api → auth}/keychain.js +1 -1
  6. package/lib/build/client/generate-client-entry.js +14 -4
  7. package/lib/build/server/generate-server-entry.js +24 -15
  8. package/lib/commands/build/build-javascript.js +13 -17
  9. package/lib/commands/build/validate-typescript.js +7 -14
  10. package/lib/commands/build.js +29 -12
  11. package/lib/commands/dev/boot.js +38 -11
  12. package/lib/commands/dev/bundle-javascript.js +29 -28
  13. package/lib/commands/dev/graphql-code-gen.js +1 -1
  14. package/lib/commands/dev/onboarding.js +19 -5
  15. package/lib/commands/dev/prepare-build-contexts.js +163 -32
  16. package/lib/{api → commands/dev}/start-graphql-server.js +2 -2
  17. package/lib/commands/dev/upload.js +34 -6
  18. package/lib/commands/dev/validate-typescript.js +3 -13
  19. package/lib/commands/dev.js +49 -3
  20. package/lib/commands/init/create-project.js +45 -18
  21. package/lib/commands/init.js +28 -4
  22. package/lib/commands/login.js +1 -1
  23. package/lib/commands/logout.js +1 -1
  24. package/lib/commands/version/create/bundle-javascript.js +28 -0
  25. package/lib/commands/version/create.js +84 -34
  26. package/lib/commands/version/list.js +23 -6
  27. package/lib/commands/whoami.js +12 -4
  28. package/lib/spinners/determine-workspace.spinner.js +58 -0
  29. package/lib/{api → spinners}/get-app-info.spinner.js +2 -2
  30. package/lib/spinners/get-app-slug-from-package-json.js +46 -0
  31. package/lib/{api → spinners}/get-versions.spinner.js +2 -4
  32. package/lib/util/copy-with-replace.js +40 -18
  33. package/lib/util/create-directory.js +21 -14
  34. package/lib/util/find-available-port.js +1 -1
  35. package/lib/util/load-attio-cli-version.js +86 -0
  36. package/lib/util/spinner.js +4 -1
  37. package/lib/util/upload-bundle.js +3 -2
  38. package/package.json +3 -1
  39. package/lib/api/api-fetch.js +0 -32
  40. package/lib/api/complete-bundle-upload.js +0 -8
  41. package/lib/api/complete-prod-bundle-upload.js +0 -8
  42. package/lib/api/create-dev-version.js +0 -17
  43. package/lib/api/create-version.js +0 -19
  44. package/lib/api/determine-workspace.spinner.js +0 -32
  45. package/lib/api/fetch-app-info.js +0 -20
  46. package/lib/api/fetch-installation.js +0 -9
  47. package/lib/api/fetch-versions.js +0 -17
  48. package/lib/api/fetch-workspaces.js +0 -25
  49. package/lib/api/get-app-slug-from-package-json.js +0 -27
  50. package/lib/api/start-upload.js +0 -11
  51. package/lib/api/whoami.js +0 -15
  52. package/lib/commands/dev/build-javascript.js +0 -47
  53. package/lib/util/load-attio-cli-package-json.js +0 -27
  54. /package/lib/{api → auth}/ensure-authed.js +0 -0
  55. /package/lib/{api → util}/hard-exit.js +0 -0
@@ -1,22 +1,36 @@
1
1
  import { listenForKey } from "../../util/listen-for-key.js";
2
2
  import open from "open";
3
- import { fetchInstallation } from "../../api/fetch-installation.js";
4
3
  import { APP } from "../../env.js";
4
+ import { API } from "../../api/api.js";
5
+ import { isErrored } from "@attio/fetchable";
5
6
  function prompt() {
6
- process.stdout.write(`\n\n🚨 IMPORTANT: You will need to install your app in your workspace. Press "i" to open the app settings page, and then click "Install".\n\n`);
7
+ process.stdout.write(`🚨 IMPORTANT: You will need to install your app in your workspace. Press "i" to open the app settings page, and then click "Install".\n\n`);
7
8
  }
8
9
  export function onboarding({ appId, appSlug, workspace, }) {
9
- const haveInstallation = async () => (await fetchInstallation({ appId, workspaceId: workspace.workspace_id })) !== null;
10
10
  const cleanup = listenForKey("i", () => {
11
11
  open(`${APP}/${workspace.slug}/settings/apps/${appSlug}`);
12
12
  });
13
13
  const poll = async () => {
14
- let installation = await haveInstallation();
14
+ const installationResult = await API.fetchInstallation({
15
+ appId,
16
+ workspaceId: workspace.workspace_id,
17
+ });
18
+ if (isErrored(installationResult)) {
19
+ return;
20
+ }
21
+ let installation = installationResult.value;
15
22
  if (!installation) {
16
23
  prompt();
17
24
  while (!installation) {
18
25
  await new Promise((resolve) => setTimeout(resolve, 60_000));
19
- installation = await haveInstallation();
26
+ const installationResult = await API.fetchInstallation({
27
+ appId,
28
+ workspaceId: workspace.workspace_id,
29
+ });
30
+ if (isErrored(installationResult)) {
31
+ return;
32
+ }
33
+ installation = installationResult.value;
20
34
  if (!installation) {
21
35
  prompt();
22
36
  }
@@ -3,73 +3,204 @@ import tmp from "tmp-promise";
3
3
  import fs from "fs/promises";
4
4
  import path from "path";
5
5
  import { createClientBuildConfig } from "../../build/client/create-client-build-config.js";
6
- import { generateClientEntry } from "../../build/client/generate-client-entry.js";
6
+ import { generateClientEntry, } from "../../build/client/generate-client-entry.js";
7
7
  import { createServerBuildConfig } from "../../build/server/create-server-build-config.js";
8
8
  import { generateServerEntry } from "../../build/server/generate-server-entry.js";
9
+ import { errored, isErrored, complete, combineAsync } from "@attio/fetchable";
10
+ import chalk from "chalk";
11
+ import { errorsAndWarningsSchema } from "../../build.js";
9
12
  export async function prepareBuildContexts(mode) {
10
13
  const srcDir = "src";
11
14
  const assetsDir = path.join(srcDir, "assets");
12
15
  const webhooksDir = path.join(srcDir, "webhooks");
13
16
  const eventsDir = path.join(srcDir, "events");
14
- return Promise.all([
15
- tmp.file({ postfix: ".js" }).then(async (tempFile) => {
17
+ return await combineAsync([
18
+ tmp
19
+ .file({ postfix: ".js" })
20
+ .then(async (tempFile) => {
16
21
  let lastJS;
17
22
  const updateTempFile = async () => {
18
- const js = await generateClientEntry({
23
+ const jsResult = await generateClientEntry({
19
24
  srcDirAbsolute: path.resolve(srcDir),
20
25
  assetsDirAbsolute: path.resolve(assetsDir),
21
26
  });
27
+ if (isErrored(jsResult)) {
28
+ return jsResult;
29
+ }
30
+ const js = jsResult.value;
22
31
  if (js === lastJS)
23
- return;
32
+ return complete(undefined);
24
33
  lastJS = js;
25
- await fs.writeFile(tempFile.path, js);
34
+ try {
35
+ await fs.writeFile(tempFile.path, js);
36
+ return complete(undefined);
37
+ }
38
+ catch (error) {
39
+ return errored({
40
+ code: "FAILED_TO_CREATE_TEMP_FILE",
41
+ path: tempFile.path,
42
+ error,
43
+ });
44
+ }
26
45
  };
27
- const esbuildContext = await esbuild.context({
28
- ...createClientBuildConfig({
29
- entryPoint: tempFile.path,
30
- srcDir,
31
- }),
32
- write: mode === "write-to-disk",
33
- outfile: path.resolve("dist", "index.js"),
34
- loader: { ".png": "dataurl", ".graphql": "text", ".gql": "text" },
35
- });
36
- return {
46
+ let esbuildContext;
47
+ const outfile = path.resolve("dist", "index.js");
48
+ try {
49
+ esbuildContext = await esbuild.context({
50
+ ...createClientBuildConfig({
51
+ entryPoint: tempFile.path,
52
+ srcDir,
53
+ }),
54
+ write: mode === "write-to-disk",
55
+ outfile,
56
+ loader: { ".png": "dataurl", ".graphql": "text", ".gql": "text" },
57
+ });
58
+ }
59
+ catch (error) {
60
+ return errored({
61
+ code: "FAILED_TO_CREATE_ESBUILD_CONTEXT",
62
+ outfile,
63
+ error,
64
+ });
65
+ }
66
+ return complete({
37
67
  rebuild: async () => {
38
68
  await updateTempFile();
39
- return esbuildContext.rebuild();
69
+ try {
70
+ return complete(await esbuildContext.rebuild());
71
+ }
72
+ catch (error) {
73
+ const parseResult = errorsAndWarningsSchema.safeParse(error);
74
+ if (!parseResult.success) {
75
+ return errored({
76
+ code: "UNPARSABLE_BUILD_ERROR",
77
+ error,
78
+ });
79
+ }
80
+ return errored({
81
+ code: "BUILD_JAVASCRIPT_ERROR",
82
+ errors: parseResult.data.errors ?? [],
83
+ warnings: parseResult.data.warnings ?? [],
84
+ });
85
+ }
40
86
  },
41
87
  dispose: async () => {
42
- await Promise.all([esbuildContext.dispose(), tempFile.cleanup()]);
88
+ try {
89
+ await Promise.all([esbuildContext.dispose(), tempFile.cleanup()]);
90
+ }
91
+ catch (error) {
92
+ return errored({
93
+ code: "FAILED_TO_DISPOSE_OF_BUILD_CONTEXT",
94
+ error,
95
+ });
96
+ }
97
+ return complete(undefined);
43
98
  },
44
- };
99
+ });
45
100
  }),
46
- tmp.file({ postfix: ".js" }).then(async (tempFile) => {
101
+ tmp
102
+ .file({ postfix: ".js" })
103
+ .then(async (tempFile) => {
47
104
  let lastJS;
48
105
  const updateTempFile = async () => {
49
- const js = await generateServerEntry({
106
+ const jsResult = await generateServerEntry({
50
107
  srcDirAbsolute: path.resolve(srcDir),
51
108
  webhooksDirAbsolute: path.resolve(webhooksDir),
52
109
  eventDirAbsolute: path.resolve(eventsDir),
53
110
  });
111
+ if (isErrored(jsResult)) {
112
+ return jsResult;
113
+ }
114
+ const js = jsResult.value;
54
115
  if (js === lastJS)
55
- return;
116
+ return complete(undefined);
56
117
  lastJS = js;
57
- await fs.writeFile(tempFile.path, js);
118
+ try {
119
+ await fs.writeFile(tempFile.path, js);
120
+ return complete(undefined);
121
+ }
122
+ catch (error) {
123
+ return errored({
124
+ code: "FAILED_TO_CREATE_TEMP_FILE",
125
+ path: tempFile.path,
126
+ error,
127
+ });
128
+ }
58
129
  };
59
- const esbuildContext = await esbuild.context({
60
- ...createServerBuildConfig(tempFile.path),
61
- write: mode === "write-to-disk",
62
- outfile: path.resolve("dist", "server.js"),
63
- });
64
- return {
130
+ let esbuildContext;
131
+ const outfile = path.resolve("dist", "server.js");
132
+ try {
133
+ esbuildContext = await esbuild.context({
134
+ ...createServerBuildConfig(tempFile.path),
135
+ write: mode === "write-to-disk",
136
+ outfile,
137
+ });
138
+ }
139
+ catch (error) {
140
+ return errored({
141
+ code: "FAILED_TO_CREATE_ESBUILD_CONTEXT",
142
+ outfile,
143
+ error,
144
+ });
145
+ }
146
+ return complete({
65
147
  rebuild: async () => {
66
148
  await updateTempFile();
67
- return esbuildContext.rebuild();
149
+ try {
150
+ return complete(await esbuildContext.rebuild());
151
+ }
152
+ catch (error) {
153
+ const parseResult = errorsAndWarningsSchema.safeParse(error);
154
+ if (!parseResult.success) {
155
+ return errored({
156
+ code: "UNPARSABLE_BUILD_ERROR",
157
+ error,
158
+ });
159
+ }
160
+ return errored({
161
+ code: "BUILD_JAVASCRIPT_ERROR",
162
+ errors: parseResult.data.errors ?? [],
163
+ warnings: parseResult.data.warnings ?? [],
164
+ });
165
+ }
68
166
  },
69
167
  dispose: async () => {
70
- await Promise.all([esbuildContext.dispose(), tempFile.cleanup()]);
168
+ try {
169
+ await Promise.all([esbuildContext.dispose(), tempFile.cleanup()]);
170
+ }
171
+ catch (error) {
172
+ return errored({
173
+ code: "FAILED_TO_DISPOSE_OF_BUILD_CONTEXT",
174
+ error,
175
+ });
176
+ }
177
+ return complete(undefined);
71
178
  },
72
- };
179
+ });
73
180
  }),
74
181
  ]);
75
182
  }
183
+ export function printBuildContextError(error) {
184
+ switch (error.code) {
185
+ case "FAILED_TO_CREATE_TEMP_FILE":
186
+ process.stderr.write(`${chalk.red("✖ ")}Failed to create temp file: ${error.path}\n`);
187
+ break;
188
+ case "FAILED_TO_GENERATE_CLIENT_ENTRY":
189
+ process.stderr.write(`${chalk.red("✖ ")}Failed to generate client entry\n`);
190
+ break;
191
+ case "FAILED_TO_DISPOSE_OF_BUILD_CONTEXT":
192
+ process.stderr.write(`${chalk.red("✖ ")}Failed to dispose of build context: ${error.error}\n`);
193
+ break;
194
+ case "FAILED_TO_CREATE_ESBUILD_CONTEXT":
195
+ process.stderr.write(`${chalk.red("✖ ")}Failed to create esbuild context (${error.outfile}): ${error.error}\n`);
196
+ break;
197
+ case "ERROR_FINDING_SURFACE_EXPORTS":
198
+ process.stderr.write(`${chalk.red("✖ ")}Failed to find surface exports: ${error.error}\n`);
199
+ break;
200
+ case "UNPARSABLE_BUILD_ERROR":
201
+ process.stderr.write(`${chalk.red("✖ ")}Failed to parse build error: ${error.error}\n`);
202
+ break;
203
+ default:
204
+ return error;
205
+ }
206
+ }
@@ -5,7 +5,7 @@ import { buildSchema } from "graphql";
5
5
  import { Hono } from "hono";
6
6
  import path, { dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
- import { findAvailablePort } from "../util/find-available-port.js";
8
+ import { findAvailablePort } from "../../util/find-available-port.js";
9
9
  export function startGraphqlServer(sendBack) {
10
10
  let server = null;
11
11
  const startServer = async () => {
@@ -21,7 +21,7 @@ export function startGraphqlServer(sendBack) {
21
21
  };
22
22
  app.use("/graphql", graphqlServer({ schema, rootResolver, graphiql: true }));
23
23
  server = serve({ fetch: app.fetch, port });
24
- sendBack({ type: "GraphQL Server Started", port });
24
+ sendBack({ code: "GraphQL Server Started", port });
25
25
  };
26
26
  startServer();
27
27
  return () => {
@@ -1,27 +1,55 @@
1
1
  import notifier from "node-notifier";
2
- import { startUpload } from "../../api/start-upload.js";
3
- import { completeBundleUpload } from "../../api/complete-bundle-upload.js";
4
2
  import { spinnerify } from "../../util/spinner.js";
5
3
  import { uploadBundle } from "../../util/upload-bundle.js";
4
+ import { API } from "../../api/api.js";
5
+ import { isErrored, combineAsync, complete } from "@attio/fetchable";
6
+ import { printFetcherError } from "../../api/fetcher.js";
7
+ import chalk from "chalk";
6
8
  export async function upload({ contents, devVersionId, appId, }) {
7
- await spinnerify("Uploading...", () => `Upload complete at ${new Date().toLocaleTimeString()}`, async () => {
8
- const { client_bundle_upload_url, server_bundle_upload_url, app_dev_version_bundle_id: bundleId, } = await startUpload({
9
+ return await spinnerify("Uploading...", () => `Upload complete at ${new Date().toLocaleTimeString()}`, async () => {
10
+ const startUploadResult = await API.startUpload({
9
11
  appId,
10
12
  devVersionId,
11
13
  });
14
+ if (isErrored(startUploadResult)) {
15
+ return startUploadResult;
16
+ }
17
+ const { client_bundle_upload_url, server_bundle_upload_url, app_dev_version_bundle_id: bundleId, } = startUploadResult.value;
12
18
  const [clientBundle, serverBundle] = contents;
13
- await Promise.all([
19
+ const uploadResults = await combineAsync([
14
20
  uploadBundle(clientBundle, client_bundle_upload_url),
15
21
  uploadBundle(serverBundle, server_bundle_upload_url),
16
22
  ]);
17
- await completeBundleUpload({
23
+ if (isErrored(uploadResults)) {
24
+ return uploadResults;
25
+ }
26
+ const completeBundleUploadResult = await API.completeBundleUpload({
18
27
  appId,
19
28
  devVersionId,
20
29
  bundleId,
21
30
  });
31
+ if (isErrored(completeBundleUploadResult)) {
32
+ return completeBundleUploadResult;
33
+ }
22
34
  notifier.notify({
23
35
  title: "Upload Complete",
24
36
  message: "New bundle uploaded to Attio",
25
37
  });
38
+ return complete(undefined);
26
39
  });
27
40
  }
41
+ export function printUploadError(error) {
42
+ switch (error.code) {
43
+ case "BUNDLE_UPLOAD_ERROR":
44
+ process.stderr.write(chalk.red(`Error uploading bundle: ${error.error}\n`));
45
+ break;
46
+ case "START_UPLOAD_ERROR":
47
+ printFetcherError("Error starting upload", error.fetcherError);
48
+ break;
49
+ case "COMPLETE_BUNDLE_UPLOAD_ERROR":
50
+ printFetcherError("Error completing bundle upload", error.fetcherError);
51
+ break;
52
+ default:
53
+ return error;
54
+ }
55
+ }
@@ -1,15 +1,7 @@
1
1
  import chokidar from "chokidar";
2
- import notifier from "node-notifier";
3
2
  import { getDiagnostics, readConfig, typeScriptErrorSchema, } from "../../util/typescript.js";
4
- import { printTsError } from "../../util/typescript.js";
5
3
  import path from "path";
6
- const notify = (errors) => {
7
- notifier.notify({
8
- title: `TypeScript Error${errors.length === 1 ? "" : "s"}`,
9
- message: `There ${errors.length === 1 ? "was one error" : `were ${errors.length} errors`} in your TypeScript code`,
10
- });
11
- };
12
- export function validateTypeScript(onSuccess) {
4
+ export function validateTypeScript(onSuccess, onError) {
13
5
  let isShuttingDown = false;
14
6
  const watcher = chokidar.watch(["src/**/*.ts", "src/**/*.tsx"], {
15
7
  ignored: [
@@ -36,8 +28,7 @@ export function validateTypeScript(onSuccess) {
36
28
  }
37
29
  const errors = await getDiagnostics(program);
38
30
  if (errors.length) {
39
- errors.forEach(printTsError);
40
- notify(errors);
31
+ onError?.(errors);
41
32
  }
42
33
  else {
43
34
  onSuccess?.();
@@ -52,8 +43,7 @@ export function validateTypeScript(onSuccess) {
52
43
  }
53
44
  if (error instanceof Error) {
54
45
  const tsError = typeScriptErrorSchema.parse({ text: error.message });
55
- printTsError(tsError);
56
- notify([tsError]);
46
+ onError?.([tsError]);
57
47
  }
58
48
  }
59
49
  }
@@ -8,7 +8,24 @@ import { bundleJavaScript } from "./dev/bundle-javascript.js";
8
8
  import { boot } from "./dev/boot.js";
9
9
  import { onboarding } from "./dev/onboarding.js";
10
10
  import { graphqlServer } from "./dev/graphql-server.js";
11
- import { upload } from "./dev/upload.js";
11
+ import { printUploadError, upload } from "./dev/upload.js";
12
+ import { isErrored } from "@attio/fetchable";
13
+ import { printJsError, printTsError } from "../util/typescript.js";
14
+ import { printBuildContextError, } from "./dev/prepare-build-contexts.js";
15
+ import notifier from "node-notifier";
16
+ const notifyTsErrors = (errors) => {
17
+ notifier.notify({
18
+ title: `TypeScript Error${errors.length === 1 ? "" : "s"}`,
19
+ message: `There ${errors.length === 1 ? "was one error" : `were ${errors.length} errors`} in your TypeScript code`,
20
+ });
21
+ };
22
+ const notifyJsErrors = (errors) => {
23
+ const totalErrors = (errors.errors?.length || 0) + (errors.warnings?.length || 0);
24
+ notifier.notify({
25
+ title: `JavaScript ${totalErrors === 1 ? "Error" : "Errors"}`,
26
+ message: `There ${totalErrors === 1 ? "was one error" : `were ${totalErrors} errors`} in your JavaScript code`,
27
+ });
28
+ };
12
29
  export const optionsSchema = z.object({
13
30
  workspace: z.string().optional(),
14
31
  });
@@ -46,12 +63,41 @@ export const dev = new Command("dev")
46
63
  cleanupFunctions.push(cleanupGraphqlServer);
47
64
  const cleanupOnboardingDaemon = onboarding({ appId, appSlug, workspace });
48
65
  cleanupFunctions.push(cleanupOnboardingDaemon);
49
- const [cleanupTs, triggerTs] = validateTypeScript();
66
+ let haveTsErrors = false;
67
+ const [cleanupTs, triggerTs] = validateTypeScript(() => {
68
+ if (haveTsErrors) {
69
+ process.stdout.write(`${chalk.green("✓")} TypeScript errors fixed\n`);
70
+ haveTsErrors = false;
71
+ }
72
+ }, (errors) => {
73
+ haveTsErrors = true;
74
+ errors.forEach(printTsError);
75
+ notifyTsErrors(errors);
76
+ });
50
77
  cleanupFunctions.push(cleanupTs);
51
78
  const cleanupGraphqlCodeGen = graphqlCodeGen(triggerTs);
52
79
  cleanupFunctions.push(cleanupGraphqlCodeGen);
80
+ let haveJsErrors = false;
53
81
  const cleanupJs = bundleJavaScript(async (contents) => {
54
- await upload({ contents, devVersionId, appId });
82
+ if (haveJsErrors) {
83
+ process.stdout.write(`${chalk.green("✓")} JavaScript errors fixed\n`);
84
+ haveJsErrors = false;
85
+ }
86
+ const uploadResult = await upload({ contents, devVersionId, appId });
87
+ if (isErrored(uploadResult)) {
88
+ printUploadError(uploadResult.error);
89
+ }
90
+ }, async (error) => {
91
+ haveJsErrors = true;
92
+ if (error.code === "BUILD_JAVASCRIPT_ERROR") {
93
+ notifyJsErrors(error);
94
+ const { errors, warnings } = error;
95
+ errors.forEach((error) => printJsError(error, "error"));
96
+ warnings.forEach((warning) => printJsError(warning, "warning"));
97
+ }
98
+ else {
99
+ printBuildContextError(error);
100
+ }
55
101
  });
56
102
  cleanupFunctions.push(cleanupJs);
57
103
  printMessage("\n👀 Watching for changes...");
@@ -1,41 +1,68 @@
1
1
  import { fileURLToPath } from "url";
2
2
  import path from "path";
3
3
  import chalk from "chalk";
4
- import boxen from "boxen";
5
4
  import { existsSync } from "fs";
6
5
  import { createDirectory } from "../../util/create-directory.js";
7
6
  import { canWrite } from "../../util/can-write.js";
8
7
  import { copyWithTransform } from "../../util/copy-with-replace.js";
9
- import { printMessage } from "../../util/print-message.js";
10
8
  import { spinnerify } from "../../util/spinner.js";
11
- import { hardExit } from "../../api/hard-exit.js";
9
+ import { combineAsync, complete, errored, isErrored } from "@attio/fetchable";
12
10
  export async function createProject({ appSlug, language, appInfo, }) {
13
- await spinnerify("Creating project...", "Project created", async () => {
14
- if (existsSync(path.join(process.cwd(), appSlug))) {
15
- hardExit(`Directory "${appSlug}" already exists`);
11
+ return await spinnerify("Creating project...", "Project created", async () => {
12
+ const cwd = process.cwd();
13
+ const projectPath = path.join(cwd, appSlug);
14
+ if (existsSync(projectPath)) {
15
+ return errored({ code: "DIRECTORY_ALREADY_EXISTS", path: projectPath });
16
16
  }
17
- if (!canWrite(process.cwd())) {
18
- hardExit("Write access denied to current directory");
17
+ if (!canWrite(cwd)) {
18
+ return errored({ code: "WRITE_ACCESS_DENIED", directory: cwd });
19
19
  }
20
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
- const projectDir = createDirectory(appSlug);
21
+ const projectDirResult = await createDirectory(appSlug);
22
+ if (isErrored(projectDirResult)) {
23
+ return projectDirResult;
24
+ }
25
+ const projectDir = projectDirResult.value;
22
26
  const templatesDir = path.resolve(__dirname, "../../templates", language);
23
27
  const commonDir = path.resolve(__dirname, "../../templates", "common");
24
28
  const transform = (contents) => contents
25
29
  .replaceAll("title-to-be-replaced", appInfo.title)
26
30
  .replaceAll("id-to-be-replaced", appInfo.app_id)
27
31
  .replaceAll("slug-to-be-replaced", appSlug);
28
- await Promise.all([
32
+ const results = await combineAsync([
29
33
  copyWithTransform(templatesDir, projectDir, transform),
30
34
  copyWithTransform(commonDir, projectDir, transform),
31
35
  ]);
32
- printMessage("\n" + chalk.green(`SUCCESS!! 🎉 Your app directory has been created.`));
33
- printMessage("\nTo get started, run:\n");
34
- printMessage(boxen(`cd ${appSlug}\nnpm install\nnpm run dev`, {
35
- padding: 1,
36
- margin: 1,
37
- borderStyle: "round",
38
- }) + "\n");
39
- printMessage(`(${chalk.yellow("yarn")}, ${chalk.yellow("pnpm")}, and ${chalk.yellow("bun")} also work!)\n`);
36
+ if (isErrored(results)) {
37
+ return results;
38
+ }
39
+ return complete(undefined);
40
40
  });
41
41
  }
42
+ export function printCreateProjectError(error) {
43
+ switch (error.code) {
44
+ case "DIRECTORY_ALREADY_EXISTS":
45
+ process.stderr.write(chalk.red(`Directory ${error.path} already exists`));
46
+ break;
47
+ case "WRITE_ACCESS_DENIED":
48
+ process.stderr.write(chalk.red(`Write access denied to ${error.directory}`));
49
+ break;
50
+ case "FAILED_TO_CREATE_DIRECTORY":
51
+ process.stderr.write(chalk.red(`Failed to create directory ${error.directory}`));
52
+ break;
53
+ case "FAILED_TO_COPY_FILE":
54
+ process.stderr.write(chalk.red(`Failed to copy file ${error.src} to ${error.dest}`));
55
+ break;
56
+ case "FAILED_TO_LIST_FILES":
57
+ process.stderr.write(chalk.red(`Failed to list files in ${error.directory}`));
58
+ break;
59
+ case "FAILED_TO_READ_FILE":
60
+ process.stderr.write(chalk.red(`Failed to read file ${error.path}`));
61
+ break;
62
+ case "FAILED_TO_WRITE_FILE":
63
+ process.stderr.write(chalk.red(`Failed to write file ${error.path}`));
64
+ break;
65
+ default:
66
+ return error;
67
+ }
68
+ }
@@ -1,10 +1,13 @@
1
1
  import { Argument, Command, Option } from "commander";
2
2
  import { z } from "zod";
3
3
  import chalk from "chalk";
4
- import { createProject } from "./init/create-project.js";
4
+ import { createProject, printCreateProjectError } from "./init/create-project.js";
5
5
  import { askLanguage } from "./init/ask-language.js";
6
6
  import { printLogo } from "../util/print-logo.js";
7
- import { getAppInfo } from "../api/get-app-info.spinner.js";
7
+ import { getAppInfo } from "../spinners/get-app-info.spinner.js";
8
+ import { printFetcherError } from "../api/fetcher.js";
9
+ import { isErrored } from "@attio/fetchable";
10
+ import boxen from "boxen";
8
11
  export const argsSchema = z.string();
9
12
  export const optionsSchema = z.object({
10
13
  language: z.enum(["javascript", "typescript"]).optional(),
@@ -18,13 +21,34 @@ export const init = new Command("init")
18
21
  printLogo();
19
22
  const appSlug = argsSchema.parse(unparsedArgs);
20
23
  const { language: cliLanguage } = optionsSchema.parse(unparsedOptions);
21
- const appInfo = await getAppInfo(appSlug);
24
+ const appInfoResult = await getAppInfo(appSlug);
25
+ if (isErrored(appInfoResult)) {
26
+ printFetcherError("Failed to fetch app info", appInfoResult.error.fetcherError);
27
+ process.exit(1);
28
+ }
29
+ const appInfo = appInfoResult.value;
22
30
  const language = cliLanguage ?? (await askLanguage());
23
- await createProject({
31
+ const result = await createProject({
24
32
  appSlug,
25
33
  language,
26
34
  appInfo,
27
35
  });
36
+ if (isErrored(result)) {
37
+ printCreateProjectError(result.error);
38
+ process.exit(1);
39
+ }
40
+ process.stdout.write(`${chalk.green(`SUCCESS!! 🎉 Your app directory has been created.`)}
41
+
42
+ To get started, run:
43
+
44
+ ${boxen(`cd ${appSlug}\nnpm install\nnpm run dev`, {
45
+ padding: 1,
46
+ margin: 1,
47
+ borderStyle: "round",
48
+ })}
49
+
50
+ (${chalk.yellow("yarn")}, ${chalk.yellow("pnpm")}, and ${chalk.yellow("bun")} also work!)
51
+ `);
28
52
  process.exit(0);
29
53
  }
30
54
  catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { auth } from "../api/auth.js";
2
+ import { auth } from "../auth/auth.js";
3
3
  export const login = new Command("login")
4
4
  .description("Authenticate with Attio")
5
5
  .action(async () => {
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { deleteAuthToken } from "../api/keychain.js";
2
+ import { deleteAuthToken } from "../auth/keychain.js";
3
3
  export const logout = new Command("logout").description("Log out from Attio").action(async () => {
4
4
  await deleteAuthToken();
5
5
  process.stdout.write("🔒 Successfully logged out.\n");
@@ -0,0 +1,28 @@
1
+ import { complete, isErrored, errored, combineAsync } from "@attio/fetchable";
2
+ import { prepareBuildContexts, } from "../../dev/prepare-build-contexts.js";
3
+ export async function bundleJavaScript() {
4
+ const buildContextsResult = await prepareBuildContexts("in-memory");
5
+ if (isErrored(buildContextsResult)) {
6
+ return errored({
7
+ code: "ERROR_PREPARING_BUILD_CONTEXT",
8
+ buildContextError: buildContextsResult.error,
9
+ });
10
+ }
11
+ const buildContexts = buildContextsResult.value;
12
+ const results = await combineAsync(buildContexts.map(async (context) => await context.rebuild()));
13
+ if (isErrored(results)) {
14
+ return errored({
15
+ code: "ERROR_BUILDING_BUNDLE",
16
+ error: results.error,
17
+ });
18
+ }
19
+ const bundles = results.value.map((result) => result.outputFiles[0].text);
20
+ const disposeResults = await combineAsync(buildContexts.map(async (context) => await context.dispose()));
21
+ if (isErrored(disposeResults)) {
22
+ return errored({
23
+ code: "ERROR_DISPOSING_BUILD_CONTEXT",
24
+ buildContextError: disposeResults.error,
25
+ });
26
+ }
27
+ return complete(bundles);
28
+ }