diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b5f4714f..7b54ab025 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -214,5 +214,5 @@ jobs: with: commit_message: "chore: update package versions [skip ci]" branch: main - file_pattern: 'package.json **/package.json **/schema.graphql' + file_pattern: 'package.json **/schema.graphql' diff --git a/bun.lockb b/bun.lockb index fb78aaa12..e79551b74 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index e6905a239..6f1744a71 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -1 +1,2 @@ -.env* \ No newline at end of file +.env* +*.schema.graphql \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 0751d37c7..ea658dfce 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,8 +52,8 @@ "@inquirer/input": "^3", "@inquirer/password": "^3", "@inquirer/select": "^3", - "@settlemint/sdk-js": "0.5.0", - "@settlemint/sdk-utils": "0.5.0", + "@settlemint/sdk-js": "workspace:*", + "@settlemint/sdk-utils": "workspace:*", "is-in-ci": "^1.0.0", "yoctocolors": "^2" }, diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 03674b95f..8ee6575d5 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -12,6 +12,7 @@ * ``` */ +import { codegenCommand } from "@/commands/codegen"; import { connectCommand } from "@/commands/connect"; import { Command } from "@commander-js/extra-typings"; import { ascii } from "@settlemint/sdk-utils/terminal"; @@ -37,6 +38,7 @@ sdkcli // Add commands to the CLI sdkcli.addCommand(connectCommand()); +sdkcli.addCommand(codegenCommand()); /** * Parses command line arguments and executes the appropriate command. diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts new file mode 100644 index 000000000..ab1610a73 --- /dev/null +++ b/packages/cli/src/commands/codegen.ts @@ -0,0 +1,36 @@ +import { gqltadaSpinner } from "@/commands/codegen/gqltada.spinner"; +import { Command } from "@commander-js/extra-typings"; +import { loadEnv } from "@settlemint/sdk-utils/environment"; +import { intro, outro } from "@settlemint/sdk-utils/terminal"; +import type { DotEnv } from "@settlemint/sdk-utils/validation"; +import { bold, italic, underline } from "yoctocolors"; + +/** + * Creates and returns the 'connect' command for the SettleMint SDK. + * This command initializes the setup of the SettleMint SDK in the user's project. + * It guides the user through a series of prompts to configure their environment, + * select services, and set up necessary files. + * + * @returns {Command} The configured 'connect' command + */ +export function codegenCommand(): Command { + return ( + new Command("codegen") + // Add options for various configuration parameters + .option("-e, --environment ", "The name of your environment, defaults to development", "development") + // Set the command description + .description("Generate GraphQL and REST types and queries") + // Define the action to be executed when the command is run + .action(async ({ environment }) => { + intro( + `Generating GraphQL types and queries for your dApp's ${italic(underline(bold(environment)))} environment`, + ); + + const env: DotEnv = await loadEnv(); + + await gqltadaSpinner(env); + + outro("Codegen complete"); + }) + ); +} diff --git a/packages/cli/src/commands/codegen/gqltada.spinner.ts b/packages/cli/src/commands/codegen/gqltada.spinner.ts new file mode 100644 index 000000000..63b0df5c3 --- /dev/null +++ b/packages/cli/src/commands/codegen/gqltada.spinner.ts @@ -0,0 +1,117 @@ +import { generateSchema } from "@gql.tada/cli-utils"; +import type { DotEnv } from "@settlemint/sdk-utils/validation"; + +/** + * Writes environment variables to .env files with a spinner for visual feedback. + * + * @param env - Partial environment variables to be written. + * @param environment - The name of the environment (e.g., "development", "production"). + * @returns A promise that resolves when the environment variables are written. + * @throws If there's an error writing the environment files. + * + * @example + * await writeEnvSpinner( + * { SETTLEMINT_INSTANCE: "https://example.com", SETTLEMINT_ACCESS_TOKEN: "token123" }, + * "development" + * ); + */ +export async function gqltadaSpinner(env: DotEnv) { + await gqltadaCodegen({ + type: "HASURA", + env, + }); + await gqltadaCodegen({ + type: "PORTAL", + env, + }); + await gqltadaCodegen({ + type: "THEGRAPH", + env, + allowToFail: true, + }); + await gqltadaCodegen({ + type: "THEGRAPH_FALLBACK", + env, + }); +} + +async function gqltadaCodegen(options: { + type: "HASURA" | "PORTAL" | "THEGRAPH" | "THEGRAPH_FALLBACK"; + env: DotEnv; + allowToFail?: boolean; +}) { + let gqlEndpoint: string | undefined = undefined; + let output: string; + let adminSecret: string | undefined = undefined; + const accessToken = options.env.SETTLEMINT_ACCESS_TOKEN; + + switch (options.type) { + case "HASURA": + gqlEndpoint = options.env.SETTLEMINT_HASURA_ENDPOINT; + output = "hasura.schema.graphql"; + adminSecret = options.env.SETTLEMINT_HASURA_ADMIN_SECRET; + break; + case "PORTAL": + gqlEndpoint = options.env.SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT; + output = "portal.schema.graphql"; + break; + case "THEGRAPH": + gqlEndpoint = options.env.SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT; + output = "thegraph.schema.graphql"; + break; + case "THEGRAPH_FALLBACK": + gqlEndpoint = options.env.SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT_FALLBACK; + output = "thegraph-fallback.schema.graphql"; + } + + if (!gqlEndpoint) { + return; + } + + const headers = { + ...(adminSecret && { "x-hasura-admin-secret": adminSecret }), + "x-auth-token": accessToken, + "Content-Type": "application/json", + }; + + try { + // Test the endpoint with a simple introspection query + const response = await fetch(gqlEndpoint, { + method: "POST", + headers, + body: JSON.stringify({ + query: ` + query { + __schema { + queryType { + name + } + } + } + `, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch schema: ${response.statusText}`); + } + + const data = await response.json(); + if (data.errors) { + throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`); + } + + await generateSchema({ + input: gqlEndpoint, + output, + tsconfig: undefined, + headers, + }); + } catch (error) { + if (options.allowToFail) { + // ignore + } else { + throw error; + } + } +} diff --git a/packages/cli/src/commands/connect.ts b/packages/cli/src/commands/connect.ts index 171fed423..f5088d9d3 100644 --- a/packages/cli/src/commands/connect.ts +++ b/packages/cli/src/commands/connect.ts @@ -68,30 +68,28 @@ export function connectCommand(): Command { const portal = await portalPrompt(env, middleware, autoAccept); const hdPrivateKey = await hdPrivateKeyPrompt(env, privateKey, autoAccept); - await writeEnvSpinner( - { - SETTLEMINT_ACCESS_TOKEN: accessToken, - SETTLEMINT_INSTANCE: instance, - SETTLEMINT_WORKSPACE: workspace.id, - SETTLEMINT_APPLICATION: application.id, - SETTLEMINT_HASURA: hasura?.id, - SETTLEMINT_HASURA_ENDPOINT: hasura?.endpoints.find((endpoint) => endpoint.id === "graphql")?.displayValue, - SETTLEMINT_HASURA_ADMIN_SECRET: hasura?.credentials.find((credential) => credential.id === "admin-secret") - ?.displayValue, - SETTLEMINT_THEGRAPH: thegraph?.id, - SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT: thegraph?.endpoints.find((endpoint) => endpoint.id === "graphql") - ?.displayValue, - SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT_FALLBACK: thegraph?.endpoints.find( - (endpoint) => endpoint.id === "default-subgraph-graphql", - )?.displayValue, - SETTLEMINT_PORTAL: portal?.id, - SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT: portal?.endpoints.find((endpoint) => endpoint.id === "graphql") - ?.displayValue, - SETTLEMINT_PORTAL_REST_ENDPOINT: portal?.endpoints.find((endpoint) => endpoint.id === "rest")?.displayValue, - SETTLEMINT_HD_PRIVATE_KEY: hdPrivateKey?.uniqueName, - }, - environment, - ); + await writeEnvSpinner({ + SETTLEMINT_ENVIRONMENT: environment, + SETTLEMINT_ACCESS_TOKEN: accessToken, + SETTLEMINT_INSTANCE: instance, + SETTLEMINT_WORKSPACE: workspace.id, + SETTLEMINT_APPLICATION: application.id, + SETTLEMINT_HASURA: hasura?.id, + SETTLEMINT_HASURA_ENDPOINT: hasura?.endpoints.find((endpoint) => endpoint.id === "graphql")?.displayValue, + SETTLEMINT_HASURA_ADMIN_SECRET: hasura?.credentials.find((credential) => credential.id === "admin-secret") + ?.displayValue, + SETTLEMINT_THEGRAPH: thegraph?.id, + SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT: thegraph?.endpoints.find((endpoint) => endpoint.id === "graphql") + ?.displayValue, + SETTLEMINT_THEGRAPH_SUBGRAPH_ENDPOINT_FALLBACK: thegraph?.endpoints.find( + (endpoint) => endpoint.id === "default-subgraph-graphql", + )?.displayValue, + SETTLEMINT_PORTAL: portal?.id, + SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT: portal?.endpoints.find((endpoint) => endpoint.id === "graphql") + ?.displayValue, + SETTLEMINT_PORTAL_REST_ENDPOINT: portal?.endpoints.find((endpoint) => endpoint.id === "rest")?.displayValue, + SETTLEMINT_HD_PRIVATE_KEY: hdPrivateKey?.uniqueName, + }); outro("Connected to SettleMint"); }) diff --git a/packages/cli/src/commands/connect/write-env.spinner.ts b/packages/cli/src/commands/connect/write-env.spinner.ts index 03f5ac155..5399ff78a 100644 --- a/packages/cli/src/commands/connect/write-env.spinner.ts +++ b/packages/cli/src/commands/connect/write-env.spinner.ts @@ -16,13 +16,14 @@ import type { DotEnv } from "@settlemint/sdk-utils/validation"; * "development" * ); */ -export async function writeEnvSpinner(env: Partial, environment: string) { +export async function writeEnvSpinner(env: Partial) { return spinner({ - startMessage: `Saving .env.${environment} and .env.${environment}.local files`, - stopMessage: `Written .env.${environment} and .env.${environment}.local file`, + startMessage: `Saving .env.${env.SETTLEMINT_ENVIRONMENT} and .env.${env.SETTLEMINT_ENVIRONMENT}.local files`, + stopMessage: `Written .env.${env.SETTLEMINT_ENVIRONMENT} and .env.${env.SETTLEMINT_ENVIRONMENT}.local file`, task: async () => { await writeEnv( { + SETTLEMINT_ENVIRONMENT: env.SETTLEMINT_ENVIRONMENT, SETTLEMINT_INSTANCE: env.SETTLEMINT_INSTANCE, SETTLEMINT_WORKSPACE: env.SETTLEMINT_WORKSPACE, SETTLEMINT_APPLICATION: env.SETTLEMINT_APPLICATION, @@ -36,7 +37,6 @@ export async function writeEnvSpinner(env: Partial, environment: string) SETTLEMINT_PORTAL_REST_ENDPOINT: env.SETTLEMINT_PORTAL_REST_ENDPOINT, SETTLEMINT_HD_PRIVATE_KEY: env.SETTLEMINT_HD_PRIVATE_KEY, }, - environment, false, ); @@ -45,7 +45,6 @@ export async function writeEnvSpinner(env: Partial, environment: string) SETTLEMINT_ACCESS_TOKEN: env.SETTLEMINT_ACCESS_TOKEN, SETTLEMINT_HASURA_ADMIN_SECRET: env.SETTLEMINT_HASURA_ADMIN_SECRET, }, - environment, true, ); }, diff --git a/packages/hasura/package.json b/packages/hasura/package.json index 7ecbad78e..56690daab 100644 --- a/packages/hasura/package.json +++ b/packages/hasura/package.json @@ -48,7 +48,7 @@ }, "devDependencies": {}, "dependencies": { - "@settlemint/sdk-utils": "0.5.0", + "@settlemint/sdk-utils": "workspace:*", "graphql-request": "^7", "zod": "^3" }, diff --git a/packages/js/package.json b/packages/js/package.json index 3241876e9..158294978 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -54,7 +54,7 @@ "gql.tada": "^1", "graphql-request": "^7", "zod": "^3", - "@settlemint/sdk-utils": "0.5.0" + "@settlemint/sdk-utils": "workspace:*" }, "peerDependencies": {}, "engines": { diff --git a/packages/portal/package.json b/packages/portal/package.json index a9111e686..bb9889317 100644 --- a/packages/portal/package.json +++ b/packages/portal/package.json @@ -48,7 +48,7 @@ }, "devDependencies": {}, "dependencies": { - "@settlemint/sdk-utils": "0.5.0", + "@settlemint/sdk-utils": "workspace:*", "graphql-request": "^7", "zod": "^3" }, diff --git a/packages/thegraph/package.json b/packages/thegraph/package.json index 350efe61a..fe42afc72 100644 --- a/packages/thegraph/package.json +++ b/packages/thegraph/package.json @@ -48,7 +48,7 @@ }, "devDependencies": {}, "dependencies": { - "@settlemint/sdk-utils": "0.5.0", + "@settlemint/sdk-utils": "workspace:*", "graphql-request": "^7", "zod": "^3" }, diff --git a/packages/utils/package.json b/packages/utils/package.json index e33017f9e..81e1bfe81 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -63,6 +63,7 @@ "deepmerge-ts": "^7", "environment": "^1", "find-up": "^7", + "nano-spawn": "^0.1.0", "yocto-spinner": "^0.1", "yoctocolors": "^2" }, diff --git a/packages/utils/src/environment/load-env.ts b/packages/utils/src/environment/load-env.ts index c0f0a62a1..2368b3b45 100644 --- a/packages/utils/src/environment/load-env.ts +++ b/packages/utils/src/environment/load-env.ts @@ -1,5 +1,6 @@ import { join } from "node:path"; import { projectRoot } from "@/filesystem.js"; +import { cancel } from "@/terminal.js"; import { type DotEnv, DotEnvSchema, validate } from "@/validation.js"; import { config } from "@dotenvx/dotenvx"; import type { DotenvParseOutput } from "dotenv"; @@ -32,6 +33,7 @@ export async function loadEnv( export async function loadEnvironmentEnv( validateEnv: T, environment?: string, + override?: boolean, ): Promise { const projectDir = await projectRoot(); @@ -43,20 +45,24 @@ export async function loadEnvironmentEnv( ".env", ].map((file) => join(projectDir, file)); - let { parsed } = config({ path: paths, logLevel: "error" }); + let { parsed } = config({ path: paths, logLevel: "error", override: !!override }); if (!parsed) { parsed = {}; } - const envToUse = environment || parsed.SETTLEMINT_ENVIRONMENT; + const envToUse = environment || parsed.SETTLEMINT_ENVIRONMENT || "development"; if (envToUse && envToUse !== environment) { - return loadEnvironmentEnv(validateEnv, envToUse); + return loadEnvironmentEnv(validateEnv, envToUse, true); } if (validateEnv) { - return validate(DotEnvSchema, parsed); + try { + return validate(DotEnvSchema, parsed); + } catch (error) { + cancel((error as Error).message); + } } return parsed as T extends true ? DotEnv : DotenvParseOutput; diff --git a/packages/utils/src/environment/write-env.ts b/packages/utils/src/environment/write-env.ts index 0eeaa59c3..72c28a768 100644 --- a/packages/utils/src/environment/write-env.ts +++ b/packages/utils/src/environment/write-env.ts @@ -5,9 +5,12 @@ import type { DotEnv } from "@/validation.js"; import { config } from "@dotenvx/dotenvx"; import { deepmerge } from "deepmerge-ts"; -export async function writeEnv(env: Partial, environment: string, secrets: boolean) { +export async function writeEnv(env: Partial, secrets: boolean) { const projectDir = await projectRoot(); - const envFile = join(projectDir, secrets ? `.env.${environment}.local` : `.env.${environment}`); + const envFile = join( + projectDir, + secrets ? `.env.${env.SETTLEMINT_ENVIRONMENT}.local` : `.env.${env.SETTLEMINT_ENVIRONMENT}`, + ); let { parsed: currentEnv } = config({ path: envFile, diff --git a/packages/utils/src/terminal.ts b/packages/utils/src/terminal.ts index 349f3412c..278c18f58 100644 --- a/packages/utils/src/terminal.ts +++ b/packages/utils/src/terminal.ts @@ -3,4 +3,5 @@ export { cancel } from "./terminal/cancel.js"; export { intro } from "./terminal/intro.js"; export { note } from "./terminal/note.js"; export { outro } from "./terminal/outro.js"; +export { run } from "./terminal/run.js"; export { spinner } from "./terminal/spinner.js"; diff --git a/packages/utils/src/terminal/run.ts b/packages/utils/src/terminal/run.ts new file mode 100644 index 000000000..7e4d8287e --- /dev/null +++ b/packages/utils/src/terminal/run.ts @@ -0,0 +1,18 @@ +import spawn, { type Options } from "nano-spawn"; + +export async function run({ + command, + args, + options, +}: { + command: string; + args: string[]; + options?: Options; +}) { + const result = await spawn(command, args, { + preferLocal: true, + ...options, + }); + + return result.output; +}