attio 0.0.1-experimental.20250324.1 → 0.0.1-experimental.20250325.1

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.
@@ -2,18 +2,14 @@ import { existsSync } from "fs";
2
2
  import Spinner from "tiny-spinner";
3
3
  import path, { join } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { assign, setup, fromCallback, fromPromise } from "xstate";
5
+ import { assign, setup, fromPromise } from "xstate";
6
6
  import { copyWithTransform } from "../util/copy-with-replace.js";
7
7
  import { createDirectory } from "../util/create-directory.js";
8
- import { loadDeveloperConfig } from "../util/load-developer-config.js";
9
8
  import { canWrite } from "../util/validate-slug.js";
10
- import { ask, askWithChoices } from "./actors.js";
11
- import { showError, printLogo } from "./actions.js";
9
+ import { ask, askWithChoices, authenticate, loadAppInfo } from "./actors.js";
10
+ import { printLogo, showActorError } from "./actions.js";
12
11
  import chalk from "chalk";
13
12
  import boxen from "boxen";
14
- import { printInstallInstructions } from "../util/print-install-instructions.js";
15
- import { getAppInfo } from "../api/get-app-info.js";
16
- import { ensureAuthed } from "../api/ensure-authed.js";
17
13
  export const languages = [
18
14
  { name: "TypeScript (recommended)", value: "typescript" },
19
15
  { name: "JavaScript", value: "javascript" },
@@ -21,96 +17,50 @@ export const languages = [
21
17
  export const initMachine = setup({
22
18
  types: {
23
19
  context: {},
24
- events: {},
25
20
  input: {},
26
21
  },
27
22
  actors: {
28
23
  ask,
29
24
  askWithChoices,
30
- createProject: fromCallback(({ sendBack, input }) => {
25
+ createProject: fromPromise(async ({ input }) => {
31
26
  const { appSlug, appInfo, language } = input;
32
- const create = async () => {
33
- const spinner = new Spinner();
34
- try {
35
- if (existsSync(join(process.cwd(), appSlug))) {
36
- sendBack({
37
- type: "Error",
38
- error: `Directory "${appSlug}" already exists`,
39
- });
40
- return;
41
- }
42
- if (!canWrite(process.cwd())) {
43
- sendBack({
44
- type: "Error",
45
- error: "Write access denied to current directory",
46
- });
47
- return;
48
- }
49
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
50
- const projectDir = createDirectory(appSlug);
51
- const templatesDir = path.resolve(__dirname, "../templates", language);
52
- const commonDir = path.resolve(__dirname, "../templates", "common");
53
- const transform = (contents) => contents
54
- .replaceAll("title-to-be-replaced", appInfo.title)
55
- .replaceAll("id-to-be-replaced", appInfo.app_id)
56
- .replaceAll("slug-to-be-replaced", appSlug);
57
- spinner.start("Creating project...");
58
- await Promise.all([
59
- copyWithTransform(templatesDir, projectDir, transform),
60
- copyWithTransform(commonDir, projectDir, transform),
61
- ]);
62
- spinner.success("Project created");
63
- sendBack({ type: "Success" });
64
- }
65
- catch (error) {
66
- spinner.error("Error creating project");
67
- sendBack({ type: "Error", error: error.message });
68
- }
69
- };
70
- create();
71
- }),
72
- loadConfig: fromCallback(({ sendBack }) => {
73
- const load = async () => {
74
- const config = await loadDeveloperConfig();
75
- if (typeof config === "string") {
76
- sendBack({ type: "No Config", configError: config });
77
- return;
78
- }
79
- const token = await ensureAuthed();
80
- sendBack({
81
- type: "Config Loaded",
82
- token,
83
- developerSlug: config.developer_slug,
84
- });
85
- };
86
- load();
87
- }),
88
- loadAppInfo: fromPromise(async ({ input: { token, developerSlug, appSlug } }) => {
89
27
  const spinner = new Spinner();
90
- spinner.start("Loading app information...");
91
28
  try {
92
- const appInfo = await getAppInfo({ token, developerSlug, appSlug });
93
- if (appInfo === null) {
94
- spinner.error("App not found");
95
- return null;
29
+ if (existsSync(join(process.cwd(), appSlug))) {
30
+ throw new Error(`Directory "${appSlug}" already exists`);
31
+ }
32
+ if (!canWrite(process.cwd())) {
33
+ throw new Error("Write access denied to current directory");
96
34
  }
97
- spinner.success(`App found: ${appInfo.title}`);
98
- return appInfo;
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+ const projectDir = createDirectory(appSlug);
37
+ const templatesDir = path.resolve(__dirname, "../templates", language);
38
+ const commonDir = path.resolve(__dirname, "../templates", "common");
39
+ const transform = (contents) => contents
40
+ .replaceAll("title-to-be-replaced", appInfo.title)
41
+ .replaceAll("id-to-be-replaced", appInfo.app_id)
42
+ .replaceAll("slug-to-be-replaced", appSlug);
43
+ spinner.start("Creating project...");
44
+ await Promise.all([
45
+ copyWithTransform(templatesDir, projectDir, transform),
46
+ copyWithTransform(commonDir, projectDir, transform),
47
+ ]);
48
+ spinner.success("Project created");
49
+ }
50
+ catch (error) {
51
+ spinner.error("Error creating project");
52
+ throw error;
99
53
  }
100
54
  finally {
101
55
  spinner.stop();
102
56
  }
103
57
  }),
58
+ authenticate,
59
+ loadAppInfo,
104
60
  },
105
61
  actions: {
106
62
  printLogo,
107
- clearError: assign({
108
- error: () => undefined,
109
- }),
110
- showError,
111
- showConfigInstructions: ({ context: { configError } }) => {
112
- printInstallInstructions(configError);
113
- },
63
+ showActorError,
114
64
  showInstructions: (_, params) => {
115
65
  process.stdout.write("\n" + chalk.green(`SUCCESS!! 🎉 Your app directory has been created.`) + "\n");
116
66
  process.stdout.write("\nTo get started, run:\n");
@@ -125,14 +75,10 @@ export const initMachine = setup({
125
75
  language: (_, params) => params.output,
126
76
  }),
127
77
  setAppInfo: assign({
128
- appInfo: (_, params) => params.appInfo,
129
- }),
130
- setConfig: assign({
131
- token: (_, params) => params.token,
132
- developerSlug: (_, params) => params.developerSlug,
78
+ appInfo: (_, params) => params.output,
133
79
  }),
134
- setConfigError: assign({
135
- configError: (_, params) => params.configError,
80
+ setToken: assign({
81
+ token: (_, params) => params.output,
136
82
  }),
137
83
  },
138
84
  guards: {
@@ -141,8 +87,6 @@ export const initMachine = setup({
141
87
  },
142
88
  }).createMachine({
143
89
  context: ({ input }) => ({
144
- availablePackageManagers: ["npm"],
145
- developerSlug: "",
146
90
  appId: "",
147
91
  token: "",
148
92
  ...input,
@@ -167,16 +111,6 @@ export const initMachine = setup({
167
111
  },
168
112
  },
169
113
  "Creating Project": {
170
- on: {
171
- Error: {
172
- target: "Error",
173
- actions: { type: "showError", params: ({ event }) => event },
174
- },
175
- Success: {
176
- target: "Success",
177
- reenter: true,
178
- },
179
- },
180
114
  invoke: {
181
115
  src: "createProject",
182
116
  input: ({ context }) => ({
@@ -184,6 +118,11 @@ export const initMachine = setup({
184
118
  appInfo: context.appInfo,
185
119
  appSlug: context.appSlug,
186
120
  }),
121
+ onDone: "Success",
122
+ onError: {
123
+ target: "Error",
124
+ actions: { type: "showActorError", params: ({ event }) => event },
125
+ },
187
126
  },
188
127
  },
189
128
  "Error": {
@@ -215,55 +154,39 @@ export const initMachine = setup({
215
154
  {
216
155
  src: "loadAppInfo",
217
156
  input: ({ context }) => context,
218
- onDone: [
219
- {
220
- target: "Do we need language?",
221
- guard: { type: "app exists", params: ({ event }) => event },
222
- actions: {
223
- type: "setAppInfo",
224
- params: ({ event }) => ({ appInfo: event.output }),
225
- },
226
- },
227
- {
228
- target: "Error",
229
- reenter: true,
157
+ onDone: {
158
+ target: "Do we need language?",
159
+ guard: { type: "app exists", params: ({ event }) => event },
160
+ actions: {
161
+ type: "setAppInfo",
162
+ params: ({ event }) => event,
230
163
  },
231
- ],
164
+ },
232
165
  onError: {
233
166
  target: "Error",
234
167
  actions: {
235
- type: "showError",
236
- params: ({ event }) => ({
237
- error: event.error instanceof Error
238
- ? event.error.message
239
- : String(event.error),
240
- }),
168
+ type: "showActorError",
169
+ params: ({ event }) => event,
241
170
  },
242
171
  },
243
172
  },
244
173
  ],
245
174
  },
246
- "Load Config": {
247
- on: {
248
- "No Config": {
249
- target: "Show config instructions",
250
- actions: { type: "setConfigError", params: ({ event }) => event },
175
+ "Authenticate": {
176
+ invoke: {
177
+ src: "authenticate",
178
+ onError: {
179
+ target: "Error",
180
+ reenter: true,
181
+ actions: { type: "showActorError", params: ({ event }) => event },
251
182
  },
252
- "Config Loaded": {
183
+ onDone: {
253
184
  target: "Loading App Info",
254
- actions: { type: "setConfig", params: ({ event }) => event },
255
- reenter: true,
185
+ actions: { type: "setToken", params: ({ event }) => event },
256
186
  },
257
187
  },
258
- invoke: {
259
- src: "loadConfig",
260
- },
261
- },
262
- "Show config instructions": {
263
- type: "final",
264
- entry: "showConfigInstructions",
265
188
  },
266
189
  },
267
- initial: "Load Config",
190
+ initial: "Authenticate",
268
191
  entry: "printLogo",
269
192
  });
@@ -1,8 +1,14 @@
1
1
  import net from "net";
2
2
  async function isPortAvailable(port) {
3
3
  return new Promise((resolve) => {
4
- const tester = net.createConnection(port, "127.0.0.1");
5
- tester.once("error", (err) => {
4
+ const portTester = net.createConnection(port, "127.0.0.1");
5
+ portTester.setTimeout(1000);
6
+ const cleanup = () => {
7
+ portTester.removeAllListeners();
8
+ portTester.destroy();
9
+ };
10
+ portTester.once("error", (err) => {
11
+ cleanup();
6
12
  if (err.code === "ECONNREFUSED") {
7
13
  resolve(true);
8
14
  }
@@ -10,8 +16,12 @@ async function isPortAvailable(port) {
10
16
  resolve(false);
11
17
  }
12
18
  });
13
- tester.once("connect", () => {
14
- tester.end();
19
+ portTester.once("connect", () => {
20
+ cleanup();
21
+ resolve(false);
22
+ });
23
+ portTester.once("timeout", () => {
24
+ cleanup();
15
25
  resolve(false);
16
26
  });
17
27
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio",
3
- "version": "0.0.1-experimental.20250324.1",
3
+ "version": "0.0.1-experimental.20250325.1",
4
4
  "bin": "lib/attio.js",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,32 +0,0 @@
1
- import { v4 as uuid } from "uuid";
2
- import { z } from "zod";
3
- import { API } from "../env.js";
4
- import { handleError } from "./handle-error.js";
5
- import { makeHeaders } from "./make-headers.js";
6
- const addConnectionSchema = z.object({
7
- app_id: z.string(),
8
- connection_definition_id: z.string(),
9
- });
10
- export async function addConnectionDefinition({ token, appId, label, description, global, connectionType, clientId, clientSecret, authorizeUrl, accessTokenUrl, major, scopes, }) {
11
- const connectionDefinitionId = uuid();
12
- const body = {
13
- connection_type: connectionType,
14
- label,
15
- description,
16
- global,
17
- };
18
- if (connectionType === "oauth2-code") {
19
- body.authorize_url = authorizeUrl;
20
- body.access_token_url = accessTokenUrl;
21
- body.client_id = clientId;
22
- body.client_secret = clientSecret;
23
- body.scopes = scopes.split(",").map((scope) => scope.trim());
24
- }
25
- const response = await fetch(`${API}/apps/${appId}/versions/${major}/connection-definitions/${connectionDefinitionId}`, {
26
- method: "PUT",
27
- headers: makeHeaders(token),
28
- body: JSON.stringify(body),
29
- });
30
- await handleError(response);
31
- return addConnectionSchema.parse(await response.json());
32
- }
@@ -1,9 +0,0 @@
1
- import { loadAppConfigFile } from "../util/app-config.js";
2
- export function loadAppId() {
3
- const appConfig = loadAppConfigFile();
4
- if (typeof appConfig === "string") {
5
- process.stderr.write(`❌ App config not found\n\n${appConfig}\n`);
6
- process.exit(1);
7
- }
8
- return appConfig.id;
9
- }
@@ -1,9 +0,0 @@
1
- import { loadDeveloperConfig } from "../util/load-developer-config.js";
2
- export async function loadDevSlug() {
3
- const devConfig = await loadDeveloperConfig();
4
- if (typeof devConfig === "string") {
5
- process.stderr.write(`❌ Developer config not found\n\n${devConfig}\n`);
6
- process.exit(1);
7
- }
8
- return devConfig.developer_slug;
9
- }
package/lib/schema.js DELETED
@@ -1,33 +0,0 @@
1
- import { z } from "zod";
2
- import { zodToJsonSchema } from "zod-to-json-schema";
3
- import { fromError } from "zod-validation-error";
4
- const configSchema = z.object({
5
- slug: z.string().describe("A unique slug for the app"),
6
- id: z.string().uuid().describe("A unique ID for the app"),
7
- major: z.number().int().min(1).default(1).describe("The major version of the app"),
8
- minor: z.number().int().min(0).default(0).describe("The minor version of the app"),
9
- connection: z
10
- .object({
11
- connection_type: z.enum(["secret", "oauth2-code"]).describe("The type of connection"),
12
- audience: z
13
- .enum(["workspace", "workspace-member"])
14
- .describe("The connection's audience"),
15
- })
16
- .nullable()
17
- .default(null)
18
- .describe("Connections to other services"),
19
- });
20
- export const emptyConfig = {
21
- slug: "",
22
- id: "",
23
- major: 1,
24
- minor: 0,
25
- connection: null,
26
- };
27
- export function parseConfig(json) {
28
- const result = configSchema.safeParse(json);
29
- if (result.success)
30
- return result.data;
31
- throw fromError(result.error).message;
32
- }
33
- export const getConfigJsonSchema = () => zodToJsonSchema(configSchema);
@@ -1,4 +0,0 @@
1
- {
2
- "slug": "slug-to-be-replaced",
3
- "id": "id-to-be-replaced"
4
- }
@@ -1,51 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
- import { parseConfig } from "../schema.js";
3
- const ERRORS = {
4
- noAttioJson: "No attio.json. Run this from your app project directory.",
5
- invalidAttioJson: "Invalid attio.json",
6
- writeError: "Error writing to attio.json",
7
- };
8
- function determineConfigPath() {
9
- if (process.env.NODE_ENV === "development") {
10
- const path = "./attio.dev.json";
11
- if (existsSync(path))
12
- return path;
13
- }
14
- const path = "./attio.json";
15
- if (existsSync(path))
16
- return path;
17
- return ERRORS.noAttioJson;
18
- }
19
- export function loadAppConfigFile() {
20
- const configPath = determineConfigPath();
21
- if (configPath === ERRORS.noAttioJson)
22
- return configPath;
23
- let doc;
24
- try {
25
- doc = JSON.parse(readFileSync(configPath, "utf8"));
26
- }
27
- catch {
28
- return ERRORS.invalidAttioJson;
29
- }
30
- try {
31
- return parseConfig(doc);
32
- }
33
- catch (e) {
34
- return e.message;
35
- }
36
- }
37
- export function updateAppConfig(mutator) {
38
- const configPath = determineConfigPath();
39
- if (configPath === ERRORS.noAttioJson)
40
- return configPath;
41
- const config = loadAppConfigFile();
42
- if (typeof config === "string")
43
- return config;
44
- const updated = parseConfig(mutator(config));
45
- try {
46
- writeFileSync(configPath, JSON.stringify(updated, null, 2));
47
- }
48
- catch {
49
- return ERRORS.writeError;
50
- }
51
- }
@@ -1,92 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
- import { stringify, parse } from "ini";
3
- import { homedir } from "os";
4
- import { z } from "zod";
5
- import { createDeveloperAccount } from "../api/create-developer-account.js";
6
- import { isValidSlug } from "./validate-slug.js";
7
- export const initialDeveloperConfigSchema = z.object({
8
- token: z.string(),
9
- developer_slug: z.string(),
10
- target_workspace_id: z.string().uuid().optional().nullable(),
11
- });
12
- const developerConfigSchema = initialDeveloperConfigSchema.extend({
13
- developer_account_id: z.string(),
14
- developer_account_member_id: z.string(),
15
- });
16
- export const configFileName = process.env.NODE_ENV === "development" ? ".attiorc.dev" : ".attiorc";
17
- function determineDeveloperConfigPath() {
18
- if (process.env.NODE_ENV === "development") {
19
- const path = `${homedir()}/.attiorc.dev`;
20
- if (existsSync(path))
21
- return path;
22
- }
23
- const path = `${homedir()}/.attiorc`;
24
- if (existsSync(path))
25
- return path;
26
- return "No config file";
27
- }
28
- function readDeveloperConfigFile() {
29
- const path = determineDeveloperConfigPath();
30
- if (path === "No config file")
31
- return "No config file";
32
- const configFileContents = readFileSync(path, { encoding: "utf8" });
33
- try {
34
- return { path, contents: parse(configFileContents) };
35
- }
36
- catch {
37
- return "Invalid config file";
38
- }
39
- }
40
- export function loadInitialDeveloperConfig() {
41
- const configFile = readDeveloperConfigFile();
42
- switch (configFile) {
43
- case "No config file":
44
- case "Invalid config file":
45
- return configFile;
46
- }
47
- const parsed = initialDeveloperConfigSchema.safeParse(configFile.contents);
48
- if (!parsed.success)
49
- return "Invalid config file";
50
- parsed.data.target_workspace_id = null;
51
- return parsed.data;
52
- }
53
- export async function loadDeveloperConfig() {
54
- const configFile = readDeveloperConfigFile();
55
- switch (configFile) {
56
- case "No config file":
57
- case "Invalid config file":
58
- return configFile;
59
- }
60
- const parsed = developerConfigSchema.safeParse(configFile.contents);
61
- if (parsed.success) {
62
- if (!isValidSlug(parsed.data.developer_slug)) {
63
- return "Invalid developer_slug: must be lower kebab case and contain only letters, numbers and hyphens. e.g. my-slug";
64
- }
65
- return parsed.data;
66
- }
67
- const initialDeveloperConfig = loadInitialDeveloperConfig();
68
- if (typeof initialDeveloperConfig === "string")
69
- return initialDeveloperConfig;
70
- const { token, developer_slug: developerSlug } = initialDeveloperConfig;
71
- if (!isValidSlug(developerSlug)) {
72
- return "Invalid developer_slug: must be lower kebab case and contain only letters, numbers and hyphens. e.g. my-slug";
73
- }
74
- const devAccount = await createDeveloperAccount({ token, developerSlug });
75
- const config = developerConfigSchema.parse({
76
- token,
77
- developer_slug: developerSlug,
78
- developer_account_id: devAccount.developer_account.developer_account_id,
79
- developer_account_member_id: devAccount.developer_account_member.developer_account_member_id,
80
- target_workspace_id: initialDeveloperConfig.target_workspace_id ?? null,
81
- });
82
- writeFileSync(configFile.path, stringify(config));
83
- return config;
84
- }
85
- export async function saveTargetWorkspaceToConfig(workspaceId) {
86
- const configFile = readDeveloperConfigFile();
87
- if (configFile === "No config file" || configFile === "Invalid config file")
88
- return;
89
- const config = developerConfigSchema.parse(configFile.contents);
90
- config.target_workspace_id = workspaceId;
91
- writeFileSync(configFile.path, stringify(config));
92
- }
@@ -1,32 +0,0 @@
1
- import { stringify } from "ini";
2
- import chalk from "chalk";
3
- import { APP, APP_NO_PROTOCOL, IS_DEV } from "../env.js";
4
- import { configFileName } from "../util/load-developer-config.js";
5
- const exampleConfig = {
6
- token: "YOUR TOKEN HERE",
7
- developer_slug: "ANY UNIQUE SLUG YOU WANT",
8
- target_workspace_id: "YOUR WORKSPACE ID HERE",
9
- };
10
- export function printInstallInstructions(reason) {
11
- const write = (str = "") => process.stdout.write(str + "\n");
12
- if (reason) {
13
- write("Failed to load config file.");
14
- write(chalk.red(reason));
15
- write();
16
- }
17
- write("You will need to:");
18
- write();
19
- write(` 1. Log into${IS_DEV ? ` ${chalk.italic("DEVELOPMENT")}` : ""} Attio web app: ${APP}`);
20
- write(` 2. Open Dev Tools > Application > Cookies > ${APP_NO_PROTOCOL}`);
21
- write(` 3. Copy the value for attio-session. That's your "token".`);
22
- write(` 4. Create a file in your home directory called ~/${configFileName}`);
23
- write(` 5. It should have the following [INI] format:`);
24
- write();
25
- write(" " + stringify(exampleConfig).trim().replace(/\n/g, "\n "));
26
- write();
27
- write(" 6. Run this command again.");
28
- write();
29
- write(" ...");
30
- write();
31
- write(" N. PROFIT!");
32
- }