studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
426 lines (425 loc) • 15.6 kB
JavaScript
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
};