feat: CLI + local linking (#313)

* fix: allow local cmd without exiting process

* fix: improves cli experience

* fix: allow running link with --develop

* test: adds snapshot testing of error logs

* chore: cleanup

* feat(medusa-cli): new command (#320)

* adds: new command

* fix: creates  command for easy project setup

* chore: deps

* chore: deps

* fix: loggin

* fix: logging

* fix: adds cli as dependency in core

* fix: consolidates CLI in medusa

* fix: use project deps medusa bin

* fix: use project deps medusa bin

* fix: use cli path

* fix: new command setup db + env vars

* fix: new command with db seed

* fix: cleanup
This commit is contained in:
Sebastian Rindom
2021-07-26 10:09:55 +02:00
committed by GitHub
parent 804a2f6ed9
commit f4a7138a58
22 changed files with 2284 additions and 159 deletions
+79 -18
View File
@@ -1,10 +1,14 @@
const axios = require("axios").default
const inquirer = require("inquirer")
const open = require("open")
const execa = require("execa")
const resolveCwd = require(`resolve-cwd`)
const { getToken } = require("../util/token-store")
const logger = require("../reporter").default
module.exports = {
link: async argv => {
const port = process.env.PORT || 9000
const appHost =
process.env.MEDUSA_APP_HOST || "https://app.medusa-commerce.com"
@@ -34,7 +38,7 @@ module.exports = {
process.exit(1)
}
// Get the currenty logged in user; we will be using the Cloud user id to
// Get the currently logged in user; we will be using the Cloud user id to
// create a user in the local DB with the same user id; allowing you to
// authenticate to the local API.
const { data: auth } = await axios
@@ -48,29 +52,86 @@ module.exports = {
process.exit(1)
})
const linkActivity = logger.activity("Linking local project")
// Create the user with the user id
if (!argv.skipLocalUser && auth.user) {
const localCmd = resolveLocalCommand(`user`)
await localCmd({
directory: argv.directory,
id: auth.user.id,
email: auth.user.email,
})
let proc
try {
proc = execa(
`./node_modules/@medusajs/medusa/cli.js`,
[`user`, `--id`, auth.user.id, `--email`, auth.user.email],
{
env: {
...process.env,
NODE_ENV: "command",
},
}
)
const res = await proc
if (res.stderr) {
const err = new Error("stderr error")
err.stderr = res.stderr
throw err
}
} catch (error) {
logger.failure(linkActivity, "Failed to perform local linking")
if (error.stderr) {
console.error(error.stderr)
} else if (error.code === "ENOENT") {
logger.error(
`Couldn't find the Medusa CLI - please make sure that you have installed it globally`
)
}
process.exit(1)
}
}
// This step sets the Cloud link by opening a browser
const bo = await open(
`${appHost}/local-link?lurl=http://localhost:9000&ltoken=${auth.user.id}`,
{
app: "browser",
wait: false,
}
logger.success(linkActivity, "Local project linked")
console.log()
console.log(
"Link Medusa Cloud to your local server. This will open the browser"
)
bo.on("error", err => {
console.warn(err)
console.log(
`Could not open browser go to: ${appHost}/local-link?lurl=http://localhost:9000&ltoken=${auth.user.id}`
console.log()
const prompts = [
{
type: "input",
name: "open",
message: "Press enter key to open browser for linking or n to exit",
},
]
await inquirer.prompt(prompts).then(async a => {
if (a.open === "n") {
process.exit(0)
}
const params = `lurl=http://localhost:${port}&ltoken=${auth.user.id}`
// This step sets the Cloud link by opening a browser
const browserOpen = await open(
`${appHost}/local-link?${encodeURI(params)}`,
{
app: "browser",
wait: false,
}
)
browserOpen.on("error", err => {
console.warn(err)
console.log(
`Could not open browser go to: ${appHost}/local-link?lurl=http://localhost:9000&ltoken=${auth.user.id}`
)
})
})
if (argv.develop) {
const proc = execa(`./node_modules/@medusajs/medusa/cli.js`, [`develop`])
proc.stdout.pipe(process.stdout)
proc.stderr.pipe(process.stderr)
await proc
}
},
}
+47 -38
View File
@@ -2,6 +2,7 @@ const axios = require("axios").default
const open = require("open")
const inquirer = require("inquirer")
const logger = require("../reporter").default
const { setToken } = require("../util/token-store")
/**
@@ -21,7 +22,9 @@ module.exports = {
const { data: urls } = await axios.post(authHost)
const qs = [
const loginUri = `${loginHost}${urls.browser_url}`
const prompts = [
{
type: "input",
name: "open",
@@ -29,52 +32,58 @@ module.exports = {
},
]
await inquirer.prompt(qs).then(async a => {
console.log()
console.log("Login to Medusa Cloud")
console.log()
await inquirer.prompt(prompts).then(async a => {
if (a.open === "n") {
process.exit(0)
}
const bo = await open(`${loginHost}${urls.browser_url}`, {
const browserOpen = await open(loginUri, {
app: "browser",
wait: false,
})
bo.on("error", err => {
browserOpen.on("error", err => {
console.warn(err)
console.log(
`Could not open browser go to: ${loginHost}${urls.browser_url}`
)
console.log(`Could not open browser go to: ${loginUri}`)
})
})
const spinner = logger.activity(`Waiting for login at ${loginUri}`)
const fetchAuth = async (retries = 3) => {
try {
const { data: auth } = await axios.get(`${authHost}${urls.cli_url}`, {
headers: { authorization: `Bearer ${urls.cli_token}` },
})
return auth
} catch (err) {
if (retries > 0 && err.http && err.http.statusCode > 500)
return fetchAuth(retries - 1)
throw err
}
}
const auth = await fetchAuth()
// This is kept alive for several seconds until the user has authenticated
// in the browser.
const { data: user } = await axios
.get(`${apiHost}/auth`, {
headers: {
authorization: `Bearer ${auth.password}`,
},
})
.catch(err => {
console.log(err)
process.exit(1)
})
const fetchAuth = async (retries = 3) => {
try {
const { data: auth } = await axios.get(`${authHost}${urls.cli_url}`, {
headers: { authorization: `Bearer ${urls.cli_token}` },
})
return auth
} catch (err) {
if (retries > 0 && err.http && err.http.statusCode > 500)
return fetchAuth(retries - 1)
throw err
}
}
const auth = await fetchAuth()
// This is kept alive for several seconds until the user has authenticated
// in the browser.
const { data: user } = await axios
.get(`${apiHost}/auth`, {
headers: {
authorization: `Bearer ${auth.password}`,
},
})
.catch(err => {
console.log(err)
process.exit(1)
})
if (user) {
setToken(auth.password)
}
})
if (user) {
logger.success(spinner, "Log in succeeded.")
setToken(auth.password)
} else {
logger.failure(spinner, "Log in failed.")
}
},
}
+450
View File
@@ -0,0 +1,450 @@
/*
* Adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-cli/src/init-starter.ts
*/
import { execSync } from "child_process"
import execa from "execa"
import { sync as existsSync } from "fs-exists-cached"
import fs from "fs-extra"
import hostedGitInfo from "hosted-git-info"
import isValid from "is-valid-path"
import sysPath from "path"
import prompts from "prompts"
import url from "url"
import { createDatabase } from "pg-god"
import reporter from "../reporter"
import { getPackageManager, setPackageManager } from "../util/package-manager"
const spawnWithArgs = (file, args, options) =>
execa(file, args, { stdio: `inherit`, preferLocal: false, ...options })
const spawn = (cmd, options) => {
const [file, ...args] = cmd.split(/\s+/)
return spawnWithArgs(file, args, options)
}
// Checks the existence of yarn package
// We use yarnpkg instead of yarn to avoid conflict with Hadoop yarn
// Refer to https://github.com/yarnpkg/yarn/issues/673
const checkForYarn = () => {
try {
execSync(`yarnpkg --version`, { stdio: `ignore` })
return true
} catch (e) {
return false
}
}
const isAlreadyGitRepository = async () => {
try {
return await spawn(`git rev-parse --is-inside-work-tree`, {
stdio: `pipe`,
}).then(output => output.stdout === `true`)
} catch (err) {
return false
}
}
// Initialize newly cloned directory as a git repo
const gitInit = async rootPath => {
reporter.info(`Initialising git in ${rootPath}`)
return await spawn(`git init`, { cwd: rootPath })
}
// Create a .gitignore file if it is missing in the new directory
const maybeCreateGitIgnore = async rootPath => {
if (existsSync(sysPath.join(rootPath, `.gitignore`))) {
return
}
const gignore = reporter.activity(
`Creating minimal .gitignore in ${rootPath}`
)
await fs.writeFile(
sysPath.join(rootPath, `.gitignore`),
`.cache\nnode_modules\npublic\n`
)
reporter.success(gignore, `Created .gitignore in ${rootPath}`)
}
// Create an initial git commit in the new directory
const createInitialGitCommit = async (rootPath, starterUrl) => {
reporter.info(`Create initial git commit in ${rootPath}`)
await spawn(`git add -A`, { cwd: rootPath })
// use execSync instead of spawn to handle git clients using
// pgp signatures (with password)
try {
execSync(`git commit -m "Initial commit from gatsby: (${starterUrl})"`, {
cwd: rootPath,
})
} catch {
// Remove git support if initial commit fails
reporter.warn(`Initial git commit failed - removing git support\n`)
fs.removeSync(sysPath.join(rootPath, `.git`))
}
}
// Executes `npm install` or `yarn install` in rootPath.
const install = async rootPath => {
const prevDir = process.cwd()
reporter.info(`Installing packages...`)
process.chdir(rootPath)
const npmConfigUserAgent = process.env.npm_config_user_agent
try {
if (!getPackageManager()) {
if (npmConfigUserAgent?.includes(`yarn`)) {
setPackageManager(`yarn`)
} else {
setPackageManager(`npm`)
}
}
if (getPackageManager() === `yarn` && checkForYarn()) {
await fs.remove(`package-lock.json`)
await spawn(`yarnpkg`)
} else {
await fs.remove(`yarn.lock`)
await spawn(`npm install`)
}
} finally {
process.chdir(prevDir)
}
}
const ignored = path => !/^\.(git|hg)$/.test(sysPath.basename(path))
// Copy starter from file system.
const copy = async (starterPath, rootPath) => {
// Chmod with 755.
// 493 = parseInt('755', 8)
await fs.ensureDir(rootPath, { mode: 493 })
if (!existsSync(starterPath)) {
throw new Error(`starter ${starterPath} doesn't exist`)
}
if (starterPath === `.`) {
throw new Error(
`You can't create a starter from the existing directory. If you want to
create a new project in the current directory, the trailing dot isn't
necessary. If you want to create a project from a local starter, run
something like "medusa new my-medusa-store ../local-medusa-starter"`
)
}
reporter.info(`Creating new site from local starter: ${starterPath}`)
const copyActivity = reporter.activity(
`Copying local starter to ${rootPath} ...`
)
await fs.copy(starterPath, rootPath, { filter: ignored })
reporter.success(copyActivity, `Created starter directory layout`)
await install(rootPath)
return true
}
// Clones starter from URI.
const clone = async (hostInfo, rootPath) => {
let url
// Let people use private repos accessed over SSH.
if (hostInfo.getDefaultRepresentation() === `sshurl`) {
url = hostInfo.ssh({ noCommittish: true })
// Otherwise default to normal git syntax.
} else {
url = hostInfo.https({ noCommittish: true, noGitPlus: true })
}
const branch = hostInfo.committish ? [`-b`, hostInfo.committish] : []
const createAct = reporter.activity(`Creating new project from git: ${url}`)
const args = [
`clone`,
...branch,
url,
rootPath,
`--recursive`,
`--depth=1`,
].filter(arg => Boolean(arg))
await execa(`git`, args, {})
.then(() => {
reporter.success(createAct, `Created starter directory layout`)
})
.catch(err => {
reporter.failure(createAct, `Failed to clone repository`)
throw err
})
await fs.remove(sysPath.join(rootPath, `.git`))
await install(rootPath)
const isGit = await isAlreadyGitRepository()
if (!isGit) await gitInit(rootPath)
await maybeCreateGitIgnore(rootPath)
if (!isGit) await createInitialGitCommit(rootPath, url)
}
const getPaths = async (starterPath, rootPath) => {
let selectedOtherStarter = false
// if no args are passed, prompt user for path and starter
if (!starterPath && !rootPath) {
const response = await prompts.prompt([
{
type: `text`,
name: `path`,
message: `What is your project called?`,
initial: `my-medusa-store`,
},
])
// exit gracefully if responses aren't provided
if (!response.starter || !response.path.trim()) {
throw new Error(
`Please mention both starter package and project name along with path(if its not in the root)`
)
}
selectedOtherStarter = response.starter === `different`
starterPath = `medusajs/medusa-starter-default`
rootPath = response.path
}
// set defaults if no root or starter has been set yet
rootPath = rootPath || process.cwd()
starterPath = starterPath || `medusajs/medusa-starter-default`
return { starterPath, rootPath, selectedOtherStarter }
}
const successMessage = path => {
reporter.info(`
Your new Medusa project is ready for you! To start developing run:
cd ${path}
medusa develop
`)
}
const defaultDBCreds = {
user: "postgres",
database: "postgres",
password: "",
port: 5432,
host: "localhost",
}
const setupDB = async (dbName, dbCreds = {}) => {
const credentials = Object.assign(defaultDBCreds, dbCreds)
const dbActivity = reporter.activity(`Setting up database "${dbName}"...`)
await createDatabase(
{
databaseName: dbName,
errorIfExist: true,
},
credentials
)
.then(() => {
reporter.success(dbActivity, `Created database "${dbName}"`)
})
.catch(err => {
if ((err.name = "PDG_ERR::DuplicateDatabase")) {
reporter.success(
dbActivity,
`Database ${dbName} already exists; skipping setup`
)
} else {
reporter.failure(dbActivity, `Skipping database setup.`)
reporter.warn(
`Failed to setup database; install PostgresQL or make sure to manage your database connection manually`
)
console.error(err)
}
})
}
const setupEnvVars = async (rootPath, dbName, dbCreds = {}) => {
const credentials = Object.assign(defaultDBCreds, dbCreds)
const templatePath = sysPath.join(rootPath, ".env.template")
const destination = sysPath.join(rootPath, ".env")
if (existsSync(templatePath)) {
fs.renameSync(templatePath, destination)
fs.appendFileSync(
destination,
`DATABASE_URL=postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}\n`
)
} else {
reporter.info(`No .env.template found. Creating .env.`)
fs.appendFileSync(
destination,
`DATABASE_URL=postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}\n`
)
}
}
const runMigrations = async rootPath => {
const migrationActivity = reporter.activity("Applying database migrations...")
const cliPath = sysPath.join(
`node_modules`,
`@medusajs`,
`medusa-cli`,
`cli.js`
)
return await execa(cliPath, [`migrations`, `run`], {
cwd: rootPath,
})
.then(() => {
reporter.success(migrationActivity, "Database migrations completed.")
})
.catch(err => {
reporter.failure(
migrationActivity,
"Failed to migrate database you must complete migration manually before starting your server."
)
console.error(err)
})
}
const attemptSeed = async rootPath => {
const seedActivity = reporter.activity("Seeding database")
const pkgPath = sysPath.resolve(rootPath, "package.json")
if (existsSync(pkgPath)) {
const pkg = require(pkgPath)
if (pkg.scripts && pkg.scripts.seed) {
const proc = execa(getPackageManager(), [`run`, `seed`], {
cwd: rootPath,
})
// Useful for development
// proc.stdout.pipe(process.stdout)
await proc
.then(() => {
reporter.success(seedActivity, "Seed completed")
})
.catch(err => {
reporter.failure(seedActivity, "Failed to complete seed; skipping")
console.error(err)
})
} else {
reporter.failure(
seedActivity,
"Starter doesn't provide a seed command; skipping."
)
}
} else {
reporter.failure(seedActivity, "Could not find package.json")
}
}
/**
* Main function that clones or copies the starter.
*/
export const newStarter = async args => {
const {
starter,
root,
skipDb,
skipMigrations,
skipEnv,
seed,
dbUser,
dbDatabase,
dbPass,
dbPort,
dbHost,
} = args
const dbCredentials = {
user: dbUser,
database: dbDatabase,
password: dbPass,
port: dbPort,
host: dbHost,
}
const { starterPath, rootPath } = await getPaths(starter, root)
const urlObject = url.parse(rootPath)
if (urlObject.protocol && urlObject.host) {
const isStarterAUrl =
starter && !url.parse(starter).hostname && !url.parse(starter).protocol
if (/medusa-starter/gi.test(rootPath) && isStarterAUrl) {
reporter.panic({
id: `11610`,
context: {
starter,
rootPath,
},
})
return
}
reporter.panic({
id: `11611`,
context: {
rootPath,
},
})
return
}
if (!isValid(rootPath)) {
reporter.panic({
id: `11612`,
context: {
path: sysPath.resolve(rootPath),
},
})
return
}
if (existsSync(sysPath.join(rootPath, `package.json`))) {
reporter.panic({
id: `11613`,
context: {
rootPath,
},
})
return
}
const hostedInfo = hostedGitInfo.fromUrl(starterPath)
if (hostedInfo) {
await clone(hostedInfo, rootPath)
} else {
await copy(starterPath, rootPath)
}
if (!skipDb) {
await setupDB(root, dbCredentials)
}
if (!skipEnv) {
await setupEnvVars(rootPath, root, dbCredentials)
}
if (!skipMigrations) {
await runMigrations(rootPath)
}
if (seed) {
await attemptSeed(rootPath)
}
successMessage(rootPath)
}
+10 -2
View File
@@ -1,5 +1,6 @@
const axios = require("axios").default
const { getToken } = require("../util/token-store")
const logger = require("../reporter").default
/**
* Fetches the locally logged in user.
@@ -18,6 +19,8 @@ module.exports = {
process.exit(0)
}
const activity = logger.activity("checking login details")
const { data: auth } = await axios
.get(`${apiHost}/auth`, {
headers: {
@@ -25,12 +28,17 @@ module.exports = {
},
})
.catch(err => {
console.log(err)
logger.failure(activity, "Couldn't gather login details")
logger.error(err)
process.exit(1)
})
if (auth.user) {
console.log(`Hi, ${auth.user.first_name}! Here are your details:`)
logger.success(
activity,
`Hi, ${auth.user.first_name}! Here are your details:`
)
console.log(`id: ${auth.user.id}`)
console.log(`email: ${auth.user.email}`)
console.log(`first_name: ${auth.user.first_name}`)
+69 -7
View File
@@ -6,6 +6,7 @@ const existsSync = require(`fs-exists-cached`).sync
const { getLocalMedusaVersion } = require(`./util/version`)
const { didYouMean } = require(`./did-you-mean`)
const { newStarter } = require("./commands/new")
const { whoami } = require("./commands/whoami")
const { login } = require("./commands/login")
const { link } = require("./commands/link")
@@ -66,6 +67,57 @@ function buildLocalCommands(cli, isLocalProject) {
}
cli
.command({
command: `new [root] [starter]`,
builder: _ =>
_.option(`seed`, {
type: `boolean`,
describe: `If flag is set the command will attempt to seed the database after setup.`,
default: false,
})
.option(`skip-db`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to complete database setup`,
default: false,
})
.option(`skip-migrations`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to complete database migration`,
default: false,
})
.option(`skip-env`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to populate .env`,
default: false,
})
.option(`db-user`, {
type: `string`,
describe: `The database user to use for database setup and migrations.`,
default: `postgres`,
})
.option(`db-database`, {
type: `string`,
describe: `The database use for database setup and migrations.`,
default: `postgres`,
})
.option(`db-pass`, {
type: `string`,
describe: `The database password to use for database setup and migrations.`,
default: ``,
})
.option(`db-port`, {
type: `number`,
describe: `The database port to use for database setup and migrations.`,
default: 5432,
})
.option(`db-host`, {
type: `string`,
describe: `The database host to use for database setup and migrations.`,
default: `localhost`,
}),
desc: `Create a new Medusa project.`,
handler: handlerP(newStarter),
})
.command({
command: `seed`,
desc: `Migrates and populates the database with the provided file.`,
@@ -112,11 +164,15 @@ function buildLocalCommands(cli, isLocalProject) {
command: `link`,
desc: `Creates your Medusa Cloud user in your local database for local testing.`,
builder: _ =>
_.option(`skip-local-user`, {
alias: `skipLocalUser`,
_.option(`su`, {
alias: `skip-local-user`,
type: `boolean`,
default: false,
describe: `If set a user will not be created in the database.`,
}).option(`develop`, {
type: `boolean`,
default: false,
describe: `If set medusa develop will be run after successful linking.`,
}),
handler: handlerP(argv => {
if (!isLocalProject) {
@@ -198,11 +254,17 @@ function buildLocalCommands(cli, isLocalProject) {
alias: `email`,
type: `string`,
describe: `User's email.`,
}).option(`p`, {
alias: `password`,
type: `string`,
describe: `User's password.`,
}),
})
.option(`p`, {
alias: `password`,
type: `string`,
describe: `User's password.`,
})
.option(`i`, {
alias: `id`,
type: `string`,
describe: `User's id.`,
}),
handler: handlerP(
getCommandHandler(`user`, (args, cmd) => {
cmd(args)
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reporter handles "Error" signature correctly 1`] = `
Object {
"level": "error",
"message": "Error",
"stack": Any<Array>,
}
`;
exports[`Reporter handles "String" signature correctly 1`] = `
Object {
"level": "error",
"message": "Test log",
}
`;
exports[`Reporter handles "String, Error" signature correctly 1`] = `
Object {
"level": "error",
"message": "Test log",
"stack": Any<Array>,
}
`;
@@ -0,0 +1,49 @@
import logger, { Reporter } from "../"
describe(`Reporter`, () => {
const winstonMock = {
log: jest.fn(),
}
const reporter = new Reporter({
logger: winstonMock,
activityLogger: {},
})
const getErrorMessages = fn =>
fn.mock.calls
.map(([firstArg]) => firstArg)
.filter(structuredMessage => structuredMessage.level === `error`)
beforeEach(() => {
winstonMock.log.mockClear()
})
it(`handles "String" signature correctly`, () => {
reporter.error("Test log")
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot()
})
it(`handles "String, Error" signature correctly`, () => {
reporter.error("Test log", new Error("String Error"))
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot({
stack: expect.any(Array),
})
})
it(`handles "Error" signature correctly`, () => {
reporter.error(new Error("Error"))
const generated = getErrorMessages(winstonMock.log)[0]
expect(generated).toMatchSnapshot({
stack: expect.any(Array),
})
})
})
+254
View File
@@ -0,0 +1,254 @@
import stackTrace from "stack-trace"
import { ulid } from "ulid"
import winston from "winston"
import ora from "ora"
const LOG_LEVEL = process.env.LOG_LEVEL || "silly"
const NODE_ENV = process.env.NODE_ENV || "development"
const transports = []
if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") {
transports.push(new winston.transports.Console())
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat()
),
})
)
}
const loggerInstance = winston.createLogger({
level: LOG_LEVEL,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
transports,
})
export class Reporter {
constructor({ logger, activityLogger }) {
this.activities_ = []
this.loggerInstance_ = logger
this.ora_ = activityLogger
}
panic = error => {
this.loggerInstance_.log({
level: "error",
details: error,
})
process.exit(1)
}
/**
* Determines if the logger should log at a given level.
* @param {string} level - the level to check if logger is configured for
* @return {boolean} whether we should log
*/
shouldLog = level => {
level = this.loggerInstance_.levels[level]
const logLevel = this.loggerInstance_.levels[this.loggerInstance_.level]
return level <= logLevel
}
/**
* Sets the log level of the logger.
* @param {string} level - the level to set the logger to
*/
setLogLevel = level => {
this.loggerInstance_.level = level
}
/**
* Resets the logger to the value specified by the LOG_LEVEL env var. If no
* LOG_LEVEL is set it defaults to "silly".
*/
unsetLogLevel = () => {
this.loggerInstance_.level = LOG_LEVEL
}
/**
* Begin an activity. In development an activity is displayed as a spinner;
* in other environments it will log the activity at the info level.
* @param {string} message - the message to log the activity under
* @returns {string} the id of the activity; this should be passed to do
* further operations on the activity such as success, failure, progress.
*/
activity = message => {
const id = ulid()
if (NODE_ENV === "development" && this.shouldLog("info")) {
const activity = this.ora_(message).start()
this.activities_[id] = {
activity,
start: Date.now(),
}
return id
} else {
this.activities_[id] = {
start: Date.now(),
}
this.loggerInstance_.log({
activity_id: id,
level: "info",
message,
})
return id
}
}
/**
* Reports progress on an activity. In development this will update the
* activity log message, in other environments a log message will be issued
* at the info level. Logging will include the activityId.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
*/
progress = (activityId, message) => {
const toLog = {
level: "info",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const activity = this.activities_[activityId]
if (activity.activity) {
activity.text = message
} else {
toLog.activity_id = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
}
/**
* Logs an error. If an error object is provided the stack trace for the error
* will also be logged.
* @param {String | Error} messageOrError - can either be a string with a
* message to log the error under; or an error object.
* @param {Error?} error - an error object to log message with
*/
error = (messageOrError, error) => {
let message = messageOrError
if (typeof messageOrError === "object") {
message = messageOrError.message
error = messageOrError
}
const toLog = {
level: "error",
message,
}
if (error) {
toLog.stack = stackTrace.parse(error)
}
this.loggerInstance_.log(toLog)
}
/**
* Reports failure of an activity. In development the activity will be udpated
* with the failure message in other environments the failure will be logged
* at the error level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
*/
failure = (activityId, message) => {
const toLog = {
level: "error",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const time = Date.now()
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.fail(`${message} ${time - activity.start}`)
} else {
toLog.duration = time - activity.start
toLog.activity_id = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
}
/**
* Reports success of an activity. In development the activity will be udpated
* with the failure message in other environments the failure will be logged
* at the info level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
*/
success = (activityId, message) => {
const toLog = {
level: "info",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const time = Date.now()
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.succeed(`${message} ${time - activity.start}ms`)
} else {
toLog.duration = time - activity.start
toLog.activity_id = activityId
this.loggerInstance_.log(toLog)
}
} else {
this.loggerInstance_.log(toLog)
}
}
/**
* Logs a message at the info level.
* @param {string} message - the message to log
*/
info = message => {
this.loggerInstance_.log({
level: "info",
message,
})
}
/**
* Logs a message at the warn level.
* @param {string} message - the message to log
*/
warn = message => {
this.loggerInstance_.warn({
level: "warn",
message,
})
}
/**
* A wrapper around winston's log method.
*/
log = (...args) => {
this.loggerInstance_.log(...args)
}
}
const logger = new Reporter({
logger: loggerInstance,
activityLogger: ora,
})
export default logger
@@ -0,0 +1,22 @@
import ConfigStore from "configstore"
import reporter from "../reporter"
let config
const packageMangerConfigKey = `cli.packageManager`
export const getPackageManager = () => {
if (!config) {
config = new ConfigStore(`medusa`, {}, { globalConfigPath: true })
}
return config.get(packageMangerConfigKey)
}
export const setPackageManager = packageManager => {
if (!config) {
config = new ConfigStore(`medusa`, {}, { globalConfigPath: true })
}
config.set(packageMangerConfigKey, packageManager)
reporter.info(`Preferred package manager set to "${packageManager}"`)
}
+2 -2
View File
@@ -8,13 +8,13 @@ module.exports = {
config = new ConfigStore(`medusa`, {}, { globalConfigPath: true })
}
return config.get("login_token")
return config.get("cloud.login_token")
},
setToken: function(token) {
if (!config) {
config = new ConfigStore(`medusa`, {}, { globalConfigPath: true })
}
return config.set("login_token", token)
return config.set("cloud.login_token", token)
},
}