diff --git a/__tests__/kickstart/index.js b/__tests__/kickstart/index.js new file mode 100644 index 0000000..5c0fefe --- /dev/null +++ b/__tests__/kickstart/index.js @@ -0,0 +1,14 @@ +import {describe} from "node:test"; +import { kickstartInstall } from "./install.js"; +import { kickstartStart } from "./start.js"; +import { kickstartUtils } from "./utils.js"; + + +export function allKickstarts() { + describe('Kickstart commands', () => { + kickstartUtils() + kickstartInstall(); + kickstartStart(); + }) + +} \ No newline at end of file diff --git a/__tests__/kickstart/install.js b/__tests__/kickstart/install.js new file mode 100644 index 0000000..f341556 --- /dev/null +++ b/__tests__/kickstart/install.js @@ -0,0 +1,167 @@ +import test, { it,describe, after, before, beforeEach, afterEach } from "node:test" +import assert from "node:assert" +import fs, { readFileSync } from "node:fs" +import mock from "mock-fs" +import path, { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { __dirname, createEnv, createKickstart, installSetup, kickstartInstallAction, moveResources } from "../../dist/commands/kickstart-install.js" + +export function kickstartInstall() { + beforeEach(() => { + mock({ + "./": {} + }) + }) + afterEach(() => { + mock.restore() + }) + + + test("Install directory throws if occupied", async () => { + before(() => { + mock({ + "./has-content": { + "does-exist.json": "Has some content" + } + }) + }) + assert.throws(() => installSetup('./has-content')) + }) + describe("createKickstart", () => { + beforeEach(async (t) => { + mock.restore() + mock({ + "./myDir": {}, + [__dirname + "/resources/kickstart"]: { + "kickstart.json": fs.readFileSync(path.resolve("./src/resources/kickstart/kickstart.json")).toString(), + "fusionauth": { + "docker-compose.yml": "yo" + } + } + }) + const answers = { + password: "password", + email: "test@test.com", + appName: "myAppName" + } + await createKickstart(path.resolve(`${__dirname}/resources/kickstart/kickstart.json`), answers, "myDir") + t.newKickstart = JSON.parse(fs.readFileSync('./myDir/kickstart/kickstart.json').toString()) + }) + + it("should write a kickstart.json file to the kickstart folder", (t) => { + assert.ok(t.newKickstart) + }) + it("should write valid JSON", (t) => { + assert.equal(typeof t.newKickstart, "object") + }) + + it("should properly write variables to the kickstart variables object", (t) => { + const {variables} = t.newKickstart + assert.ok(variables.adminEmail, "adminEmail") + assert.ok(variables.adminPassword, "adminPassword") + assert.ok(variables.applicationName, "applicationName") + assert.ok(variables.saltPassword, "saltPassword") + assert.ok(variables.apiKey, "apiKey") + assert.ok(variables.asymmetricKeyId, "asymmetricKeyId") + assert.ok(variables.applicationId, "applicationId") + assert.ok(variables.clientSecret, "clientSecret") + assert.ok(variables.defaultTenantId, "defaultTenantId") + assert.ok(variables.adminUserId, "adminUserId") + }) + + }) + describe("moveResources", () => { + it("should throw error if target directory already exists", async () => { + before(() => { + mock({ + "./myTargetDir": {}, + [__dirname + "/resources/kickstart/fusionauth"]: { + "myfile.json": {"hi": "you"} + } + }) + }) + assert.throws(() => moveResources('./myTargetDir'), /exists/) + }) + it("should throw error if package resources don't exist", async () => { + assert.throws(() => moveResources('./myTargetDir'), /package/) + }) + it("should throw error if kickstart template doesn't exist", async () => { + before(() => { + mock({ + [__dirname + "/resources/kickstart"]: { + "fusionauth": { + ".env.defaults": "hi", + "docker-compose.yml": "yo" + } + } + }) + }) + assert.throws(() => moveResources('./myTargetDir'), /Kickstart template/) + }) + it("should throw error if .env.defaults doesn't exist", async () => { + before(() => { + mock({ + [__dirname + "/resources/kickstart"]: { + "kickstart.json": "", + "fusionauth": { + "docker-compose.yml": "yo" + } + } + }) + }) + assert.throws(() => moveResources('./myTargetDir'), /.env.defaults/) + }) + it("should throw error if docker-compose.yml doesn't exist", async () => { + before(() => { + mock({ + "./": {}, + [__dirname + "/resources/kickstart"]: { + "kickstart.json": "", + "fusionauth": { + ".env.defaults": "hi", + } + } + }) + }) + assert.throws(() => moveResources('./myTargetDir'), /docker-compose.yml/) + }) + it("should copy all of the files", () => { + const targetDir = "./myTargetDir" + /* mock-fs does NOT support the fs.cp() method and won't be + Therefore, we have to remove the mock, allow use of the regular resources + copy the resources to the targetDir, then remove the targetDir after the test + */ + before(() => { + mock.restore() + }) + after(() => { + fs.rmSync(targetDir, { recursive: true, force: true }) + }) + const newResources = moveResources('./myTargetDir') + assert.deepEqual(newResources, [".env.defaults", "docker-compose.yml"]) + }) + test(".env.default copies properly", () => { + before(() => { + mock({ + "myDir": { + ".env.defaults": "has defaults" + } + }) + }) + const options = { + postgresPass: crypto.randomUUID(), + dbPass: crypto.randomUUID() + } + + const expected = `has defaults\nPOSTGRES_PASSWORD=${options.postgresPass}\nDATABASE_PASSWORD=${options.dbPass}\nCLI_DIR=./myDir` + + createEnv("./myDir", options) + assert.equal(fs.readFileSync('./myDir/.env').toString(), expected, "Environment strings don't match") + }) + + }) + describe("Directory is created with proper name", () => { + test("When no name is specified, create directory at fusionauth") + }) + +} \ No newline at end of file diff --git a/__tests__/kickstart/start.js b/__tests__/kickstart/start.js new file mode 100644 index 0000000..30e69f9 --- /dev/null +++ b/__tests__/kickstart/start.js @@ -0,0 +1,58 @@ +import test, { it,describe, after, before, beforeEach, afterEach } from "node:test" +import assert from "node:assert" +import fs, { readdirSync, readFileSync } from "node:fs" +import mock from "mock-fs" +import path, { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import {kickstartStartAction} from '../../dist/commands/kickstart-start.js' +import { createEnv, createKickstart, kickstartInstall, moveResources } from "../../dist/commands/kickstart-install.js"; + +import { __dirname } from "../../dist/utils.js"; +import { execSync } from "node:child_process"; + +export function kickstartStart() { + + describe("Kickstart:start runs properly", () => { + before(() => mock.restore()) + afterEach(() => { + mock?.restore() + }) + test("Docker throws error when it doesn't connect properly", async (t) => { + before(() => { + mock.restore() + // Sets up docker to interfere with ports + execSync("docker run -d --name my-apache-app -p 9011:80 -v $(PWD)/website:/usr/local/apache2/htdocs/ httpd:latest") + + // properly sets up files to run FA docker + t.envContents = fs.readFileSync('./.env').toString() + fs.writeFileSync('./kickstart/kickstart.json', fs.readFileSync(path.resolve('./__tests__/resources/kickstart.json'))) + fs.writeFileSync('./docker-compose.yml', fs.readFileSync(path.resolve('./dist/commands/resources/kickstart/fusionauth/docker-compose.yml')).toString()) + fs.appendFileSync("./.env", `OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m"\n + FUSIONAUTH_APP_MEMORY=512M\n + FUSIONAUTH_APP_RUNTIME_MODE=development\n + FUSIONAUTH_APP_KICKSTART_FILE=/usr/local/fusionauth/kickstart/kickstart.json\n + FUSIONAUTH_APP_INSTALLATION_SOURCE=fusionauth-node-cli\n + POSTGRES_USER=postgres\n + DATABASE_USERNAME=fusionauth\n + + POSTGRES_PASSWORD=29587d14-0cc7-40f8-8bc9-292044a4688e\n + DATABASE_PASSWORD=99b8a98f-3cf4-4cb1-9114-c52158a4f4a5\n + CLI_DIR=/Users/bryanrobinson/Documents/Dev/cli-test/mytest`) + process.env.CLI_DIR = path.resolve('./') + + + }) + after(() => { + fs.writeFileSync("./.env", t.envContents) + fs.rmSync('./docker-compose.yml') + execSync('docker stop my-apache-app && docker rm my-apache-app') + }) + assert.rejects(kickstartStartAction()) + // const startPage = await fetch('http://localhost:9011') + // assert.equal(startPage.status, 200, "localhost:9011 is not returning 200") + }) + + }) + +} \ No newline at end of file diff --git a/__tests__/kickstart/utils.js b/__tests__/kickstart/utils.js new file mode 100644 index 0000000..19e9ec7 --- /dev/null +++ b/__tests__/kickstart/utils.js @@ -0,0 +1,47 @@ +import test, { describe, before, after } from "node:test" +import assert from "node:assert"; +import fs from "node:fs" +import path, { dirname } from 'node:path'; +import { startSetup } from "../../dist/commands/kickstart/utils.js"; +import mock from "mock-fs"; +export function kickstartUtils() { + + describe("Kickstart Utilities work", () => { + test("Errors if the CLI_DIR doesn't match", async () => { + before(() => { + const cliDirPath = path.resolve('../') + process.env.CLI_DIR = cliDirPath + }) + after(() => { + delete process.env.CLI_DIR + }) + assert.throws(startSetup, /Error: Current directory was not kickstarted./) + }) + test("Errors with no compose file", () => { + after(() => { + delete process.env.CLI_DIR + }) + before(() => { + process.env.CLI_DIR = path.resolve('./') + }) + + assert.throws(startSetup, /Error: Current directory does not contain docker-compose.yml/, "startSetup doesn't throw proper error on docker-compose mistake") + }) + + test("Errors with no kickstart file", () => { + after(() => { + delete process.env.CLI_DIR + }) + before(() => { + process.env.CLI_DIR = path.resolve('./') + mock({ + './docker-compose.yml': 'yo' + }) + }) + assert.throws(startSetup, /The kickstart.json file does not exist./, "startSetup doesn't throw proper error on kickstart.json mistake") + }) + + + }) + +} diff --git a/__tests__/resources/kickstart.json b/__tests__/resources/kickstart.json new file mode 100644 index 0000000..e10bfa4 --- /dev/null +++ b/__tests__/resources/kickstart.json @@ -0,0 +1,93 @@ +{ + "variables": { + "apiKey": "33052c8a-c283-4e96-9d2a-eb1215c69f8f-not-for-prod", + "asymmetricKeyId": "#{UUID()}", + "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e", + "clientSecret": "super-secret-secret-that-should-be-regenerated-for-production", + "defaultTenantId": "d7d09513-a3f5-401c-9685-34ab6c552453", + "adminUserId": "00000000-0000-0000-0000-000000000001", + "adminEmail": "admin@example.com", + "adminPassword": "Djg7Bw3QCZ3ownaFLDwI3G8FjZBgUBG", + "applicationName": "Example App", + "saltPassword": "wcz0xkQdRFh30enrtemuD." + }, + "apiKeys": [ + { + "key": "#{apiKey}", + "description": "Unrestricted API key" + } + ], + "requests": [ + { + "method": "POST", + "url": "/api/key/generate/#{asymmetricKeyId}", + "body": { + "key": { + "algorithm": "RS256", + "name": "For example app", + "length": 2048 + } + } + }, + { + "method": "POST", + "url": "/api/application/#{applicationId}", + "tenantId": "#{defaultTenantId}", + "body": { + "application": { + "name": "#{applicationName}", + "oauthConfiguration": { + "authorizedRedirectURLs": [ + "https://fusionauth.io" + ], + "logoutURL": "https://fusionauth.io", + "clientSecret": "#{clientSecret}", + "enabledGrants": [ + "authorization_code", + "refresh_token" + ], + "generateRefreshTokens": true, + "requireRegistration": true + }, + "jwtConfiguration": { + "enabled": true, + "accessTokenKeyId": "#{asymmetricKeyId}", + "idTokenKeyId": "#{asymmetricKeyId}" + } + } + } + }, + { + "method": "POST", + "url": "/api/user/import", + "body": { + "users": [ + { + "email": "#{adminEmail}", + "password": "#{adminPassword}", + "salt": "#{saltPassword}", + "encryptionScheme": "bcrypt", + "factor": 10, + "active": true, + "verified": true, + "skipRegistrationVerification": true, + "roles": [ + "admin" + ], + "registrations": [ + { + "applicationId": "#{applicationId}" + }, + { + "applicationId": "#{FUSIONAUTH_APPLICATION_ID}", + "roles": [ + "admin" + ] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/__tests__/telemetry/index.js b/__tests__/telemetry/index.js index 1a5df7b..e2daac5 100644 --- a/__tests__/telemetry/index.js +++ b/__tests__/telemetry/index.js @@ -12,11 +12,13 @@ import nock from 'nock' export function telemetry() { const mockedTrueConfig = { id: '8c0a77f2-27e4-4284-b5d3-5618ec2a56eb', - telemetry: true + telemetry: true, + version: '1.0' } const mockedFalseConfig = { id: '8c0a77f2-27e4-4284-b5d3-5618ec2a56eb', - telemetry: false + telemetry: false, + version: '1.0' } describe('telemetry runs properly', () => { test("Creates config if no config exists", (t) => { diff --git a/__tests__/test.js b/__tests__/test.js index 26fcbc2..b3c7f24 100644 --- a/__tests__/test.js +++ b/__tests__/test.js @@ -1,6 +1,8 @@ +import { allKickstarts } from "./kickstart/index.js"; import { postInstall } from "./postInstall/index.js"; import { telemetry } from "./telemetry/index.js"; postInstall() -telemetry() \ No newline at end of file +telemetry() +allKickstarts() \ No newline at end of file diff --git a/kickstart/kickstart.json b/kickstart/kickstart.json new file mode 100644 index 0000000..e10bfa4 --- /dev/null +++ b/kickstart/kickstart.json @@ -0,0 +1,93 @@ +{ + "variables": { + "apiKey": "33052c8a-c283-4e96-9d2a-eb1215c69f8f-not-for-prod", + "asymmetricKeyId": "#{UUID()}", + "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e", + "clientSecret": "super-secret-secret-that-should-be-regenerated-for-production", + "defaultTenantId": "d7d09513-a3f5-401c-9685-34ab6c552453", + "adminUserId": "00000000-0000-0000-0000-000000000001", + "adminEmail": "admin@example.com", + "adminPassword": "Djg7Bw3QCZ3ownaFLDwI3G8FjZBgUBG", + "applicationName": "Example App", + "saltPassword": "wcz0xkQdRFh30enrtemuD." + }, + "apiKeys": [ + { + "key": "#{apiKey}", + "description": "Unrestricted API key" + } + ], + "requests": [ + { + "method": "POST", + "url": "/api/key/generate/#{asymmetricKeyId}", + "body": { + "key": { + "algorithm": "RS256", + "name": "For example app", + "length": 2048 + } + } + }, + { + "method": "POST", + "url": "/api/application/#{applicationId}", + "tenantId": "#{defaultTenantId}", + "body": { + "application": { + "name": "#{applicationName}", + "oauthConfiguration": { + "authorizedRedirectURLs": [ + "https://fusionauth.io" + ], + "logoutURL": "https://fusionauth.io", + "clientSecret": "#{clientSecret}", + "enabledGrants": [ + "authorization_code", + "refresh_token" + ], + "generateRefreshTokens": true, + "requireRegistration": true + }, + "jwtConfiguration": { + "enabled": true, + "accessTokenKeyId": "#{asymmetricKeyId}", + "idTokenKeyId": "#{asymmetricKeyId}" + } + } + } + }, + { + "method": "POST", + "url": "/api/user/import", + "body": { + "users": [ + { + "email": "#{adminEmail}", + "password": "#{adminPassword}", + "salt": "#{saltPassword}", + "encryptionScheme": "bcrypt", + "factor": 10, + "active": true, + "verified": true, + "skipRegistrationVerification": true, + "roles": [ + "admin" + ], + "registrations": [ + { + "applicationId": "#{applicationId}" + }, + { + "applicationId": "#{FUSIONAUTH_APPLICATION_ID}", + "roles": [ + "admin" + ] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index 983babb..c4d239e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,9 +5,9 @@ export * from './email-duplicate.js'; export * from './email-html-to-text.js'; export * from './email-upload.js'; export * from './email-watch.js'; -export * from './kickstart-install.js' +export {kickstartInstall} from './kickstart-install.js' export * from './kickstart-kill.js' -export * from './kickstart-start.js'; +export {kickstartStart} from './kickstart-start.js'; export * from './kickstart-stop.js'; export * from './lambda-update.js'; export * from './lambda-delete.js'; diff --git a/src/commands/kickstart-install.ts b/src/commands/kickstart-install.ts index 206797a..71d5146 100644 --- a/src/commands/kickstart-install.ts +++ b/src/commands/kickstart-install.ts @@ -10,9 +10,30 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { betaWarning, isDirEmpty, isDockerInstalled, logEvent } from "../utils.js"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +export const __dirname = dirname(fileURLToPath(import.meta.url)); -async function createKickstart(kickstartPath: string, answers: any, newDir: string) { + +export function moveResources(targetDir:string) { + try { + if (fs.existsSync(targetDir)) throw new Error(chalk.red("The target directory already exists")) + + if (!fs.existsSync(`${__dirname}/resources/kickstart`)) throw new Error(chalk.red("The package's resources don't exist")) + + if (!fs.existsSync(`${__dirname}/resources/kickstart/kickstart.json`)) throw new Error(chalk.red("Kickstart template doesn't exist")) + + if (!fs.existsSync(`${__dirname}/resources/kickstart/fusionauth/.env.defaults`)) throw new Error(chalk.red(".env.defaults file doesn't exist")) + + if (!fs.existsSync(`${__dirname}/resources/kickstart/fusionauth/docker-compose.yml`)) throw new Error(chalk.red("docker-compose.yml file doesn't exist")) + fs.cpSync(`${__dirname}/resources/kickstart/fusionauth`, targetDir, { recursive: true }) + + return fs.readdirSync(targetDir) + } catch(e) { + throw(e) + } + +} + +export async function createKickstart(kickstartPath: string, answers: any, newDir: string) { const salt = bcrypt.genSaltSync(10) const saltBase = salt.split('$10$')[1]; const fullHash = bcrypt.hashSync(answers.password, salt) @@ -29,29 +50,45 @@ async function createKickstart(kickstartPath: string, answers: any, newDir: stri fs.writeFileSync(`${newDir}/kickstart/kickstart.json`, JSON.stringify(kickstartObject, null, 2)) } -const action = async function (dir: string) { - const dockerInstalled = isDockerInstalled(); +export function createEnv(directory: string, options: any = {postgresPass: crypto.randomUUID(), dbPass: crypto.randomUUID()}) { + try { + const { postgresPass, dbPass } = options + + console.log(chalk.green(`Transferring environment variables`)) + fs.renameSync(`${directory}/.env.defaults`, `${directory}/.env`) + fs.appendFileSync(`${directory}/.env`, `\nPOSTGRES_PASSWORD=${postgresPass}\nDATABASE_PASSWORD=${dbPass}\nCLI_DIR=${directory}`) + } catch(e) { + throw(".env file was not able to be created") + } +} + +export function installSetup(dir: string) { const directory = path.resolve(dir) - logEvent('cli command kickstart:install') - - betaWarning() + if (fs.existsSync(directory) && !isDirEmpty(directory)) { + throw(chalk.redBright(`Error: `) + `Target directory (${chalk.yellow(directory)}) has files.\n\nPlease choose an empty or non-existent directory\n`) + } + + const parentDir = path.dirname(directory) try { - if (!dockerInstalled) { - throw (chalk.red("Error: You don't have Docker installed. It's the easiest way to get everything you need\n") + chalk.cyan("Please install Docker. For developers new to Docker, we suggest Orbstack: https://docs.orbstack.dev/quick-start")) - } + fs.accessSync(parentDir, fs.constants.W_OK) + } catch (err) { + throw(chalk.red(`Can't write to ${parentDir}. Please check permissions on the directory`)) + } - if (fs.existsSync(directory) && !isDirEmpty(directory)) { - throw (chalk.redBright(`Error: `) + `Target directory (${chalk.yellow(directory)}) has files.\n\nPlease choose an empty or non-existent directory\n`) - } + return directory +} - const parentDir = path.dirname(directory) +export const kickstartInstallAction = async function (dir: string) { + logEvent('cli command kickstart:install') + + // Throw exception if no docker + isDockerInstalled(); + // Display a masthead of this being a beta + betaWarning() - try { - fs.accessSync(parentDir, fs.constants.W_OK) - } catch (err) { - console.error(chalk.red(`Can't write to ${parentDir}. Please check permissions on the directory`)) - } + try { + const directory = installSetup(dir) inquirer.prompt([ { @@ -89,22 +126,18 @@ const action = async function (dir: string) { const spinner = yoctoSpinner({ text: "Building..." }).start() setTimeout(() => { // move fusionauth folder to user's project + moveResources(directory) console.log(chalk.green(`\nTransferring files to ${dir}`)) - fs.cpSync(`${__dirname}/resources/kickstart/fusionauth`, directory, { recursive: true }) }, 500) setTimeout(() => { console.log(chalk.green(`Creating Kickstart file`)) + console.log(answers) if (!fs.existsSync(directory)) throw (chalk.red(`Something went wrong. ${directory} does not exists.`)) createKickstart(__dirname + '/resources/kickstart/kickstart.json', answers, directory) }, 1500) setTimeout(() => { - const postgresPass = crypto.randomUUID() - const dbPass = crypto.randomUUID() - - console.log(chalk.green(`Transferring environment variables`)) - fs.renameSync(`${directory}/.env.defaults`, `${directory}/.env`) - fs.appendFileSync(`${directory}/.env`, `\nPOSTGRES_PASSWORD=${postgresPass}\nDATABASE_PASSWORD=${dbPass}\nCLI_DIR=${directory}`) + createEnv(directory) }, 2500) setTimeout(() => { @@ -114,20 +147,16 @@ const action = async function (dir: string) { }, 3500) - }).catch((err) => { + }).catch(() => { console.error(chalk.yellow('Cancelling kickstart installation...')) }) - - - } catch (e) { console.error(e) } - } export const kickstartInstall = new Command() .command('kickstart:install') .description('Adds a directory with a FusionAuth Docker + Kickstart') .argument('[dir]', 'Optional directory to install FusionAuth', 'fusionauth') - .action((dir) => action(dir)) \ No newline at end of file + .action((dir) => kickstartInstallAction(dir)) \ No newline at end of file diff --git a/src/commands/kickstart-start.ts b/src/commands/kickstart-start.ts index e3853cb..b24da68 100644 --- a/src/commands/kickstart-start.ts +++ b/src/commands/kickstart-start.ts @@ -1,46 +1,78 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; -import { spawn } from 'node:child_process'; +import { exec, execSync, spawn } from 'node:child_process'; import { betaWarning, isDockerInstalled, logEvent } from "../utils.js"; import 'dotenv/config'; import boxen from "boxen"; import yoctoSpinner from "yocto-spinner"; +import { startSetup } from "./kickstart/utils.js"; -const action = async function () { + + +export async function runDocker() { + return new Promise((resolve, reject) => { + exec('docker compose up -d',(error, stdout, stderr) => { + // console.log({error, stdout, stderr}) + + if (error) { + reject(stderr) + } else { + resolve(stderr) + } + }) + +}) +} + + +export const kickstartStartAction = async function () { betaWarning(); console.log(chalk.yellow('Starting FusionAuth...')) + const spinner = yoctoSpinner({ text: "Configuring ...\n" }).start() try { - if (process.cwd() != process.env.CLI_DIR) throw (chalk.red('Error: Current directory was not kickstarted.')) - - if (!isDockerInstalled()) console.error(chalk.red('Error: You need Docker to run.')) + startSetup() logEvent('cli command kickstart:start') - const starting = spawn('docker compose up -d', { shell: true, stdio: 'inherit' }) - starting.on('error', e => { - console.error(e) - }) - if (starting?.stdout) { - for await (const data of starting.stdout) { - console.log(`${chalk.green(`FusionAuth:`)} ${data}`); - }; - } - - starting.on('close', () => { - const spinner = yoctoSpinner({ text: "Configuring ..." }).start() - setTimeout(() => { + await runDocker().then((result: any) => { + const messages = result.split('\n') + const messageDelay = 500 + // console.log({messages}) + const messageDisplays = new Promise((res, rej) => { + messages.forEach((message:string, i:number, array:[any]) => { + setTimeout(() => { + console.info(message) + if (i === array.length -1) res('Finished messages'); + + }, messageDelay*i) + }) + }) + + messageDisplays.then(() => { spinner.stop() + console.log(boxen(`${chalk.magenta('Login URL:')} http://localhost:9011/admin`, { padding: 2, margin: 1, titleAlignment: 'center', borderStyle: 'bold', borderColor: 'green', title: "Your FusionAuth Docker is Running" })) - }, 5000) // Timeout to allow kickstart to run in the Docker + }).catch(() => { + console.log("ERRORED?") + }) + + }).catch(err => { + throw new Error('Docker was unable to run, check that proper ports are available.') }) - } catch (err) { - console.log(err) + + } catch (err: any) { + console.log(err.message) + + console.log("Attempting to gracefully shutdown Docker") + + execSync("docker compose down", {stdio: ["inherit", "ignore", "ignore"]}) + spinner.stop() } } export const kickstartStart = new Command() .command('kickstart:start') .description('Runs Docker container in the current directory') - .action(action) \ No newline at end of file + .action(kickstartStartAction) \ No newline at end of file diff --git a/src/commands/kickstart/utils.ts b/src/commands/kickstart/utils.ts new file mode 100644 index 0000000..ce35708 --- /dev/null +++ b/src/commands/kickstart/utils.ts @@ -0,0 +1,14 @@ +import fs from 'node:fs' +import { isDockerInstalled, logEvent } from "../../utils.js"; +import chalk from "chalk"; + + + +export function startSetup() { + const cwd = process.cwd() + if (cwd != process.env.CLI_DIR) throw new Error(chalk.red('Error: Current directory was not kickstarted via the CLI')) + if (!fs.existsSync('./docker-compose.yml')) throw new Error(chalk.red('Error: Current directory does not contain docker-compose.yml')) + if (!isDockerInstalled()) throw new Error(chalk.red('Error: You need Docker to run.')) + if (!fs.existsSync('./kickstart/kickstart.json')) throw new Error(chalk.red('Error: The kickstart.json file does not exist.')) + logEvent('cli command kickstart:start') +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 4e9e725..91d9cfc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -190,7 +190,7 @@ export function isDockerInstalled() { execSync('docker --version'); return true; } catch (e) { - return false; + throw(chalk.red("Error: You don't have Docker installed. It's the easiest way to get everything you need\n") + chalk.cyan("Please install Docker. For developers new to Docker, we suggest Orbstack: https://docs.orbstack.dev/quick-start") ) } }