@crossmint/cli 1.1.2

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 (140) hide show
  1. package/README.md +111 -0
  2. package/build-pkg.js +13 -0
  3. package/dist/bin/crossmint.d.ts +3 -0
  4. package/dist/bin/crossmint.d.ts.map +1 -0
  5. package/dist/bin/crossmint.js +18 -0
  6. package/dist/commands/keys/create.d.ts +2 -0
  7. package/dist/commands/keys/create.d.ts.map +1 -0
  8. package/dist/commands/keys/create.js +2 -0
  9. package/dist/commands/keys/create.test.d.ts +2 -0
  10. package/dist/commands/keys/create.test.d.ts.map +1 -0
  11. package/dist/commands/keys/delete.d.ts +2 -0
  12. package/dist/commands/keys/delete.d.ts.map +1 -0
  13. package/dist/commands/keys/delete.js +1 -0
  14. package/dist/commands/keys/delete.test.d.ts +2 -0
  15. package/dist/commands/keys/delete.test.d.ts.map +1 -0
  16. package/dist/commands/keys/edit.d.ts +2 -0
  17. package/dist/commands/keys/edit.d.ts.map +1 -0
  18. package/dist/commands/keys/edit.js +2 -0
  19. package/dist/commands/keys/edit.test.d.ts +2 -0
  20. package/dist/commands/keys/edit.test.d.ts.map +1 -0
  21. package/dist/commands/keys/index.d.ts +5 -0
  22. package/dist/commands/keys/index.d.ts.map +1 -0
  23. package/dist/commands/keys/index.js +11 -0
  24. package/dist/commands/keys/list.d.ts +3 -0
  25. package/dist/commands/keys/list.d.ts.map +1 -0
  26. package/dist/commands/keys/list.js +10 -0
  27. package/dist/commands/keys/list.test.d.ts +2 -0
  28. package/dist/commands/keys/list.test.d.ts.map +1 -0
  29. package/dist/commands/login.d.ts +2 -0
  30. package/dist/commands/login.d.ts.map +1 -0
  31. package/dist/commands/login.js +1 -0
  32. package/dist/commands/login.test.d.ts +2 -0
  33. package/dist/commands/login.test.d.ts.map +1 -0
  34. package/dist/commands/logout.d.ts +2 -0
  35. package/dist/commands/logout.d.ts.map +1 -0
  36. package/dist/commands/logout.js +1 -0
  37. package/dist/commands/projects/create.d.ts +6 -0
  38. package/dist/commands/projects/create.d.ts.map +1 -0
  39. package/dist/commands/projects/create.js +6 -0
  40. package/dist/commands/projects/create.test.d.ts +2 -0
  41. package/dist/commands/projects/create.test.d.ts.map +1 -0
  42. package/dist/commands/projects/details.d.ts +2 -0
  43. package/dist/commands/projects/details.d.ts.map +1 -0
  44. package/dist/commands/projects/details.js +2 -0
  45. package/dist/commands/projects/details.test.d.ts +2 -0
  46. package/dist/commands/projects/details.test.d.ts.map +1 -0
  47. package/dist/commands/projects/index.d.ts +4 -0
  48. package/dist/commands/projects/index.d.ts.map +1 -0
  49. package/dist/commands/projects/index.js +7 -0
  50. package/dist/commands/projects/select.d.ts +2 -0
  51. package/dist/commands/projects/select.d.ts.map +1 -0
  52. package/dist/commands/projects/select.js +1 -0
  53. package/dist/commands/projects/select.test.d.ts +2 -0
  54. package/dist/commands/projects/select.test.d.ts.map +1 -0
  55. package/dist/commands/whoami.d.ts +2 -0
  56. package/dist/commands/whoami.d.ts.map +1 -0
  57. package/dist/commands/whoami.js +1 -0
  58. package/dist/types/keys.d.ts +11 -0
  59. package/dist/types/keys.d.ts.map +1 -0
  60. package/dist/types/keys.js +0 -0
  61. package/dist/utils/Pager.d.ts +9 -0
  62. package/dist/utils/Pager.d.ts.map +1 -0
  63. package/dist/utils/Pager.js +2 -0
  64. package/dist/utils/Pager.test.d.ts +2 -0
  65. package/dist/utils/Pager.test.d.ts.map +1 -0
  66. package/dist/utils/fetch.d.ts +5 -0
  67. package/dist/utils/fetch.d.ts.map +1 -0
  68. package/dist/utils/fetch.js +1 -0
  69. package/dist/utils/getProject.d.ts +4 -0
  70. package/dist/utils/getProject.d.ts.map +1 -0
  71. package/dist/utils/getProject.js +1 -0
  72. package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.d.ts +6 -0
  73. package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.d.ts.map +1 -0
  74. package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.js +2 -0
  75. package/dist/utils/keytar.d.ts +4 -0
  76. package/dist/utils/keytar.d.ts.map +1 -0
  77. package/dist/utils/keytar.js +1 -0
  78. package/dist/utils/oauth/codeChallenge.d.ts +3 -0
  79. package/dist/utils/oauth/codeChallenge.d.ts.map +1 -0
  80. package/dist/utils/oauth/codeChallenge.js +1 -0
  81. package/dist/utils/oauth/getStytchConfig.d.ts +6 -0
  82. package/dist/utils/oauth/getStytchConfig.d.ts.map +1 -0
  83. package/dist/utils/oauth/getStytchConfig.js +1 -0
  84. package/dist/utils/oauth/server.d.ts +3 -0
  85. package/dist/utils/oauth/server.d.ts.map +1 -0
  86. package/dist/utils/oauth/server.js +1 -0
  87. package/dist/utils/scopes.d.ts +14 -0
  88. package/dist/utils/scopes.d.ts.map +1 -0
  89. package/dist/utils/scopes.js +1 -0
  90. package/dist/utils/spinner.d.ts +6 -0
  91. package/dist/utils/spinner.d.ts.map +1 -0
  92. package/dist/utils/spinner.js +1 -0
  93. package/dist/utils/store.d.ts +11 -0
  94. package/dist/utils/store.d.ts.map +1 -0
  95. package/dist/utils/store.js +1 -0
  96. package/dist/utils/urls.d.ts +9 -0
  97. package/dist/utils/urls.d.ts.map +1 -0
  98. package/dist/utils/urls.js +1 -0
  99. package/package.json +50 -0
  100. package/shims/prelude.js +33 -0
  101. package/src/bin/crossmint.ts +34 -0
  102. package/src/commands/keys/create.test.ts +230 -0
  103. package/src/commands/keys/create.ts +96 -0
  104. package/src/commands/keys/delete.test.ts +87 -0
  105. package/src/commands/keys/delete.ts +36 -0
  106. package/src/commands/keys/edit.test.ts +159 -0
  107. package/src/commands/keys/edit.ts +98 -0
  108. package/src/commands/keys/index.ts +4 -0
  109. package/src/commands/keys/list.test.ts +123 -0
  110. package/src/commands/keys/list.ts +83 -0
  111. package/src/commands/login.test.ts +87 -0
  112. package/src/commands/login.ts +59 -0
  113. package/src/commands/logout.ts +9 -0
  114. package/src/commands/projects/create.test.ts +82 -0
  115. package/src/commands/projects/create.ts +45 -0
  116. package/src/commands/projects/details.test.ts +64 -0
  117. package/src/commands/projects/details.ts +36 -0
  118. package/src/commands/projects/index.ts +3 -0
  119. package/src/commands/projects/select.test.ts +86 -0
  120. package/src/commands/projects/select.ts +50 -0
  121. package/src/commands/whoami.ts +19 -0
  122. package/src/types/keys.ts +13 -0
  123. package/src/utils/Pager.test.ts +70 -0
  124. package/src/utils/Pager.ts +34 -0
  125. package/src/utils/fetch.ts +63 -0
  126. package/src/utils/getProject.ts +10 -0
  127. package/src/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.ts +52 -0
  128. package/src/utils/keytar.ts +15 -0
  129. package/src/utils/oauth/codeChallenge.ts +18 -0
  130. package/src/utils/oauth/getStytchConfig.ts +28 -0
  131. package/src/utils/oauth/server.ts +66 -0
  132. package/src/utils/scopes.ts +66 -0
  133. package/src/utils/spinner.ts +20 -0
  134. package/src/utils/store.ts +38 -0
  135. package/src/utils/urls.ts +11 -0
  136. package/tsconfig.json +35 -0
  137. package/tsconfig.tsbuildinfo +1 -0
  138. package/tsup.config.ts +11 -0
  139. package/vitest.config.ts +9 -0
  140. package/vitest.setup.ts +8 -0
