attio 0.0.1-experimental.20250218 → 0.0.1-experimental.20250227

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.
@@ -0,0 +1,26 @@
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({
9
+ app_id: z.string().uuid(),
10
+ title: z.string(),
11
+ })
12
+ .nullable(),
13
+ });
14
+ export async function getAppInfo({ token, developerSlug, appSlug, }) {
15
+ if (isTest) {
16
+ return {
17
+ app_id: "test-id",
18
+ title: "Test App",
19
+ };
20
+ }
21
+ const response = await fetch(`${API}/developer-accounts/${developerSlug}/apps/by-slug/${appSlug}`, {
22
+ headers: makeHeaders(token),
23
+ });
24
+ await handleError(response);
25
+ return appSchema.parse(await response.json()).app;
26
+ }
package/lib/attio.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { create } from "./commands/create.js";
3
+ import { init } from "./commands/init.js";
4
4
  import { build } from "./commands/build.js";
5
5
  import { dev } from "./commands/dev.js";
6
6
  import { connection } from "./commands/connection/index.js";
@@ -10,7 +10,7 @@ program
10
10
  .name("attio")
11
11
  .description("CLI tool to create Attio apps")
12
12
  .version("0.0.1")
13
- .addCommand(create, { isDefault: true })
13
+ .addCommand(init)
14
14
  .addCommand(build)
15
15
  .addCommand(dev)
16
16
  .addCommand(connection)
@@ -14,6 +14,10 @@ export function createClientBuildConfig({ srcDir, entryPoint, }) {
14
14
  varName: "ATTIO_CLIENT_EXTENSION_SDK",
15
15
  type: "cjs",
16
16
  },
17
+ "attio/shared": {
18
+ varName: "ATTIO_SHARED_SDK",
19
+ type: "cjs",
20
+ },
17
21
  }),
18
22
  proxyServerModulesPlugin({ srcDir }),
19
23
  ],
@@ -18,7 +18,7 @@ import { Attribute, ObjectSlug } from "./object-slug";
18
18
  * console.log("send invoice", records)
19
19
  * },
20
20
  * label: "Send Invoice",
21
- * icon: "money-bag"
21
+ * icon: "Sales"
22
22
  * }
23
23
  * ```
24
24
  * ----
@@ -3,5 +3,6 @@ import React from "react";
3
3
  * A component that wraps React.Suspense and provides Attio-specific error handling.
4
4
  *
5
5
  * @see https://react.dev/reference/react/Suspense
6
+ * @deprecated Just use React.Suspense directly.
6
7
  */
7
8
  export declare const AttioSuspense: typeof React.Suspense;
@@ -4,4 +4,4 @@ export { Section } from "./section.js";
4
4
  export { TextBlock } from "./text-block.js";
5
5
  export { Column } from "./column.js";
6
6
  export { Row } from "./row.js";
7
- export { AttioSuspense } from "./attio-suspense.js";
7
+ export { Typography } from "./typography.js";
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ /**
3
+ * A set of components for styling text.
4
+ */
5
+ interface TypographyType {
6
+ Body: (props: {
7
+ /**
8
+ * The variant of the text.
9
+ *
10
+ * @default "standard"
11
+ */
12
+ variant?: "standard" | "large" | "strong" | "interactive";
13
+ children: React.ReactNode;
14
+ }) => React.JSX.Element;
15
+ Title: (props: {
16
+ /**
17
+ * The variant of the text.
18
+ *
19
+ * @default "standard"
20
+ */
21
+ variant?: "extraLarge" | "large" | "standard" | "medium" | "small" | "extraSmall";
22
+ children: React.ReactNode;
23
+ }) => React.JSX.Element;
24
+ Caption: (props: {
25
+ children: React.ReactNode;
26
+ }) => React.JSX.Element;
27
+ }
28
+ export declare const Typography: TypographyType;
29
+ export {};
@@ -162,13 +162,6 @@ type CollectionItem = (props: {
162
162
  type SubmitButton = (props: {
163
163
  /** The label of the submit button */
164
164
  label: string;
165
- /**
166
- * Whether the submit button is disabled
167
- *
168
- * It will automatically become disabled while the form is submitting.
169
- * You do not need to manage that state.
170
- */
171
- disabled?: boolean;
172
165
  }) => JSX.Element;
173
166
  export interface WithStateProps<TSchema extends FormSchema, TValues extends boolean = false, TSubmitting extends boolean = false, TErrors extends boolean = false> {
174
167
  values?: TValues;
@@ -225,10 +218,12 @@ export interface FormApi<TSchema extends FormSchema> {
225
218
  Combobox: Combobox<TSchema>;
226
219
  /**
227
220
  * A component used to render a collection of inputs.
221
+ * @deprecated This is not fully built yet.
228
222
  */
229
223
  Collection: Collection<TSchema, any>;
230
224
  /**
231
225
  * A component used to group inputs in a collection together.
226
+ * @deprecated This is not fully built yet.
232
227
  */
233
228
  CollectionItem: CollectionItem;
234
229
  /**
@@ -17,7 +17,7 @@ import { Attribute, ObjectSlug } from "./object-slug";
17
17
  * console.log("send invoice", recordId, object)
18
18
  * },
19
19
  * label: "Send Invoice",
20
- * icon: "money-bag"
20
+ * icon: "Sales"
21
21
  * }
22
22
  * ```
