attio 0.0.1-experimental.20250403 → 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 (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 +2 -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,51 +1,101 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { spinnerify } from "../../util/spinner.js";
4
- import { buildJavaScript } from "../dev/build-javascript.js";
5
- import { createVersion } from "../../api/create-version.js";
6
- import { completeProdBundleUpload } from "../../api/complete-prod-bundle-upload.js";
7
- import { loadAttioCliPackageJson } from "../../util/load-attio-cli-package-json.js";
4
+ import { loadAttioCliVersion, printCliVersionError } from "../../util/load-attio-cli-version.js";
8
5
  import { uploadBundle } from "../../util/upload-bundle.js";
9
- import { getVersions } from "../../api/get-versions.spinner.js";
10
- import { getAppInfo } from "../../api/get-app-info.spinner.js";
11
- import { getAppSlugFromPackageJson } from "../../api/get-app-slug-from-package-json.js";
6
+ import { getVersions } from "../../spinners/get-versions.spinner.js";
7
+ import { getAppInfo } from "../../spinners/get-app-info.spinner.js";
8
+ import { printFetcherError } from "../../api/fetcher.js";
9
+ import { combineAsync, isErrored } from "@attio/fetchable";
10
+ import { getAppSlugFromPackageJson, printPackageJsonError, } from "../../spinners/get-app-slug-from-package-json.js";
11
+ import { bundleJavaScript } from "./create/bundle-javascript.js";
12
+ import { printBuildContextError } from "../dev/prepare-build-contexts.js";
13
+ import { API } from "../../api/api.js";
14
+ import { printJsError } from "../../util/typescript.js";
12
15
  export const versionCreate = new Command("create")
13
16
  .description("Create a new unpublished version of your Attio app")
14
17
  .action(async () => {
15
- const appSlug = await getAppSlugFromPackageJson();
16
- const appInfo = await getAppInfo(appSlug);
17
- const [clientBundle, serverBundle] = await spinnerify("Bundling JavaScript...", "Bundling complete", async () => {
18
- return await new Promise((resolve) => {
19
- const cleanup = buildJavaScript((bundles) => {
20
- cleanup();
21
- resolve(bundles);
22
- });
23
- });
24
- });
25
- const versions = await getVersions(appInfo);
26
- const version = await spinnerify("Uploading...", "Upload complete", async () => {
27
- const packageJson = loadAttioCliPackageJson();
28
- const version = await createVersion({
18
+ const appSlugResult = await getAppSlugFromPackageJson();
19
+ if (isErrored(appSlugResult)) {
20
+ printPackageJsonError(appSlugResult.error);
21
+ process.exit(1);
22
+ }
23
+ const appSlug = appSlugResult.value;
24
+ const appInfoResult = await getAppInfo(appSlug);
25
+ if (isErrored(appInfoResult)) {
26
+ printFetcherError("Error loading app info", appInfoResult.error.fetcherError);
27
+ process.exit(1);
28
+ }
29
+ const appInfo = appInfoResult.value;
30
+ const bundleResult = await spinnerify("Bundling JavaScript...", "Bundling complete", bundleJavaScript);
31
+ if (isErrored(bundleResult)) {
32
+ if (bundleResult.error.code === "ERROR_BUILDING_BUNDLE") {
33
+ const { error } = bundleResult.error;
34
+ if (error.code === "BUILD_JAVASCRIPT_ERROR") {
35
+ const { errors, warnings } = error;
36
+ errors.forEach((error) => printJsError(error, "error"));
37
+ warnings.forEach((warning) => printJsError(warning, "warning"));
38
+ }
39
+ else {
40
+ printBuildContextError(error);
41
+ }
42
+ }
43
+ else {
44
+ printBuildContextError(bundleResult.error.buildContextError);
45
+ }
46
+ process.exit(1);
47
+ }
48
+ const [clientBundle, serverBundle] = bundleResult.value;
49
+ const versionsResult = await getVersions(appInfo);
50
+ if (isErrored(versionsResult)) {
51
+ printFetcherError("Error fetching versions", versionsResult.error.fetcherError);
52
+ process.exit(1);
53
+ }
54
+ const versions = versionsResult.value;
55
+ const versionResult = await spinnerify("Uploading...", "Upload complete", async () => {
56
+ const cliVersionResult = loadAttioCliVersion();
57
+ if (isErrored(cliVersionResult)) {
58
+ printCliVersionError(cliVersionResult);
59
+ process.exit(1);
60
+ }
61
+ const cliVersion = cliVersionResult.value;
62
+ const versionResult = await API.createVersion({
29
63
  appId: appInfo.app_id,
30
64
  major: versions.length === 0
31
65
  ? 1
32
66
  : Math.max(...versions.map((version) => version.major), 1),
33
- cliVersion: packageJson.version,
67
+ cliVersion,
34
68
  });
35
- await Promise.all([
36
- uploadBundle(clientBundle, version.client_bundle_upload_url),
37
- uploadBundle(serverBundle, version.server_bundle_upload_url),
69
+ if (isErrored(versionResult)) {
70
+ printFetcherError("Error creating version", versionResult.error.fetcherError);
71
+ process.exit(1);
72
+ }
73
+ const { client_bundle_upload_url, server_bundle_upload_url } = versionResult.value;
74
+ const uploadResult = await combineAsync([
75
+ uploadBundle(clientBundle, client_bundle_upload_url),
76
+ uploadBundle(serverBundle, server_bundle_upload_url),
38
77
  ]);
39
- return version;
40
- });
41
- await spinnerify("Signing bundles...", "Bundles signed", async () => {
42
- await completeProdBundleUpload({
43
- appId: appInfo.app_id,
44
- major: version.major,
45
- minor: version.minor,
46
- bundleId: version.app_prod_version_bundle_id,
47
- });
78
+ if (isErrored(uploadResult)) {
79
+ process.stderr.write(`${chalk.red("✖ ")}Failed to upload bundle to: ${uploadResult.error.uploadUrl}\n`);
80
+ process.exit(1);
81
+ }
82
+ return versionResult;
48
83
  });
84
+ if (isErrored(versionResult)) {
85
+ process.stderr.write(`${chalk.red("✖ ")}Failed to create version: ${versionResult.error}\n`);
86
+ process.exit(1);
87
+ }
88
+ const version = versionResult.value;
89
+ const signingResult = await spinnerify("Signing bundles...", "Bundles signed", async () => await API.completeProdBundleUpload({
90
+ appId: appInfo.app_id,
91
+ major: version.major,
92
+ minor: version.minor,
93
+ bundleId: version.app_prod_version_bundle_id,
94
+ }));
95
+ if (isErrored(signingResult)) {
96
+ printFetcherError("Error signing bundles", signingResult.error.fetcherError);
97
+ process.exit(1);
98
+ }
49
99
  process.stdout.write(`\nVersion ${chalk.green(`${version.major}.${version.minor}`)} created!\n\n`);
50
100
  process.exit(0);
51
101
  });
@@ -1,17 +1,34 @@
1
1
  import { Command } from "commander";
2
- import { getAppInfo } from "../../api/get-app-info.spinner.js";
3
- import { getAppSlugFromPackageJson } from "../../api/get-app-slug-from-package-json.js";
4
- import { getVersions } from "../../api/get-versions.spinner.js";
2
+ import { getAppInfo } from "../../spinners/get-app-info.spinner.js";
3
+ import { getAppSlugFromPackageJson, printPackageJsonError, } from "../../spinners/get-app-slug-from-package-json.js";
4
+ import { getVersions } from "../../spinners/get-versions.spinner.js";
5
5
  import chalk from "chalk";
6
6
  import Table from "cli-table3";
7
7
  import { format as formatDate } from "date-fns";
8
+ import { isErrored } from "@attio/fetchable";
9
+ import { printFetcherError } from "../../api/fetcher.js";
8
10
  export const versionList = new Command()
9
11
  .name("list")
10
12
  .description("List all versions of your app")
11
13
  .action(async () => {
12
- const appSlug = await getAppSlugFromPackageJson();
13
- const appInfo = await getAppInfo(appSlug);
14
- const versions = await getVersions(appInfo);
14
+ const appSlugResult = await getAppSlugFromPackageJson();
15
+ if (isErrored(appSlugResult)) {
16
+ printPackageJsonError(appSlugResult.error);
17
+ process.exit(1);
18
+ }
19
+ const appSlug = appSlugResult.value;
20
+ const appInfoResult = await getAppInfo(appSlug);
21
+ if (isErrored(appInfoResult)) {
22
+ printFetcherError("Error loading app info", appInfoResult.error.fetcherError);
23
+ process.exit(1);
24
+ }
25
+ const appInfo = appInfoResult.value;
26
+ const versionsResult = await getVersions(appInfo);
27
+ if (isErrored(versionsResult)) {
28
+ printFetcherError("Error loading versions", versionsResult.error.fetcherError);
29
+ process.exit(1);
30
+ }
31
+ const versions = versionsResult.value;
15
32
  if (versions.length === 0) {
16
33
  process.stdout.write("No versions found\n");
17
34
  process.exit(0);
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
- import { whoami as whoamiApi } from "../api/whoami.js";
3
- import { loadAuthToken } from "../api/keychain.js";
2
+ import { loadAuthToken } from "../auth/keychain.js";
3
+ import { API } from "../api/api.js";
4
+ import { isErrored } from "@attio/fetchable";
5
+ import { printFetcherError } from "../api/fetcher.js";
4
6
  export const whoami = new Command()
5
7
  .name("whoami")
6
8
  .description("Identify the current user")
@@ -10,7 +12,13 @@ export const whoami = new Command()
10
12
  process.stdout.write("🔒 Not logged in.\n");
11
13
  process.exit(0);
12
14
  }
13
- const user = await whoamiApi();
14
- process.stdout.write(`👤 ${user.name.full} (${user.email_address})\n`);
15
+ const result = await API.whoami();
16
+ if (isErrored(result)) {
17
+ const { fetcherError } = result.error;
18
+ printFetcherError("Error fetching user", fetcherError);
19
+ process.exit(1);
20
+ }
21
+ const { name, email_address } = result.value;
22
+ process.stdout.write(`👤 ${name.full} (${email_address})\n`);
15
23
  process.exit(0);
16
24
  });
@@ -0,0 +1,58 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import { APP } from "../env.js";
3
+ import { spinnerify } from "../util/spinner.js";
4
+ import { API } from "../api/api.js";
5
+ import { isErrored, complete, errored } from "@attio/fetchable";
6
+ import { printFetcherError } from "../api/fetcher.js";
7
+ export async function determineWorkspace(workspaceSlug) {
8
+ const workspacesResult = await spinnerify("Loading workspaces...", "Workspaces loaded", async () => await API.fetchWorkspaces());
9
+ if (isErrored(workspacesResult)) {
10
+ return workspacesResult;
11
+ }
12
+ const workspaces = workspacesResult.value;
13
+ const workspace = workspaces.find((workspace) => workspace.slug === workspaceSlug);
14
+ if (workspace) {
15
+ process.stdout.write(`Using workspace: ${workspace.name}`);
16
+ return complete(workspace);
17
+ }
18
+ if (workspaceSlug) {
19
+ return errored({ code: "NO_WORKSPACE_FOUND", workspaceSlug });
20
+ }
21
+ if (workspaces.length === 0) {
22
+ return errored({ code: "NO_WORKSPACES_FOUND" });
23
+ }
24
+ if (workspaces.length === 1) {
25
+ process.stdout.write(`Using workspace: ${workspaces[0].name}`);
26
+ return complete(workspaces[0]);
27
+ }
28
+ const choice = await select({
29
+ message: "Choose a workspace",
30
+ choices: workspaces.map((workspace) => ({
31
+ name: workspace.name,
32
+ value: workspace,
33
+ })),
34
+ });
35
+ process.stdout.write(`Using workspace: ${choice.name}`);
36
+ return complete(choice);
37
+ }
38
+ export function printDetermineWorkspaceError(error) {
39
+ switch (error.code) {
40
+ case "FETCH_WORKSPACES_ERROR":
41
+ printFetcherError("Error fetching workspaces", error.fetcherError);
42
+ break;
43
+ case "NO_WORKSPACE_FOUND":
44
+ process.stderr.write(`You are not the admin any workspace with the slug "${error.workspaceSlug}". Either request permission from "${error.workspaceSlug}" or create your own.
45
+
46
+ ${APP}/welcome/workspace-details
47
+ `);
48
+ break;
49
+ case "NO_WORKSPACES_FOUND":
50
+ process.stderr.write(`You are not the admin of any workspaces. Either request permission from an existing workspace or create your own.
51
+
52
+ ${APP}/welcome/workspace-details
53
+ `);
54
+ break;
55
+ default:
56
+ return error;
57
+ }
58
+ }
@@ -1,7 +1,7 @@
1
- import { fetchAppInfo } from "./fetch-app-info.js";
1
+ import { API } from "../api/api.js";
2
2
  import { spinnerify } from "../util/spinner.js";
3
3
  export async function getAppInfo(appSlug) {
4
4
  return await spinnerify("Loading app information...", (app) => `App found: ${app.title}`, async () => {
5
- return await fetchAppInfo(appSlug);
5
+ return await API.fetchAppInfo(appSlug);
6
6
  });
7
7
  }
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { z } from "zod";
4
+ import { complete, errored } from "@attio/fetchable";
5
+ import chalk from "chalk";
6
+ const packageJsonSchema = z.object({
7
+ name: z.string({
8
+ required_error: "No name field found in package.json",
9
+ invalid_type_error: `"name" must be a string in package.json`,
10
+ }),
11
+ });
12
+ export async function getAppSlugFromPackageJson() {
13
+ try {
14
+ const packageJsonPath = join(process.cwd(), "package.json");
15
+ const packageJsonRaw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
16
+ const result = packageJsonSchema.safeParse(packageJsonRaw);
17
+ if (!result.success) {
18
+ return errored({ code: "MALFORMED_PACKAGE_JSON", error: result.error.issues[0]?.message });
19
+ }
20
+ return complete(result.data.name);
21
+ }
22
+ catch (error) {
23
+ if (error instanceof SyntaxError) {
24
+ return errored({ code: "INVALID_JSON", error });
25
+ }
26
+ return errored({ code: "FILE_SYSTEM_ERROR", error });
27
+ }
28
+ }
29
+ export function printPackageJsonError({ code, error }) {
30
+ switch (code) {
31
+ case "MALFORMED_PACKAGE_JSON":
32
+ if (error) {
33
+ process.stderr.write(`${chalk.red("✖ ")}Malformed package.json: ${error}\n`);
34
+ }
35
+ else {
36
+ process.stderr.write(`${chalk.red("✖ ")}Malformed package.json\n`);
37
+ }
38
+ break;
39
+ case "FILE_SYSTEM_ERROR":
40
+ process.stderr.write(`${chalk.red("✖ ")}Failed to read package.json\n`);
41
+ break;
42
+ case "INVALID_JSON":
43
+ process.stderr.write(`${chalk.red("✖ ")}Invalid JSON in package.json: ${error}\n`);
44
+ break;
45
+ }
46
+ }
@@ -1,7 +1,5 @@
1
- import { fetchVersions } from "./fetch-versions.js";
2
1
  import { spinnerify } from "../util/spinner.js";
2
+ import { API } from "../api/api.js";
3
3
  export async function getVersions(appInfo) {
4
- return await spinnerify("Loading versions...", "Versions loaded", async () => {
5
- return await fetchVersions(appInfo.app_id);
6
- });
4
+ return await spinnerify("Loading versions...", "Versions loaded", async () => await API.fetchVersions(appInfo.app_id));
7
5
  }
@@ -1,34 +1,56 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
- import { hardExit } from "../api/hard-exit.js";
4
- export const copyWithTransform = async (srcDir, destDir, transform) => {
3
+ import { combineAsync, complete, errored, isErrored } from "@attio/fetchable";
4
+ export async function copyWithTransform(srcDir, destDir, transform) {
5
5
  try {
6
6
  await fs.mkdir(destDir, { recursive: true });
7
7
  }
8
8
  catch {
9
- hardExit(`Failed to create "${destDir}".`);
9
+ return errored({ code: "FAILED_TO_CREATE_DIRECTORY", directory: destDir });
10
10
  }
11
- const entries = await fs.readdir(srcDir, { withFileTypes: true });
12
- await Promise.all(entries.map(async (entry) => {
11
+ let entries;
12
+ try {
13
+ entries = await fs.readdir(srcDir, { withFileTypes: true });
14
+ }
15
+ catch {
16
+ return errored({ code: "FAILED_TO_LIST_FILES", directory: srcDir });
17
+ }
18
+ const results = await combineAsync(entries.map(async (entry) => {
13
19
  const srcPath = path.join(srcDir, entry.name);
14
20
  const destPath = path.join(destDir, entry.name);
15
- try {
16
- if (entry.isDirectory()) {
17
- await copyWithTransform(srcPath, destPath, transform);
18
- }
19
- else if (entry.isFile()) {
20
- if (/\.(jpg|jpeg|png|gif|svg|webp|ico)$/i.test(entry.name)) {
21
+ if (entry.isDirectory()) {
22
+ return await copyWithTransform(srcPath, destPath, transform);
23
+ }
24
+ else if (entry.isFile()) {
25
+ if (/\.(jpg|jpeg|png|gif|svg|webp|ico)$/i.test(entry.name)) {
26
+ try {
21
27
  await fs.copyFile(srcPath, destPath);
22
28
  }
23
- else {
24
- let content = await fs.readFile(srcPath, "utf8");
25
- content = transform(content);
29
+ catch {
30
+ return errored({ code: "FAILED_TO_COPY_FILE", src: srcPath, dest: destPath });
31
+ }
32
+ }
33
+ else {
34
+ let content;
35
+ try {
36
+ content = await fs.readFile(srcPath, "utf8");
37
+ }
38
+ catch {
39
+ return errored({ code: "FAILED_TO_READ_FILE", path: srcPath });
40
+ }
41
+ content = transform(content);
42
+ try {
26
43
  await fs.writeFile(destPath, content, "utf8");
27
44
  }
45
+ catch {
46
+ return errored({ code: "FAILED_TO_WRITE_FILE", path: destPath });
47
+ }
28
48
  }
29
49
  }
30
- catch {
31
- hardExit(`Failed to copy "${srcPath}" to "${destPath}"`);
32
- }
50
+ return complete(undefined);
33
51
  }));
34
- };
52
+ if (isErrored(results)) {
53
+ return results;
54
+ }
55
+ return complete(undefined);
56
+ }
@@ -1,20 +1,27 @@
1
- import { accessSync, constants, existsSync, mkdirSync } from "fs";
1
+ import { constants, promises as fs } from "fs";
2
2
  import { join } from "path";
3
- import { hardExit } from "../api/hard-exit.js";
4
- export const createDirectory = (name) => {
3
+ import { complete, errored } from "@attio/fetchable";
4
+ export const createDirectory = async (name) => {
5
5
  const currentDir = process.cwd();
6
6
  const newPath = join(currentDir, name);
7
- if (existsSync(newPath)) {
8
- hardExit(`The directory '${name}' already exists.`);
9
- }
10
- else {
11
- try {
12
- accessSync(currentDir, constants.W_OK);
13
- mkdirSync(newPath);
14
- return newPath;
15
- }
16
- catch {
17
- hardExit(`Write access to create the directory "${name}" in the current directory is denied.`);
7
+ try {
8
+ if (await fs
9
+ .access(newPath)
10
+ .then(() => true)
11
+ .catch(() => false)) {
12
+ return errored({
13
+ code: "DIRECTORY_ALREADY_EXISTS",
14
+ path: newPath,
15
+ });
18
16
  }
17
+ await fs.access(currentDir, constants.W_OK);
18
+ await fs.mkdir(newPath);
19
+ return complete(newPath);
20
+ }
21
+ catch {
22
+ return errored({
23
+ code: "WRITE_ACCESS_DENIED",
24
+ directory: name,
25
+ });
19
26
  }
20
27
  };
@@ -1,5 +1,5 @@
1
1
  import net from "net";
2
- import { hardExit } from "../api/hard-exit.js";
2
+ import { hardExit } from "./hard-exit.js";
3
3
  async function isPortAvailable(port) {
4
4
  return new Promise((resolve) => {
5
5
  const portTester = net.createConnection(port, "127.0.0.1");
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "fs";
2
+ import { findUpSync } from "find-up-simple";
3
+ import { z } from "zod";
4
+ import { fileURLToPath } from "url";
5
+ import { errored, complete } from "@attio/fetchable";
6
+ import chalk from "chalk";
7
+ const FILE_NAME = "package.json";
8
+ const packageJsonSchema = z.object({
9
+ name: z.literal("attio"),
10
+ version: z.string({
11
+ required_error: "No CLI version found",
12
+ invalid_type_error: "CLI version must be a string in package.json",
13
+ }),
14
+ });
15
+ let packageJson;
16
+ export function loadAttioCliVersion() {
17
+ if (packageJson === undefined) {
18
+ const cwd = fileURLToPath(import.meta.url);
19
+ const packageJsonPath = findUpSync(FILE_NAME, { cwd });
20
+ if (packageJsonPath === undefined) {
21
+ return errored({
22
+ code: "UNABLE_TO_FIND_PACKAGE_JSON",
23
+ directory: cwd,
24
+ });
25
+ }
26
+ let contents;
27
+ try {
28
+ contents = readFileSync(packageJsonPath, "utf8");
29
+ }
30
+ catch (error) {
31
+ return errored({
32
+ code: "UNABLE_TO_READ_PACKAGE_JSON",
33
+ error,
34
+ });
35
+ }
36
+ let json;
37
+ try {
38
+ json = JSON.parse(contents);
39
+ }
40
+ catch (error) {
41
+ return errored({
42
+ code: "UNABLE_TO_PARSE_PACKAGE_JSON",
43
+ error,
44
+ });
45
+ }
46
+ const result = packageJsonSchema.safeParse(json);
47
+ if (!result.success) {
48
+ return errored({
49
+ code: "INVALID_PACKAGE_JSON",
50
+ error: result.error,
51
+ });
52
+ }
53
+ packageJson = result.data;
54
+ }
55
+ const { version } = packageJson;
56
+ if (!version) {
57
+ return errored({
58
+ code: "NO_CLI_VERSION_FOUND",
59
+ });
60
+ }
61
+ return complete(version);
62
+ }
63
+ export function printCliVersionError({ error }) {
64
+ switch (error.code) {
65
+ case "UNABLE_TO_FIND_PACKAGE_JSON":
66
+ process.stderr.write(`${chalk.red("✖ ")}Failed to find package.json in ${error.directory}\n`);
67
+ break;
68
+ case "UNABLE_TO_READ_PACKAGE_JSON":
69
+ process.stderr.write(`${chalk.red("✖ ")}Failed to read package.json: ${error.error}\n`);
70
+ break;
71
+ case "UNABLE_TO_PARSE_PACKAGE_JSON":
72
+ process.stderr.write(`${chalk.red("✖ ")}Failed to parse package.json: ${error.error}\n`);
73
+ break;
74
+ case "INVALID_PACKAGE_JSON":
75
+ process.stderr.write(`${chalk.red("✖ ")}Invalid package.json: ${error.error}\n`);
76
+ break;
77
+ case "ERROR_LOADING_PACKAGE_JSON":
78
+ process.stderr.write(`${chalk.red("✖ ")}Error loading package.json: ${error.error}\n`);
79
+ break;
80
+ case "NO_CLI_VERSION_FOUND":
81
+ process.stderr.write(`${chalk.red("✖ ")}No CLI version found in attio's package.json\n`);
82
+ break;
83
+ default:
84
+ return error;
85
+ }
86
+ }
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { isComplete } from "@attio/fetchable";
2
3
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
4
  class Spinner {
4
5
  frameIndex = 0;
@@ -49,7 +50,9 @@ export async function spinnerify(busyMessage, successMessage, fn) {
49
50
  spinner.start(busyMessage);
50
51
  try {
51
52
  const result = await fn();
52
- spinner.success(typeof successMessage === "string" ? successMessage : successMessage(result));
53
+ if (isComplete(result)) {
54
+ spinner.success(typeof successMessage === "string" ? successMessage : successMessage(result.value));
55
+ }
53
56
  return result;
54
57
  }
55
58
  finally {
@@ -1,4 +1,4 @@
1
- import { hardExit } from "../api/hard-exit.js";
1
+ import { complete, errored } from "@attio/fetchable";
2
2
  export async function uploadBundle(bundle, uploadUrl) {
3
3
  try {
4
4
  await fetch(uploadUrl, {
@@ -9,8 +9,9 @@ export async function uploadBundle(bundle, uploadUrl) {
9
9
  "Content-Length": String(Buffer.from(bundle).length),
10
10
  },
11
11
  });
12
+ return complete(undefined);
12
13
  }
13
14
  catch (error) {
14
- hardExit(`Failed to upload bundle: ${error}`);
15
+ return errored({ code: "BUNDLE_UPLOAD_ERROR", error, uploadUrl });
15
16
  }
16
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio",
3
- "version": "0.0.1-experimental.20250403",
3
+ "version": "0.0.1-experimental.20250407",
4
4
  "bin": "lib/attio.js",
5
5
  "type": "module",
6
6
  "files": [
@@ -31,6 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
+ "@attio/fetchable": "0.0.1-experimental.4",
34
35
  "@babel/code-frame": "7.24.7",
35
36
  "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
36
37
  "@graphql-codegen/core": "4.0.2",
@@ -1,32 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
1
- import { z } from "zod";
2
- import { apiFetch } from "./api-fetch.js";
3
- const completeBundleUploadSchema = z.object({
4
- success: z.literal(true),
5
- });
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;
8
- }
@@ -1,8 +0,0 @@
1
- import { z } from "zod";
2
- import { apiFetch } from "./api-fetch.js";
3
- const completeBundleUploadSchema = z.object({
4
- success: z.literal(true),
5
- });
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;
8
- }