Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions bin/conf-gen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import CliCommand from '../lib/CliCommand.js'
import storeDefaults from '../lib/utils/storeDefaults.js'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import { globSync } from 'glob'
import path from 'node:path'
import { pathToFileURL } from 'node:url'

export default class Confgen extends CliCommand {
get config () {
return {
...super.config,
description: 'Generates a template config file which can be populated with required values',
options: [
['--env <environment>', 'The environment to write the config for (defaults to NODE_ENV)'],
['--defaults', 'Include default values'],
['--replace', 'Override any existing values'],
['--update', 'Update existing configuration with any missing values']
]
}
}

async runTask () {
const env = this.options.env || process.env.NODE_ENV
if (!env) {
return console.log('ERROR: environment must be specified via --env or NODE_ENV\n')
}
if (this.options.replace && this.options.update) {
return console.log('ERROR: --update and --replace cannot both be specified, please choose one and run the utility again')
}
if (this.options.defaults) {
console.log('Default values will be included')
}
const confDir = path.resolve('conf')
const outpath = path.join(confDir, `${env}.config.js`)
const configJson = {}

let isExistingConfig = false
let existingConfig
try {
existingConfig = (await import(pathToFileURL(outpath).href)).default
isExistingConfig = true
} catch (e) {
console.log(`No config found for NODE_ENV '${env}'. File will be written to ${outpath}\n`)
}
if (isExistingConfig) {
const msg = `Config already exists for NODE_ENV '${env}'. `
if (this.options.replace) {
console.log(`${msg}All existing values will be replaced.`)
} else if (this.options.update) {
console.log(`${msg}Any missing values will be added.`)
Object.assign(configJson, existingConfig)
} else {
return console.log(`${msg}Must specifiy --replace or --update to make changes.`)
}
}
try {
this.generateConfig(configJson)
await fsp.mkdir(confDir, { recursive: true })
await fsp.writeFile(outpath, `export default ${JSON.stringify(configJson, null, 2)};`)
console.log(`Config file written successfully to ${outpath}.\n`)
this.logRequired(configJson)
} catch (e) {
console.log(`ERROR: Failed to write ${outpath}\n${e}`)
}
}

logRequired (configJson) {
const requiredAttrs = []
Object.entries(configJson).forEach(([name, config]) => {
Object.entries(config).forEach(([key, value]) => value === null && requiredAttrs.push(`${name}.${key}`))
})
if (requiredAttrs.length) {
console.log('Note: the following required attributes have been given a value of null and must be set for the application to run:\n')
console.log(requiredAttrs.join('\n'))
console.log('')
}
}

getDeps () {
try {
const depRoot = `${process.cwd()}/node_modules/`.replaceAll(path.sep, path.posix.sep)
return globSync(`${depRoot}**/adapt-authoring.json`).map(f => {
const dirname = path.dirname(f)
return [dirname.replace(depRoot, ''), dirname]
})
} catch (e) {
console.log('Failed to load package', e)
return []
}
}

generateConfig (configJson) {
for (const [name, dir] of this.getDeps()) {
let schema
try {
schema = JSON.parse(fs.readFileSync(path.resolve(dir, 'conf/config.schema.json')))
} catch (e) {
continue
}
if (!configJson[name]) {
configJson[name] = {}
}
storeDefaults(schema, configJson[name], { replace: this.options.replace, useDefaults: this.options.defaults })
}
// remove any empty objects
Object.entries(configJson).forEach(([key, config]) => !Object.keys(config).length && delete configJson[key])
}
}
16 changes: 9 additions & 7 deletions bin/deps-check.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import SimpleCliCommand from '../lib/SimpleCliCommand.js'
import Utils from '../lib/Utils.js'
import { isAdaptModule, deriveExpectedPeerDeps, deriveExpectedDeps, findOutdatedVersions } from '../lib/peerDeps.js'
import buildPackageIndex from '../lib/utils/buildPackageIndex.js'
import CliCommand from '../lib/CliCommand.js'
import getModuleDirs from '../lib/utils/getModuleDirs.js'
import isModule from '../lib/utils/isModule.js'
import { isAdaptModule, deriveExpectedPeerDeps, deriveExpectedDeps, findOutdatedVersions } from '../lib/utils/peerDeps.js'

const CORE_PKG = 'adapt-authoring-core'

