UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

426 lines (425 loc) 15.6 kB
import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { StudioCMSColorwayError, StudioCMSColorwayInfo, StudioCMSColorwayWarnBg, TursoColorway } from "@withstudiocms/cli-kit/colors"; import { label } from "@withstudiocms/cli-kit/messages"; import { commandExists, exists, runInteractiveCommand, runShellCommand } from "@withstudiocms/cli-kit/utils"; import { askToContinue, confirm, group, log, multiselect, select, spinner, text } from "@withstudiocms/effect/clack"; import { Effect, runEffect } from "../../../effect.js"; import { buildDebugLogger } from "../../utils/logger.js"; import { buildEnvFile, ExampleEnv } from "../../utils/studiocmsEnv.js"; var EnvBuilderAction = /* @__PURE__ */ ((EnvBuilderAction2) => { EnvBuilderAction2["builder"] = "builder"; EnvBuilderAction2["example"] = "example"; EnvBuilderAction2["none"] = "none"; return EnvBuilderAction2; })(EnvBuilderAction || {}); const env = Effect.fn(function* (context, debug, dryRun) { const { chalk, cwd, pCancel, pOnCancel } = context; const debugLogger = yield* buildDebugLogger(debug); yield* debugLogger("Running env..."); let _env = false; let envFileContent; const envExists = exists(path.join(cwd, ".env")); yield* debugLogger(`Environment file exists: ${envExists}`); if (envExists) { yield* log.warn( `${label("Warning", StudioCMSColorwayWarnBg, chalk.black)} An environment file already exists. Would you like to overwrite it?` ); const confirm2 = yield* askToContinue(); if (!confirm2) { return yield* context.exit(0); } yield* debugLogger("User opted to overwrite existing .env file"); } const envPrompt = yield* select({ message: "What kind of environment file would you like to create?", options: [ { value: "builder" /* builder */, label: "Use Interactive .env Builder" }, { value: "example" /* example */, label: "Use the Example .env file" }, { value: "none" /* none */, label: "Skip Environment File Creation", hint: "Cancel" } ] }); if (typeof envPrompt === "symbol") { return yield* pCancel(envPrompt); } yield* debugLogger(`Environment file type selected: ${envPrompt}`); _env = envPrompt !== "none"; switch (envPrompt) { case "none" /* none */: { break; } case "example" /* example */: { envFileContent = ExampleEnv; break; } case "builder" /* builder */: { let envBuilderOpts = {}; const isWindows = os.platform() === "win32"; if (isWindows) { yield* log.warn( `${label("Warning", StudioCMSColorwayWarnBg, chalk.black)} Turso DB CLI is not supported on Windows outside of WSL.` ); } let tursoDB = "no"; if (!isWindows) { tursoDB = yield* select({ message: "Would you like us to setup a new Turso DB for you? (Runs `turso db create`)", options: [ { value: "yes", label: "Yes" }, { value: "no", label: "No" } ] }); } if (typeof tursoDB === "symbol") { return yield* pCancel(tursoDB); } if (tursoDB === "yes") { if (!commandExists("turso")) { yield* log.error(StudioCMSColorwayError("Turso CLI is not installed.")); const installTurso = yield* confirm({ message: "Would you like to install Turso CLI now?" }); if (typeof installTurso === "symbol") { return yield* pCancel(installTurso); } if (installTurso) { if (isWindows) { yield* log.error( StudioCMSColorwayError( "Automatic installation is not supported on Windows. Please install Turso CLI manually from https://turso.tech/docs/getting-started/installation" ) ); return yield* context.exit(1); } yield* Effect.try({ try: () => runInteractiveCommand("curl -fsSL https://get.turso.tech/cli.sh | sh", { cwd, shell: true, env: process.env }), catch: (cause) => new Error(`Failed to install Turso CLI: ${String(cause)}`) }); yield* log.success("Turso CLI installed successfully."); } else { yield* log.warn( `${label("Warning", StudioCMSColorwayWarnBg, chalk.black)} You will need to setup your own AstroDB and provide the URL and Token.` ); } } const checkLogin = yield* Effect.tryPromise({ try: () => runShellCommand("turso auth login --headless"), catch: (cause) => new Error(`Turso CLI Error: ${String(cause)}`) }); if (!checkLogin.includes("Already signed in as") && !checkLogin.includes("Success! Existing JWT still valid")) { yield* log.message(`Please sign in to Turso to continue. ${checkLogin}`); const loginToken = yield* text({ message: 'Enter the login token ( the code within the " " )', placeholder: "eyJhb...tnPnw" }); if (typeof loginToken === "symbol") { return yield* pCancel(loginToken); } const loginResult = yield* Effect.tryPromise({ try: () => runShellCommand(`turso config set token "${loginToken}"`), catch: (cause) => new Error(`Turso CLI Error: ${String(cause)}`) }); if (loginResult.includes("Token set successfully.")) { yield* log.success("Successfully logged in to Turso."); } else { yield* log.error(StudioCMSColorwayError("Unable to login to Turso.")); yield* context.exit(1); } } const setCustomDbName = yield* confirm({ message: "Would you like to provide a custom name for the database?", initialValue: false }); if (typeof setCustomDbName === "symbol") { return yield* pCancel(setCustomDbName); } let dbName = `scms_db_${crypto.randomBytes(4).toString("hex")}`; if (setCustomDbName) { const customDbName = yield* text({ message: "Enter a custom name for the database", placeholder: "my_custom_db_name" }); if (typeof customDbName === "symbol") { return yield* pCancel(customDbName); } dbName = customDbName; } yield* debugLogger(`New database name: ${dbName}`); const tursoSetup = yield* spinner(); yield* tursoSetup.start( `${label("Turso", TursoColorway, chalk.black)} Setting up Turso DB...` ); yield* tursoSetup.message( `${label("Turso", TursoColorway, chalk.black)} Creating Database...` ); const createResponse = yield* Effect.tryPromise({ try: () => runShellCommand(`turso db create ${dbName}`), catch: (cause) => new Error(`Turso CLI Error: ${String(cause)}`) }); const dbNameMatch = createResponse.match(/^Created database (\S+) at group/m); const dbFinalName = dbNameMatch ? dbNameMatch[1] : void 0; yield* tursoSetup.message( `${label("Turso", TursoColorway, chalk.black)} Retrieving database information...` ); const showCMD = `turso db show ${dbName}`; const tokenCMD = `turso db tokens create ${dbName}`; const showResponse = yield* Effect.tryPromise({ try: () => runShellCommand(showCMD), catch: (cause) => new Error(`Turso CLI Error: ${String(cause)}`) }); const urlMatch = showResponse.match(/^URL:\s+(\S+)/m); const dbURL = urlMatch ? urlMatch[1] : void 0; yield* debugLogger(`Database URL: ${dbURL}`); const tokenResponse = yield* Effect.tryPromise({ try: () => runShellCommand(tokenCMD), catch: (cause) => new Error(`Turso CLI Error: ${String(cause)}`) }); const dbToken = tokenResponse.trim(); yield* debugLogger(`Database Token: ${dbToken}`); envBuilderOpts.astroDbRemoteUrl = dbURL; envBuilderOpts.astroDbToken = dbToken; yield* tursoSetup.stop( `${label("Turso", TursoColorway, chalk.black)} Database setup complete. New Database: ${dbFinalName}` ); yield* log.message("Database Token and Url saved to environment file."); } else { yield* log.warn( `${label("Warning", StudioCMSColorwayWarnBg, chalk.black)} You will need to setup your own AstroDB and provide the URL and Token.` ); const envBuilderStep_AstroDB = yield* group( { astroDbRemoteUrl: async () => await runEffect( text({ message: "Remote URL for AstroDB", initialValue: "libsql://your-database.turso.io" }) ), astroDbToken: async () => await runEffect( text({ message: "AstroDB Token", initialValue: "your-astrodb-token" }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`AstroDB setup: ${envBuilderStep_AstroDB}`); envBuilderOpts = { ...envBuilderStep_AstroDB }; } const envBuilderStep1 = yield* group( { encryptionKey: async () => await runEffect( text({ message: "StudioCMS Auth Encryption Key", initialValue: crypto.randomBytes(16).toString("base64") }) ), oAuthOptions: async () => await runEffect( multiselect({ message: "Setup OAuth Providers", options: [ { value: "github", label: "GitHub" }, { value: "discord", label: "Discord" }, { value: "google", label: "Google" }, { value: "auth0", label: "Auth0" } ], required: false }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`Environment Builder Step 1: ${envBuilderStep1}`); envBuilderOpts = { ...envBuilderStep1 }; if (envBuilderStep1.oAuthOptions.includes("github")) { const githubOAuth = yield* group( { clientId: async () => await runEffect( text({ message: "GitHub Client ID", initialValue: "your-github-client-id" }) ), clientSecret: async () => await runEffect( text({ message: "GitHub Client Secret", initialValue: "your-github-client-secret" }) ), redirectUri: async () => await runEffect( text({ message: "GitHub Redirect URI Domain", initialValue: "http://localhost:4321" }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`GitHub OAuth: ${githubOAuth}`); envBuilderOpts.githubOAuth = githubOAuth; } if (envBuilderStep1.oAuthOptions.includes("discord")) { const discordOAuth = yield* group( { clientId: async () => await runEffect( text({ message: "Discord Client ID", initialValue: "your-discord-client-id" }) ), clientSecret: async () => await runEffect( text({ message: "Discord Client Secret", initialValue: "your-discord-client-secret" }) ), redirectUri: async () => await runEffect( text({ message: "Discord Redirect URI Domain", initialValue: "http://localhost:4321" }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`Discord OAuth: ${discordOAuth}`); envBuilderOpts.discordOAuth = discordOAuth; } if (envBuilderStep1.oAuthOptions.includes("google")) { const googleOAuth = yield* group( { clientId: async () => await runEffect( text({ message: "Google Client ID", initialValue: "your-google-client-id" }) ), clientSecret: async () => await runEffect( text({ message: "Google Client Secret", initialValue: "your-google-client-secret" }) ), redirectUri: async () => await runEffect( text({ message: "Google Redirect URI Domain", initialValue: "http://localhost:4321" }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`Google OAuth: ${googleOAuth}`); envBuilderOpts.googleOAuth = googleOAuth; } if (envBuilderStep1.oAuthOptions.includes("auth0")) { const auth0OAuth = yield* group( { clientId: async () => await runEffect( text({ message: "Auth0 Client ID", initialValue: "your-auth0-client-id" }) ), clientSecret: async () => await runEffect( text({ message: "Auth0 Client Secret", initialValue: "your-auth0-client-secret" }) ), domain: async () => await runEffect( text({ message: "Auth0 Domain", initialValue: "your-auth0-domain" }) ), redirectUri: async () => await runEffect( text({ message: "Auth0 Redirect URI Domain", initialValue: "http://localhost:4321" }) ) }, { onCancel: async () => await runEffect(pOnCancel()) } ); yield* debugLogger(`Auth0 OAuth: ${auth0OAuth}`); envBuilderOpts.auth0OAuth = auth0OAuth; } envFileContent = buildEnvFile(envBuilderOpts); break; } } if (dryRun) { context.tasks.push({ title: `${StudioCMSColorwayInfo.bold("--dry-run")} ${chalk.dim("Skipping environment file creation")}`, task: async (message) => { message("Creating environment file... (skipped)"); } }); } else if (_env) { context.tasks.push({ title: chalk.dim("Creating environment file..."), task: async (message) => { try { await fs.writeFile(path.join(cwd, ".env"), envFileContent, { encoding: "utf-8" }); message("Environment file created"); } catch (e) { if (e instanceof Error) { await runEffect(log.error(StudioCMSColorwayError(`Error: ${e.message}`))); process.exit(1); } else { await runEffect( log.error(StudioCMSColorwayError("Unknown Error: Unable to create environment file.")) ); process.exit(1); } } } }); } yield* debugLogger("Environment complete"); }); export { EnvBuilderAction, env };