attio 0.0.1-experimental.20250311 → 0.0.1-experimental.20250324
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.
- package/lib/api/add-connection-definition.js +2 -2
- package/lib/api/auth.js +114 -0
- package/lib/api/complete-bundle-upload.js +2 -2
- package/lib/api/complete-prod-bundle-upload.js +2 -2
- package/lib/api/create-dev-version.js +2 -2
- package/lib/api/create-version.js +2 -2
- package/lib/api/ensure-authed.js +24 -0
- package/lib/api/fetch-connections.js +2 -2
- package/lib/api/fetch-installation.js +2 -2
- package/lib/api/fetch-versions.js +2 -2
- package/lib/api/keychain.js +30 -0
- package/lib/api/load-app-id.js +9 -0
- package/lib/api/load-dev-slug.js +9 -0
- package/lib/api/make-headers.js +1 -1
- package/lib/api/remove-connection-definition.js +2 -2
- package/lib/api/start-upload.js +2 -2
- package/lib/api/whoami.js +21 -0
- package/lib/attio.js +6 -2
- package/lib/commands/login.js +11 -0
- package/lib/commands/logout.js +11 -0
- package/lib/commands/version/index.js +1 -3
- package/lib/commands/version/list.js +31 -17
- package/lib/commands/whoami.js +19 -0
- package/lib/graphql/graphql-error.js +3 -0
- package/lib/machines/actors.js +3 -1
- package/lib/machines/dev-machine.js +5 -2
- package/lib/machines/init-machine.js +14 -7
- package/lib/templates/javascript/src/cat-fact.jsx +16 -0
- package/lib/templates/javascript/src/get-cat-fact.server.js +6 -0
- package/lib/templates/javascript/src/hello-world-dialog.jsx +6 -5
- package/lib/templates/typescript/src/cat-fact.tsx +16 -0
- package/lib/templates/typescript/src/get-cat-fact.server.ts +6 -0
- package/lib/templates/typescript/src/hello-world-dialog.tsx +5 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/util/load-developer-config.js +1 -0
- package/package.json +2 -1
- package/lib/api/publish-version.js +0 -15
- package/lib/commands/connection/add.js +0 -67
- package/lib/commands/connection/index.js +0 -9
- package/lib/commands/connection/list.js +0 -20
- package/lib/commands/connection/remove.js +0 -20
- package/lib/commands/version/publish.js +0 -48
- package/lib/machines/add-connection-machine.js +0 -559
- package/lib/machines/list-connections-machine.js +0 -174
- package/lib/machines/list-versions-machine.js +0 -151
- package/lib/machines/publish-version-machine.js +0 -230
- package/lib/machines/remove-connection-machine.js +0 -253
- package/lib/templates/javascript/src/advice.jsx +0 -16
- package/lib/templates/javascript/src/get-advice.server.js +0 -6
- package/lib/templates/typescript/src/advice.tsx +0 -16
- package/lib/templates/typescript/src/get-advice.server.ts +0 -6
|
@@ -7,7 +7,7 @@ const addConnectionSchema = z.object({
|
|
|
7
7
|
app_id: z.string(),
|
|
8
8
|
connection_definition_id: z.string(),
|
|
9
9
|
});
|
|
10
|
-
export async function addConnectionDefinition({ token,
|
|
10
|
+
export async function addConnectionDefinition({ token, appId, label, description, global, connectionType, clientId, clientSecret, authorizeUrl, accessTokenUrl, major, scopes, }) {
|
|
11
11
|
const connectionDefinitionId = uuid();
|
|
12
12
|
const body = {
|
|
13
13
|
connection_type: connectionType,
|
|
@@ -22,7 +22,7 @@ export async function addConnectionDefinition({ token, devSlug, appId, label, de
|
|
|
22
22
|
body.client_secret = clientSecret;
|
|
23
23
|
body.scopes = scopes.split(",").map((scope) => scope.trim());
|
|
24
24
|
}
|
|
25
|
-
const response = await fetch(`${API}/
|
|
25
|
+
const response = await fetch(`${API}/apps/${appId}/versions/${major}/connection-definitions/${connectionDefinitionId}`, {
|
|
26
26
|
method: "PUT",
|
|
27
27
|
headers: makeHeaders(token),
|
|
28
28
|
body: JSON.stringify(body),
|
package/lib/api/auth.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { randomBytes, createHash } from "crypto";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import { APP } from "../env.js";
|
|
6
|
+
import { findAvailablePort } from "../util/find-available-port.js";
|
|
7
|
+
import { loadAuthToken, saveAuthToken } from "./keychain.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const CLIENT_ID = "f881c6f1-82d7-48a5-a581-649596167845";
|
|
10
|
+
const tokenResponseSchema = z.object({
|
|
11
|
+
access_token: z.string(),
|
|
12
|
+
token_type: z.string(),
|
|
13
|
+
expires_in: z.number(),
|
|
14
|
+
});
|
|
15
|
+
async function exchangeCodeForToken(code, codeVerifier, redirectUri) {
|
|
16
|
+
const tokenUrl = `${APP}/oidc/token`;
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
params.append("grant_type", "authorization_code");
|
|
19
|
+
params.append("code", code);
|
|
20
|
+
params.append("client_id", CLIENT_ID);
|
|
21
|
+
params.append("redirect_uri", redirectUri);
|
|
22
|
+
params.append("code_verifier", codeVerifier);
|
|
23
|
+
const response = await fetch(tokenUrl, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
27
|
+
},
|
|
28
|
+
body: params.toString(),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorText = await response.text();
|
|
32
|
+
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
33
|
+
}
|
|
34
|
+
return tokenResponseSchema.parse(await response.json());
|
|
35
|
+
}
|
|
36
|
+
export async function auth() {
|
|
37
|
+
const existingToken = await loadAuthToken();
|
|
38
|
+
if (existingToken) {
|
|
39
|
+
return existingToken;
|
|
40
|
+
}
|
|
41
|
+
const verifier = randomBytes(32);
|
|
42
|
+
const verifierString = verifier.toString("base64url");
|
|
43
|
+
const hash = createHash("sha256");
|
|
44
|
+
hash.update(verifier);
|
|
45
|
+
const challenge = hash.digest();
|
|
46
|
+
const challengeString = challenge.toString("base64url");
|
|
47
|
+
const state = randomBytes(32).toString("base64url");
|
|
48
|
+
const port = await findAvailablePort(3000, 65000);
|
|
49
|
+
const redirectUri = `http://localhost:${port}`;
|
|
50
|
+
const { promise: tokenPromise, resolve: resolveToken, reject: rejectToken, } = Promise.withResolvers();
|
|
51
|
+
const app = new Hono();
|
|
52
|
+
let serverRef;
|
|
53
|
+
app.get("/", async (c) => {
|
|
54
|
+
try {
|
|
55
|
+
const query = c.req.query();
|
|
56
|
+
const receivedCode = query.authorization_code;
|
|
57
|
+
const receivedState = query.state;
|
|
58
|
+
if (receivedState !== state) {
|
|
59
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
60
|
+
}
|
|
61
|
+
if (!receivedCode) {
|
|
62
|
+
throw new Error("No authorization code received");
|
|
63
|
+
}
|
|
64
|
+
const tokenResponse = await exchangeCodeForToken(receivedCode, verifierString, redirectUri);
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
serverRef.close();
|
|
67
|
+
}, 1000);
|
|
68
|
+
resolveToken(tokenResponse.access_token);
|
|
69
|
+
return c.html(`
|
|
70
|
+
<html>
|
|
71
|
+
<body>
|
|
72
|
+
<h1>Authentication Successful</h1>
|
|
73
|
+
<p>You can close this window and return to the CLI.</p>
|
|
74
|
+
<script>window.close();</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
rejectToken(error instanceof Error ? error : new Error(String(error)));
|
|
81
|
+
return c.html(`
|
|
82
|
+
<html>
|
|
83
|
+
<body>
|
|
84
|
+
<h1>Authentication Failed</h1>
|
|
85
|
+
<p>Error: ${error instanceof Error ? error.message : String(error)}</p>
|
|
86
|
+
<p>Please close this window and try again.</p>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
serverRef = serve({
|
|
93
|
+
fetch: app.fetch,
|
|
94
|
+
port,
|
|
95
|
+
});
|
|
96
|
+
const authUrl = new URL(`${APP}/oidc/authorize`);
|
|
97
|
+
authUrl.searchParams.append("scope", "openid");
|
|
98
|
+
authUrl.searchParams.append("response_type", "code");
|
|
99
|
+
authUrl.searchParams.append("client_id", CLIENT_ID);
|
|
100
|
+
authUrl.searchParams.append("redirect_uri", redirectUri);
|
|
101
|
+
authUrl.searchParams.append("state", state);
|
|
102
|
+
authUrl.searchParams.append("code_challenge", challengeString);
|
|
103
|
+
authUrl.searchParams.append("code_challenge_method", "S256");
|
|
104
|
+
await open(authUrl.toString());
|
|
105
|
+
try {
|
|
106
|
+
const accessToken = await tokenPromise;
|
|
107
|
+
await saveAuthToken(accessToken);
|
|
108
|
+
return accessToken;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
serverRef.close();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -5,8 +5,8 @@ import { makeHeaders } from "./make-headers.js";
|
|
|
5
5
|
const completeBundleUploadSchema = z.object({
|
|
6
6
|
success: z.literal(true),
|
|
7
7
|
});
|
|
8
|
-
export async function completeBundleUpload({ token,
|
|
9
|
-
const response = await fetch(`${API}/
|
|
8
|
+
export async function completeBundleUpload({ token, appId, devVersionId, bundleId, }) {
|
|
9
|
+
const response = await fetch(`${API}/apps/${appId}/dev-versions/${devVersionId}/bundles/${bundleId}/complete`, {
|
|
10
10
|
method: "POST",
|
|
11
11
|
headers: makeHeaders(token),
|
|
12
12
|
});
|
|
@@ -5,8 +5,8 @@ import { makeHeaders } from "./make-headers.js";
|
|
|
5
5
|
const completeBundleUploadSchema = z.object({
|
|
6
6
|
success: z.literal(true),
|
|
7
7
|
});
|
|
8
|
-
export async function completeProdBundleUpload({ token,
|
|
9
|
-
const response = await fetch(`${API}/
|
|
8
|
+
export async function completeProdBundleUpload({ token, appId, bundleId, major, minor, }) {
|
|
9
|
+
const response = await fetch(`${API}/apps/${appId}/prod-versions/${major}/${minor}/bundles/${bundleId}/complete`, {
|
|
10
10
|
method: "POST",
|
|
11
11
|
headers: makeHeaders(token),
|
|
12
12
|
});
|
|
@@ -6,8 +6,8 @@ const createDevVersionSchema = z.object({
|
|
|
6
6
|
app_id: z.string(),
|
|
7
7
|
app_dev_version_id: z.string(),
|
|
8
8
|
});
|
|
9
|
-
export async function createDevVersion({ token,
|
|
10
|
-
const response = await fetch(`${API}/
|
|
9
|
+
export async function createDevVersion({ token, appId, targetWorkspaceId, environmentVariables, cliVersion, }) {
|
|
10
|
+
const response = await fetch(`${API}/apps/${appId}/dev-versions`, {
|
|
11
11
|
method: "POST",
|
|
12
12
|
headers: makeHeaders(token),
|
|
13
13
|
body: JSON.stringify({
|
|
@@ -10,8 +10,8 @@ const createVersionSchema = z.object({
|
|
|
10
10
|
client_bundle_upload_url: z.string(),
|
|
11
11
|
server_bundle_upload_url: z.string(),
|
|
12
12
|
});
|
|
13
|
-
export async function createVersion({ token,
|
|
14
|
-
const response = await fetch(`${API}/
|
|
13
|
+
export async function createVersion({ token, appId, major, cliVersion, }) {
|
|
14
|
+
const response = await fetch(`${API}/apps/${appId}/prod-versions`, {
|
|
15
15
|
method: "POST",
|
|
16
16
|
headers: makeHeaders(token),
|
|
17
17
|
body: JSON.stringify({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { auth as authApi } from "./auth.js";
|
|
2
|
+
import { deleteAuthToken, loadAuthToken } from "./keychain.js";
|
|
3
|
+
import { whoami } from "./whoami.js";
|
|
4
|
+
async function auth() {
|
|
5
|
+
process.stdout.write("You need to log in with Attio.\n\n");
|
|
6
|
+
await deleteAuthToken();
|
|
7
|
+
return await authApi();
|
|
8
|
+
}
|
|
9
|
+
export async function ensureAuthed() {
|
|
10
|
+
let token = await loadAuthToken();
|
|
11
|
+
if (!token) {
|
|
12
|
+
token = await auth();
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const user = await whoami({ token });
|
|
16
|
+
if (!user) {
|
|
17
|
+
token = await auth();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return await auth();
|
|
22
|
+
}
|
|
23
|
+
return token;
|
|
24
|
+
}
|
|
@@ -10,8 +10,8 @@ const connectionDefinitionsResponseSchema = z.object({
|
|
|
10
10
|
global: z.boolean(),
|
|
11
11
|
})),
|
|
12
12
|
});
|
|
13
|
-
export async function fetchConnections({ token,
|
|
14
|
-
const response = await fetch(`${API}/
|
|
13
|
+
export async function fetchConnections({ token, appId, major, }) {
|
|
14
|
+
const response = await fetch(`${API}/apps/${appId}/versions/${major}/connection-definitions`, {
|
|
15
15
|
method: "GET",
|
|
16
16
|
headers: makeHeaders(token),
|
|
17
17
|
});
|
|
@@ -8,8 +8,8 @@ const installationSchema = z.object({
|
|
|
8
8
|
app_id: z.string().uuid(),
|
|
9
9
|
installation_id: z.string().uuid(),
|
|
10
10
|
});
|
|
11
|
-
export async function fetchInstallation({ token,
|
|
12
|
-
const response = await fetch(`${API}/
|
|
11
|
+
export async function fetchInstallation({ token, appId, workspaceId, }) {
|
|
12
|
+
const response = await fetch(`${API}/apps/${appId}/workspace/${workspaceId}/dev-installation`, {
|
|
13
13
|
method: "GET",
|
|
14
14
|
headers: makeHeaders(token),
|
|
15
15
|
});
|
|
@@ -31,8 +31,8 @@ function onlyPublishableVersions(versions) {
|
|
|
31
31
|
(v.major === highestPublishedVersion.major &&
|
|
32
32
|
v.minor > highestPublishedVersion.minor)));
|
|
33
33
|
}
|
|
34
|
-
export async function fetchVersions({ token,
|
|
35
|
-
const response = await fetch(`${API}/
|
|
34
|
+
export async function fetchVersions({ token, appId, }) {
|
|
35
|
+
const response = await fetch(`${API}/apps/${appId}/prod-versions`, {
|
|
36
36
|
method: "GET",
|
|
37
37
|
headers: makeHeaders(token),
|
|
38
38
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
let keytar = null;
|
|
2
|
+
if (process.env.NODE_ENV !== "test") {
|
|
3
|
+
try {
|
|
4
|
+
keytar = (await import("keytar")).default;
|
|
5
|
+
}
|
|
6
|
+
catch (e) {
|
|
7
|
+
console.warn("Keychain functionality not available");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const SERVICE_NAME = "attio-cli";
|
|
11
|
+
const ACCOUNT_NAME = "developer";
|
|
12
|
+
export async function saveAuthToken(token) {
|
|
13
|
+
if (!keytar) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
17
|
+
}
|
|
18
|
+
export async function loadAuthToken() {
|
|
19
|
+
if (!keytar) {
|
|
20
|
+
return "TEST";
|
|
21
|
+
}
|
|
22
|
+
const token = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
23
|
+
return token || null;
|
|
24
|
+
}
|
|
25
|
+
export async function deleteAuthToken() {
|
|
26
|
+
if (!keytar) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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/api/make-headers.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { API } from "../env.js";
|
|
2
2
|
import { handleError } from "./handle-error.js";
|
|
3
3
|
import { makeHeaders } from "./make-headers.js";
|
|
4
|
-
export async function removeConnectionDefinition({ token,
|
|
5
|
-
const response = await fetch(`${API}/
|
|
4
|
+
export async function removeConnectionDefinition({ token, appId, global, major, }) {
|
|
5
|
+
const response = await fetch(`${API}/apps/${appId}/versions/${major}/connection-definitions/${global ? "workspace" : "user"}/remove`, {
|
|
6
6
|
method: "DELETE",
|
|
7
7
|
headers: makeHeaders(token),
|
|
8
8
|
});
|
package/lib/api/start-upload.js
CHANGED
|
@@ -8,8 +8,8 @@ const startUploadSchema = z.object({
|
|
|
8
8
|
client_bundle_upload_url: z.string(),
|
|
9
9
|
server_bundle_upload_url: z.string(),
|
|
10
10
|
});
|
|
11
|
-
export async function startUpload({ token,
|
|
12
|
-
const response = await fetch(`${API}/
|
|
11
|
+
export async function startUpload({ token, appId, devVersionId, }) {
|
|
12
|
+
const response = await fetch(`${API}/apps/${appId}/dev-versions/${devVersionId}/bundles`, {
|
|
13
13
|
method: "POST",
|
|
14
14
|
headers: makeHeaders(token),
|
|
15
15
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { APP } from "../env.js";
|
|
3
|
+
import { handleError } from "./handle-error.js";
|
|
4
|
+
import { makeHeaders } from "./make-headers.js";
|
|
5
|
+
const whoamiSchema = z.object({
|
|
6
|
+
user: z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
email_address: z.string(),
|
|
9
|
+
name: z.object({
|
|
10
|
+
full: z.string(),
|
|
11
|
+
}),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
export async function whoami({ token }) {
|
|
15
|
+
const response = await fetch(`${APP}/api/auth/whoami`, {
|
|
16
|
+
method: "GET",
|
|
17
|
+
headers: makeHeaders(token),
|
|
18
|
+
});
|
|
19
|
+
await handleError(response);
|
|
20
|
+
return whoamiSchema.parse(await response.json()).user;
|
|
21
|
+
}
|
package/lib/attio.js
CHANGED
|
@@ -3,8 +3,10 @@ import { Command } from "commander";
|
|
|
3
3
|
import { init } from "./commands/init.js";
|
|
4
4
|
import { build } from "./commands/build.js";
|
|
5
5
|
import { dev } from "./commands/dev.js";
|
|
6
|
-
import { connection } from "./commands/connection/index.js";
|
|
7
6
|
import { version } from "./commands/version/index.js";
|
|
7
|
+
import { login } from "./commands/login.js";
|
|
8
|
+
import { logout } from "./commands/logout.js";
|
|
9
|
+
import { whoami } from "./commands/whoami.js";
|
|
8
10
|
const program = new Command();
|
|
9
11
|
program
|
|
10
12
|
.name("attio")
|
|
@@ -13,6 +15,8 @@ program
|
|
|
13
15
|
.addCommand(init)
|
|
14
16
|
.addCommand(build)
|
|
15
17
|
.addCommand(dev)
|
|
16
|
-
.addCommand(connection)
|
|
17
18
|
.addCommand(version)
|
|
19
|
+
.addCommand(login)
|
|
20
|
+
.addCommand(logout)
|
|
21
|
+
.addCommand(whoami)
|
|
18
22
|
.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { auth as authApi } from "../api/auth.js";
|
|
3
|
+
export const login = new Command("login").description("Authenticate with Attio").action(() => {
|
|
4
|
+
authApi()
|
|
5
|
+
.then((token) => {
|
|
6
|
+
process.stdout.write("🔓 Successfully authenticated.\n");
|
|
7
|
+
})
|
|
8
|
+
.catch((error) => {
|
|
9
|
+
process.stderr.write(`❌ Authentication failed\n\n${error}\n`);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { deleteAuthToken } from "../api/keychain.js";
|
|
3
|
+
export const logout = new Command("logout").description("Log out from Attio").action(() => {
|
|
4
|
+
deleteAuthToken()
|
|
5
|
+
.then(() => {
|
|
6
|
+
process.stdout.write("🔒 Successfully logged out.\n");
|
|
7
|
+
})
|
|
8
|
+
.catch((error) => {
|
|
9
|
+
process.stderr.write(`❌ Logout failed\n\n${error}\n`);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { versionCreate } from "./create.js";
|
|
3
3
|
import { versionList } from "./list.js";
|
|
4
|
-
import { versionPublish } from "./publish.js";
|
|
5
4
|
export const version = new Command("version")
|
|
6
5
|
.description("Manage app versions")
|
|
7
6
|
.addCommand(versionCreate)
|
|
8
|
-
.addCommand(versionList)
|
|
9
|
-
.addCommand(versionPublish);
|
|
7
|
+
.addCommand(versionList);
|
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
import { Command
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { fetchVersions } from "../../api/fetch-versions.js";
|
|
3
|
+
import Table from "cli-table3";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import formatDate from "date-fns/format/index.js";
|
|
6
|
+
import { loadAppId } from "../../api/load-app-id.js";
|
|
7
|
+
import { loadDevSlug } from "../../api/load-dev-slug.js";
|
|
8
|
+
import { ensureAuthed } from "../../api/ensure-authed.js";
|
|
8
9
|
export const versionList = new Command("list")
|
|
9
10
|
.description("List all versions of your Attio app")
|
|
10
|
-
.addOption(new Option("--dev", "Run in development mode (additional debugging info)"))
|
|
11
11
|
.alias("ls")
|
|
12
|
-
.action((
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const token = await ensureAuthed();
|
|
14
|
+
const [appId, devSlug] = await Promise.all([loadAppId(), loadDevSlug()]);
|
|
15
|
+
const versions = await fetchVersions({
|
|
16
|
+
token,
|
|
17
|
+
devSlug,
|
|
18
|
+
appId,
|
|
19
|
+
});
|
|
20
|
+
const table = new Table({
|
|
21
|
+
head: ["Version", "Published", "Installations", "Created"].map((h) => chalk.bold(h)),
|
|
22
|
+
style: {
|
|
23
|
+
head: [],
|
|
24
|
+
border: [],
|
|
25
|
+
},
|
|
26
|
+
colAligns: ["center", "center", "right", "left"],
|
|
27
|
+
});
|
|
28
|
+
table.push(...versions.map((version) => [
|
|
29
|
+
`${version.major}.${version.minor}`,
|
|
30
|
+
version.is_published ? "Yes" : "No",
|
|
31
|
+
version.num_installations.toLocaleString(),
|
|
32
|
+
formatDate(new Date(version.created_at), "MMMM d, yyyy, HH:mm"),
|
|
33
|
+
]));
|
|
34
|
+
process.stdout.write(table.toString());
|
|
21
35
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { auth as authApi } from "../api/auth.js";
|
|
3
|
+
import { whoami as whoamiApi } from "../api/whoami.js";
|
|
4
|
+
export const whoami = new Command("whoami")
|
|
5
|
+
.description("Identify the current user")
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const token = await authApi();
|
|
8
|
+
if (!token) {
|
|
9
|
+
process.stdout.write("🔒 Not logged in.\n");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const user = await whoamiApi({ token });
|
|
14
|
+
process.stdout.write(`👤 ${user.name.full} (${user.email_address})\n`);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
process.stderr.write(`❌ User lookup failed\n\n${error}\n`);
|
|
18
|
+
}
|
|
19
|
+
});
|
package/lib/machines/actors.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fromCallback, fromPromise } from "xstate";
|
|
|
4
4
|
import { loadAppConfigFile } from "../util/app-config.js";
|
|
5
5
|
import { loadDeveloperConfig as loadDeveloperConfigFile } from "../util/load-developer-config.js";
|
|
6
6
|
import Spinner from "tiny-spinner";
|
|
7
|
+
import { ensureAuthed } from "../api/ensure-authed.js";
|
|
7
8
|
export const loadDeveloperConfig = fromCallback(({ sendBack }) => {
|
|
8
9
|
const load = async () => {
|
|
9
10
|
const config = await loadDeveloperConfigFile();
|
|
@@ -11,9 +12,10 @@ export const loadDeveloperConfig = fromCallback(({ sendBack }) => {
|
|
|
11
12
|
sendBack({ type: "No Developer Config", configError: config });
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
15
|
+
const token = await ensureAuthed();
|
|
14
16
|
sendBack({
|
|
15
17
|
type: "Developer Config Loaded",
|
|
16
|
-
token
|
|
18
|
+
token,
|
|
17
19
|
slug: config.developer_slug,
|
|
18
20
|
});
|
|
19
21
|
};
|
|
@@ -25,6 +25,7 @@ import { printInstallInstructions } from "../util/print-install-instructions.js"
|
|
|
25
25
|
import { printLogo } from "./actions.js";
|
|
26
26
|
import { askWithTypedChoices, fromCallbackWithErrorHandling } from "./actors.js";
|
|
27
27
|
import { fetchWorkspaces } from "../api/fetch-workspaces.js";
|
|
28
|
+
import { ensureAuthed } from "../api/ensure-authed.js";
|
|
28
29
|
process.on("SIGINT", () => {
|
|
29
30
|
process.stdout.write("\x1B[?25h");
|
|
30
31
|
process.exit();
|
|
@@ -59,7 +60,9 @@ export const devMachine = setup({
|
|
|
59
60
|
sendBack({ type: "Initialization Error", error: config });
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
ensureAuthed().then((token) => {
|
|
64
|
+
sendBack({ type: "Initialized", config: { ...config, token } });
|
|
65
|
+
});
|
|
63
66
|
}),
|
|
64
67
|
"keyboard": fromCallbackWithErrorHandling(({ sendBack }) => {
|
|
65
68
|
if (!process.stdin.isTTY)
|
|
@@ -129,7 +132,7 @@ export const devMachine = setup({
|
|
|
129
132
|
if (typeof packageJson === "string")
|
|
130
133
|
throw packageJson;
|
|
131
134
|
const devVersion = await createDevVersion({
|
|
132
|
-
token:
|
|
135
|
+
token: await ensureAuthed(),
|
|
133
136
|
devSlug: config.developer_slug,
|
|
134
137
|
appId: app.id,
|
|
135
138
|
targetWorkspaceId: config.target_workspace_id,
|
|
@@ -13,6 +13,7 @@ import chalk from "chalk";
|
|
|
13
13
|
import boxen from "boxen";
|
|
14
14
|
import { printInstallInstructions } from "../util/print-install-instructions.js";
|
|
15
15
|
import { getAppInfo } from "../api/get-app-info.js";
|
|
16
|
+
import { ensureAuthed } from "../api/ensure-authed.js";
|
|
16
17
|
export const languages = [
|
|
17
18
|
{ name: "TypeScript (recommended)", value: "typescript" },
|
|
18
19
|
{ name: "JavaScript", value: "javascript" },
|
|
@@ -75,9 +76,10 @@ export const initMachine = setup({
|
|
|
75
76
|
sendBack({ type: "No Config", configError: config });
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
79
|
+
const token = await ensureAuthed();
|
|
78
80
|
sendBack({
|
|
79
81
|
type: "Config Loaded",
|
|
80
|
-
token
|
|
82
|
+
token,
|
|
81
83
|
developerSlug: config.developer_slug,
|
|
82
84
|
});
|
|
83
85
|
};
|
|
@@ -86,13 +88,18 @@ export const initMachine = setup({
|
|
|
86
88
|
loadAppInfo: fromPromise(async ({ input: { token, developerSlug, appSlug } }) => {
|
|
87
89
|
const spinner = new Spinner();
|
|
88
90
|
spinner.start("Loading app information...");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
try {
|
|
92
|
+
const appInfo = await getAppInfo({ token, developerSlug, appSlug });
|
|
93
|
+
if (appInfo === null) {
|
|
94
|
+
spinner.error("App not found");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
spinner.success(`App found: ${appInfo.title}`);
|
|
98
|
+
return appInfo;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
spinner.stop();
|
|
93
102
|
}
|
|
94
|
-
spinner.success(`App found: ${appInfo.title}`);
|
|
95
|
-
return appInfo;
|
|
96
103
|
}),
|
|
97
104
|
},
|
|
98
105
|
actions: {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {TextBlock, useAsyncCache} from "attio/client"
|
|
3
|
+
import getCatFact from "./get-cat-fact.server"
|
|
4
|
+
|
|
5
|
+
export function CatFact({recordId}) {
|
|
6
|
+
// By passing in the recordId, the result will be cached for each recordId
|
|
7
|
+
const {
|
|
8
|
+
values: {catFact},
|
|
9
|
+
// ^^^^^^^– this key matches
|
|
10
|
+
// vvvvvvv– this key
|
|
11
|
+
} = useAsyncCache({catFact: [getCatFact, recordId]})
|
|
12
|
+
// ^^^^^^^^^^ ^^^^^^^^
|
|
13
|
+
// async fn parameter(s)
|
|
14
|
+
|
|
15
|
+
return <TextBlock align="center">{`"${catFact}"`}</TextBlock>
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export default async function getCatFact(recordId) {
|
|
2
|
+
// We don't really need the recordId for this API, but this is how we could use a parameter
|
|
3
|
+
const response = await fetch(`https://catfact.ninja/fact?${recordId}`)
|
|
4
|
+
const data = await response.json()
|
|
5
|
+
return data.fact
|
|
6
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import {TextBlock} from "attio/client"
|
|
3
|
-
import {
|
|
3
|
+
import {CatFact} from "./cat-fact"
|
|
4
4
|
|
|
5
|
-
const Loading = () => <TextBlock>Loading
|
|
5
|
+
const Loading = () => <TextBlock>Loading cat fact...</TextBlock>
|
|
6
6
|
|
|
7
|
-
export function HelloWorldDialog
|
|
7
|
+
export function HelloWorldDialog({recordId}) {
|
|
8
|
+
// A simple counter to demonstrate that this is just regular React code.
|
|
8
9
|
const [seconds, setSeconds] = React.useState(0)
|
|
9
10
|
React.useEffect(() => {
|
|
10
11
|
const timeout = setTimeout(() => setSeconds(seconds + 1), 1000)
|
|
@@ -16,9 +17,9 @@ export function HelloWorldDialog ({recordId}) {
|
|
|
16
17
|
<TextBlock align="left">
|
|
17
18
|
I am a dialog. I have been open for: {seconds} second{seconds === 1 ? "" : "s"}
|
|
18
19
|
</TextBlock>
|
|
19
|
-
{/* The hook in
|
|
20
|
+
{/* The hook in CatFact will suspend until the cat fact is loaded. */}
|
|
20
21
|
<React.Suspense fallback={<Loading />}>
|
|
21
|
-
<
|
|
22
|
+
<CatFact recordId={recordId} />
|
|
22
23
|
</React.Suspense>
|
|
23
24
|
</>
|
|
24
25
|
)
|