cli-engine-config
Version:
base cli-engine config objects and interfaces
469 lines (433 loc) • 11.5 kB
Flow
// @flow
import path from 'path'
import os from 'os'
import fs from 'fs-extra'
import type { UserConfig } from './user_config'
export type Topic = {
name: string,
description?: ?string,
hidden?: ?boolean,
}
type S3 = {
host?: string,
}
type CLI = {
dirname?: string,
defaultCommand?: string,
commands?: string,
s3?: S3,
hooks?: { [name: string]: string | string[] },
userPlugins: boolean,
plugins?: string[],
legacyConverter?: string,
topics?: { [name: string]: Topic },
npmRegistry?: string,
}
export type PJSON = {
name: string,
version: string,
dependencies: { [name: string]: string },
'cli-engine': CLI,
}
export type Config = {
name: string, // name of CLI
dirname: string, // name of CLI directory
initPath: string, // path to init script
commandsDir: string, // root path to CLI commands
bin: string, // name of binary
s3: S3, // S3 config
root: string, // root of CLI
home: string, // user home directory
pjson: PJSON, // parsed CLI package.json
updateDisabled: ?string, // CLI updates are disabled
defaultCommand: string, // default command if no args passed (usually help)
channel: string, // CLI channel for updates
version: string, // CLI version
debug: number, // debugging level
dataDir: string, // directory for storing CLI data
cacheDir: string, // directory for storing temporary CLI data
configDir: string, // directory for storing CLI config
arch: string, // CPU architecture
platform: string, // operating system
windows: boolean, // is windows OS
_version: '1', // config schema version
skipAnalytics: boolean, // skip processing of analytics
install: ?string, // generated uuid of this install
userAgent: string, // user agent for API calls
shell: string, // the shell in which the command is run
hooks: { [name: string]: string[] }, // scripts to run in the CLI on lifecycle events like prerun
userConfig: UserConfig, // users custom configuration json
argv: string[],
mock: boolean,
userPlugins: boolean,
topics: { [name: string]: Topic },
legacyConverter?: string,
errlog: string,
npmRegistry: string,
__cache: any, // memoization cache
}
export type ConfigOptions = $Shape<Config>
function dir(config: Config, category: string, d: ?string): string {
let cacheKey = `dir:${category}`
let cache = config.__cache[cacheKey]
if (cache) return cache
d = d || path.join(config.home, category === 'data' ? '.local/share' : '.' + category)
if (config.windows) d = process.env.LOCALAPPDATA || d
d = process.env.XDG_DATA_HOME || d
d = path.join(d, config.dirname)
fs.mkdirpSync(d)
config.__cache[cacheKey] = d
return d
}
function debug(bin: string) {
const debug = require('debug')(bin).enabled || envVarTrue(envVarKey(bin, 'DEBUG'))
return debug ? 1 : 0
}
function envVarKey(...parts: string[]) {
return parts
.map(p => p.replace(/-/g, '_'))
.join('_')
.toUpperCase()
}
function envVarTrue(k: string): boolean {
let v = process.env[k]
return v === '1' || v === 'true'
}
function loadUserConfig(config: Config): UserConfig {
const cache = config.__cache['userConfig']
if (cache) return cache
const configPath = path.join(config.configDir, 'config.json')
let userConfig: UserConfig
try {
userConfig = fs.readJSONSync(configPath)
} catch (e) {
if (e.code === 'ENOENT') {
userConfig = {
skipAnalytics: false,
install: null,
}
} else {
throw e
}
}
config.__cache['userConfig'] = userConfig
if (config.skipAnalytics) userConfig.install = null
else if (!userConfig.install) {
const uuid = require('uuid/v4')
userConfig.install = uuid()
try {
fs.writeJSONSync(configPath, userConfig)
} catch (e) {
userConfig.install = null
}
}
return userConfig
}
function shell(onWindows: boolean = false): string {
let shellPath
if (process.env['SHELL']) {
shellPath = process.env['SHELL'].split(`/`)
} else if (onWindows && process.env['COMSPEC']) {
shellPath = process.env['COMSPEC'].split(/\\|\//)
} else {
shellPath = ['unknown']
}
return shellPath[shellPath.length - 1]
}
function userAgent(config: Config) {
const channel = config.channel === 'stable' ? '' : ` ${config.channel}`
return `${config.name}/${config.version}${channel} (${config.platform}-${config.arch}) node-${process.version}`
}
function registry(config: Config): string {
const env = process.env[envVarKey(config.bin, 'NPM_REGISTRY')]
return env || config.pjson['cli-engine'].npmRegistry || 'https://registry.yarnpkg.com'
}
function s3(config: Config): S3 {
const env = process.env[envVarKey(config.bin, 'S3_HOST')]
const host = env || (config.pjson['cli-engine'].s3 && config.pjson['cli-engine'].s3.host)
return host ? { host } : {}
}
function commandsDir(config: Config): ?string {
let commandsDir = config.pjson['cli-engine'].commands
if (!commandsDir) return
return path.join(config.root, commandsDir)
}
function hooks(config: Config): { [name: string]: string[] } {
let hooks = {}
for (let [k, v] of Object.entries(config.pjson['cli-engine'].hooks || {})) {
hooks[k] = Array.isArray(v) ? v : [v]
}
return hooks
}
function envSkipAnalytics(config: Config) {
if (config.userConfig.skipAnalytics) {
return true
} else if (envVarTrue('TESTING') || envVarTrue(envVarKey(config.bin, 'SKIP_ANALYTICS'))) {
return true
}
return false
}
function topics(config: Config) {
if (!config.__cache['topics']) {
config.__cache['topics'] = config.pjson['cli-engine'].topics || {}
for (let [k, v]: [string, any] of Object.entries(config.__cache['topics'])) {
if (!v.name) v.name = k
}
}
return config.__cache['topics']
}
function validatePJSON(pjson: PJSON) {
// const exampleCLI = {
// bin: 'heroku',
// dirname: 'heroku',
// node: '8.0.0',
// defaultCommand: 'dashboard',
// commands: './lib/commands',
// hooks: {
// init: './lib/hooks/init.js',
// update: './lib/hooks/update.js',
// prerun: './lib/hooks/prerun.js',
// 'plugins:preinstall': './lib/hooks/plugins/preinstall.js'
// },
// s3: {host: 'host'},
// plugins: ['heroku-pg', 'heroku-redis']
// }
// TODO: validate
// const cli = pjson['cli-engine'] || {}
// const comment = 'cli-engine-config'
// const title = {
// warning: 'invalid CLI package.json',
// error: 'invalid CLI package.json' }
// validate(cli, {comment, title, exampleConfig: exampleCLI})
// validate(cli.hooks, {
// comment,
// condition: (option, validOption) => {
// console.dir({option, validOption})
// },
// title,
// exampleConfig: exampleCLI.hooks
// })
}
export interface RunReturn {
+stdout?: string;
+stderr?: string;
}
export type Arg = {
name: string,
description?: string,
required?: boolean,
optional?: boolean,
hidden?: boolean,
}
type AlphabetUppercase =
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'X'
| 'Y'
| 'Z'
type AlphabetLowercase =
| 'a'
| 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'i'
| 'j'
| 'k'
| 'l'
| 'm'
| 'n'
| 'o'
| 'p'
| 'q'
| 'r'
| 's'
| 't'
| 'u'
| 'v'
| 'x'
| 'y'
| 'z'
type CompletionContext = {
args?: ?{ [name: string]: string },
flags?: ?{ [name: string]: string },
argv?: ?(string[]),
config: Config,
}
export type Completion = {
skipCache?: boolean,
cacheDuration?: number,
cacheKey?: CompletionContext => Promise<string>,
options: CompletionContext => Promise<string[]>,
}
export type Flag = {
char?: AlphabetLowercase | AlphabetUppercase,
description?: string,
hidden?: boolean,
}
export type BooleanFlag = Flag & {
parse: null,
}
export type OptionFlag<T> = Flag & {
required?: ?boolean,
optional?: ?boolean,
parse: (?string, any | void, string | void) => Promise<?T> | ?T,
completion?: Completion,
}
export type Plugin = {
+name: string,
+version: string,
}
export interface ICommand {
+topic?: string;
+command?: ?string;
+description: ?string;
+hidden: ?boolean;
+usage: ?string;
+help: ?string;
+aliases: string[];
+_version: string;
+id: string;
+buildHelp?: (config: Config) => string;
+buildHelpLine?: (config: Config) => [string, ?string];
+args?: Arg[];
+flags?: { [name: string]: BooleanFlag | OptionFlag<*> };
+run: (options: ?ConfigOptions) => Promise<RunReturn>;
plugin?: ?Plugin;
}
export function buildConfig(existing: ?ConfigOptions = {}): Config {
if (!existing) existing = {}
if (existing._version) return (existing: any)
if (existing.root && !existing.pjson) {
let pjsonPath = path.join(existing.root, 'package.json')
if (fs.existsSync(pjsonPath)) {
// parse the package.json at the root
let pjson = fs.readJSONSync(path.join(existing.root, 'package.json'))
existing.pjson = {
...defaultConfig.pjson,
'cli-engine': {
...defaultConfig.pjson['cli-engine'],
...(pjson['cli-engine'] || {}),
},
...pjson,
}
validatePJSON(existing.pjson)
}
}
return {
_version: '1',
pjson: {
name: 'cli-engine',
version: '0.0.0',
dependencies: {},
'cli-engine': {
hooks: {},
defaultCommand: 'help',
userPlugins: false,
s3: { host: null },
},
},
channel: 'stable',
home: os.homedir() || os.tmpdir(),
root: path.join(__dirname, '..'),
arch: os.arch() === 'ia32' ? 'x86' : os.arch(),
platform: os.platform() === 'win32' ? 'windows' : os.platform(),
mock: false,
argv: process.argv.slice(1),
get defaultCommand() {
return this.pjson['cli-engine'].defaultCommand
},
get name() {
return this.pjson.name
},
get version() {
return this.pjson.version
},
get hooks() {
return hooks(this)
},
get windows() {
return this.platform === 'windows'
},
get userAgent() {
return userAgent(this)
},
get dirname() {
return this.pjson['cli-engine'].dirname || this.bin
},
get shell() {
return shell(this.windows)
},
get bin() {
return this.pjson['cli-engine'].bin || this.name
},
get debug() {
return debug(this.bin || 'cli-engine') || 0
},
get dataDir() {
return dir(this, 'data')
},
get configDir() {
return dir(this, 'config')
},
get cacheDir() {
return dir(this, 'cache', this.platform === 'darwin' ? path.join(this.home, 'Library', 'Caches') : null)
},
get userConfig() {
return loadUserConfig(this)
},
get skipAnalytics() {
return envSkipAnalytics(this)
},
get install() {
return this.userConfig.install
},
get s3() {
return s3(this)
},
get commandsDir() {
return commandsDir(this)
},
get legacyConverter() {
return this.pjson['cli-engine'].legacyConverter
},
get userPlugins() {
return this.pjson['cli-engine'].userPlugins
},
get topics() {
return topics(this)
},
get errlog() {
return path.join(this.cacheDir, 'error.log')
},
get npmRegistry() {
return registry(this)
},
...(existing: any),
__cache: {},
}
}
export const defaultConfig = buildConfig()