23
23
  * ----
@@ -1,25 +1,23 @@
1
1
  import { Argument, Command, Option } from "commander";
2
2
  import { createActor } from "xstate";
3
3
  import { z } from "zod";
4
- import { createMachine } from "../machines/create-machine.js";
5
- export const argsSchema = z.string().optional().default("");
4
+ import { initMachine } from "../machines/init-machine.js";
5
+ export const argsSchema = z.string();
6
6
  export const optionsSchema = z.object({
7
- title: z.string().optional(),
8
7
  language: z.enum(["javascript", "typescript"]).optional(),
9
8
  dev: z.boolean().default(false),
10
9
  });
11
- export const create = new Command("create")
12
- .description("Create a new Attio app")
13
- .addArgument(new Argument("<title>", 'Title of the extension (in "Title Case")').argOptional())
14
- .addOption(new Option("--title <title>", 'Title of the extension (in "Title Case")'))
10
+ export const init = new Command("init")
11
+ .description("Initialize a new Attio app")
12
+ .addArgument(new Argument("<app-slug>", "The app slug, chosen in the developer dashboard"))
15
13
  .addOption(new Option("--language <language>", "Language").choices(["javascript", "typescript"]))
16
14
  .addOption(new Option("--dev", "Run in development mode (additional debugging info)"))