@@ -0,0 +1,59 @@
1
+ import { Environment, getCrossmintUrl } from "@/utils/urls";
2
+ import { setEnvironment } from "@/utils/store";
3
+ import { select } from "@inquirer/prompts";
4
+ import selectProject from "./projects/select";
5
+ import open from "open";
6
+ import { generateCodeChallenge } from "@/utils/oauth/codeChallenge";
7
+ import server from "@/utils/oauth/server";
8
+ import { getStytchConfig } from "@/utils/oauth/getStytchConfig";
9
+ import { routes } from "@crossmint/common-consts";
10
+
11
+ const isLocal = process.env.NODE_ENV === "development";
12
+
13
+ export default async function login() {
14
+ try {
15
+ console.log("🔐 Crossmint CLI - Login");
16
+
17
+ const environment = await select<Environment>({
18
+ message: "Select environment:",
19
+ choices: [
20
+ { name: "Production", value: "production" },
21
+ { name: "Staging", value: "staging" },
22
+ ...(isLocal ? [{ name: "Local", value: "local" as Environment }] : []),
23
+ ],
24
+ });
25
+
26
+ const baseUrl = getCrossmintUrl(environment);
27
+ if (!baseUrl) {
28
+ console.error(`Error: No URL found for ${environment} environment`);
29
+ process.exit(1);
30
+ }
31
+
32
+ await setEnvironment(environment);
33
+
34
+ const serverPromise = server(environment);
35
+
36
+ const { clientId } = await getStytchConfig();
37
+
38
+ const codeChallenge = await generateCodeChallenge();
39
+ const options = {
40
+ code_challenge: codeChallenge,
41
+ code_challenge_method: "S256",
42
+ scope: "full_access",
43
+ response_type: "code",
44
+ grant_type: "authorization_code",
45
+ redirect_uri: "http://127.0.0.1:3456/callback",
46
+ client_id: clientId,
47
+ };
48
+ const authUrl = `${baseUrl}${routes.console.authorizeDevice.index(options)}`;
49
+ console.log("Opening browser for authentication...");
50
+ open(authUrl);
51
+ await serverPromise;
52
+
53
+ console.log(`✅ Login successful! You are now authenticated in the ${environment} environment.`);
54
+ await selectProject();
55
+ } catch {
56
+ console.error("❌ Login failed, please try again.");
57
+ process.exit(1);
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { deleteEnvironment, deleteProjectId, deleteSessionToken } from "@/utils/store";
2
+
3
+ export default async function logout(): Promise<void> {
4
+ await deleteSessionToken();
5
+ await deleteEnvironment();
6
+ await deleteProjectId();
7
+ console.log("Logged out");
8
+ process.exit(0);
9
+ }
@@ -0,0 +1,82 @@
1
+ import { vi, describe, it, expect, beforeEach } from "vitest";
2
+ import createProject from "./create";
3
+ import { fetchAuthenticated } from "@/utils/fetch";
4
+ import { confirm, input } from "@inquirer/prompts";
5
+ import { HttpMethods } from "@crossmint/common-consts";
6
+ import { routes } from "@crossmint/common-consts";
7
+ import { setProjectId } from "@/utils/store";
8
+ import { createSpinner } from "@/utils/spinner";
9
+
10
+ vi.mock("@/utils/fetch");
11
+ vi.mock("@inquirer/prompts");
12
+ vi.mock("@/utils/store");
13
+ vi.mock("@/utils/spinner");
14
+
15
+ describe("createProject", () => {
16
+ let spinner: any;
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ spinner = {
20
+ start: vi.fn(),
21
+ succeed: vi.fn(),
22
+ fail: vi.fn(),
23
+ };
24
+ vi.mocked(createSpinner).mockReturnValue(spinner);
25
+ });
26
+
27
+ const mockResponse = JSON.stringify({ _id: "123", kableClientId: "456" });
28
+ it("should create a project with provided name", async () => {
29
+ const projectName = "Test Project";
30
+ vi.mocked(fetchAuthenticated).mockResolvedValue(mockResponse);
31
+ vi.mocked(confirm).mockResolvedValue(true);
32
+
33
+ await createProject({ name: projectName });
34
+
35
+ expect(fetchAuthenticated).toHaveBeenCalledWith(routes.api.console.projects.index, HttpMethods.POST, {
36
+ name: projectName,
37
+ walletType: "smart_wallet",
38
+ storageProvider: "ipfs",
39
+ projectThumbnail: "",
40
+ });
41
+ expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining("Creating project"));
42
+ expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining("✅ Project created successfully!"));
43
+ expect(setProjectId).toHaveBeenCalledWith("123");
44
+ expect(input).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it("should prompt for project name if not provided", async () => {
48
+ const projectName = "Test Project";
49
+ vi.mocked(fetchAuthenticated).mockResolvedValue(mockResponse);
50
+ vi.mocked(input).mockResolvedValue(projectName);
51
+ vi.mocked(confirm).mockResolvedValue(true);
52
+
53
+ await createProject();
54
+
55
+ expect(input).toHaveBeenCalledWith({
56
+ message: "Enter project name:",
57
+ validate: expect.any(Function),
58
+ });
59
+ expect(fetchAuthenticated).toHaveBeenCalledWith(routes.api.console.projects.index, HttpMethods.POST, {
60
+ name: projectName,
61
+ walletType: "smart_wallet",
62
+ storageProvider: "ipfs",
63
+ projectThumbnail: "",
64
+ });
65
+ expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining("Creating project"));
66
+ expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining("✅ Project created successfully!"));
67
+ expect(setProjectId).toHaveBeenCalledWith("123");
68
+ });
69
+ it("should not select project if user does not confirm", async () => {
70
+ vi.mocked(confirm).mockResolvedValue(false);
71
+ await createProject();
72
+ expect(setProjectId).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it("should handle API errors", async () => {
76
+ const error = new Error("API Error");
77
+ vi.mocked(fetchAuthenticated).mockRejectedValue(error);
78
+
79
+ await expect(createProject({ name: "Test Project" })).rejects.toThrow();
80
+ expect(spinner.fail).toHaveBeenCalledWith(expect.stringContaining("Failed to create project"));
81
+ });
82
+ });
@@ -0,0 +1,45 @@
1
+ import { confirm, input } from "@inquirer/prompts";
2
+ import { fetchAuthenticated } from "@/utils/fetch";
3
+ import { HttpMethods } from "@crossmint/common-consts";
4
+ import { routes } from "@crossmint/common-consts";
5
+ import { setProjectId } from "@/utils/store";
6
+ import { createSpinner } from "@/utils/spinner";
7
+ interface CreateProjectOptions {
8
+ name?: string;
9
+ }
10
+
11
+ export default async function createProject(options: CreateProjectOptions = {}) {
12
+ const spinner = createSpinner();
13
+ try {
14
+ let projectName = options.name;
15
+
16
+ if (!projectName) {
17
+ projectName = await input({
18
+ message: "Enter project name:",
19
+ validate: (input) => input.length > 0 || "Project name is required",
20
+ });
21
+ }
22
+
23
+ spinner.start(`🚀 Creating project "${projectName}" with Smart Wallets...`);
24
+ const response = await fetchAuthenticated(routes.api.console.projects.index, HttpMethods.POST, {
25
+ name: projectName,
26
+ walletType: "smart_wallet",
27
+ storageProvider: "ipfs",
28
+ projectThumbnail: "",
29
+ });
30
+ const project = JSON.parse(response);
31
+ spinner.succeed(`✅ Project created successfully!\n\n`);
32
+ console.log(`📌 Project Details:\nName: ${projectName}\nID: ${project.kableClientId}\n`);
33
+ const selectProject = await confirm({
34
+ message: "Do you want to select this project?",
35
+ });
36
+ if (selectProject) {
37
+ await setProjectId(project._id);
38
+ console.log(`✅Project '${projectName}' selected`);
39
+ }
40
+ process.exit(0);
41
+ } catch (error) {
42
+ spinner.fail(`❌ Failed to create project: ${error instanceof Error ? error.message : "Unknown error"}`);
43
+ process.exit(1);
44
+ }
45
+ }
@@ -0,0 +1,64 @@
1
+ import { vi, describe, it, expect, beforeEach } from "vitest";
2
+ import showProjectDetails from "./details";
3
+ import { fetchAuthenticated } from "@/utils/fetch";
4
+ import { getProjectId } from "@/utils/store";
5
+
6
+ // Mock dependencies
7
+ vi.mock("@/utils/fetch", () => ({
8
+ fetchAuthenticated: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("@/utils/store", () => ({
12
+ getProjectId: vi.fn(),
13
+ }));
14
+
15
+ describe("showProjectDetails", () => {
16
+ const mockProject = {
17
+ id: "test-project-id",
18
+ name: "Test Project",
19
+ kableClientId: "test-client-id",
20
+ addons: {
21
+ nonCustodialWallets: true,
22
+ },
23
+ };
24
+
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ vi.mocked(getProjectId).mockResolvedValue("test-project-id");
28
+ vi.mocked(fetchAuthenticated).mockResolvedValue([mockProject]);
29
+ });
30
+
31
+ it("should show details of the currently selected project", async () => {
32
+ const consoleSpy = vi.spyOn(console, "log");
33
+ await showProjectDetails();
34
+
35
+ expect(getProjectId).toHaveBeenCalled();
36
+ expect(fetchAuthenticated).toHaveBeenCalled();
37
+ expect(consoleSpy).toHaveBeenCalledWith("\n📌 Project Details:");
38
+ expect(consoleSpy).toHaveBeenCalledWith("Name: Test Project");
39
+ expect(consoleSpy).toHaveBeenCalledWith("ID: test-client-id");
40
+ expect(consoleSpy).toHaveBeenCalledWith("Wallet Type: Smart");
41
+ });
42
+
43
+ it("should handle case when no project is selected", async () => {
44
+ vi.mocked(getProjectId).mockResolvedValue(null);
45
+
46
+ await expect(showProjectDetails()).rejects.toThrow();
47
+ expect(process.exit).toHaveBeenCalledWith(1);
48
+ });
49
+
50
+ it("should handle case when project is not found", async () => {
51
+ vi.mocked(fetchAuthenticated).mockResolvedValue([]);
52
+
53
+ await expect(showProjectDetails()).rejects.toThrow();
54
+ expect(process.exit).toHaveBeenCalledWith(1);
55
+ });
56
+
57
+ it("should handle API errors", async () => {
58
+ const error = new Error("API Error");
59
+ vi.mocked(fetchAuthenticated).mockRejectedValue(error);
60
+
61
+ await expect(showProjectDetails()).rejects.toThrow();
62
+ expect(process.exit).toHaveBeenCalledWith(1);
63
+ });
64
+ });
@@ -0,0 +1,36 @@
1
+ import { getProjectId } from "@/utils/store";
2
+ import { createSpinner } from "@/utils/spinner";
3
+ import { getProject } from "@/utils/getProject";
4
+
5
+ export default async function showProjectDetails() {
6
+ const projectId = await getProjectId();
7
+ if (!projectId) {
8
+ console.error("No project selected. Please select a project first using `crossmint project select`");
9
+ process.exit(1);
10
+ }
11
+
12
+ const spinner = createSpinner("Fetching project details...").start();
13
+
14
+ try {
15
+ const project = await getProject(projectId);
16
+
17
+ if (!project) {
18
+ spinner.fail("Project not found");
19
+ process.exit(1);
20
+ }
21
+
22
+ spinner.succeed("Fetched project details");
23
+
24
+ console.log("\n📌 Project Details:");
25
+
26
+ console.log(`Name: ${project.name}`);
27
+ console.log(`ID: ${project.kableClientId}`);
28
+ console.log(`Wallet Type: ${project.addons.nonCustodialWallets ? "Smart" : "Custodial"}`);
29
+
30
+ process.exit(0);
31
+ } catch (error) {
32
+ spinner.fail("Failed to fetch project details");
33
+ console.error("Error:", error instanceof Error ? error.message : "Unknown error");
34
+ process.exit(1);
35
+ }
36
+ }
@@ -0,0 +1,3 @@
1
+ export { default as select } from "./select";
2
+ export { default as create } from "./create";
3
+ export { default as details } from "./details";
@@ -0,0 +1,86 @@
1
+ import { vi, describe, it, expect, beforeEach } from "vitest";
2
+ import selectProject from "./select";
3
+ import { fetchAuthenticated } from "@/utils/fetch";
4
+ import { setProjectId } from "@/utils/store";
5
+ import { select } from "@inquirer/prompts";
6
+ import { createSpinner } from "@/utils/spinner";
7
+
8
+ vi.mock("@/utils/fetch", () => ({
9
+ fetchAuthenticated: vi.fn(),
10
+ }));
11
+ vi.mock("@/utils/store");
12
+ vi.mock("@inquirer/prompts");
13
+ vi.mock("@/utils/spinner");
14
+
15
+ describe("selectProject", () => {
16
+ const mockProjects = [
17
+ { id: "1", name: "Project 1", description: "First project" },
18
+ { id: "2", name: "Project 2", description: "Second project" },
19
+ ];
20
+
21
+ let spinner: any;
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ spinner = {
26
+ start: vi.fn(() => spinner),
27
+ succeed: vi.fn(),
28
+ fail: vi.fn(),
29
+ };
30
+ vi.mocked(createSpinner).mockReturnValue(spinner);
31
+ });
32
+
33
+ it("should handle no projects", async () => {
34
+ vi.mocked(fetchAuthenticated).mockResolvedValue([]);
35
+ const consoleSpy = vi.spyOn(console, "log");
36
+
37
+ await selectProject();
38
+
39
+ expect(spinner.fail).toHaveBeenCalledWith(
40
+ "No projects found. Create a new project using `crossmint project create`"
41
+ );
42
+ expect(setProjectId).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it("should auto-select single project", async () => {
46
+ const singleProject = [mockProjects[0]];
47
+ vi.mocked(fetchAuthenticated).mockResolvedValue(singleProject);
48
+ const consoleSpy = vi.spyOn(console, "log");
49
+
50
+ await selectProject();
51
+
52
+ expect(consoleSpy).toHaveBeenCalledWith(`You only have one project: ${singleProject[0].name}`);
53
+ expect(consoleSpy).toHaveBeenCalledWith("To create another project, run `crossmint project create`");
54
+ expect(setProjectId).toHaveBeenCalledWith(singleProject[0].id);
55
+ expect(select).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("should allow project selection from multiple projects", async () => {
59
+ vi.mocked(fetchAuthenticated).mockResolvedValue(mockProjects);
60
+ vi.mocked(select).mockResolvedValue(mockProjects[1]);
61
+ const consoleSpy = vi.spyOn(console, "log");
62
+
63
+ await selectProject();
64
+
65
+ expect(select).toHaveBeenCalledWith({
66
+ message: "Select a project:",
67
+ choices: mockProjects.map((project) => ({
68
+ name: project.name,
69
+ value: project,
70
+ description: project.description,
71
+ })),
72
+ });
73
+ expect(setProjectId).toHaveBeenCalledWith(mockProjects[1].id);
74
+ expect(consoleSpy).toHaveBeenCalledWith(`✅ Project ${mockProjects[1].name} selected`);
75
+ });
76
+
77
+ it("should handle API errors", async () => {
78
+ const error = new Error("API Error");
79
+ vi.mocked(fetchAuthenticated).mockRejectedValue(error);
80
+
81
+ await expect(selectProject()).rejects.toThrow();
82
+ expect(spinner.fail).toHaveBeenCalledWith(expect.stringContaining("Failed to fetch projects"));
83
+ expect(setProjectId).not.toHaveBeenCalled();
84
+ expect(select).not.toHaveBeenCalled();
85
+ });
86
+ });
@@ -0,0 +1,50 @@
1
+ import { fetchAuthenticated } from "@/utils/fetch";
2
+ import { routes } from "@crossmint/common-consts";
3
+ import { setProjectId } from "@/utils/store";
4
+ import { select } from "@inquirer/prompts";
5
+ import { createSpinner } from "@/utils/spinner";
6
+
7
+ interface Project {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ }
12
+
13
+ export default async function selectProject() {
14
+ const spinner = createSpinner("Fetching projects...").start();
15
+ try {
16
+ const projects = (await fetchAuthenticated(routes.api.projects)) as Project[];
17
+
18
+ if (!projects || projects.length === 0) {
19
+ spinner.fail("No projects found. Create a new project using `crossmint project create`");
20
+ return process.exit(0);
21
+ }
22
+
23
+ spinner.succeed("Fetched projects");
24
+
25
+ if (projects.length === 1) {
26
+ const project = projects[0];
27
+ console.log(`You only have one project: ${project.name}`);
28
+ console.log("To create another project, run `crossmint project create`");
29
+ await setProjectId(project.id);
30
+ console.log(`✅ Project ${project.name} selected`);
31
+ return process.exit(0);
32
+ }
33
+
34
+ const selectedProject = await select<Project>({
35
+ message: "Select a project:",
36
+ choices: projects.map((project) => ({
37
+ name: project.name,
38
+ value: project,
39
+ description: project.description,
40
+ })),
41
+ });
42
+
43
+ await setProjectId(selectedProject.id);
44
+ console.log(`✅ Project ${selectedProject.name} selected`);
45
+ process.exit(0);
46
+ } catch (error) {
47
+ spinner.fail("Failed to fetch projects");
48
+ process.exit(1);
49
+ }
50
+ }
@@ -0,0 +1,19 @@
1
+ import { fetchAuthenticated } from "@/utils/fetch";
2
+ import { getEnvironment } from "@/utils/store";
3
+ import { routes } from "@crossmint/common-consts";
4
+ import { getProjectId } from "@/utils/store";
5
+ import { getProject } from "@/utils/getProject";
6
+ import { capitalizeFirstLetter } from "@crossmint/common-string-utils";
7
+
8
+ export default async function whoami() {
9
+ const environment = await getEnvironment();
10
+ const projectId = await getProjectId();
11
+
12
+ const project = await getProject(projectId || "");
13
+
14
+ const response = await fetchAuthenticated(routes.authentication.getSession);
15
+ console.log(
16
+ `You are logged in to 🍀Crossmint as: ${response.user.email} - Environment: ${capitalizeFirstLetter(environment!)} - ${project ? `Project: ${project.name}` : "No project selected"}`
17
+ );
18
+ process.exit(0);
19
+ }
@@ -0,0 +1,13 @@
1
+ import { APIKeyScopes } from "@crossmint/products-console-types";
2
+
3
+ import { APIKeyUsageOrigin } from "@crossmint/products-console-types";
4
+
5
+ export type ClientPlatform = "web" | "mobile";
6
+
7
+ export type APIKeyPayload = {
8
+ apiKeyId?: string;
9
+ usageOrigin: APIKeyUsageOrigin;
10
+ scopes: APIKeyScopes[];
11
+ whitelistedOrigins?: string[];
12
+ whitelistedAppIdentifiers?: string[];
13
+ };
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Pager } from "./Pager";
3
+
4
+ // Mock child_process.spawn
5
+ vi.mock("child_process", () => ({
6
+ spawn: vi.fn(() => ({
7
+ stdin: {
8
+ write: vi.fn(),
9
+ end: vi.fn(),
10
+ kill: vi.fn(),
11
+ },
12
+ on: vi.fn((event, callback) => {
13
+ if (event === "exit") {
14
+ callback(0);
15
+ }
16
+ }),
17
+ })),
18
+ }));
19
+
20
+ describe("Pager", () => {
21
+ let pager: Pager;
22
+ let consoleSpy: ReturnType<typeof vi.spyOn>;
23
+ let mockStdin: any;
24
+ beforeEach(() => {
25
+ pager = new Pager();
26
+ consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
27
+ vi.clearAllMocks();
28
+ mockStdin = {
29
+ write: vi.fn(),
30
+ end: vi.fn(),
31
+ };
32
+ (pager as any).pager.stdin = mockStdin;
33
+ (pager as any).pager.kill = vi.fn();
34
+ });
35
+
36
+ it("should accumulate output when adding content", () => {
37
+ pager.add("test1");
38
+ pager.add("test2");
39
+ pager.write();
40
+
41
+ expect(pager["output"]).toBe("test1test2");
42
+ });
43
+
44
+ it("should write accumulated output to stdin when terminal height is exceeded", () => {
45
+ process.stdout.rows = 1;
46
+ const textContent = "test content\nwith multiple lines";
47
+
48
+ pager.add(textContent);
49
+ pager.write();
50
+
51
+ expect(mockStdin.write).toHaveBeenCalledWith(textContent);
52
+ expect(mockStdin.end).toHaveBeenCalled();
53
+ });
54
+ it("should not use pager when terminal height is not exceeded", () => {
55
+ process.stdout.rows = 10;
56
+
57
+ pager.add("test content");
58
+ pager.write();
59
+
60
+ expect(mockStdin.write).not.toHaveBeenCalled();
61
+ expect(mockStdin.end).not.toHaveBeenCalled();
62
+ expect((pager as any).pager.kill).toHaveBeenCalled();
63
+ });
64
+
65
+ it("should log output and exit when pager closes", () => {
66
+ pager = new Pager();
67
+ expect(consoleSpy).toHaveBeenCalledWith("");
68
+ expect(process.exit).toHaveBeenCalledWith(0);
69
+ });
70
+ });
@@ -0,0 +1,34 @@
1
+ import { spawn } from "child_process";
2
+
3
+ export class Pager {
4
+ private output = "";
5
+ private pager: ReturnType<typeof spawn>;
6
+
7
+ constructor() {
8
+ this.pager = spawn("less", ["-R"], { stdio: ["pipe", "inherit", "inherit"] });
9
+ this.pager.on("exit", (code) => {
10
+ console.log(this.output);
11
+ process.exit(code ?? 0);
12
+ });
13
+ }
14
+
15
+ add(content: string) {
16
+ this.output += content;
17
+ }
18
+
19
+ private shouldUsePager(): boolean {
20
+ const terminalHeight = process.stdout.rows;
21
+ const contentLines = this.output.split("\n").length;
22
+
23
+ return contentLines > terminalHeight;
24
+ }
25
+
26
+ write() {
27
+ if (this.shouldUsePager()) {
28
+ this.pager.stdin?.write(this.output);
29
+ this.pager.stdin?.end();
30
+ } else {
31
+ this.pager.kill();
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,63 @@
1
+ import { HttpMethods } from "@crossmint/common-consts";
2
+ import { getSessionToken, getEnvironment } from "./store";
3
+ import { getCrossmintUrl } from "./urls";
4
+
5
+ export async function fetchAuthenticated(path: string, method: HttpMethods = HttpMethods.GET, body?: any) {
6
+ const sessionToken = await getSessionToken();
7
+ const environment = await getEnvironment();
8
+ if (!sessionToken) {
9
+ console.error("❌ Session not found, please run crossmint login");
10
+ process.exit(1);
11
+ }
12
+
13
+ if (!environment) {
14
+ console.error("❌ Environment not found, please run crossmint login");
15
+ process.exit(1);
16
+ }
17
+
18
+ const url = `${getCrossmintUrl(environment)}${path}`;
19
+
20
+ return fetchJSON(url, method, body, {
21
+ Cookie: `sessionToken=${sessionToken}`,
22
+ });
23
+ }
24
+
25
+ export async function fetchUnauthenticated(path: string, method: HttpMethods = HttpMethods.GET, body?: any) {
26
+ const environment = await getEnvironment();
27
+ if (!environment) {
28
+ console.error("❌ Environment not found");
29
+ process.exit(1);
30
+ }
31
+
32
+ const url = `${getCrossmintUrl(environment)}${path}`;
33
+
34
+ return fetchJSON(url, method, body);
35
+ }
36
+
37
+ export async function fetchJSON(
38
+ url: string,
39
+ method: HttpMethods = HttpMethods.GET,
40
+ body?: any,
41
+ headers?: Record<string, string>
42
+ ) {
43
+ const options: RequestInit = {
44
+ method,
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ ...headers,
48
+ },
49
+ };
50
+
51
+ if (body) {
52
+ options.body = JSON.stringify(body);
53
+ }
54
+
55
+ const response = await fetch(url, options);
56
+
57
+ if (response.status >= 400) {
58
+ const error = await response.json();
59
+ throw new Error(`${error.message}`);
60
+ }
61
+
62
+ return await response.json();
63
+ }
@@ -0,0 +1,10 @@
1
+ import { DeveloperProject } from "@crossmint/common-types";
2
+ import { fetchAuthenticated } from "./fetch";
3
+ import { routes } from "@crossmint/common-consts";
4
+ import { PrimitiveProperties } from "@crossmint/data-dbs-types";
5
+
6
+ export async function getProject(projectId: string) {
7
+ const projects: PrimitiveProperties<DeveloperProject>[] = await fetchAuthenticated(routes.api.projects);
8
+ const project = projects.find((p) => p.id === projectId);
9
+ return project;
10
+ }