export default class DepsCheck extends SimpleCliCommand {
export default class DepsCheck extends CliCommand {
get config () {
return {
...super.config,
Expand All @@ -25,22 +27,22 @@ export default class DepsCheck extends SimpleCliCommand {
let moduleDirs

if (this.options.recursive) {
moduleDirs = Utils.getModuleDirs(cwd)
moduleDirs = getModuleDirs(cwd)
if (moduleDirs.length === 0) {
console.log('No modules found in child directories.')
process.exitCode = 1
return
}
} else {
if (!this.options.versionsOnly && !Utils.isModule(cwd)) {
if (!this.options.versionsOnly && !isModule(cwd)) {
console.error(`Not a valid module directory (no adapt-authoring.json found in ${cwd})`)
process.exitCode = 1
return
}
moduleDirs = [cwd]
}

const pkgIndex = Utils.buildPackageIndex(join(cwd, this.options.recursive ? '.' : '..'))
const pkgIndex = buildPackageIndex(join(cwd, this.options.recursive ? '.' : '..'))
let totalErrors = 0

for (const moduleDir of moduleDirs) {
Expand Down
19 changes: 11 additions & 8 deletions bin/deps-gen.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import SimpleCliCommand from '../lib/SimpleCliCommand.js'
import Utils from '../lib/Utils.js'
import { isAdaptModule, deriveExpectedPeerDeps, deriveExpectedDeps, findOutdatedVersions } from '../lib/peerDeps.js'
import buildPackageIndex from '../lib/utils/buildPackageIndex.js'
import CliCommand from '../lib/CliCommand.js'
import exec from '../lib/utils/exec.js'
import getModuleDirs from '../lib/utils/getModuleDirs.js'
import isModule from '../lib/utils/isModule.js'
import { isAdaptModule, deriveExpectedPeerDeps, deriveExpectedDeps, findOutdatedVersions } from '../lib/utils/peerDeps.js'

const CORE_PKG = 'adapt-authoring-core'

export default class DepsGen extends SimpleCliCommand {
export default class DepsGen extends CliCommand {
get config () {
return {
...super.config,
Expand All @@ -26,22 +29,22 @@ export default class DepsGen extends SimpleCliCommand {
let moduleDirs

if (this.options.recursive) {
moduleDirs = Utils.getModuleDirs(cwd)
moduleDirs = getModuleDirs(cwd)
if (moduleDirs.length === 0) {
console.log('No modules found in child directories.')
process.exitCode = 1
return
}
} else {
if (!this.options.versionsOnly && !Utils.isModule(cwd)) {
if (!this.options.versionsOnly && !isModule(cwd)) {
console.error(`Not a valid module directory (no adapt-authoring.json found in ${cwd})`)
process.exitCode = 1
return
}
moduleDirs = [cwd]
}

const pkgIndex = Utils.buildPackageIndex(join(cwd, this.options.recursive ? '.' : '..'))
const pkgIndex = buildPackageIndex(join(cwd, this.options.recursive ? '.' : '..'))
const updatedDirs = []
let count = 0

Expand All @@ -65,7 +68,7 @@ export default class DepsGen extends SimpleCliCommand {
for (const dir of updatedDirs) {
console.log(` Running npm update in ${dir}...`)
try {
await Utils.exec('npm update', dir)
await exec('npm update', dir)
console.log(' ✓ npm update complete')
} catch (e) {
console.error(` ✗ npm update failed: ${e.message}`)
Expand Down
118 changes: 67 additions & 51 deletions bin/install.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,106 @@
import CliCommand from '../lib/CliCommand.js'
import DEFAULT_OPTIONS from '../lib/DEFAULT_OPTIONS.js'
import { randomBytes } from 'crypto'
import fs from 'fs/promises'
import path from 'path'
import prompts from 'prompts'
import CliCommand from '../lib/CliCommand.js'
import DEFAULT_OPTIONS from '../lib/DEFAULT_OPTIONS.js'
import exec from '../lib/utils/exec.js'
import getInstalledVersion from '../lib/utils/getInstalledVersion.js'
import Installer from '../lib/Installer.js'
import UiServer from '../lib/UiServer.js'
import Utils from '../lib/Utils.js'

export default class Install extends CliCommand {
get config () {
return {
...super.config,
description: 'Installs the application into destination directory',
params: { destination: 'The destination folder for the install' },
checkPrerequisites: true,
getReleaseData: true,
options: [
...DEFAULT_OPTIONS,
['-e --super-email <email>', 'The admin user email address'],
['-p --pipe-passwd', 'Whether the admin password will be piped into the script']
['-e --super-email <email>', 'The admin user email address']
]
}
}

async runTask () {
await this.handleExistingInstall()
await this.checkTargetDir()

if (this.options.ui) {
return new UiServer(this.options)
.on('exit', e => this.cleanUp(e))
}
if (this.options.devMode) {
console.log('IMPORTANT: dev mode flag currently has no effect when running in headless mode\n')
}
try {
if (!this.options.tag) this.options.tag = await this.getReleaseInput()

console.log(`Installing Adapt authoring tool ${this.options.tag} in ${this.options.cwd}`)
await this.cloneRepo()
await Utils.registerSuperUser(this.options)
await Utils.clearInstallState(this.options.cwd)
if (this.options.resumeInstall) {
console.log('Resuming previous install, reinstalling dependencies')
await exec('npm ci', this.options.cwd)
} else {
await new Installer(this.options).install()
}
await this.createSuperUser()

this.cleanUp()
this.logSuccess('Install completed successfully!')
} catch (e) {
this.cleanUp(e)
}
}

async handleExistingInstall () {
const checkpoint = await Utils.getInstallState(this.options.cwd)
if (checkpoint) {
const { resume } = await this.getInput([{
type: 'confirm',
name: 'resume',
message: 'A previous install was interrupted. Resume from where you left off?',
initial: true
async createSuperUser () {
let email = this.options.superEmail
if (!email) {
const { emailInput } = await prompts([{
type: 'text',
name: 'emailInput',
message: 'Enter an email address for the super admin account'
}])
if (resume) {
this.options.resumeStep = checkpoint.step
return
}
await Utils.clearInstallState(this.options.cwd)
await fs.rm(this.options.cwd, { recursive: true, force: true })
await fs.mkdir(this.options.cwd, { recursive: true })
return
email = emailInput
}
let files
try {
files = await fs.readdir(this.options.cwd)
} catch (e) {}
if (files?.some(f => f !== 'conf')) throw new Error('Install directory must be empty')
if (!email) throw new Error('Email is required for super admin account')

const password = randomBytes(16).toString('base64url')
const confDir = path.resolve(this.options.cwd, 'conf')
await fs.mkdir(confDir, { recursive: true })
await fs.writeFile(path.resolve(confDir, '.superuser'), JSON.stringify({ email, password }, null, 2))

console.log('\nSuper admin account will be created on first app start.')
console.log(`Email: ${email}`)
console.log(`Password: ${password}`)
console.log('Please save this password. You will be asked to change it on first login.\n')
}

async cleanUp (error) {
if (error) {
console.log('Install failed, performing cleanup operation')
try { // for obvious reasons don't remove dest if git clone threw EEXIST
if (error.code !== 'GITCLONEEEXIST') {
console.log('- Removing broken install files')
await fs.rm(this.options.cwd, { recursive: true, force: true })
if (this.configContents) {
console.log('- Reinstating original config file')
await fs.writeFile(path.resolve(this.options.cwd, `conf/${process.env.NODE_ENV}.config.js`), this.configContents)
}
}
} catch (e) {
console.log('Oh dear, cleanup failed.\n')
console.trace(e)
}
async checkTargetDir () {
const previousTag = await getInstalledVersion(this.options.cwd)
if (!previousTag) {
let files
try {
files = await fs.readdir(this.options.cwd)
} catch (e) {}
if (files?.some(f => f !== 'conf')) throw new Error('Install directory must be empty')
return
}
const hasPackageJson = await fs.access(path.resolve(this.options.cwd, 'package.json')).then(() => true, () => false)
const choices = [
{ title: 'Remove and start fresh', value: 'fresh' },
...(hasPackageJson ? [{ title: `Resume install (${previousTag})`, value: 'resume' }] : []),
{ title: 'Cancel', value: 'cancel' }
]
const { action } = await prompts([{
type: 'select',
name: 'action',
message: `A previous install (${previousTag}) was found in this directory.`,
choices
}])
if (action === 'cancel' || !action) throw new Error('Install cancelled')
if (action === 'resume') {
this.options.tag = previousTag
this.options.resumeInstall = true
return
}
super.cleanUp(error)
await fs.rm(this.options.cwd, { recursive: true, force: true })
await fs.mkdir(this.options.cwd, { recursive: true })
}
}
Loading
Loading