17
15
  .action((unparsedArgs, unparsedOptions) => {
18
- const args = argsSchema.parse(unparsedArgs);
16
+ const appSlug = argsSchema.parse(unparsedArgs);
19
17
  const options = optionsSchema.parse(unparsedOptions);
20
- const actor = createActor(createMachine, {
18
+ const actor = createActor(initMachine, {
21
19
  input: {
22
- title: options.title || args,
20
+ appSlug,
23
21
  language: options.language,
24
22
  },
25
23
  });
@@ -0,0 +1,262 @@
1
+ import { existsSync } from "fs";
2
+ import Spinner from "tiny-spinner";
3
+ import path, { join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { assign, setup, fromCallback, fromPromise } from "xstate";
6
+ import { copyWithTransform } from "../util/copy-with-replace.js";
7
+ import { createDirectory } from "../util/create-directory.js";
8
+ import { loadDeveloperConfig } from "../util/load-developer-config.js";
9
+ import { canWrite } from "../util/validate-slug.js";
10
+ import { ask, askWithChoices } from "./actors.js";
11
+ import { showError, printLogo } from "./actions.js";
12
+ import chalk from "chalk";
13
+ import boxen from "boxen";
14
+ import { printInstallInstructions } from "../util/print-install-instructions.js";
15
+ import { getAppInfo } from "../api/get-app-info.js";
16
+ export const languages = [
17
+ { name: "TypeScript (recommended)", value: "typescript" },
18
+ { name: "JavaScript", value: "javascript" },
19
+ ];
20
+ export const initMachine = setup({
21
+ types: {
22
+ context: {},
23
+ events: {},
24
+ input: {},
25
+ },
26
+ actors: {
27
+ ask,
28
+ askWithChoices,
29
+ createProject: fromCallback(({ sendBack, input }) => {
30
+ const { appSlug, appInfo, language } = input;
31
+ const create = async () => {
32
+ const spinner = new Spinner();
33
+ try {
34
+ if (existsSync(join(process.cwd(), appSlug))) {
35
+ sendBack({
36
+ type: "Error",
37
+ error: `Directory "${appSlug}" already exists`,
38
+ });
39
+ return;
40
+ }
41
+ if (!canWrite(process.cwd())) {
42
+ sendBack({
43
+ type: "Error",
44
+ error: "Write access denied to current directory",
45
+ });
46
+ return;
47
+ }
48
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
49
+ const projectDir = createDirectory(appSlug);
50
+ const templatesDir = path.resolve(__dirname, "../templates", language);
51
+ const commonDir = path.resolve(__dirname, "../templates", "common");
52
+ const transform = (contents) => contents
53
+ .replaceAll("title-to-be-replaced", appInfo.title)
54
+ .replaceAll("id-to-be-replaced", appInfo.app_id)
55
+ .replaceAll("slug-to-be-replaced", appSlug);
56
+ spinner.start("Creating project...");
57
+ await Promise.all([
58
+ copyWithTransform(templatesDir, projectDir, transform),
59
+ copyWithTransform(commonDir, projectDir, transform),
60
+ ]);
61
+ spinner.success("Project created");
62
+ sendBack({ type: "Success" });
63
+ }
64
+ catch (error) {
65
+ spinner.error("Error creating project");
66
+ sendBack({ type: "Error", error: error.message });
67
+ }
68
+ };
69
+ create();
70
+ }),
71
+ loadConfig: fromCallback(({ sendBack }) => {
72
+ const load = async () => {
73
+ const config = await loadDeveloperConfig();
74
+ if (typeof config === "string") {
75
+ sendBack({ type: "No Config", configError: config });
76
+ return;
77
+ }
78
+ sendBack({
79
+ type: "Config Loaded",
80
+ token: config.token,
81
+ developerSlug: config.developer_slug,
82
+ });
83
+ };
84
+ load();
85
+ }),
86
+ loadAppInfo: fromPromise(async ({ input: { token, developerSlug, appSlug } }) => {
87
+ const spinner = new Spinner();
88
+ spinner.start("Loading app information...");
89
+ const appInfo = await getAppInfo({ token, developerSlug, appSlug });
90
+ if (appInfo === null) {
91
+ spinner.error("App not found");
92
+ return null;
93
+ }
94
+ spinner.success(`App found: ${appInfo.title}`);
95
+ return appInfo;
96
+ }),
97
+ },
98
+ actions: {
99
+ printLogo,
100
+ clearError: assign({
101
+ error: () => undefined,
102
+ }),
103
+ showError,
104
+ showConfigInstructions: ({ context: { configError } }) => {
105
+ printInstallInstructions(configError);
106
+ },
107
+ showInstructions: (_, params) => {
108
+ process.stdout.write("\n" + chalk.green(`SUCCESS!! 🎉 Your app directory has been created.`) + "\n");
109
+ process.stdout.write("\nTo get started, run:\n");
110
+ process.stdout.write(boxen(`cd ${params.appSlug}\nnpm install\nnpm run dev`, {
111
+ padding: 1,
112
+ margin: 1,
113
+ borderStyle: "round",
114
+ }) + "\n");
115
+ process.stdout.write(`(${chalk.yellow("yarn")}, ${chalk.yellow("pnpm")}, and ${chalk.yellow("bun")} also work!)\n`);
116
+ },
117
+ setLanguage: assign({
118
+ language: (_, params) => params.output,
119
+ }),
120
+ setAppInfo: assign({
121
+ appInfo: (_, params) => params.appInfo,
122
+ }),
123
+ setConfig: assign({
124
+ token: (_, params) => params.token,
125
+ developerSlug: (_, params) => params.developerSlug,
126
+ }),
127
+ setConfigError: assign({
128
+ configError: (_, params) => params.configError,
129
+ }),
130
+ },
131
+ guards: {
132
+ "app exists": (_, params) => Boolean(params.output),
133
+ "have language": (_, params) => Boolean(params.language),
134
+ },
135
+ }).createMachine({
136
+ context: ({ input }) => ({
137
+ availablePackageManagers: ["npm"],
138
+ developerSlug: "",
139
+ appId: "",
140
+ token: "",
141
+ ...input,
142
+ }),
143
+ id: "Init Machine",
144
+ states: {
145
+ "Ask for language": {
146
+ invoke: {
147
+ src: "askWithChoices",
148
+ input: {
149
+ message: "What language would you like to use?",
150
+ choices: languages,
151
+ },
152
+ onDone: {
153
+ target: "Creating Project",
154
+ actions: {
155
+ type: "setLanguage",
156
+ params: ({ event }) => event,
157
+ },
158
+ reenter: true,
159
+ },
160
+ },
161
+ },
162
+ "Creating Project": {
163
+ on: {
164
+ Error: {
165
+ target: "Error",
166
+ actions: { type: "showError", params: ({ event }) => event },
167
+ },
168
+ Success: {
169
+ target: "Success",
170
+ reenter: true,
171
+ },
172
+ },
173
+ invoke: {
174
+ src: "createProject",
175
+ input: ({ context }) => ({
176
+ language: context.language,
177
+ appInfo: context.appInfo,
178
+ appSlug: context.appSlug,
179
+ }),
180
+ },
181
+ },
182
+ "Error": {
183
+ type: "final",
184
+ },
185
+ "Success": {
186
+ type: "final",
187
+ entry: {
188
+ type: "showInstructions",
189
+ params: ({ context }) => ({
190
+ appSlug: context.appSlug,
191
+ title: context.appInfo.title,
192
+ }),
193
+ },
194
+ },
195
+ "Do we need language?": {
196
+ always: [
197
+ {
198
+ target: "Creating Project",
199
+ guard: { type: "have language", params: ({ context }) => context },
200
+ reenter: true,
201
+ description: `language may have been provided by CLI option`,
202
+ },
203
+ "Ask for language",
204
+ ],
205
+ },
206
+ "Loading App Info": {
207
+ invoke: [
208
+ {
209
+ src: "loadAppInfo",
210
+ input: ({ context }) => context,
211
+ onDone: [
212
+ {
213
+ target: "Do we need language?",
214
+ guard: { type: "app exists", params: ({ event }) => event },
215
+ actions: {
216
+ type: "setAppInfo",
217
+ params: ({ event }) => ({ appInfo: event.output }),
218
+ },
219
+ },
220
+ {
221
+ target: "Error",
222
+ reenter: true,
223
+ },
224
+ ],
225
+ onError: {
226
+ target: "Error",
227
+ actions: {
228
+ type: "showError",
229
+ params: ({ event }) => ({
230
+ error: event.error instanceof Error
231
+ ? event.error.message
232
+ : String(event.error),
233
+ }),
234
+ },
235
+ },
236
+ },
237
+ ],
238
+ },
239
+ "Load Config": {
240
+ on: {
241
+ "No Config": {
242
+ target: "Show config instructions",
243
+ actions: { type: "setConfigError", params: ({ event }) => event },
244
+ },
245
+ "Config Loaded": {
246
+ target: "Loading App Info",
247
+ actions: { type: "setConfig", params: ({ event }) => event },
248
+ reenter: true,
249
+ },
250
+ },
251
+ invoke: {
252
+ src: "loadConfig",
253
+ },
254
+ },
255
+ "Show config instructions": {
256
+ type: "final",
257
+ entry: "showConfigInstructions",
258
+ },
259
+ },
260
+ initial: "Load Config",
261
+ entry: "printLogo",
262
+ });
@@ -1,33 +1,29 @@
1
- export interface Connection {
1
+ interface BaseConnection {
2
2
  /** The unique identifier for the connection. */
3
3
  id: string;
4
4
  /** The access token or secret for the connection. */
5
5
  value: string;
6
- ownedBy: {
7
- type: "workspace" | "user";
8
- /**
9
- * Workspace ID or user ID
10
- */
6
+ createdBy: {
7
+ type: "user";
8
+ /** User ID */
11
9
  id: string;
12
10
  };
13
- createdBy: {
11
+ }
12
+ interface UserConnection extends BaseConnection {
13
+ ownedBy: {
14
14
  type: "user";
15
- /**
16
- * User ID
17
- */
15
+ /** User ID */
16
+ id: string;
17
+ };
18
+ }
19
+ interface WorkspaceConnection extends BaseConnection {
20
+ ownedBy: {
21
+ type: "workspace";
22
+ /** Workspace ID */
18
23
  id: string;
19
24
  };
20
25
  }
21
- /**
22
- * Gets the access token or secret for a connection by id.
23
- *
24
- * @returns An object containing the connection's id, access token or secret,
25
- * and slug, or null if the connection is not found.
26
- */
27
- export declare function getConnectionById(
28
- /**
29
- * The unique identifier for the connection.
30
- */
31
- id: string): Connection | null;
32
- export declare function getUserConnection(): Connection | null;
33
- export declare function getWorkspaceConnection(): Connection | null;
26
+ export type Connection = UserConnection | WorkspaceConnection;
27
+ export declare function getUserConnection(): UserConnection;
28
+ export declare function getWorkspaceConnection(): WorkspaceConnection;
29
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Reserved for internal errors that are thrown by the extension SDK.
3
+ */
4
+ export declare abstract class AttioError extends Error {
5
+ static is(error: unknown): error is AttioError;
6
+ abstract get code(): "no-user-connection" | "no-workspace-connection" | "unexpected-transport-error";
7
+ }
8
+ /**
9
+ * Thrown when there is a missing user connection.
10
+ */
11
+ export declare class AttioNoUserConnectionError extends AttioError {
12
+ constructor();
13
+ static is(error: unknown): error is AttioNoUserConnectionError;
14
+ get code(): "no-user-connection";
15
+ }
16
+ /**
17
+ * Thrown when there is a missing workspace connection.
18
+ */
19
+ export declare class AttioNoWorkspaceConnectionError extends AttioError {
20
+ constructor();
21
+ static is(error: unknown): error is AttioNoWorkspaceConnectionError;
22
+ get code(): "no-workspace-connection";
23
+ }
24
+ /**
25
+ * Thrown when there is an unexpected transport error.
26
+ */
27
+ export declare class AttioUnexpectedTransportError extends AttioError {
28
+ constructor();
29
+ static is(error: unknown): error is AttioUnexpectedTransportError;
30
+ get code(): "unexpected-transport-error";
31
+ }
@@ -0,0 +1 @@
1
+ export * from "./errors.js";
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import {TextBlock, AttioSuspense} from "attio/client"
2
+ import {TextBlock} from "attio/client"
3
3
  import {Advice} from "./advice"
4
4
 
5
5
  const Loading = () => <TextBlock>Loading advice...</TextBlock>
@@ -17,9 +17,9 @@ export function HelloWorldDialog ({recordId}) {
17
17
  I am a dialog. I have been open for: {seconds} second{seconds === 1 ? "" : "s"}
18
18
  </TextBlock>
19
19
  {/* The hook in Advice will suspend until the advice is loaded. */}
20
- <AttioSuspense fallback={<Loading />}>
20
+ <React.Suspense fallback={<Loading />}>
21
21
  <Advice recordId={recordId} />
22
- </AttioSuspense>
22
+ </React.Suspense>
23
23
  </>
24
24
  )
25
25
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import {TextBlock, AttioSuspense} from "attio/client"
2
+ import {TextBlock} from "attio/client"
3
3
  import {Advice} from "./advice"
4
4
 
5
5
  const Loading = () => <TextBlock>Loading advice...</TextBlock>
@@ -17,9 +17,9 @@ export function HelloWorldDialog({recordId}: {recordId: string}) {
17
17
  I am a dialog. I have been open for: {seconds} second{seconds === 1 ? "" : "s"}
18
18
  </TextBlock>
19
19
  {/* The hook in Advice will suspend until the advice is loaded. */}
20
- <AttioSuspense fallback={<Loading />}>
20
+ <React.Suspense fallback={<Loading />}>
21
21
  <Advice recordId={recordId} />
22
- </AttioSuspense>
22
+ </React.Suspense>
23
23
  </>
24
24
  )
25
25
  }