diff --git a/src/apm-cli.coffee b/src/apm-cli.coffee deleted file mode 100644 index e205ba6..0000000 --- a/src/apm-cli.coffee +++ /dev/null @@ -1,261 +0,0 @@ -{spawn} = require 'child_process' -path = require 'path' - -_ = require 'underscore-plus' -colors = require 'colors' -npm = require 'npm' -yargs = require 'yargs' -wordwrap = require 'wordwrap' - -# Enable "require" scripts in asar archives -require 'asar-require' - -config = require './apm' -fs = require './fs' -git = require './git' - -setupTempDirectory = -> - temp = require 'temp' - tempDirectory = require('os').tmpdir() - # Resolve ~ in tmp dir atom/atom#2271 - tempDirectory = path.resolve(fs.absolute(tempDirectory)) - temp.dir = tempDirectory - try - fs.makeTreeSync(temp.dir) - temp.track() - -setupTempDirectory() - -ciClass = -> require './ci' -cleanClass = -> require './clean' -configClass = -> require './config' -dedupClass = -> require './dedupe' -developClass = -> require './develop' -disableClass = -> require './disable' -docsClass = -> require './docs' -enableClass = -> require './enable' -featuredClass = -> require './featured' -initClass = -> require './init' -installClass = -> require './install' -linksClass = -> require './links' -linkClass = -> require './link' -listClass = -> require './list' -loginClass = -> require './login' -publishClass = -> require './publish' -rebuildClass = -> require './rebuild' -rebuildModuleCacheClass = -> require './rebuild-module-cache' -searchClass = -> require './search' -starClass = -> require './star' -starsClass = -> require './stars' -testClass = -> require './test' -uninstallClass = -> require './uninstall' -unlinkClass = -> require './unlink' -unpublishClass = -> require './unpublish' -unstarClass = -> require './unstar' -upgradeClass = -> require './upgrade' -viewClass = -> require './view' - -commands = { - 'ci': ciClass, - 'clean': cleanClass, - 'prune': cleanClass, - 'config': configClass, - 'dedupe': dedupClass, - 'dev': developClass, - 'develop': developClass, - 'disable': disableClass, - 'docs': docsClass, - 'home': docsClass, - 'open': docsClass, - 'enable': enableClass - 'featured': featuredClass - 'init': initClass, - 'install': installClass, - 'i': installClass, - 'link': linkClass, - 'ln': linkClass - 'linked': linksClass, - 'links': linksClass, - 'lns': linksClass, - 'list': listClass, - 'ls': listClass, - 'login': loginClass, - 'publish': publishClass, - 'rebuild-module-cache': rebuildModuleCacheClass, - 'rebuild': rebuildClass, - 'search': searchClass, - 'star': starClass, - 'stars': starsClass, - 'starred': starsClass - 'test': testClass - 'deinstall': uninstallClass, - 'delete': uninstallClass, - 'erase': uninstallClass, - 'remove': uninstallClass, - 'rm': uninstallClass, - 'uninstall': uninstallClass, - 'unlink': unlinkClass, - 'unpublish': unpublishClass, - 'unstar': unstarClass, - 'upgrade': upgradeClass, - 'outdated': upgradeClass, - 'update': upgradeClass, - 'view': viewClass, - 'show': viewClass, -} - -parseOptions = (args=[]) -> - options = yargs(args).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - apm - Atom Package Manager powered by https://atom.io - - Usage: apm - - where is one of: - #{wordwrap(4, 80)(Object.keys(commands).sort().join(', '))}. - - Run `apm help ` to see the more details about a specific command. - """ - options.alias('v', 'version').describe('version', 'Print the apm version') - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('color').default('color', true).describe('color', 'Enable colored output') - options.command = options.argv._[0] - for arg, index in args when arg is options.command - options.commandArgs = args[index+1..] - break - options - -showHelp = (options) -> - return unless options? - - help = options.help() - if help.indexOf('Options:') >= 0 - help += "\n Prefix an option with `no-` to set it to false such as --no-color to disable" - help += "\n colored output." - - console.error(help) - -printVersions = (args, callback) -> - apmVersion = require('../package.json').version ? '' - npmVersion = require('npm/package.json').version ? '' - nodeVersion = process.versions.node ? '' - - getPythonVersion (pythonVersion) -> - git.getGitVersion (gitVersion) -> - getAtomVersion (atomVersion) -> - if args.json - versions = - apm: apmVersion - npm: npmVersion - node: nodeVersion - atom: atomVersion - python: pythonVersion - git: gitVersion - nodeArch: process.arch - if config.isWin32() - versions.visualStudio = config.getInstalledVisualStudioFlag() - console.log JSON.stringify(versions) - else - pythonVersion ?= '' - gitVersion ?= '' - atomVersion ?= '' - versions = """ - #{'apm'.red} #{apmVersion.red} - #{'npm'.green} #{npmVersion.green} - #{'node'.blue} #{nodeVersion.blue} #{process.arch.blue} - #{'atom'.cyan} #{atomVersion.cyan} - #{'python'.yellow} #{pythonVersion.yellow} - #{'git'.magenta} #{gitVersion.magenta} - """ - - if config.isWin32() - visualStudioVersion = config.getInstalledVisualStudioFlag() ? '' - versions += "\n#{'visual studio'.cyan} #{visualStudioVersion.cyan}" - - console.log versions - callback() - -getAtomVersion = (callback) -> - config.getResourcePath (resourcePath) -> - unknownVersion = 'unknown' - try - {version} = require(path.join(resourcePath, 'package.json')) ? unknownVersion - callback(version) - catch error - callback(unknownVersion) - -getPythonVersion = (callback) -> - npmOptions = - userconfig: config.getUserConfigPath() - globalconfig: config.getGlobalConfigPath() - npm.load npmOptions, -> - python = npm.config.get('python') ? process.env.PYTHON - if config.isWin32() and not python - rootDir = process.env.SystemDrive ? 'C:\\' - rootDir += '\\' unless rootDir[rootDir.length - 1] is '\\' - pythonExe = path.resolve(rootDir, 'Python27', 'python.exe') - python = pythonExe if fs.isFileSync(pythonExe) - - python ?= 'python' - - spawned = spawn(python, ['--version']) - outputChunks = [] - spawned.stderr.on 'data', (chunk) -> outputChunks.push(chunk) - spawned.stdout.on 'data', (chunk) -> outputChunks.push(chunk) - spawned.on 'error', -> - spawned.on 'close', (code) -> - if code is 0 - [name, version] = Buffer.concat(outputChunks).toString().split(' ') - version = version?.trim() - callback(version) - -module.exports = - run: (args, callback) -> - config.setupApmRcFile() - options = parseOptions(args) - - unless options.argv.color - colors.disable() - - callbackCalled = false - options.callback = (error) -> - return if callbackCalled - callbackCalled = true - if error? - if _.isString(error) - message = error - else - message = error.message ? error - - if message is 'canceled' - # A prompt was canceled so just log an empty line - console.log() - else if message - console.error(message.red) - callback?(error) - - args = options.argv - command = options.command - if args.version - printVersions(args, options.callback) - else if args.help - if Command = commands[options.command]?() - showHelp(new Command().parseOptions?(options.command)) - else - showHelp(options) - options.callback() - else if command - if command is 'help' - if Command = commands[options.commandArgs]?() - showHelp(new Command().parseOptions?(options.commandArgs)) - else - showHelp(options) - options.callback() - else if Command = commands[command]?() - new Command().run(options) - else - options.callback("Unrecognized command: #{command}") - else - showHelp(options) - options.callback() diff --git a/src/apm-cli.js b/src/apm-cli.js new file mode 100644 index 0000000..c3bba80 --- /dev/null +++ b/src/apm-cli.js @@ -0,0 +1,310 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import { spawn } from 'child_process'; +import path from 'path'; +import _ from 'underscore-plus'; +import colors from 'colors'; +import npm from 'npm'; +import yargs from 'yargs'; +import wordwrap from 'wordwrap'; + +// Enable "require" scripts in asar archives +import 'asar-require'; + +import config from './apm'; +import fs from './fs'; +import git from './git'; + +const setupTempDirectory = function() { + const temp = require('temp'); + let tempDirectory = require('os').tmpdir(); + // Resolve ~ in tmp dir atom/atom#2271 + tempDirectory = path.resolve(fs.absolute(tempDirectory)); + temp.dir = tempDirectory; + try { + fs.makeTreeSync(temp.dir); + } catch (error) {} + return temp.track(); +}; + +setupTempDirectory(); + +const ciClass = () => require('./ci'); +const cleanClass = () => require('./clean'); +const configClass = () => require('./config'); +const dedupClass = () => require('./dedupe'); +const developClass = () => require('./develop'); +const disableClass = () => require('./disable'); +const docsClass = () => require('./docs'); +const enableClass = () => require('./enable'); +const featuredClass = () => require('./featured'); +const initClass = () => require('./init'); +const installClass = () => require('./install'); +const linksClass = () => require('./links'); +const linkClass = () => require('./link'); +const listClass = () => require('./list'); +const loginClass = () => require('./login'); +const publishClass = () => require('./publish'); +const rebuildClass = () => require('./rebuild'); +const rebuildModuleCacheClass = () => require('./rebuild-module-cache'); +const searchClass = () => require('./search'); +const starClass = () => require('./star'); +const starsClass = () => require('./stars'); +const testClass = () => require('./test'); +const uninstallClass = () => require('./uninstall'); +const unlinkClass = () => require('./unlink'); +const unpublishClass = () => require('./unpublish'); +const unstarClass = () => require('./unstar'); +const upgradeClass = () => require('./upgrade'); +const viewClass = () => require('./view'); + +const commands = { + 'ci': ciClass, + 'clean': cleanClass, + 'prune': cleanClass, + 'config': configClass, + 'dedupe': dedupClass, + 'dev': developClass, + 'develop': developClass, + 'disable': disableClass, + 'docs': docsClass, + 'home': docsClass, + 'open': docsClass, + 'enable': enableClass, + 'featured': featuredClass, + 'init': initClass, + 'install': installClass, + 'i': installClass, + 'link': linkClass, + 'ln': linkClass, + 'linked': linksClass, + 'links': linksClass, + 'lns': linksClass, + 'list': listClass, + 'ls': listClass, + 'login': loginClass, + 'publish': publishClass, + 'rebuild-module-cache': rebuildModuleCacheClass, + 'rebuild': rebuildClass, + 'search': searchClass, + 'star': starClass, + 'stars': starsClass, + 'starred': starsClass, + 'test': testClass, + 'deinstall': uninstallClass, + 'delete': uninstallClass, + 'erase': uninstallClass, + 'remove': uninstallClass, + 'rm': uninstallClass, + 'uninstall': uninstallClass, + 'unlink': unlinkClass, + 'unpublish': unpublishClass, + 'unstar': unstarClass, + 'upgrade': upgradeClass, + 'outdated': upgradeClass, + 'update': upgradeClass, + 'view': viewClass, + 'show': viewClass, +}; + +const parseOptions = function(args=[]) { + const options = yargs(args).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +apm - Atom Package Manager powered by https://atom.io + +Usage: apm + +where is one of: +${wordwrap(4, 80)(Object.keys(commands).sort().join(', '))}. + +Run \`apm help \` to see the more details about a specific command.\ +` + ); + options.alias('v', 'version').describe('version', 'Print the apm version'); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.boolean('color').default('color', true).describe('color', 'Enable colored output'); + options.command = options.argv._[0]; + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + if (arg === options.command) { + options.commandArgs = args.slice(index+1); + break; + } + } + return options; +}; + +const showHelp = function(options) { + if (options == null) { return; } + + let help = options.help(); + if (help.indexOf('Options:') >= 0) { + help += "\n Prefix an option with `no-` to set it to false such as --no-color to disable"; + help += "\n colored output."; + } + + return console.error(help); +}; + +const printVersions = function(args, callback) { + let left, left1; + const apmVersion = (left = require('../package.json').version) != null ? left : ''; + const npmVersion = (left1 = require('npm/package.json').version) != null ? left1 : ''; + const nodeVersion = process.versions.node != null ? process.versions.node : ''; + + return getPythonVersion(pythonVersion => git.getGitVersion(gitVersion => getAtomVersion(function(atomVersion) { + let versions; + if (args.json) { + versions = { + apm: apmVersion, + npm: npmVersion, + node: nodeVersion, + atom: atomVersion, + python: pythonVersion, + git: gitVersion, + nodeArch: process.arch + }; + if (config.isWin32()) { + versions.visualStudio = config.getInstalledVisualStudioFlag(); + } + console.log(JSON.stringify(versions)); + } else { + if (pythonVersion == null) { pythonVersion = ''; } + if (gitVersion == null) { gitVersion = ''; } + if (atomVersion == null) { atomVersion = ''; } + versions = `\ +${'apm'.red} ${apmVersion.red} +${'npm'.green} ${npmVersion.green} +${'node'.blue} ${nodeVersion.blue} ${process.arch.blue} +${'atom'.cyan} ${atomVersion.cyan} +${'python'.yellow} ${pythonVersion.yellow} +${'git'.magenta} ${gitVersion.magenta}\ +`; + + if (config.isWin32()) { + let left2; + const visualStudioVersion = (left2 = config.getInstalledVisualStudioFlag()) != null ? left2 : ''; + versions += `\n${'visual studio'.cyan} ${visualStudioVersion.cyan}`; + } + + console.log(versions); + } + return callback(); + }))); +}; + +var getAtomVersion = callback => config.getResourcePath(function(resourcePath) { + const unknownVersion = 'unknown'; + try { + let left; + const {version} = (left = require(path.join(resourcePath, 'package.json'))) != null ? left : unknownVersion; + return callback(version); + } catch (error) { + return callback(unknownVersion); + } +}); + +var getPythonVersion = function(callback) { + const npmOptions = { + userconfig: config.getUserConfigPath(), + globalconfig: config.getGlobalConfigPath() + }; + return npm.load(npmOptions, function() { + let left; + let python = (left = npm.config.get('python')) != null ? left : process.env.PYTHON; + if (config.isWin32() && !python) { + let rootDir = process.env.SystemDrive != null ? process.env.SystemDrive : 'C:\\'; + if (rootDir[rootDir.length - 1] !== '\\') { rootDir += '\\'; } + const pythonExe = path.resolve(rootDir, 'Python27', 'python.exe'); + if (fs.isFileSync(pythonExe)) { python = pythonExe; } + } + + if (python == null) { python = 'python'; } + + const spawned = spawn(python, ['--version']); + const outputChunks = []; + spawned.stderr.on('data', chunk => outputChunks.push(chunk)); + spawned.stdout.on('data', chunk => outputChunks.push(chunk)); + spawned.on('error', function() {}); + return spawned.on('close', function(code) { + let version; + if (code === 0) { + let name; + [name, version] = Buffer.concat(outputChunks).toString().split(' '); + version = version?.trim(); + } + return callback(version); + }); + }); +}; + +export default { + run(args, callback) { + let Command; + config.setupApmRcFile(); + const options = parseOptions(args); + + if (!options.argv.color) { + colors.disable(); + } + + let callbackCalled = false; + options.callback = function(error) { + if (callbackCalled) { return; } + callbackCalled = true; + if (error != null) { + let message; + if (_.isString(error)) { + message = error; + } else { + message = error.message != null ? error.message : error; + } + + if (message === 'canceled') { + // A prompt was canceled so just log an empty line + console.log(); + } else if (message) { + console.error(message.red); + } + } + return callback?.(error); + }; + + args = options.argv; + const { + command + } = options; + if (args.version) { + return printVersions(args, options.callback); + } else if (args.help) { + if ((Command = commands[options.command]?.())) { + showHelp(new Command().parseOptions?.(options.command)); + } else { + showHelp(options); + } + return options.callback(); + } else if (command) { + if (command === 'help') { + if ((Command = commands[options.commandArgs]?.())) { + showHelp(new Command().parseOptions?.(options.commandArgs)); + } else { + showHelp(options); + } + return options.callback(); + } else if ((Command = commands[command]?.())) { + return new Command().run(options); + } else { + return options.callback(`Unrecognized command: ${command}`); + } + } else { + showHelp(options); + return options.callback(); + } + } +}; diff --git a/src/apm.coffee b/src/apm.coffee deleted file mode 100644 index d39b9ec..0000000 --- a/src/apm.coffee +++ /dev/null @@ -1,130 +0,0 @@ -child_process = require 'child_process' -fs = require './fs' -path = require 'path' -npm = require 'npm' -semver = require 'semver' -asarPath = null - -module.exports = - getHomeDirectory: -> - if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME - - getAtomDirectory: -> - process.env.ATOM_HOME ? path.join(@getHomeDirectory(), '.atom') - - getRustupHomeDirPath: -> - if process.env.RUSTUP_HOME - process.env.RUSTUP_HOME - else - path.join(@getHomeDirectory(), '.multirust') - - getCacheDirectory: -> - path.join(@getAtomDirectory(), '.apm') - - getResourcePath: (callback) -> - if process.env.ATOM_RESOURCE_PATH - return process.nextTick -> callback(process.env.ATOM_RESOURCE_PATH) - - if asarPath # already calculated - return process.nextTick -> callback(asarPath) - - apmFolder = path.resolve(__dirname, '..') - appFolder = path.dirname(apmFolder) - if path.basename(apmFolder) is 'apm' and path.basename(appFolder) is 'app' - asarPath = "#{appFolder}.asar" - if fs.existsSync(asarPath) - return process.nextTick -> callback(asarPath) - - apmFolder = path.resolve(__dirname, '..', '..', '..') - appFolder = path.dirname(apmFolder) - if path.basename(apmFolder) is 'apm' and path.basename(appFolder) is 'app' - asarPath = "#{appFolder}.asar" - if fs.existsSync(asarPath) - return process.nextTick -> callback(asarPath) - - switch process.platform - when 'darwin' - child_process.exec 'mdfind "kMDItemCFBundleIdentifier == \'com.github.atom\'"', (error, stdout='', stderr) -> - [appLocation] = stdout.split('\n') unless error - appLocation = '/Applications/Atom.app' unless appLocation - asarPath = "#{appLocation}/Contents/Resources/app.asar" - return process.nextTick -> callback(asarPath) - when 'linux' - asarPath = '/usr/local/share/atom/resources/app.asar' - unless fs.existsSync(asarPath) - asarPath = '/usr/share/atom/resources/app.asar' - return process.nextTick -> callback(asarPath) - when 'win32' - glob = require 'glob' - pattern = "/Users/#{process.env.USERNAME}/AppData/Local/atom/app-+([0-9]).+([0-9]).+([0-9])/resources/app.asar" - asarPaths = glob.sync(pattern, null) # [] | a sorted array of locations with the newest version being last - asarPath = asarPaths[asarPaths.length - 1] - return process.nextTick -> callback(asarPath) - else - return process.nextTick -> callback('') - - getReposDirectory: -> - process.env.ATOM_REPOS_HOME ? path.join(@getHomeDirectory(), 'github') - - getElectronUrl: -> - process.env.ATOM_ELECTRON_URL ? 'https://atom.io/download/electron' - - getAtomPackagesUrl: -> - process.env.ATOM_PACKAGES_URL ? "#{@getAtomApiUrl()}/packages" - - getAtomApiUrl: -> - process.env.ATOM_API_URL ? 'https://atom.io/api' - - getElectronArch: -> - switch process.platform - when 'darwin' then 'x64' - else process.env.ATOM_ARCH ? process.arch - - getUserConfigPath: -> - path.resolve(@getAtomDirectory(), '.apmrc') - - getGlobalConfigPath: -> - path.resolve(@getAtomDirectory(), '.apm', '.apmrc') - - isWin32: -> - process.platform is 'win32' - - x86ProgramFilesDirectory: -> - process.env["ProgramFiles(x86)"] or process.env["ProgramFiles"] - - getInstalledVisualStudioFlag: -> - return null unless @isWin32() - - # Use the explictly-configured version when set - return process.env.GYP_MSVS_VERSION if process.env.GYP_MSVS_VERSION - - return '2019' if @visualStudioIsInstalled("2019") - return '2017' if @visualStudioIsInstalled("2017") - return '2015' if @visualStudioIsInstalled("14.0") - - visualStudioIsInstalled: (version) -> - if version < 2017 - fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio #{version}", "Common7", "IDE")) - else - fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio", "#{version}", "BuildTools", "Common7", "IDE")) or fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio", "#{version}", "Community", "Common7", "IDE")) or fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio", "#{version}", "Enterprise", "Common7", "IDE")) or fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio", "#{version}", "Professional", "Common7", "IDE")) or fs.existsSync(path.join(@x86ProgramFilesDirectory(), "Microsoft Visual Studio", "#{version}", "WDExpress", "Common7", "IDE")) - - loadNpm: (callback) -> - npmOptions = - userconfig: @getUserConfigPath() - globalconfig: @getGlobalConfigPath() - npm.load npmOptions, -> callback(null, npm) - - getSetting: (key, callback) -> - @loadNpm -> callback(npm.config.get(key)) - - setupApmRcFile: -> - try - fs.writeFileSync @getGlobalConfigPath(), """ - ; This file is auto-generated and should not be edited since any - ; modifications will be lost the next time any apm command is run. - ; - ; You should instead edit your .apmrc config located in ~/.atom/.apmrc - cache = #{@getCacheDirectory()} - ; Hide progress-bar to prevent npm from altering apm console output. - progress = false - """ diff --git a/src/apm.js b/src/apm.js new file mode 100644 index 0000000..5895e22 --- /dev/null +++ b/src/apm.js @@ -0,0 +1,172 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import child_process from 'child_process'; +import fs from './fs'; +import path from 'path'; +import npm from 'npm'; +import semver from 'semver'; +let asarPath = null; + +export default { + getHomeDirectory() { + if (process.platform === 'win32') { return process.env.USERPROFILE; } else { return process.env.HOME; } + }, + + getAtomDirectory() { + return process.env.ATOM_HOME != null ? process.env.ATOM_HOME : path.join(this.getHomeDirectory(), '.atom'); + }, + + getRustupHomeDirPath() { + if (process.env.RUSTUP_HOME) { + return process.env.RUSTUP_HOME; + } else { + return path.join(this.getHomeDirectory(), '.multirust'); + } + }, + + getCacheDirectory() { + return path.join(this.getAtomDirectory(), '.apm'); + }, + + getResourcePath(callback) { + if (process.env.ATOM_RESOURCE_PATH) { + return process.nextTick(() => callback(process.env.ATOM_RESOURCE_PATH)); + } + + if (asarPath) { // already calculated + return process.nextTick(() => callback(asarPath)); + } + + let apmFolder = path.resolve(__dirname, '..'); + let appFolder = path.dirname(apmFolder); + if ((path.basename(apmFolder) === 'apm') && (path.basename(appFolder) === 'app')) { + asarPath = `${appFolder}.asar`; + if (fs.existsSync(asarPath)) { + return process.nextTick(() => callback(asarPath)); + } + } + + apmFolder = path.resolve(__dirname, '..', '..', '..'); + appFolder = path.dirname(apmFolder); + if ((path.basename(apmFolder) === 'apm') && (path.basename(appFolder) === 'app')) { + asarPath = `${appFolder}.asar`; + if (fs.existsSync(asarPath)) { + return process.nextTick(() => callback(asarPath)); + } + } + + switch (process.platform) { + case 'darwin': + return child_process.exec('mdfind "kMDItemCFBundleIdentifier == \'com.github.atom\'"', function(error, stdout='', stderr) { + let appLocation; + if (!error) { [appLocation] = stdout.split('\n'); } + if (!appLocation) { appLocation = '/Applications/Atom.app'; } + asarPath = `${appLocation}/Contents/Resources/app.asar`; + return process.nextTick(() => callback(asarPath)); + }); + case 'linux': + asarPath = '/usr/local/share/atom/resources/app.asar'; + if (!fs.existsSync(asarPath)) { + asarPath = '/usr/share/atom/resources/app.asar'; + } + return process.nextTick(() => callback(asarPath)); + case 'win32': + var glob = require('glob'); + var pattern = `/Users/${process.env.USERNAME}/AppData/Local/atom/app-+([0-9]).+([0-9]).+([0-9])/resources/app.asar`; + var asarPaths = glob.sync(pattern, null); // [] | a sorted array of locations with the newest version being last + asarPath = asarPaths[asarPaths.length - 1]; + return process.nextTick(() => callback(asarPath)); + default: + return process.nextTick(() => callback('')); + } + }, + + getReposDirectory() { + return process.env.ATOM_REPOS_HOME != null ? process.env.ATOM_REPOS_HOME : path.join(this.getHomeDirectory(), 'github'); + }, + + getElectronUrl() { + return process.env.ATOM_ELECTRON_URL != null ? process.env.ATOM_ELECTRON_URL : 'https://atom.io/download/electron'; + }, + + getAtomPackagesUrl() { + return process.env.ATOM_PACKAGES_URL != null ? process.env.ATOM_PACKAGES_URL : `${this.getAtomApiUrl()}/packages`; + }, + + getAtomApiUrl() { + return process.env.ATOM_API_URL != null ? process.env.ATOM_API_URL : 'https://atom.io/api'; + }, + + getElectronArch() { + switch (process.platform) { + case 'darwin': return 'x64'; + default: return process.env.ATOM_ARCH != null ? process.env.ATOM_ARCH : process.arch; + } + }, + + getUserConfigPath() { + return path.resolve(this.getAtomDirectory(), '.apmrc'); + }, + + getGlobalConfigPath() { + return path.resolve(this.getAtomDirectory(), '.apm', '.apmrc'); + }, + + isWin32() { + return process.platform === 'win32'; + }, + + x86ProgramFilesDirectory() { + return process.env["ProgramFiles(x86)"] || process.env["ProgramFiles"]; + }, + + getInstalledVisualStudioFlag() { + if (!this.isWin32()) { return null; } + + // Use the explictly-configured version when set + if (process.env.GYP_MSVS_VERSION) { return process.env.GYP_MSVS_VERSION; } + + if (this.visualStudioIsInstalled("2019")) { return '2019'; } + if (this.visualStudioIsInstalled("2017")) { return '2017'; } + if (this.visualStudioIsInstalled("14.0")) { return '2015'; } + }, + + visualStudioIsInstalled(version) { + if (version < 2017) { + return fs.existsSync(path.join(this.x86ProgramFilesDirectory(), `Microsoft Visual Studio ${version}`, "Common7", "IDE")); + } else { + return fs.existsSync(path.join(this.x86ProgramFilesDirectory(), "Microsoft Visual Studio", `${version}`, "BuildTools", "Common7", "IDE")) || fs.existsSync(path.join(this.x86ProgramFilesDirectory(), "Microsoft Visual Studio", `${version}`, "Community", "Common7", "IDE")) || fs.existsSync(path.join(this.x86ProgramFilesDirectory(), "Microsoft Visual Studio", `${version}`, "Enterprise", "Common7", "IDE")) || fs.existsSync(path.join(this.x86ProgramFilesDirectory(), "Microsoft Visual Studio", `${version}`, "Professional", "Common7", "IDE")) || fs.existsSync(path.join(this.x86ProgramFilesDirectory(), "Microsoft Visual Studio", `${version}`, "WDExpress", "Common7", "IDE")); + } + }, + + loadNpm(callback) { + const npmOptions = { + userconfig: this.getUserConfigPath(), + globalconfig: this.getGlobalConfigPath() + }; + return npm.load(npmOptions, () => callback(null, npm)); + }, + + getSetting(key, callback) { + return this.loadNpm(() => callback(npm.config.get(key))); + }, + + setupApmRcFile() { + try { + return fs.writeFileSync(this.getGlobalConfigPath(), `\ +; This file is auto-generated and should not be edited since any +; modifications will be lost the next time any apm command is run. +; +; You should instead edit your .apmrc config located in ~/.atom/.apmrc +cache = ${this.getCacheDirectory()} +; Hide progress-bar to prevent npm from altering apm console output. +progress = false\ +` + ); + } catch (error) {} + } +}; diff --git a/src/auth.coffee b/src/auth.coffee deleted file mode 100644 index 954cadc..0000000 --- a/src/auth.coffee +++ /dev/null @@ -1,39 +0,0 @@ -try - keytar = require 'keytar' -catch error - # Gracefully handle keytar failing to load due to missing library on Linux - if process.platform is 'linux' - keytar = - findPassword: -> Promise.reject() - setPassword: -> Promise.reject() - else - throw error - -tokenName = 'Atom.io API Token' - -module.exports = - # Get the Atom.io API token from the keychain. - # - # callback - A function to call with an error as the first argument and a - # string token as the second argument. - getToken: (callback) -> - keytar.findPassword(tokenName) - .then (token) -> - if token - callback(null, token) - else - Promise.reject() - .catch -> - if token = process.env.ATOM_ACCESS_TOKEN - callback(null, token) - else - callback """ - No Atom.io API token in keychain - Run `apm login` or set the `ATOM_ACCESS_TOKEN` environment variable. - """ - - # Save the given token to the keychain. - # - # token - A string token to save. - saveToken: (token) -> - keytar.setPassword(tokenName, 'atom.io', token) diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..3137d2a --- /dev/null +++ b/src/auth.js @@ -0,0 +1,55 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let keytar; +try { + keytar = require('keytar'); +} catch (error) { + // Gracefully handle keytar failing to load due to missing library on Linux + if (process.platform === 'linux') { + keytar = { + findPassword() { return Promise.reject(); }, + setPassword() { return Promise.reject(); } + }; + } else { + throw error; + } +} + +const tokenName = 'Atom.io API Token'; + +export default { + // Get the Atom.io API token from the keychain. + // + // callback - A function to call with an error as the first argument and a + // string token as the second argument. + getToken(callback) { + return keytar.findPassword(tokenName) + .then(function(token) { + if (token) { + return callback(null, token); + } else { + return Promise.reject(); + }}).catch(function() { + let token; + if ((token = process.env.ATOM_ACCESS_TOKEN)) { + return callback(null, token); + } else { + return callback(`\ +No Atom.io API token in keychain +Run \`apm login\` or set the \`ATOM_ACCESS_TOKEN\` environment variable.\ +` + ); + } + }); + }, + + // Save the given token to the keychain. + // + // token - A string token to save. + saveToken(token) { + return keytar.setPassword(tokenName, 'atom.io', token); + } +}; diff --git a/src/ci.coffee b/src/ci.coffee deleted file mode 100644 index 59b8a24..0000000 --- a/src/ci.coffee +++ /dev/null @@ -1,71 +0,0 @@ -path = require 'path' -fs = require './fs' -yargs = require 'yargs' -async = require 'async' -_ = require 'underscore-plus' - -config = require './apm' -Command = require './command' - -module.exports = -class Ci extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomNodeDirectory = path.join(@atomDirectory, '.node-gyp') - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - Usage: apm ci - - Install a package with a clean slate. - - If you have an up-to-date package-lock.json file created by apm install, - apm ci will install its locked contents exactly. It is substantially - faster than apm install and produces consistently reproduceable builds, - but cannot be used to install new packages or dependencies. - """ - - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information') - - installModules: (options, callback) -> - process.stdout.write 'Installing locked modules' - if options.argv.verbose - process.stdout.write '\n' - else - process.stdout.write ' ' - - installArgs = [ - 'ci' - '--globalconfig', config.getGlobalConfigPath() - '--userconfig', config.getUserConfigPath() - @getNpmBuildFlags()... - ] - installArgs.push('--verbose') if options.argv.verbose - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - installOptions = {env, streaming: options.argv.verbose} - - @fork @atomNpmPath, installArgs, installOptions, (args...) => - @logCommandResults(callback, args...) - - run: (options) -> - {callback} = options - opts = @parseOptions(options.commandArgs) - - commands = [] - commands.push (callback) => config.loadNpm (error, @npm) => callback(error) - commands.push (cb) => @loadInstalledAtomMetadata(cb) - commands.push (cb) => @installModules(opts, cb) - - iteratee = (item, next) -> item(next) - async.mapSeries commands, iteratee, (err) -> - return callback(err) if err - callback(null) diff --git a/src/ci.js b/src/ci.js new file mode 100644 index 0000000..90e5fff --- /dev/null +++ b/src/ci.js @@ -0,0 +1,84 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Ci; +import path from 'path'; +import fs from './fs'; +import yargs from 'yargs'; +import async from 'async'; +import _ from 'underscore-plus'; +import config from './apm'; +import Command from './command'; + +export default Ci = class Ci extends Command { + constructor() { + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomNodeDirectory = path.join(this.atomDirectory, '.node-gyp'); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ +Usage: apm ci + +Install a package with a clean slate. + +If you have an up-to-date package-lock.json file created by apm install, +apm ci will install its locked contents exactly. It is substantially +faster than apm install and produces consistently reproduceable builds, +but cannot be used to install new packages or dependencies.\ +` + ); + + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information'); + } + + installModules(options, callback) { + process.stdout.write('Installing locked modules'); + if (options.argv.verbose) { + process.stdout.write('\n'); + } else { + process.stdout.write(' '); + } + + const installArgs = [ + 'ci', + '--globalconfig', config.getGlobalConfigPath(), + '--userconfig', config.getUserConfigPath(), + ...this.getNpmBuildFlags() + ]; + if (options.argv.verbose) { installArgs.push('--verbose'); } + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + const installOptions = {env, streaming: options.argv.verbose}; + + return this.fork(this.atomNpmPath, installArgs, installOptions, (...args) => { + return this.logCommandResults(callback, ...args); + }); + } + + run(options) { + const {callback} = options; + const opts = this.parseOptions(options.commandArgs); + + const commands = []; + commands.push(callback => { return config.loadNpm((error, npm) => { this.npm = npm; return callback(error); }); }); + commands.push(cb => this.loadInstalledAtomMetadata(cb)); + commands.push(cb => this.installModules(opts, cb)); + + const iteratee = (item, next) => item(next); + return async.mapSeries(commands, iteratee, function(err) { + if (err) { return callback(err); } + return callback(null); + }); + } +}; diff --git a/src/clean.coffee b/src/clean.coffee deleted file mode 100644 index 62e2d3b..0000000 --- a/src/clean.coffee +++ /dev/null @@ -1,32 +0,0 @@ -path = require 'path' - -async = require 'async' -CSON = require 'season' -yargs = require 'yargs' -_ = require 'underscore-plus' - -Command = require './command' -config = require './apm' -fs = require './fs' - -module.exports = -class Clean extends Command - constructor: -> - super() - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: apm clean - - Deletes all packages in the node_modules folder that are not referenced - as a dependency in the package.json file. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - run: (options) -> - process.stdout.write("Removing extraneous modules ") - @fork @atomNpmPath, ['prune'], (args...) => - @logCommandResults(options.callback, args...) diff --git a/src/clean.js b/src/clean.js new file mode 100644 index 0000000..8caf920 --- /dev/null +++ b/src/clean.js @@ -0,0 +1,41 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Clean; +import path from 'path'; +import async from 'async'; +import CSON from 'season'; +import yargs from 'yargs'; +import _ from 'underscore-plus'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; + +export default Clean = class Clean extends Command { + constructor() { + super(); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: apm clean + +Deletes all packages in the node_modules folder that are not referenced +as a dependency in the package.json file.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + run(options) { + process.stdout.write("Removing extraneous modules "); + return this.fork(this.atomNpmPath, ['prune'], (...args) => { + return this.logCommandResults(options.callback, ...args); + }); + } +}; diff --git a/src/cli.coffee b/src/cli.coffee deleted file mode 100644 index e56d941..0000000 --- a/src/cli.coffee +++ /dev/null @@ -1,6 +0,0 @@ -apm = require './apm-cli' - -process.title = 'apm' - -apm.run process.argv.slice(2), (error) -> - process.exitCode = if error? then 1 else 0 diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..4f9d209 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,11 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import apm from './apm-cli'; + +process.title = 'apm'; + +apm.run(process.argv.slice(2), error => process.exitCode = (error != null) ? 1 : 0); diff --git a/src/command.coffee b/src/command.coffee deleted file mode 100644 index 8cac243..0000000 --- a/src/command.coffee +++ /dev/null @@ -1,150 +0,0 @@ -child_process = require 'child_process' -path = require 'path' -_ = require 'underscore-plus' -semver = require 'semver' -config = require './apm' -git = require './git' - -module.exports = -class Command - spawn: (command, args, remaining...) -> - options = remaining.shift() if remaining.length >= 2 - callback = remaining.shift() - - spawned = child_process.spawn(command, args, options) - - errorChunks = [] - outputChunks = [] - - spawned.stdout.on 'data', (chunk) -> - if options?.streaming - process.stdout.write chunk - else - outputChunks.push(chunk) - - spawned.stderr.on 'data', (chunk) -> - if options?.streaming - process.stderr.write chunk - else - errorChunks.push(chunk) - - onChildExit = (errorOrExitCode) -> - spawned.removeListener 'error', onChildExit - spawned.removeListener 'close', onChildExit - callback?(errorOrExitCode, Buffer.concat(errorChunks).toString(), Buffer.concat(outputChunks).toString()) - - spawned.on 'error', onChildExit - spawned.on 'close', onChildExit - - spawned - - fork: (script, args, remaining...) -> - args.unshift(script) - @spawn(process.execPath, args, remaining...) - - packageNamesFromArgv: (argv) -> - @sanitizePackageNames(argv._) - - sanitizePackageNames: (packageNames=[]) -> - packageNames = packageNames.map (packageName) -> packageName.trim() - _.compact(_.uniq(packageNames)) - - logSuccess: -> - if process.platform is 'win32' - process.stdout.write 'done\n'.green - else - process.stdout.write '\u2713\n'.green - - logFailure: -> - if process.platform is 'win32' - process.stdout.write 'failed\n'.red - else - process.stdout.write '\u2717\n'.red - - logCommandResults: (callback, code, stderr='', stdout='') => - if code is 0 - @logSuccess() - callback() - else - @logFailure() - callback("#{stdout}\n#{stderr}".trim()) - - logCommandResultsIfFail: (callback, code, stderr='', stdout='') => - if code is 0 - callback() - else - @logFailure() - callback("#{stdout}\n#{stderr}".trim()) - - normalizeVersion: (version) -> - if typeof version is 'string' - # Remove commit SHA suffix - version.replace(/-.*$/, '') - else - version - - loadInstalledAtomMetadata: (callback) -> - @getResourcePath (resourcePath) => - try - {version, electronVersion} = require(path.join(resourcePath, 'package.json')) ? {} - version = @normalizeVersion(version) - @installedAtomVersion = version if semver.valid(version) - - @electronVersion = process.env.ATOM_ELECTRON_VERSION ? electronVersion - unless @electronVersion? - throw new Error('Could not determine Electron version') - - callback() - - getResourcePath: (callback) -> - if @resourcePath - process.nextTick => callback(@resourcePath) - else - config.getResourcePath (@resourcePath) => callback(@resourcePath) - - addBuildEnvVars: (env) -> - @updateWindowsEnv(env) if config.isWin32() - @addNodeBinToEnv(env) - @addProxyToEnv(env) - env.npm_config_runtime = "electron" - env.npm_config_target = @electronVersion - env.npm_config_disturl = config.getElectronUrl() - env.npm_config_arch = config.getElectronArch() - env.npm_config_target_arch = config.getElectronArch() # for node-pre-gyp - - getNpmBuildFlags: -> - ["--target=#{@electronVersion}", "--disturl=#{config.getElectronUrl()}", "--arch=#{config.getElectronArch()}"] - - updateWindowsEnv: (env) -> - env.USERPROFILE = env.HOME - - git.addGitToEnv(env) - - addNodeBinToEnv: (env) -> - nodeBinFolder = path.resolve(__dirname, '..', 'bin') - pathKey = if config.isWin32() then 'Path' else 'PATH' - if env[pathKey] - env[pathKey] = "#{nodeBinFolder}#{path.delimiter}#{env[pathKey]}" - else - env[pathKey]= nodeBinFolder - - addProxyToEnv: (env) -> - httpProxy = @npm.config.get('proxy') - if httpProxy - env.HTTP_PROXY ?= httpProxy - env.http_proxy ?= httpProxy - - httpsProxy = @npm.config.get('https-proxy') - if httpsProxy - env.HTTPS_PROXY ?= httpsProxy - env.https_proxy ?= httpsProxy - - # node-gyp only checks HTTP_PROXY (as of node-gyp@4.0.0) - env.HTTP_PROXY ?= httpsProxy - env.http_proxy ?= httpsProxy - - # node-gyp doesn't currently have an option for this so just set the - # environment variable to bypass strict SSL - # https://github.com/nodejs/node-gyp/issues/448 - useStrictSsl = @npm.config.get('strict-ssl') ? true - env.NODE_TLS_REJECT_UNAUTHORIZED = 0 unless useStrictSsl diff --git a/src/command.js b/src/command.js new file mode 100644 index 0000000..d743379 --- /dev/null +++ b/src/command.js @@ -0,0 +1,200 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Command; +import child_process from 'child_process'; +import path from 'path'; +import _ from 'underscore-plus'; +import semver from 'semver'; +import config from './apm'; +import git from './git'; + +export default Command = class Command { + constructor() { + this.logCommandResults = this.logCommandResults.bind(this); + this.logCommandResultsIfFail = this.logCommandResultsIfFail.bind(this); + } + + spawn(command, args, ...remaining) { + let options; + if (remaining.length >= 2) { options = remaining.shift(); } + const callback = remaining.shift(); + + const spawned = child_process.spawn(command, args, options); + + const errorChunks = []; + const outputChunks = []; + + spawned.stdout.on('data', function(chunk) { + if (options?.streaming) { + return process.stdout.write(chunk); + } else { + return outputChunks.push(chunk); + } + }); + + spawned.stderr.on('data', function(chunk) { + if (options?.streaming) { + return process.stderr.write(chunk); + } else { + return errorChunks.push(chunk); + } + }); + + var onChildExit = function(errorOrExitCode) { + spawned.removeListener('error', onChildExit); + spawned.removeListener('close', onChildExit); + return callback?.(errorOrExitCode, Buffer.concat(errorChunks).toString(), Buffer.concat(outputChunks).toString()); + }; + + spawned.on('error', onChildExit); + spawned.on('close', onChildExit); + + return spawned; + } + + fork(script, args, ...remaining) { + args.unshift(script); + return this.spawn(process.execPath, args, ...remaining); + } + + packageNamesFromArgv(argv) { + return this.sanitizePackageNames(argv._); + } + + sanitizePackageNames(packageNames=[]) { + packageNames = packageNames.map(packageName => packageName.trim()); + return _.compact(_.uniq(packageNames)); + } + + logSuccess() { + if (process.platform === 'win32') { + return process.stdout.write('done\n'.green); + } else { + return process.stdout.write('\u2713\n'.green); + } + } + + logFailure() { + if (process.platform === 'win32') { + return process.stdout.write('failed\n'.red); + } else { + return process.stdout.write('\u2717\n'.red); + } + } + + logCommandResults(callback, code, stderr='', stdout='') { + if (code === 0) { + this.logSuccess(); + return callback(); + } else { + this.logFailure(); + return callback(`${stdout}\n${stderr}`.trim()); + } + } + + logCommandResultsIfFail(callback, code, stderr='', stdout='') { + if (code === 0) { + return callback(); + } else { + this.logFailure(); + return callback(`${stdout}\n${stderr}`.trim()); + } + } + + normalizeVersion(version) { + if (typeof version === 'string') { + // Remove commit SHA suffix + return version.replace(/-.*$/, ''); + } else { + return version; + } + } + + loadInstalledAtomMetadata(callback) { + return this.getResourcePath(resourcePath => { + let electronVersion; + try { + let left, version; + ({version, electronVersion} = (left = require(path.join(resourcePath, 'package.json'))) != null ? left : {}); + version = this.normalizeVersion(version); + if (semver.valid(version)) { this.installedAtomVersion = version; } + } catch (error) {} + + this.electronVersion = process.env.ATOM_ELECTRON_VERSION != null ? process.env.ATOM_ELECTRON_VERSION : electronVersion; + if (this.electronVersion == null) { + throw new Error('Could not determine Electron version'); + } + + return callback(); + }); + } + + getResourcePath(callback) { + if (this.resourcePath) { + return process.nextTick(() => callback(this.resourcePath)); + } else { + return config.getResourcePath(resourcePath => { this.resourcePath = resourcePath; return callback(this.resourcePath); }); + } + } + + addBuildEnvVars(env) { + if (config.isWin32()) { this.updateWindowsEnv(env); } + this.addNodeBinToEnv(env); + this.addProxyToEnv(env); + env.npm_config_runtime = "electron"; + env.npm_config_target = this.electronVersion; + env.npm_config_disturl = config.getElectronUrl(); + env.npm_config_arch = config.getElectronArch(); + return env.npm_config_target_arch = config.getElectronArch(); // for node-pre-gyp + } + + getNpmBuildFlags() { + return [`--target=${this.electronVersion}`, `--disturl=${config.getElectronUrl()}`, `--arch=${config.getElectronArch()}`]; + } + + updateWindowsEnv(env) { + env.USERPROFILE = env.HOME; + + return git.addGitToEnv(env); + } + + addNodeBinToEnv(env) { + const nodeBinFolder = path.resolve(__dirname, '..', 'bin'); + const pathKey = config.isWin32() ? 'Path' : 'PATH'; + if (env[pathKey]) { + return env[pathKey] = `${nodeBinFolder}${path.delimiter}${env[pathKey]}`; + } else { + return env[pathKey]= nodeBinFolder; + } + } + + addProxyToEnv(env) { + let left; + const httpProxy = this.npm.config.get('proxy'); + if (httpProxy) { + if (env.HTTP_PROXY == null) { env.HTTP_PROXY = httpProxy; } + if (env.http_proxy == null) { env.http_proxy = httpProxy; } + } + + const httpsProxy = this.npm.config.get('https-proxy'); + if (httpsProxy) { + if (env.HTTPS_PROXY == null) { env.HTTPS_PROXY = httpsProxy; } + if (env.https_proxy == null) { env.https_proxy = httpsProxy; } + + // node-gyp only checks HTTP_PROXY (as of node-gyp@4.0.0) + if (env.HTTP_PROXY == null) { env.HTTP_PROXY = httpsProxy; } + if (env.http_proxy == null) { env.http_proxy = httpsProxy; } + } + + // node-gyp doesn't currently have an option for this so just set the + // environment variable to bypass strict SSL + // https://github.com/nodejs/node-gyp/issues/448 + const useStrictSsl = (left = this.npm.config.get('strict-ssl')) != null ? left : true; + if (!useStrictSsl) { return env.NODE_TLS_REJECT_UNAUTHORIZED = 0; } + } +}; diff --git a/src/config.coffee b/src/config.coffee deleted file mode 100644 index 5dc448e..0000000 --- a/src/config.coffee +++ /dev/null @@ -1,44 +0,0 @@ -path = require 'path' -_ = require 'underscore-plus' -yargs = require 'yargs' -apm = require './apm' -Command = require './command' - -module.exports = -class Config extends Command - constructor: -> - super() - atomDirectory = apm.getAtomDirectory() - @atomNodeDirectory = path.join(atomDirectory, '.node-gyp') - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm config set - apm config get - apm config delete - apm config list - apm config edit - - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - configArgs = ['--globalconfig', apm.getGlobalConfigPath(), '--userconfig', apm.getUserConfigPath(), 'config'] - configArgs = configArgs.concat(options.argv._) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: apm.getRustupHomeDirPath()}) - configOptions = {env} - - @fork @atomNpmPath, configArgs, configOptions, (code, stderr='', stdout='') -> - if code is 0 - process.stdout.write(stdout) if stdout - callback() - else - process.stdout.write(stderr) if stderr - callback(new Error("npm config failed: #{code}")) diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..9fcfe06 --- /dev/null +++ b/src/config.js @@ -0,0 +1,56 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Config; +import path from 'path'; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import apm from './apm'; +import Command from './command'; + +export default Config = class Config extends Command { + constructor() { + super(); + const atomDirectory = apm.getAtomDirectory(); + this.atomNodeDirectory = path.join(atomDirectory, '.node-gyp'); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm config set + apm config get + apm config delete + apm config list + apm config edit +\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + let configArgs = ['--globalconfig', apm.getGlobalConfigPath(), '--userconfig', apm.getUserConfigPath(), 'config']; + configArgs = configArgs.concat(options.argv._); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: apm.getRustupHomeDirPath()}); + const configOptions = {env}; + + return this.fork(this.atomNpmPath, configArgs, configOptions, function(code, stderr='', stdout='') { + if (code === 0) { + if (stdout) { process.stdout.write(stdout); } + return callback(); + } else { + if (stderr) { process.stdout.write(stderr); } + return callback(new Error(`npm config failed: ${code}`)); + } + }); + } +}; diff --git a/src/dedupe.coffee b/src/dedupe.coffee deleted file mode 100644 index 4edfcf9..0000000 --- a/src/dedupe.coffee +++ /dev/null @@ -1,70 +0,0 @@ -path = require 'path' - -async = require 'async' -_ = require 'underscore-plus' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -fs = require './fs' - -module.exports = -class Dedupe extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomPackagesDirectory = path.join(@atomDirectory, 'packages') - @atomNodeDirectory = path.join(@atomDirectory, '.node-gyp') - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm dedupe [...] - - Reduce duplication in the node_modules folder in the current directory. - - This command is experimental. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - dedupeModules: (options, callback) -> - process.stdout.write 'Deduping modules ' - - @forkDedupeCommand options, (args...) => - @logCommandResults(callback, args...) - - forkDedupeCommand: (options, callback) -> - dedupeArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'dedupe'] - dedupeArgs.push(@getNpmBuildFlags()...) - dedupeArgs.push('--silent') if options.argv.silent - dedupeArgs.push('--quiet') if options.argv.quiet - - dedupeArgs.push(packageName) for packageName in options.argv._ - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - dedupeOptions = {env} - dedupeOptions.cwd = options.cwd if options.cwd - - @fork(@atomNpmPath, dedupeArgs, dedupeOptions, callback) - - createAtomDirectories: -> - fs.makeTreeSync(@atomDirectory) - fs.makeTreeSync(@atomNodeDirectory) - - run: (options) -> - {callback, cwd} = options - options = @parseOptions(options.commandArgs) - options.cwd = cwd - - @createAtomDirectories() - - commands = [] - commands.push (callback) => @loadInstalledAtomMetadata(callback) - commands.push (callback) => @dedupeModules(options, callback) - async.waterfall commands, callback diff --git a/src/dedupe.js b/src/dedupe.js new file mode 100644 index 0000000..fbbeff5 --- /dev/null +++ b/src/dedupe.js @@ -0,0 +1,82 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Dedupe; +import path from 'path'; +import async from 'async'; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import fs from './fs'; + +export default Dedupe = class Dedupe extends Command { + constructor() { + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomPackagesDirectory = path.join(this.atomDirectory, 'packages'); + this.atomNodeDirectory = path.join(this.atomDirectory, '.node-gyp'); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm dedupe [...] + +Reduce duplication in the node_modules folder in the current directory. + +This command is experimental.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + dedupeModules(options, callback) { + process.stdout.write('Deduping modules '); + + return this.forkDedupeCommand(options, (...args) => { + return this.logCommandResults(callback, ...args); + }); + } + + forkDedupeCommand(options, callback) { + const dedupeArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'dedupe']; + dedupeArgs.push(...this.getNpmBuildFlags()); + if (options.argv.silent) { dedupeArgs.push('--silent'); } + if (options.argv.quiet) { dedupeArgs.push('--quiet'); } + + for (let packageName of options.argv._) { dedupeArgs.push(packageName); } + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + const dedupeOptions = {env}; + if (options.cwd) { dedupeOptions.cwd = options.cwd; } + + return this.fork(this.atomNpmPath, dedupeArgs, dedupeOptions, callback); + } + + createAtomDirectories() { + fs.makeTreeSync(this.atomDirectory); + return fs.makeTreeSync(this.atomNodeDirectory); + } + + run(options) { + const {callback, cwd} = options; + options = this.parseOptions(options.commandArgs); + options.cwd = cwd; + + this.createAtomDirectories(); + + const commands = []; + commands.push(callback => this.loadInstalledAtomMetadata(callback)); + commands.push(callback => this.dedupeModules(options, callback)); + return async.waterfall(commands, callback); + } +}; diff --git a/src/deprecated-packages.coffee b/src/deprecated-packages.coffee deleted file mode 100644 index 3de2c3d..0000000 --- a/src/deprecated-packages.coffee +++ /dev/null @@ -1,11 +0,0 @@ -semver = require 'semver' -deprecatedPackages = null - -exports.isDeprecatedPackage = (name, version) -> - deprecatedPackages ?= require('../deprecated-packages') ? {} - return false unless deprecatedPackages.hasOwnProperty(name) - - deprecatedVersionRange = deprecatedPackages[name].version - return true unless deprecatedVersionRange - - semver.valid(version) and semver.validRange(deprecatedVersionRange) and semver.satisfies(version, deprecatedVersionRange) diff --git a/src/deprecated-packages.js b/src/deprecated-packages.js new file mode 100644 index 0000000..0e14d5a --- /dev/null +++ b/src/deprecated-packages.js @@ -0,0 +1,20 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import semver from 'semver'; +let deprecatedPackages = null; + +export function isDeprecatedPackage(name, version) { + if (deprecatedPackages == null) { let left; + deprecatedPackages = (left = require('../deprecated-packages')) != null ? left : {}; } + if (!deprecatedPackages.hasOwnProperty(name)) { return false; } + + const deprecatedVersionRange = deprecatedPackages[name].version; + if (!deprecatedVersionRange) { return true; } + + return semver.valid(version) && semver.validRange(deprecatedVersionRange) && semver.satisfies(version, deprecatedVersionRange); +} diff --git a/src/develop.coffee b/src/develop.coffee deleted file mode 100644 index 5c1220c..0000000 --- a/src/develop.coffee +++ /dev/null @@ -1,106 +0,0 @@ -fs = require 'fs' -path = require 'path' - -_ = require 'underscore-plus' -async = require 'async' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -Install = require './install' -git = require './git' -Link = require './link' -request = require './request' - -module.exports = -class Develop extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomDevPackagesDirectory = path.join(@atomDirectory, 'dev', 'packages') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: apm develop [] - - Clone the given package's Git repository to the directory specified, - install its dependencies, and link it for development to - ~/.atom/dev/packages/. - - If no directory is specified then the repository is cloned to - ~/github/. The default folder to clone packages into can - be overridden using the ATOM_REPOS_HOME environment variable. - - Once this command completes you can open a dev window from atom using - cmd-shift-o to run the package out of the newly cloned repository. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - getRepositoryUrl: (packageName, callback) -> - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{packageName}" - json: true - request.get requestSettings, (error, response, body={}) -> - if error? - callback("Request for package information failed: #{error.message}") - else if response.statusCode is 200 - if repositoryUrl = body.repository.url - callback(null, repositoryUrl) - else - callback("No repository URL found for package: #{packageName}") - else - message = request.getErrorMessage(response, body) - callback("Request for package information failed: #{message}") - - cloneRepository: (repoUrl, packageDirectory, options, callback = ->) -> - config.getSetting 'git', (command) => - command ?= 'git' - args = ['clone', '--recursive', repoUrl, packageDirectory] - process.stdout.write "Cloning #{repoUrl} " unless options.argv.json - git.addGitToEnv(process.env) - @spawn command, args, (args...) => - if options.argv.json - @logCommandResultsIfFail(callback, args...) - else - @logCommandResults(callback, args...) - - installDependencies: (packageDirectory, options, callback = ->) -> - process.chdir(packageDirectory) - installOptions = _.clone(options) - installOptions.callback = callback - - new Install().run(installOptions) - - linkPackage: (packageDirectory, options, callback) -> - linkOptions = _.clone(options) - if callback - linkOptions.callback = callback - linkOptions.commandArgs = [packageDirectory, '--dev'] - new Link().run(linkOptions) - - run: (options) -> - packageName = options.commandArgs.shift() - - unless packageName?.length > 0 - return options.callback("Missing required package name") - - packageDirectory = options.commandArgs.shift() ? path.join(config.getReposDirectory(), packageName) - packageDirectory = path.resolve(packageDirectory) - - if fs.existsSync(packageDirectory) - @linkPackage(packageDirectory, options) - else - @getRepositoryUrl packageName, (error, repoUrl) => - if error? - options.callback(error) - else - tasks = [] - tasks.push (callback) => @cloneRepository repoUrl, packageDirectory, options, callback - - tasks.push (callback) => @installDependencies packageDirectory, options, callback - - tasks.push (callback) => @linkPackage packageDirectory, options, callback - - async.waterfall tasks, options.callback diff --git a/src/develop.js b/src/develop.js new file mode 100644 index 0000000..bd0387f --- /dev/null +++ b/src/develop.js @@ -0,0 +1,134 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Develop; +import fs from 'fs'; +import path from 'path'; +import _ from 'underscore-plus'; +import async from 'async'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import Install from './install'; +import git from './git'; +import Link from './link'; +import request from './request'; + +export default Develop = class Develop extends Command { + constructor() { + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomDevPackagesDirectory = path.join(this.atomDirectory, 'dev', 'packages'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: apm develop [] + +Clone the given package's Git repository to the directory specified, +install its dependencies, and link it for development to +~/.atom/dev/packages/. + +If no directory is specified then the repository is cloned to +~/github/. The default folder to clone packages into can +be overridden using the ATOM_REPOS_HOME environment variable. + +Once this command completes you can open a dev window from atom using +cmd-shift-o to run the package out of the newly cloned repository.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + getRepositoryUrl(packageName, callback) { + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${packageName}`, + json: true + }; + return request.get(requestSettings, function(error, response, body={}) { + if (error != null) { + return callback(`Request for package information failed: ${error.message}`); + } else if (response.statusCode === 200) { + let repositoryUrl; + if ((repositoryUrl = body.repository.url)) { + return callback(null, repositoryUrl); + } else { + return callback(`No repository URL found for package: ${packageName}`); + } + } else { + const message = request.getErrorMessage(response, body); + return callback(`Request for package information failed: ${message}`); + } + }); + } + + cloneRepository(repoUrl, packageDirectory, options, callback = function() {}) { + return config.getSetting('git', command => { + if (command == null) { command = 'git'; } + const args = ['clone', '--recursive', repoUrl, packageDirectory]; + if (!options.argv.json) { process.stdout.write(`Cloning ${repoUrl} `); } + git.addGitToEnv(process.env); + return this.spawn(command, args, (...args) => { + if (options.argv.json) { + return this.logCommandResultsIfFail(callback, ...args); + } else { + return this.logCommandResults(callback, ...args); + } + }); + }); + } + + installDependencies(packageDirectory, options, callback = function() {}) { + process.chdir(packageDirectory); + const installOptions = _.clone(options); + installOptions.callback = callback; + + return new Install().run(installOptions); + } + + linkPackage(packageDirectory, options, callback) { + const linkOptions = _.clone(options); + if (callback) { + linkOptions.callback = callback; + } + linkOptions.commandArgs = [packageDirectory, '--dev']; + return new Link().run(linkOptions); + } + + run(options) { + let left; + const packageName = options.commandArgs.shift(); + + if (packageName?.length <= 0) { + return options.callback("Missing required package name"); + } + + let packageDirectory = (left = options.commandArgs.shift()) != null ? left : path.join(config.getReposDirectory(), packageName); + packageDirectory = path.resolve(packageDirectory); + + if (fs.existsSync(packageDirectory)) { + return this.linkPackage(packageDirectory, options); + } else { + return this.getRepositoryUrl(packageName, (error, repoUrl) => { + if (error != null) { + return options.callback(error); + } else { + const tasks = []; + tasks.push(callback => this.cloneRepository(repoUrl, packageDirectory, options, callback)); + + tasks.push(callback => this.installDependencies(packageDirectory, options, callback)); + + tasks.push(callback => this.linkPackage(packageDirectory, options, callback)); + + return async.waterfall(tasks, options.callback); + } + }); + } + } +}; diff --git a/src/disable.coffee b/src/disable.coffee deleted file mode 100644 index 71b1b31..0000000 --- a/src/disable.coffee +++ /dev/null @@ -1,81 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -CSON = require 'season' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -List = require './list' - -module.exports = -class Disable extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm disable []... - - Disables the named package(s). - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - getInstalledPackages: (callback) -> - options = - argv: - theme: false - bare: true - - lister = new List() - lister.listBundledPackages options, (error, core_packages) -> - lister.listDevPackages options, (error, dev_packages) -> - lister.listUserPackages options, (error, user_packages) -> - callback(null, core_packages.concat(dev_packages, user_packages)) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - packageNames = @packageNamesFromArgv(options.argv) - - configFilePath = CSON.resolve(path.join(config.getAtomDirectory(), 'config')) - unless configFilePath - callback("Could not find config.cson. Run Atom first?") - return - - try - settings = CSON.readFileSync(configFilePath) - catch error - callback "Failed to load `#{configFilePath}`: #{error.message}" - return - - @getInstalledPackages (error, installedPackages) => - return callback(error) if error - - installedPackageNames = (pkg.name for pkg in installedPackages) - - # uninstalledPackages = (name for name in packageNames when !installedPackageNames[name]) - uninstalledPackageNames = _.difference(packageNames, installedPackageNames) - if uninstalledPackageNames.length > 0 - console.log "Not Installed:\n #{uninstalledPackageNames.join('\n ')}" - - # only installed packages can be disabled - packageNames = _.difference(packageNames, uninstalledPackageNames) - - if packageNames.length is 0 - callback("Please specify a package to disable") - return - - keyPath = '*.core.disabledPackages' - disabledPackages = _.valueForKeyPath(settings, keyPath) ? [] - result = _.union(disabledPackages, packageNames) - _.setValueForKeyPath(settings, keyPath, result) - - try - CSON.writeFileSync(configFilePath, settings) - catch error - callback "Failed to save `#{configFilePath}`: #{error.message}" - return - - console.log "Disabled:\n #{packageNames.join('\n ')}" - @logSuccess() - callback() diff --git a/src/disable.js b/src/disable.js new file mode 100644 index 0000000..9b36c9d --- /dev/null +++ b/src/disable.js @@ -0,0 +1,101 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Disable; +import _ from 'underscore-plus'; +import path from 'path'; +import CSON from 'season'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import List from './list'; + +export default Disable = class Disable extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm disable []... + +Disables the named package(s).\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + getInstalledPackages(callback) { + const options = { + argv: { + theme: false, + bare: true + } + }; + + const lister = new List(); + return lister.listBundledPackages(options, (error, core_packages) => lister.listDevPackages(options, (error, dev_packages) => lister.listUserPackages(options, (error, user_packages) => callback(null, core_packages.concat(dev_packages, user_packages))))); + } + + run(options) { + let settings; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + let packageNames = this.packageNamesFromArgv(options.argv); + + const configFilePath = CSON.resolve(path.join(config.getAtomDirectory(), 'config')); + if (!configFilePath) { + callback("Could not find config.cson. Run Atom first?"); + return; + } + + try { + settings = CSON.readFileSync(configFilePath); + } catch (error1) { + const error = error1; + callback(`Failed to load \`${configFilePath}\`: ${error.message}`); + return; + } + + return this.getInstalledPackages((error, installedPackages) => { + let left; + if (error) { return callback(error); } + + const installedPackageNames = (installedPackages.map((pkg) => pkg.name)); + + // uninstalledPackages = (name for name in packageNames when !installedPackageNames[name]) + const uninstalledPackageNames = _.difference(packageNames, installedPackageNames); + if (uninstalledPackageNames.length > 0) { + console.log(`Not Installed:\n ${uninstalledPackageNames.join('\n ')}`); + } + + // only installed packages can be disabled + packageNames = _.difference(packageNames, uninstalledPackageNames); + + if (packageNames.length === 0) { + callback("Please specify a package to disable"); + return; + } + + const keyPath = '*.core.disabledPackages'; + const disabledPackages = (left = _.valueForKeyPath(settings, keyPath)) != null ? left : []; + const result = _.union(disabledPackages, packageNames); + _.setValueForKeyPath(settings, keyPath, result); + + try { + CSON.writeFileSync(configFilePath, settings); + } catch (error2) { + error = error2; + callback(`Failed to save \`${configFilePath}\`: ${error.message}`); + return; + } + + console.log(`Disabled:\n ${packageNames.join('\n ')}`); + this.logSuccess(); + return callback(); + }); + } +}; diff --git a/src/docs.coffee b/src/docs.coffee deleted file mode 100644 index 064e53a..0000000 --- a/src/docs.coffee +++ /dev/null @@ -1,42 +0,0 @@ -yargs = require 'yargs' -open = require 'open' - -View = require './view' -config = require './apm' - -module.exports = -class Docs extends View - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm docs [options] - - Open a package's homepage in the default browser. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('p').alias('p', 'print').describe('print', 'Print the URL instead of opening it') - - openRepositoryUrl: (repositoryUrl) -> - open(repositoryUrl) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - [packageName] = options.argv._ - - unless packageName - callback("Missing required package name") - return - - @getPackage packageName, options, (error, pack) => - return callback(error) if error? - - if repository = @getRepository(pack) - if options.argv.print - console.log repository - else - @openRepositoryUrl(repository) - callback() - else - callback("Package \"#{packageName}\" does not contain a repository URL") diff --git a/src/docs.js b/src/docs.js new file mode 100644 index 0000000..bfd6ad0 --- /dev/null +++ b/src/docs.js @@ -0,0 +1,57 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Docs; +import yargs from 'yargs'; +import open from 'open'; +import View from './view'; +import config from './apm'; + +export default Docs = class Docs extends View { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm docs [options] + +Open a package's homepage in the default browser.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.boolean('p').alias('p', 'print').describe('print', 'Print the URL instead of opening it'); + } + + openRepositoryUrl(repositoryUrl) { + return open(repositoryUrl); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const [packageName] = options.argv._; + + if (!packageName) { + callback("Missing required package name"); + return; + } + + return this.getPackage(packageName, options, (error, pack) => { + let repository; + if (error != null) { return callback(error); } + + if (repository = this.getRepository(pack)) { + if (options.argv.print) { + console.log(repository); + } else { + this.openRepositoryUrl(repository); + } + return callback(); + } else { + return callback(`Package \"${packageName}\" does not contain a repository URL`); + } + }); + } +}; diff --git a/src/enable.coffee b/src/enable.coffee deleted file mode 100644 index caed944..0000000 --- a/src/enable.coffee +++ /dev/null @@ -1,62 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -CSON = require 'season' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' - -module.exports = -class Enable extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm enable []... - - Enables the named package(s). - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - packageNames = @packageNamesFromArgv(options.argv) - - configFilePath = CSON.resolve(path.join(config.getAtomDirectory(), 'config')) - unless configFilePath - callback("Could not find config.cson. Run Atom first?") - return - - try - settings = CSON.readFileSync(configFilePath) - catch error - callback "Failed to load `#{configFilePath}`: #{error.message}" - return - - keyPath = '*.core.disabledPackages' - disabledPackages = _.valueForKeyPath(settings, keyPath) ? [] - - errorPackages = _.difference(packageNames, disabledPackages) - if errorPackages.length > 0 - console.log "Not Disabled:\n #{errorPackages.join('\n ')}" - - # can't enable a package that isn't disabled - packageNames = _.difference(packageNames, errorPackages) - - if packageNames.length is 0 - callback("Please specify a package to enable") - return - - result = _.difference(disabledPackages, packageNames) - _.setValueForKeyPath(settings, keyPath, result) - - try - CSON.writeFileSync(configFilePath, settings) - catch error - callback "Failed to save `#{configFilePath}`: #{error.message}" - return - - console.log "Enabled:\n #{packageNames.join('\n ')}" - @logSuccess() - callback() diff --git a/src/enable.js b/src/enable.js new file mode 100644 index 0000000..e688577 --- /dev/null +++ b/src/enable.js @@ -0,0 +1,80 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Enable; +import _ from 'underscore-plus'; +import path from 'path'; +import CSON from 'season'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; + +export default Enable = class Enable extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm enable []... + +Enables the named package(s).\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + run(options) { + let error, left, settings; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + let packageNames = this.packageNamesFromArgv(options.argv); + + const configFilePath = CSON.resolve(path.join(config.getAtomDirectory(), 'config')); + if (!configFilePath) { + callback("Could not find config.cson. Run Atom first?"); + return; + } + + try { + settings = CSON.readFileSync(configFilePath); + } catch (error1) { + error = error1; + callback(`Failed to load \`${configFilePath}\`: ${error.message}`); + return; + } + + const keyPath = '*.core.disabledPackages'; + const disabledPackages = (left = _.valueForKeyPath(settings, keyPath)) != null ? left : []; + + const errorPackages = _.difference(packageNames, disabledPackages); + if (errorPackages.length > 0) { + console.log(`Not Disabled:\n ${errorPackages.join('\n ')}`); + } + + // can't enable a package that isn't disabled + packageNames = _.difference(packageNames, errorPackages); + + if (packageNames.length === 0) { + callback("Please specify a package to enable"); + return; + } + + const result = _.difference(disabledPackages, packageNames); + _.setValueForKeyPath(settings, keyPath, result); + + try { + CSON.writeFileSync(configFilePath, settings); + } catch (error2) { + error = error2; + callback(`Failed to save \`${configFilePath}\`: ${error.message}`); + return; + } + + console.log(`Enabled:\n ${packageNames.join('\n ')}`); + this.logSuccess(); + return callback(); + } +}; diff --git a/src/featured.coffee b/src/featured.coffee deleted file mode 100644 index a65425c..0000000 --- a/src/featured.coffee +++ /dev/null @@ -1,85 +0,0 @@ -_ = require 'underscore-plus' -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -request = require './request' -tree = require './tree' - -module.exports = -class Featured extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm featured - apm featured --themes - apm featured --compatible 0.49.0 - - List the Atom packages and themes that are currently featured in the - atom.io registry. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes') - options.alias('c', 'compatible').string('compatible').describe('compatible', 'Only list packages/themes compatible with this Atom version') - options.boolean('json').describe('json', 'Output featured packages as JSON array') - - getFeaturedPackagesByType: (atomVersion, packageType, callback) -> - [callback, atomVersion] = [atomVersion, null] if _.isFunction(atomVersion) - - requestSettings = - url: "#{config.getAtomApiUrl()}/#{packageType}/featured" - json: true - requestSettings.qs = engine: atomVersion if atomVersion - - request.get requestSettings, (error, response, body=[]) -> - if error? - callback(error) - else if response.statusCode is 200 - packages = body.filter (pack) -> pack?.releases?.latest? - packages = packages.map ({readme, metadata, downloads, stargazers_count}) -> _.extend({}, metadata, {readme, downloads, stargazers_count}) - packages = _.sortBy(packages, 'name') - callback(null, packages) - else - message = request.getErrorMessage(response, body) - callback("Requesting packages failed: #{message}") - - getAllFeaturedPackages: (atomVersion, callback) -> - @getFeaturedPackagesByType atomVersion, 'packages', (error, packages) => - return callback(error) if error? - - @getFeaturedPackagesByType atomVersion, 'themes', (error, themes) -> - return callback(error) if error? - callback(null, packages.concat(themes)) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - listCallback = (error, packages) -> - return callback(error) if error? - - if options.argv.json - console.log(JSON.stringify(packages)) - else - if options.argv.themes - console.log "#{'Featured Atom Themes'.cyan} (#{packages.length})" - else - console.log "#{'Featured Atom Packages'.cyan} (#{packages.length})" - - tree packages, ({name, version, description, downloads, stargazers_count}) -> - label = name.yellow - label += " #{description.replace(/\s+/g, ' ')}" if description - label += " (#{_.pluralize(downloads, 'download')}, #{_.pluralize(stargazers_count, 'star')})".grey if downloads >= 0 and stargazers_count >= 0 - label - - console.log() - console.log "Use `apm install` to install them or visit #{'http://atom.io/packages'.underline} to read more about them." - console.log() - - callback() - - if options.argv.themes - @getFeaturedPackagesByType(options.argv.compatible, 'themes', listCallback) - else - @getAllFeaturedPackages(options.argv.compatible, listCallback) diff --git a/src/featured.js b/src/featured.js new file mode 100644 index 0000000..0c9aff3 --- /dev/null +++ b/src/featured.js @@ -0,0 +1,106 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Featured; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import request from './request'; +import tree from './tree'; + +export default Featured = class Featured extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm featured + apm featured --themes + apm featured --compatible 0.49.0 + +List the Atom packages and themes that are currently featured in the +atom.io registry.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes'); + options.alias('c', 'compatible').string('compatible').describe('compatible', 'Only list packages/themes compatible with this Atom version'); + return options.boolean('json').describe('json', 'Output featured packages as JSON array'); + } + + getFeaturedPackagesByType(atomVersion, packageType, callback) { + if (_.isFunction(atomVersion)) { [callback, atomVersion] = [atomVersion, null]; } + + const requestSettings = { + url: `${config.getAtomApiUrl()}/${packageType}/featured`, + json: true + }; + if (atomVersion) { requestSettings.qs = {engine: atomVersion}; } + + return request.get(requestSettings, function(error, response, body=[]) { + if (error != null) { + return callback(error); + } else if (response.statusCode === 200) { + let packages = body.filter(pack => pack?.releases?.latest != null); + packages = packages.map(({readme, metadata, downloads, stargazers_count}) => _.extend({}, metadata, {readme, downloads, stargazers_count})); + packages = _.sortBy(packages, 'name'); + return callback(null, packages); + } else { + const message = request.getErrorMessage(response, body); + return callback(`Requesting packages failed: ${message}`); + } + }); + } + + getAllFeaturedPackages(atomVersion, callback) { + return this.getFeaturedPackagesByType(atomVersion, 'packages', (error, packages) => { + if (error != null) { return callback(error); } + + return this.getFeaturedPackagesByType(atomVersion, 'themes', function(error, themes) { + if (error != null) { return callback(error); } + return callback(null, packages.concat(themes)); + }); + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + const listCallback = function(error, packages) { + if (error != null) { return callback(error); } + + if (options.argv.json) { + console.log(JSON.stringify(packages)); + } else { + if (options.argv.themes) { + console.log(`${'Featured Atom Themes'.cyan} (${packages.length})`); + } else { + console.log(`${'Featured Atom Packages'.cyan} (${packages.length})`); + } + + tree(packages, function({name, version, description, downloads, stargazers_count}) { + let label = name.yellow; + if (description) { label += ` ${description.replace(/\s+/g, ' ')}`; } + if ((downloads >= 0) && (stargazers_count >= 0)) { label += ` (${_.pluralize(downloads, 'download')}, ${_.pluralize(stargazers_count, 'star')})`.grey; } + return label; + }); + + console.log(); + console.log(`Use \`apm install\` to install them or visit ${'http://atom.io/packages'.underline} to read more about them.`); + console.log(); + } + + return callback(); + }; + + if (options.argv.themes) { + return this.getFeaturedPackagesByType(options.argv.compatible, 'themes', listCallback); + } else { + return this.getAllFeaturedPackages(options.argv.compatible, listCallback); + } + } +}; diff --git a/src/fs.coffee b/src/fs.coffee deleted file mode 100644 index b5905e6..0000000 --- a/src/fs.coffee +++ /dev/null @@ -1,42 +0,0 @@ -_ = require 'underscore-plus' -fs = require 'fs-plus' -ncp = require 'ncp' -rm = require 'rimraf' -wrench = require 'wrench' -path = require 'path' - -fsAdditions = - list: (directoryPath) -> - if fs.isDirectorySync(directoryPath) - try - fs.readdirSync(directoryPath) - catch e - [] - else - [] - - listRecursive: (directoryPath) -> - wrench.readdirSyncRecursive(directoryPath) - - cp: (sourcePath, destinationPath, callback) -> - rm destinationPath, (error) -> - if error? - callback(error) - else - ncp(sourcePath, destinationPath, callback) - - mv: (sourcePath, destinationPath, callback) -> - rm destinationPath, (error) -> - if error? - callback(error) - else - wrench.mkdirSyncRecursive(path.dirname(destinationPath), 0o755) - fs.rename(sourcePath, destinationPath, callback) - -module.exports = new Proxy({}, { - get: (target, key) -> - fsAdditions[key] or fs[key] - - set: (target, key, value) -> - fsAdditions[key] = value -}) diff --git a/src/fs.js b/src/fs.js new file mode 100644 index 0000000..34fd7fa --- /dev/null +++ b/src/fs.js @@ -0,0 +1,61 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import _ from 'underscore-plus'; +import fs from 'fs-plus'; +import ncp from 'ncp'; +import rm from 'rimraf'; +import wrench from 'wrench'; +import path from 'path'; + +const fsAdditions = { + list(directoryPath) { + if (fs.isDirectorySync(directoryPath)) { + try { + return fs.readdirSync(directoryPath); + } catch (e) { + return []; + } + } else { + return []; + } + }, + + listRecursive(directoryPath) { + return wrench.readdirSyncRecursive(directoryPath); + }, + + cp(sourcePath, destinationPath, callback) { + return rm(destinationPath, function(error) { + if (error != null) { + return callback(error); + } else { + return ncp(sourcePath, destinationPath, callback); + } + }); + }, + + mv(sourcePath, destinationPath, callback) { + return rm(destinationPath, function(error) { + if (error != null) { + return callback(error); + } else { + wrench.mkdirSyncRecursive(path.dirname(destinationPath), 0o755); + return fs.rename(sourcePath, destinationPath, callback); + } + }); + } +}; + +export default new Proxy({}, { + get(target, key) { + return fsAdditions[key] || fs[key]; + }, + + set(target, key, value) { + return fsAdditions[key] = value; + } +}); diff --git a/src/git.coffee b/src/git.coffee deleted file mode 100644 index ca6b1e0..0000000 --- a/src/git.coffee +++ /dev/null @@ -1,68 +0,0 @@ -{spawn} = require 'child_process' -path = require 'path' -_ = require 'underscore-plus' -npm = require 'npm' -config = require './apm' -fs = require './fs' - -addPortableGitToEnv = (env) -> - localAppData = env.LOCALAPPDATA - return unless localAppData - - githubPath = path.join(localAppData, 'GitHub') - - try - children = fs.readdirSync(githubPath) - catch error - return - - for child in children when child.indexOf('PortableGit_') is 0 - cmdPath = path.join(githubPath, child, 'cmd') - binPath = path.join(githubPath, child, 'bin') - if env.Path - env.Path += "#{path.delimiter}#{cmdPath}#{path.delimiter}#{binPath}" - else - env.Path = "#{cmdPath}#{path.delimiter}#{binPath}" - break - - return - -addGitBashToEnv = (env) -> - if env.ProgramFiles - gitPath = path.join(env.ProgramFiles, 'Git') - - unless fs.isDirectorySync(gitPath) - if env['ProgramFiles(x86)'] - gitPath = path.join(env['ProgramFiles(x86)'], 'Git') - - return unless fs.isDirectorySync(gitPath) - - cmdPath = path.join(gitPath, 'cmd') - binPath = path.join(gitPath, 'bin') - if env.Path - env.Path += "#{path.delimiter}#{cmdPath}#{path.delimiter}#{binPath}" - else - env.Path = "#{cmdPath}#{path.delimiter}#{binPath}" - -exports.addGitToEnv = (env) -> - return if process.platform isnt 'win32' - addPortableGitToEnv(env) - addGitBashToEnv(env) - -exports.getGitVersion = (callback) -> - npmOptions = - userconfig: config.getUserConfigPath() - globalconfig: config.getGlobalConfigPath() - npm.load npmOptions, -> - git = npm.config.get('git') ? 'git' - exports.addGitToEnv(process.env) - spawned = spawn(git, ['--version']) - outputChunks = [] - spawned.stderr.on 'data', (chunk) -> outputChunks.push(chunk) - spawned.stdout.on 'data', (chunk) -> outputChunks.push(chunk) - spawned.on 'error', -> - spawned.on 'close', (code) -> - if code is 0 - [gitName, versionName, version] = Buffer.concat(outputChunks).toString().split(' ') - version = version?.trim() - callback(version) diff --git a/src/git.js b/src/git.js new file mode 100644 index 0000000..5b2aa4b --- /dev/null +++ b/src/git.js @@ -0,0 +1,96 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import { spawn } from 'child_process'; +import path from 'path'; +import _ from 'underscore-plus'; +import npm from 'npm'; +import config from './apm'; +import fs from './fs'; + +const addPortableGitToEnv = function(env) { + let children; + const localAppData = env.LOCALAPPDATA; + if (!localAppData) { return; } + + const githubPath = path.join(localAppData, 'GitHub'); + + try { + children = fs.readdirSync(githubPath); + } catch (error) { + return; + } + + for (let child of children) { + if (child.indexOf('PortableGit_') === 0) { + const cmdPath = path.join(githubPath, child, 'cmd'); + const binPath = path.join(githubPath, child, 'bin'); + if (env.Path) { + env.Path += `${path.delimiter}${cmdPath}${path.delimiter}${binPath}`; + } else { + env.Path = `${cmdPath}${path.delimiter}${binPath}`; + } + break; + } + } + +}; + +const addGitBashToEnv = function(env) { + let gitPath; + if (env.ProgramFiles) { + gitPath = path.join(env.ProgramFiles, 'Git'); + } + + if (!fs.isDirectorySync(gitPath)) { + if (env['ProgramFiles(x86)']) { + gitPath = path.join(env['ProgramFiles(x86)'], 'Git'); + } + } + + if (!fs.isDirectorySync(gitPath)) { return; } + + const cmdPath = path.join(gitPath, 'cmd'); + const binPath = path.join(gitPath, 'bin'); + if (env.Path) { + return env.Path += `${path.delimiter}${cmdPath}${path.delimiter}${binPath}`; + } else { + return env.Path = `${cmdPath}${path.delimiter}${binPath}`; + } +}; + +export function addGitToEnv(env) { + if (process.platform !== 'win32') { return; } + addPortableGitToEnv(env); + return addGitBashToEnv(env); +} + +export function getGitVersion(callback) { + const npmOptions = { + userconfig: config.getUserConfigPath(), + globalconfig: config.getGlobalConfigPath() + }; + return npm.load(npmOptions, function() { + let left; + const git = (left = npm.config.get('git')) != null ? left : 'git'; + exports.addGitToEnv(process.env); + const spawned = spawn(git, ['--version']); + const outputChunks = []; + spawned.stderr.on('data', chunk => outputChunks.push(chunk)); + spawned.stdout.on('data', chunk => outputChunks.push(chunk)); + spawned.on('error', function() {}); + return spawned.on('close', function(code) { + let version; + if (code === 0) { + let gitName, versionName; + [gitName, versionName, version] = Buffer.concat(outputChunks).toString().split(' '); + version = version?.trim(); + } + return callback(version); + }); + }); +} diff --git a/src/init.coffee b/src/init.coffee deleted file mode 100644 index b633932..0000000 --- a/src/init.coffee +++ /dev/null @@ -1,180 +0,0 @@ -path = require 'path' - -yargs = require 'yargs' - -Command = require './command' -fs = require './fs' - -module.exports = -class Init extends Command - supportedSyntaxes: ['coffeescript', 'javascript'] - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: - apm init -p - apm init -p --syntax - apm init -p -c ~/Downloads/r.tmbundle - apm init -p -c https://github.com/textmate/r.tmbundle - apm init -p --template /path/to/your/package/template - - apm init -t - apm init -t -c ~/Downloads/Dawn.tmTheme - apm init -t -c https://raw.github.com/chriskempson/tomorrow-theme/master/textmate/Tomorrow-Night-Eighties.tmTheme - apm init -t --template /path/to/your/theme/template - - apm init -l - - Generates code scaffolding for either a theme or package depending - on the option selected. - """ - options.alias('p', 'package').string('package').describe('package', 'Generates a basic package') - options.alias('s', 'syntax').string('syntax').describe('syntax', 'Sets package syntax to CoffeeScript or JavaScript') - options.alias('t', 'theme').string('theme').describe('theme', 'Generates a basic theme') - options.alias('l', 'language').string('language').describe('language', 'Generates a basic language package') - options.alias('c', 'convert').string('convert').describe('convert', 'Path or URL to TextMate bundle/theme to convert') - options.alias('h', 'help').describe('help', 'Print this usage message') - options.string('template').describe('template', 'Path to the package or theme template') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - if options.argv.package?.length > 0 - if options.argv.convert - @convertPackage(options.argv.convert, options.argv.package, callback) - else - packagePath = path.resolve(options.argv.package) - syntax = options.argv.syntax or @supportedSyntaxes[0] - if syntax not in @supportedSyntaxes - return callback("You must specify one of #{@supportedSyntaxes.join(', ')} after the --syntax argument") - templatePath = @getTemplatePath(options.argv, "package-#{syntax}") - @generateFromTemplate(packagePath, templatePath) - callback() - else if options.argv.theme?.length > 0 - if options.argv.convert - @convertTheme(options.argv.convert, options.argv.theme, callback) - else - themePath = path.resolve(options.argv.theme) - templatePath = @getTemplatePath(options.argv, 'theme') - @generateFromTemplate(themePath, templatePath) - callback() - else if options.argv.language?.length > 0 - languagePath = path.resolve(options.argv.language) - languageName = path.basename(languagePath).replace(/^language-/, '') - languagePath = path.join(path.dirname(languagePath), "language-#{languageName}") - templatePath = @getTemplatePath(options.argv, 'language') - @generateFromTemplate(languagePath, templatePath, languageName) - callback() - else if options.argv.package? - callback('You must specify a path after the --package argument') - else if options.argv.theme? - callback('You must specify a path after the --theme argument') - else - callback('You must specify either --package, --theme or --language to `apm init`') - - convertPackage: (sourcePath, destinationPath, callback) -> - unless destinationPath - callback("Specify directory to create package in using --package") - return - - PackageConverter = require './package-converter' - converter = new PackageConverter(sourcePath, destinationPath) - converter.convert (error) => - if error? - callback(error) - else - destinationPath = path.resolve(destinationPath) - templatePath = path.resolve(__dirname, '..', 'templates', 'bundle') - @generateFromTemplate(destinationPath, templatePath) - callback() - - convertTheme: (sourcePath, destinationPath, callback) -> - unless destinationPath - callback("Specify directory to create theme in using --theme") - return - - ThemeConverter = require './theme-converter' - converter = new ThemeConverter(sourcePath, destinationPath) - converter.convert (error) => - if error? - callback(error) - else - destinationPath = path.resolve(destinationPath) - templatePath = path.resolve(__dirname, '..', 'templates', 'theme') - @generateFromTemplate(destinationPath, templatePath) - fs.removeSync(path.join(destinationPath, 'styles', 'colors.less')) - fs.removeSync(path.join(destinationPath, 'LICENSE.md')) - callback() - - generateFromTemplate: (packagePath, templatePath, packageName) -> - packageName ?= path.basename(packagePath) - packageAuthor = process.env.GITHUB_USER or 'atom' - - fs.makeTreeSync(packagePath) - - for childPath in fs.listRecursive(templatePath) - templateChildPath = path.resolve(templatePath, childPath) - relativePath = templateChildPath.replace(templatePath, "") - relativePath = relativePath.replace(/^\//, '') - relativePath = relativePath.replace(/\.template$/, '') - relativePath = @replacePackageNamePlaceholders(relativePath, packageName) - - sourcePath = path.join(packagePath, relativePath) - continue if fs.existsSync(sourcePath) - if fs.isDirectorySync(templateChildPath) - fs.makeTreeSync(sourcePath) - else if fs.isFileSync(templateChildPath) - fs.makeTreeSync(path.dirname(sourcePath)) - contents = fs.readFileSync(templateChildPath).toString() - contents = @replacePackageNamePlaceholders(contents, packageName) - contents = @replacePackageAuthorPlaceholders(contents, packageAuthor) - contents = @replaceCurrentYearPlaceholders(contents) - fs.writeFileSync(sourcePath, contents) - - replacePackageAuthorPlaceholders: (string, packageAuthor) -> - string.replace(/__package-author__/g, packageAuthor) - - replacePackageNamePlaceholders: (string, packageName) -> - placeholderRegex = /__(?:(package-name)|([pP]ackageName)|(package_name))__/g - string = string.replace placeholderRegex, (match, dash, camel, underscore) => - if dash - @dasherize(packageName) - else if camel - if /[a-z]/.test(camel[0]) - packageName = packageName[0].toLowerCase() + packageName[1...] - else if /[A-Z]/.test(camel[0]) - packageName = packageName[0].toUpperCase() + packageName[1...] - @camelize(packageName) - - else if underscore - @underscore(packageName) - - replaceCurrentYearPlaceholders: (string) -> - string.replace '__current_year__', new Date().getFullYear() - - getTemplatePath: (argv, templateType) -> - if argv.template? - path.resolve(argv.template) - else - path.resolve(__dirname, '..', 'templates', templateType) - - dasherize: (string) -> - string = string[0].toLowerCase() + string[1..] - string.replace /([A-Z])|(_)/g, (m, letter, underscore) -> - if letter - "-" + letter.toLowerCase() - else - "-" - - camelize: (string) -> - string.replace /[_-]+(\w)/g, (m) -> m[1].toUpperCase() - - underscore: (string) -> - string = string[0].toLowerCase() + string[1..] - string.replace /([A-Z])|(-)/g, (m, letter, dash) -> - if letter - "_" + letter.toLowerCase() - else - "_" diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..d9f9d5a --- /dev/null +++ b/src/init.js @@ -0,0 +1,233 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Init; +import path from 'path'; +import yargs from 'yargs'; +import Command from './command'; +import fs from './fs'; + +export default Init = (function() { + Init = class Init extends Command { + static initClass() { + this.prototype.supportedSyntaxes = ['coffeescript', 'javascript']; + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: + apm init -p + apm init -p --syntax + apm init -p -c ~/Downloads/r.tmbundle + apm init -p -c https://github.com/textmate/r.tmbundle + apm init -p --template /path/to/your/package/template + + apm init -t + apm init -t -c ~/Downloads/Dawn.tmTheme + apm init -t -c https://raw.github.com/chriskempson/tomorrow-theme/master/textmate/Tomorrow-Night-Eighties.tmTheme + apm init -t --template /path/to/your/theme/template + + apm init -l + +Generates code scaffolding for either a theme or package depending +on the option selected.\ +` + ); + options.alias('p', 'package').string('package').describe('package', 'Generates a basic package'); + options.alias('s', 'syntax').string('syntax').describe('syntax', 'Sets package syntax to CoffeeScript or JavaScript'); + options.alias('t', 'theme').string('theme').describe('theme', 'Generates a basic theme'); + options.alias('l', 'language').string('language').describe('language', 'Generates a basic language package'); + options.alias('c', 'convert').string('convert').describe('convert', 'Path or URL to TextMate bundle/theme to convert'); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.string('template').describe('template', 'Path to the package or theme template'); + } + + run(options) { + let templatePath; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + if (options.argv.package?.length > 0) { + if (options.argv.convert) { + return this.convertPackage(options.argv.convert, options.argv.package, callback); + } else { + const packagePath = path.resolve(options.argv.package); + const syntax = options.argv.syntax || this.supportedSyntaxes[0]; + if (!this.supportedSyntaxes.includes(syntax)) { + return callback(`You must specify one of ${this.supportedSyntaxes.join(', ')} after the --syntax argument`); + } + templatePath = this.getTemplatePath(options.argv, `package-${syntax}`); + this.generateFromTemplate(packagePath, templatePath); + return callback(); + } + } else if (options.argv.theme?.length > 0) { + if (options.argv.convert) { + return this.convertTheme(options.argv.convert, options.argv.theme, callback); + } else { + const themePath = path.resolve(options.argv.theme); + templatePath = this.getTemplatePath(options.argv, 'theme'); + this.generateFromTemplate(themePath, templatePath); + return callback(); + } + } else if (options.argv.language?.length > 0) { + let languagePath = path.resolve(options.argv.language); + const languageName = path.basename(languagePath).replace(/^language-/, ''); + languagePath = path.join(path.dirname(languagePath), `language-${languageName}`); + templatePath = this.getTemplatePath(options.argv, 'language'); + this.generateFromTemplate(languagePath, templatePath, languageName); + return callback(); + } else if (options.argv.package != null) { + return callback('You must specify a path after the --package argument'); + } else if (options.argv.theme != null) { + return callback('You must specify a path after the --theme argument'); + } else { + return callback('You must specify either --package, --theme or --language to `apm init`'); + } + } + + convertPackage(sourcePath, destinationPath, callback) { + if (!destinationPath) { + callback("Specify directory to create package in using --package"); + return; + } + + const PackageConverter = require('./package-converter'); + const converter = new PackageConverter(sourcePath, destinationPath); + return converter.convert(error => { + if (error != null) { + return callback(error); + } else { + destinationPath = path.resolve(destinationPath); + const templatePath = path.resolve(__dirname, '..', 'templates', 'bundle'); + this.generateFromTemplate(destinationPath, templatePath); + return callback(); + } + }); + } + + convertTheme(sourcePath, destinationPath, callback) { + if (!destinationPath) { + callback("Specify directory to create theme in using --theme"); + return; + } + + const ThemeConverter = require('./theme-converter'); + const converter = new ThemeConverter(sourcePath, destinationPath); + return converter.convert(error => { + if (error != null) { + return callback(error); + } else { + destinationPath = path.resolve(destinationPath); + const templatePath = path.resolve(__dirname, '..', 'templates', 'theme'); + this.generateFromTemplate(destinationPath, templatePath); + fs.removeSync(path.join(destinationPath, 'styles', 'colors.less')); + fs.removeSync(path.join(destinationPath, 'LICENSE.md')); + return callback(); + } + }); + } + + generateFromTemplate(packagePath, templatePath, packageName) { + if (packageName == null) { packageName = path.basename(packagePath); } + const packageAuthor = process.env.GITHUB_USER || 'atom'; + + fs.makeTreeSync(packagePath); + + return (() => { + const result = []; + for (let childPath of fs.listRecursive(templatePath)) { + const templateChildPath = path.resolve(templatePath, childPath); + let relativePath = templateChildPath.replace(templatePath, ""); + relativePath = relativePath.replace(/^\//, ''); + relativePath = relativePath.replace(/\.template$/, ''); + relativePath = this.replacePackageNamePlaceholders(relativePath, packageName); + + const sourcePath = path.join(packagePath, relativePath); + if (fs.existsSync(sourcePath)) { continue; } + if (fs.isDirectorySync(templateChildPath)) { + result.push(fs.makeTreeSync(sourcePath)); + } else if (fs.isFileSync(templateChildPath)) { + fs.makeTreeSync(path.dirname(sourcePath)); + let contents = fs.readFileSync(templateChildPath).toString(); + contents = this.replacePackageNamePlaceholders(contents, packageName); + contents = this.replacePackageAuthorPlaceholders(contents, packageAuthor); + contents = this.replaceCurrentYearPlaceholders(contents); + result.push(fs.writeFileSync(sourcePath, contents)); + } else { + result.push(undefined); + } + } + return result; + })(); + } + + replacePackageAuthorPlaceholders(string, packageAuthor) { + return string.replace(/__package-author__/g, packageAuthor); + } + + replacePackageNamePlaceholders(string, packageName) { + const placeholderRegex = /__(?:(package-name)|([pP]ackageName)|(package_name))__/g; + return string = string.replace(placeholderRegex, (match, dash, camel, underscore) => { + if (dash) { + return this.dasherize(packageName); + } else if (camel) { + if (/[a-z]/.test(camel[0])) { + packageName = packageName[0].toLowerCase() + packageName.slice(1); + } else if (/[A-Z]/.test(camel[0])) { + packageName = packageName[0].toUpperCase() + packageName.slice(1); + } + return this.camelize(packageName); + + } else if (underscore) { + return this.underscore(packageName); + } + }); + } + + replaceCurrentYearPlaceholders(string) { + return string.replace('__current_year__', new Date().getFullYear()); + } + + getTemplatePath(argv, templateType) { + if (argv.template != null) { + return path.resolve(argv.template); + } else { + return path.resolve(__dirname, '..', 'templates', templateType); + } + } + + dasherize(string) { + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|(_)/g, function(m, letter, underscore) { + if (letter) { + return "-" + letter.toLowerCase(); + } else { + return "-"; + } + }); + } + + camelize(string) { + return string.replace(/[_-]+(\w)/g, m => m[1].toUpperCase()); + } + + underscore(string) { + string = string[0].toLowerCase() + string.slice(1); + return string.replace(/([A-Z])|(-)/g, function(m, letter, dash) { + if (letter) { + return "_" + letter.toLowerCase(); + } else { + return "_"; + } + }); + } + }; + Init.initClass(); + return Init; +})(); diff --git a/src/install.coffee b/src/install.coffee deleted file mode 100644 index 4fc73d2..0000000 --- a/src/install.coffee +++ /dev/null @@ -1,612 +0,0 @@ -assert = require 'assert' -path = require 'path' - -_ = require 'underscore-plus' -async = require 'async' -CSON = require 'season' -yargs = require 'yargs' -Git = require 'git-utils' -semver = require 'semver' -temp = require 'temp' -hostedGitInfo = require 'hosted-git-info' - -config = require './apm' -Command = require './command' -fs = require './fs' -RebuildModuleCache = require './rebuild-module-cache' -request = require './request' -{isDeprecatedPackage} = require './deprecated-packages' - -module.exports = -class Install extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomPackagesDirectory = path.join(@atomDirectory, 'packages') - @atomNodeDirectory = path.join(@atomDirectory, '.node-gyp') - @atomNpmPath = require.resolve('npm/bin/npm-cli') - @repoLocalPackagePathRegex = /^file:(?!\/\/)(.*)/ - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm install [...] - apm install @ - apm install - apm install / - apm install --packages-file my-packages.txt - apm i (with any of the previous argument usage) - - Install the given Atom package to ~/.atom/packages/. - - If no package name is given then all the dependencies in the package.json - file are installed to the node_modules folder in the current working - directory. - - A packages file can be specified that is a newline separated list of - package names to install with optional versions using the - `package-name@version` syntax. - """ - options.alias('c', 'compatible').string('compatible').describe('compatible', 'Only install packages/themes compatible with this Atom version') - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('s', 'silent').boolean('silent').describe('silent', 'Set the npm log level to silent') - options.alias('q', 'quiet').boolean('quiet').describe('quiet', 'Set the npm log level to warn') - options.boolean('check').describe('check', 'Check that native build tools are installed') - options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information') - options.string('packages-file').describe('packages-file', 'A text file containing the packages to install') - options.boolean('production').describe('production', 'Do not install dev dependencies') - - installModule: (options, pack, moduleURI, callback) -> - installGlobally = options.installGlobally ? true - - installArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'install'] - installArgs.push(moduleURI) - installArgs.push(@getNpmBuildFlags()...) - installArgs.push("--global-style") if installGlobally - installArgs.push('--silent') if options.argv.silent - installArgs.push('--quiet') if options.argv.quiet - installArgs.push('--production') if options.argv.production - installArgs.push('--verbose') if options.argv.verbose - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - installOptions = {env} - installOptions.streaming = true if @verbose - - if installGlobally - installDirectory = temp.mkdirSync('apm-install-dir-') - nodeModulesDirectory = path.join(installDirectory, 'node_modules') - fs.makeTreeSync(nodeModulesDirectory) - installOptions.cwd = installDirectory - - @fork @atomNpmPath, installArgs, installOptions, (code, stderr='', stdout='') => - if code is 0 - if installGlobally - commands = [] - children = fs.readdirSync(nodeModulesDirectory) - .filter (dir) -> dir isnt ".bin" - assert.equal(children.length, 1, "Expected there to only be one child in node_modules") - child = children[0] - source = path.join(nodeModulesDirectory, child) - destination = path.join(@atomPackagesDirectory, child) - commands.push (next) -> fs.cp(source, destination, next) - commands.push (next) => @buildModuleCache(pack.name, next) - commands.push (next) => @warmCompileCache(pack.name, next) - - async.waterfall commands, (error) => - if error? - @logFailure() - else - @logSuccess() unless options.argv.json - callback(error, {name: child, installPath: destination}) - else - callback(null, {name: child, installPath: destination}) - else - if installGlobally - fs.removeSync(installDirectory) - @logFailure() - - error = "#{stdout}\n#{stderr}" - error = @getGitErrorMessage(pack) if error.indexOf('code ENOGIT') isnt -1 - callback(error) - - getGitErrorMessage: (pack) -> - message = """ - Failed to install #{pack.name} because Git was not found. - - The #{pack.name} package has module dependencies that cannot be installed without Git. - - You need to install Git and add it to your path environment variable in order to install this package. - - """ - - switch process.platform - when 'win32' - message += """ - - You can install Git by downloading, installing, and launching GitHub for Windows: https://windows.github.com - - """ - when 'linux' - message += """ - - You can install Git from your OS package manager. - - """ - - message += """ - - Run apm -v after installing Git to see what version has been detected. - """ - - message - - installModules: (options, callback) => - process.stdout.write 'Installing modules ' unless options.argv.json - - @forkInstallCommand options, (args...) => - if options.argv.json - @logCommandResultsIfFail(callback, args...) - else - @logCommandResults(callback, args...) - - forkInstallCommand: (options, callback) -> - installArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'install'] - installArgs.push(@getNpmBuildFlags()...) - installArgs.push('--silent') if options.argv.silent - installArgs.push('--quiet') if options.argv.quiet - installArgs.push('--production') if options.argv.production - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - installOptions = {env} - installOptions.cwd = options.cwd if options.cwd - installOptions.streaming = true if @verbose - - @fork(@atomNpmPath, installArgs, installOptions, callback) - - # Request package information from the atom.io API for a given package name. - # - # packageName - The string name of the package to request. - # callback - The function to invoke when the request completes with an error - # as the first argument and an object as the second. - requestPackage: (packageName, callback) -> - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{packageName}" - json: true - retries: 4 - request.get requestSettings, (error, response, body={}) -> - if error? - message = "Request for package information failed: #{error.message}" - message += " (#{error.code})" if error.code - callback(message) - else if response.statusCode isnt 200 - message = request.getErrorMessage(response, body) - callback("Request for package information failed: #{message}") - else - if body.releases.latest - callback(null, body) - else - callback("No releases available for #{packageName}") - - # Is the package at the specified version already installed? - # - # * packageName: The string name of the package. - # * packageVersion: The string version of the package. - isPackageInstalled: (packageName, packageVersion) -> - try - {version} = CSON.readFileSync(CSON.resolve(path.join('node_modules', packageName, 'package'))) ? {} - packageVersion is version - catch error - false - - # Install the package with the given name and optional version - # - # metadata - The package metadata object with at least a name key. A version - # key is also supported. The version defaults to the latest if - # unspecified. - # options - The installation options object. - # callback - The function to invoke when installation completes with an - # error as the first argument. - installRegisteredPackage: (metadata, options, callback) -> - packageName = metadata.name - packageVersion = metadata.version - - installGlobally = options.installGlobally ? true - unless installGlobally - if packageVersion and @isPackageInstalled(packageName, packageVersion) - callback(null, {}) - return - - label = packageName - label += "@#{packageVersion}" if packageVersion - unless options.argv.json - process.stdout.write "Installing #{label} " - if installGlobally - process.stdout.write "to #{@atomPackagesDirectory} " - - @requestPackage packageName, (error, pack) => - if error? - @logFailure() - callback(error) - else - packageVersion ?= @getLatestCompatibleVersion(pack) - unless packageVersion - @logFailure() - callback("No available version compatible with the installed Atom version: #{@installedAtomVersion}") - return - - {tarball} = pack.versions[packageVersion]?.dist ? {} - unless tarball - @logFailure() - callback("Package version: #{packageVersion} not found") - return - - commands = [] - commands.push (next) => @installModule(options, pack, tarball, next) - if installGlobally and (packageName.localeCompare(pack.name, 'en', {sensitivity: 'accent'}) isnt 0) - commands.push (newPack, next) => # package was renamed; delete old package folder - fs.removeSync(path.join(@atomPackagesDirectory, packageName)) - next(null, newPack) - commands.push ({installPath}, next) -> - if installPath? - metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')) - json = {installPath, metadata} - next(null, json) - else - next(null, {}) # installed locally, no install path data - - async.waterfall commands, (error, json) => - unless installGlobally - if error? - @logFailure() - else - @logSuccess() unless options.argv.json - callback(error, json) - - # Install the package with the given name and local path - # - # packageName - The name of the package - # packagePath - The local path of the package in the form "file:./packages/package-name" - # options - The installation options object. - # callback - The function to invoke when installation completes with an - # error as the first argument. - installLocalPackage: (packageName, packagePath, options, callback) -> - unless options.argv.json - process.stdout.write "Installing #{packageName} from #{packagePath.slice('file:'.length)} " - commands = [] - commands.push (next) => - @installModule(options, {name: packageName}, packagePath, next) - commands.push ({installPath}, next) -> - if installPath? - metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')) - json = {installPath, metadata} - next(null, json) - else - next(null, {}) # installed locally, no install path data - - async.waterfall commands, (error, json) => - if error? - @logFailure() - else - @logSuccess() unless options.argv.json - callback(error, json) - - # Install all the package dependencies found in the package.json file. - # - # options - The installation options - # callback - The callback function to invoke when done with an error as the - # first argument. - installPackageDependencies: (options, callback) -> - options = _.extend({}, options, installGlobally: false) - commands = [] - for name, version of @getPackageDependencies() - do (name, version) => - commands.push (next) => - if @repoLocalPackagePathRegex.test(version) - @installLocalPackage(name, version, options, next) - else - @installRegisteredPackage({name, version}, options, next) - - async.series(commands, callback) - - installDependencies: (options, callback) -> - options.installGlobally = false - commands = [] - commands.push (callback) => @installModules(options, callback) - commands.push (callback) => @installPackageDependencies(options, callback) - - async.waterfall commands, callback - - # Get all package dependency names and versions from the package.json file. - getPackageDependencies: -> - try - metadata = fs.readFileSync('package.json', 'utf8') - {packageDependencies, dependencies} = JSON.parse(metadata) ? {} - - return {} unless packageDependencies - return packageDependencies unless dependencies - - # This code filters out any `packageDependencies` that have an equivalent - # normalized repo-local package path entry in the `dependencies` section of - # `package.json`. Versioned `packageDependencies` are always returned. - filteredPackages = {} - for packageName, packageSpec of packageDependencies - dependencyPath = @getRepoLocalPackagePath(dependencies[packageName]) - packageDependencyPath = @getRepoLocalPackagePath(packageSpec) - unless packageDependencyPath and dependencyPath is packageDependencyPath - filteredPackages[packageName] = packageSpec - - filteredPackages - catch error - {} - - getRepoLocalPackagePath: (packageSpec) -> - return undefined if not packageSpec - repoLocalPackageMatch = packageSpec.match(@repoLocalPackagePathRegex) - if repoLocalPackageMatch - path.normalize(repoLocalPackageMatch[1]) - else - undefined - - createAtomDirectories: -> - fs.makeTreeSync(@atomDirectory) - fs.makeTreeSync(@atomPackagesDirectory) - fs.makeTreeSync(@atomNodeDirectory) - - # Compile a sample native module to see if a useable native build toolchain - # is instlalled and successfully detected. This will include both Python - # and a compiler. - checkNativeBuildTools: (callback) -> - process.stdout.write 'Checking for native build tools ' - - buildArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'build'] - buildArgs.push(path.resolve(__dirname, '..', 'native-module')) - buildArgs.push(@getNpmBuildFlags()...) - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - buildOptions = {env} - buildOptions.streaming = true if @verbose - - fs.removeSync(path.resolve(__dirname, '..', 'native-module', 'build')) - - @fork @atomNpmPath, buildArgs, buildOptions, (args...) => - @logCommandResults(callback, args...) - - packageNamesFromPath: (filePath) -> - filePath = path.resolve(filePath) - - unless fs.isFileSync(filePath) - throw new Error("File '#{filePath}' does not exist") - - packages = fs.readFileSync(filePath, 'utf8') - @sanitizePackageNames(packages.split(/\s/)) - - buildModuleCache: (packageName, callback) -> - packageDirectory = path.join(@atomPackagesDirectory, packageName) - rebuildCacheCommand = new RebuildModuleCache() - rebuildCacheCommand.rebuild packageDirectory, -> - # Ignore cache errors and just finish the install - callback() - - warmCompileCache: (packageName, callback) -> - packageDirectory = path.join(@atomPackagesDirectory, packageName) - - @getResourcePath (resourcePath) => - try - CompileCache = require(path.join(resourcePath, 'src', 'compile-cache')) - - onDirectory = (directoryPath) -> - path.basename(directoryPath) isnt 'node_modules' - - onFile = (filePath) => - try - CompileCache.addPathToCache(filePath, @atomDirectory) - - fs.traverseTreeSync(packageDirectory, onFile, onDirectory) - callback(null) - - isBundledPackage: (packageName, callback) -> - @getResourcePath (resourcePath) -> - try - atomMetadata = JSON.parse(fs.readFileSync(path.join(resourcePath, 'package.json'))) - catch error - return callback(false) - - callback(atomMetadata?.packageDependencies?.hasOwnProperty(packageName)) - - getLatestCompatibleVersion: (pack) -> - unless @installedAtomVersion - if isDeprecatedPackage(pack.name, pack.releases.latest) - return null - else - return pack.releases.latest - - latestVersion = null - for version, metadata of pack.versions ? {} - continue unless semver.valid(version) - continue unless metadata - continue if isDeprecatedPackage(pack.name, version) - - engine = metadata.engines?.atom ? '*' - continue unless semver.validRange(engine) - continue unless semver.satisfies(@installedAtomVersion, engine) - - latestVersion ?= version - latestVersion = version if semver.gt(version, latestVersion) - - latestVersion - - getHostedGitInfo: (name) -> - hostedGitInfo.fromUrl(name) - - installGitPackage: (packageUrl, options, callback) -> - tasks = [] - - cloneDir = temp.mkdirSync("atom-git-package-clone-") - - tasks.push (data, next) => - urls = @getNormalizedGitUrls(packageUrl) - @cloneFirstValidGitUrl urls, cloneDir, options, (err) -> - next(err, data) - - tasks.push (data, next) => - @installGitPackageDependencies cloneDir, options, (err) -> - next(err, data) - - tasks.push (data, next) => - @getRepositoryHeadSha cloneDir, (err, sha) -> - data.sha = sha - next(err, data) - - tasks.push (data, next) -> - metadataFilePath = CSON.resolve(path.join(cloneDir, 'package')) - CSON.readFile metadataFilePath, (err, metadata) -> - data.metadataFilePath = metadataFilePath - data.metadata = metadata - next(err, data) - - tasks.push (data, next) -> - data.metadata.apmInstallSource = - type: "git" - source: packageUrl - sha: data.sha - CSON.writeFile data.metadataFilePath, data.metadata, (err) -> - next(err, data) - - tasks.push (data, next) => - {name} = data.metadata - targetDir = path.join(@atomPackagesDirectory, name) - process.stdout.write "Moving #{name} to #{targetDir} " unless options.argv.json - fs.cp cloneDir, targetDir, (err) => - if err - next(err) - else - @logSuccess() unless options.argv.json - json = {installPath: targetDir, metadata: data.metadata} - next(null, json) - - iteratee = (currentData, task, next) -> task(currentData, next) - async.reduce tasks, {}, iteratee, callback - - getNormalizedGitUrls: (packageUrl) -> - packageInfo = @getHostedGitInfo(packageUrl) - - if packageUrl.indexOf('file://') is 0 - [packageUrl] - else if packageInfo.default is 'sshurl' - [packageInfo.toString()] - else if packageInfo.default is 'https' - [packageInfo.https().replace(/^git\+https:/, "https:")] - else if packageInfo.default is 'shortcut' - [ - packageInfo.https().replace(/^git\+https:/, "https:"), - packageInfo.sshurl() - ] - - cloneFirstValidGitUrl: (urls, cloneDir, options, callback) -> - async.detectSeries(urls, (url, next) => - @cloneNormalizedUrl url, cloneDir, options, (error) -> - next(null, not error) - , (err, result) -> - if err or not result - invalidUrls = "Couldn't clone #{urls.join(' or ')}" - invalidUrlsError = new Error(invalidUrls) - callback(invalidUrlsError) - else - callback() - ) - - cloneNormalizedUrl: (url, cloneDir, options, callback) -> - # Require here to avoid circular dependency - Develop = require './develop' - develop = new Develop() - - develop.cloneRepository url, cloneDir, options, (err) -> - callback(err) - - installGitPackageDependencies: (directory, options, callback) => - options.cwd = directory - @installDependencies(options, callback) - - getRepositoryHeadSha: (repoDir, callback) -> - try - repo = Git.open(repoDir) - sha = repo.getReferenceTarget("HEAD") - callback(null, sha) - catch err - callback(err) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - packagesFilePath = options.argv['packages-file'] - - @createAtomDirectories() - - if options.argv.check - config.loadNpm (error, @npm) => - @loadInstalledAtomMetadata => - @checkNativeBuildTools(callback) - return - - @verbose = options.argv.verbose - if @verbose - request.debug(true) - process.env.NODE_DEBUG = 'request' - - installPackage = (name, nextInstallStep) => - gitPackageInfo = @getHostedGitInfo(name) - - if gitPackageInfo or name.indexOf('file://') is 0 - @installGitPackage name, options, nextInstallStep - else if name is '.' - @installDependencies(options, nextInstallStep) - else # is registered package - atIndex = name.indexOf('@') - if atIndex > 0 - version = name.substring(atIndex + 1) - name = name.substring(0, atIndex) - - @isBundledPackage name, (isBundledPackage) => - if isBundledPackage - console.error """ - The #{name} package is bundled with Atom and should not be explicitly installed. - You can run `apm uninstall #{name}` to uninstall it and then the version bundled - with Atom will be used. - """.yellow - @installRegisteredPackage({name, version}, options, nextInstallStep) - - if packagesFilePath - try - packageNames = @packageNamesFromPath(packagesFilePath) - catch error - return callback(error) - else - packageNames = @packageNamesFromArgv(options.argv) - packageNames.push('.') if packageNames.length is 0 - - commands = [] - commands.push (callback) => config.loadNpm (error, @npm) => callback(error) - commands.push (callback) => @loadInstalledAtomMetadata -> callback() - packageNames.forEach (packageName) -> - commands.push (callback) -> installPackage(packageName, callback) - iteratee = (item, next) -> item(next) - async.mapSeries commands, iteratee, (err, installedPackagesInfo) -> - return callback(err) if err - installedPackagesInfo = _.compact(installedPackagesInfo) - installedPackagesInfo = installedPackagesInfo.filter (item, idx) -> - packageNames[idx] isnt "." - console.log(JSON.stringify(installedPackagesInfo, null, " ")) if options.argv.json - callback(null) diff --git a/src/install.js b/src/install.js new file mode 100644 index 0000000..eca7be7 --- /dev/null +++ b/src/install.js @@ -0,0 +1,742 @@ +/* + * decaffeinate suggestions: + * DS002: Fix invalid constructor + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Install; +import assert from 'assert'; +import path from 'path'; +import _ from 'underscore-plus'; +import async from 'async'; +import CSON from 'season'; +import yargs from 'yargs'; +import Git from 'git-utils'; +import semver from 'semver'; +import temp from 'temp'; +import hostedGitInfo from 'hosted-git-info'; +import config from './apm'; +import Command from './command'; +import fs from './fs'; +import RebuildModuleCache from './rebuild-module-cache'; +import request from './request'; +import { isDeprecatedPackage } from './deprecated-packages'; + +export default Install = class Install extends Command { + constructor() { + this.installModules = this.installModules.bind(this); + this.installGitPackageDependencies = this.installGitPackageDependencies.bind(this); + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomPackagesDirectory = path.join(this.atomDirectory, 'packages'); + this.atomNodeDirectory = path.join(this.atomDirectory, '.node-gyp'); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + this.repoLocalPackagePathRegex = /^file:(?!\/\/)(.*)/; + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm install [...] + apm install @ + apm install + apm install / + apm install --packages-file my-packages.txt + apm i (with any of the previous argument usage) + +Install the given Atom package to ~/.atom/packages/. + +If no package name is given then all the dependencies in the package.json +file are installed to the node_modules folder in the current working +directory. + +A packages file can be specified that is a newline separated list of +package names to install with optional versions using the +\`package-name@version\` syntax.\ +` + ); + options.alias('c', 'compatible').string('compatible').describe('compatible', 'Only install packages/themes compatible with this Atom version'); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('s', 'silent').boolean('silent').describe('silent', 'Set the npm log level to silent'); + options.alias('q', 'quiet').boolean('quiet').describe('quiet', 'Set the npm log level to warn'); + options.boolean('check').describe('check', 'Check that native build tools are installed'); + options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information'); + options.string('packages-file').describe('packages-file', 'A text file containing the packages to install'); + return options.boolean('production').describe('production', 'Do not install dev dependencies'); + } + + installModule(options, pack, moduleURI, callback) { + let installDirectory, nodeModulesDirectory; + const installGlobally = options.installGlobally != null ? options.installGlobally : true; + + const installArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'install']; + installArgs.push(moduleURI); + installArgs.push(...this.getNpmBuildFlags()); + if (installGlobally) { installArgs.push("--global-style"); } + if (options.argv.silent) { installArgs.push('--silent'); } + if (options.argv.quiet) { installArgs.push('--quiet'); } + if (options.argv.production) { installArgs.push('--production'); } + if (options.argv.verbose) { installArgs.push('--verbose'); } + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + const installOptions = {env}; + if (this.verbose) { installOptions.streaming = true; } + + if (installGlobally) { + installDirectory = temp.mkdirSync('apm-install-dir-'); + nodeModulesDirectory = path.join(installDirectory, 'node_modules'); + fs.makeTreeSync(nodeModulesDirectory); + installOptions.cwd = installDirectory; + } + + return this.fork(this.atomNpmPath, installArgs, installOptions, (code, stderr='', stdout='') => { + if (code === 0) { + let child, destination; + if (installGlobally) { + const commands = []; + const children = fs.readdirSync(nodeModulesDirectory) + .filter(dir => dir !== ".bin"); + assert.equal(children.length, 1, "Expected there to only be one child in node_modules"); + child = children[0]; + const source = path.join(nodeModulesDirectory, child); + destination = path.join(this.atomPackagesDirectory, child); + commands.push(next => fs.cp(source, destination, next)); + commands.push(next => this.buildModuleCache(pack.name, next)); + commands.push(next => this.warmCompileCache(pack.name, next)); + + return async.waterfall(commands, error => { + if (error != null) { + this.logFailure(); + } else { + if (!options.argv.json) { this.logSuccess(); } + } + return callback(error, {name: child, installPath: destination}); + }); + } else { + return callback(null, {name: child, installPath: destination}); + } + } else { + if (installGlobally) { + fs.removeSync(installDirectory); + this.logFailure(); + } + + let error = `${stdout}\n${stderr}`; + if (error.indexOf('code ENOGIT') !== -1) { error = this.getGitErrorMessage(pack); } + return callback(error); + } + }); + } + + getGitErrorMessage(pack) { + let message = `\ +Failed to install ${pack.name} because Git was not found. + +The ${pack.name} package has module dependencies that cannot be installed without Git. + +You need to install Git and add it to your path environment variable in order to install this package. +\ +`; + + switch (process.platform) { + case 'win32': + message += `\ + +You can install Git by downloading, installing, and launching GitHub for Windows: https://windows.github.com +\ +`; + break; + case 'linux': + message += `\ + +You can install Git from your OS package manager. +\ +`; + break; + } + + message += `\ + +Run apm -v after installing Git to see what version has been detected.\ +`; + + return message; + } + + installModules(options, callback) { + if (!options.argv.json) { process.stdout.write('Installing modules '); } + + return this.forkInstallCommand(options, (...args) => { + if (options.argv.json) { + return this.logCommandResultsIfFail(callback, ...args); + } else { + return this.logCommandResults(callback, ...args); + } + }); + } + + forkInstallCommand(options, callback) { + const installArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'install']; + installArgs.push(...this.getNpmBuildFlags()); + if (options.argv.silent) { installArgs.push('--silent'); } + if (options.argv.quiet) { installArgs.push('--quiet'); } + if (options.argv.production) { installArgs.push('--production'); } + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + const installOptions = {env}; + if (options.cwd) { installOptions.cwd = options.cwd; } + if (this.verbose) { installOptions.streaming = true; } + + return this.fork(this.atomNpmPath, installArgs, installOptions, callback); + } + + // Request package information from the atom.io API for a given package name. + // + // packageName - The string name of the package to request. + // callback - The function to invoke when the request completes with an error + // as the first argument and an object as the second. + requestPackage(packageName, callback) { + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${packageName}`, + json: true, + retries: 4 + }; + return request.get(requestSettings, function(error, response, body={}) { + let message; + if (error != null) { + message = `Request for package information failed: ${error.message}`; + if (error.code) { message += ` (${error.code})`; } + return callback(message); + } else if (response.statusCode !== 200) { + message = request.getErrorMessage(response, body); + return callback(`Request for package information failed: ${message}`); + } else { + if (body.releases.latest) { + return callback(null, body); + } else { + return callback(`No releases available for ${packageName}`); + } + } + }); + } + + // Is the package at the specified version already installed? + // + // * packageName: The string name of the package. + // * packageVersion: The string version of the package. + isPackageInstalled(packageName, packageVersion) { + try { + let left; + const {version} = (left = CSON.readFileSync(CSON.resolve(path.join('node_modules', packageName, 'package')))) != null ? left : {}; + return packageVersion === version; + } catch (error) { + return false; + } + } + + // Install the package with the given name and optional version + // + // metadata - The package metadata object with at least a name key. A version + // key is also supported. The version defaults to the latest if + // unspecified. + // options - The installation options object. + // callback - The function to invoke when installation completes with an + // error as the first argument. + installRegisteredPackage(metadata, options, callback) { + const packageName = metadata.name; + let packageVersion = metadata.version; + + const installGlobally = options.installGlobally != null ? options.installGlobally : true; + if (!installGlobally) { + if (packageVersion && this.isPackageInstalled(packageName, packageVersion)) { + callback(null, {}); + return; + } + } + + let label = packageName; + if (packageVersion) { label += `@${packageVersion}`; } + if (!options.argv.json) { + process.stdout.write(`Installing ${label} `); + if (installGlobally) { + process.stdout.write(`to ${this.atomPackagesDirectory} `); + } + } + + return this.requestPackage(packageName, (error, pack) => { + if (error != null) { + this.logFailure(); + return callback(error); + } else { + if (packageVersion == null) { packageVersion = this.getLatestCompatibleVersion(pack); } + if (!packageVersion) { + this.logFailure(); + callback(`No available version compatible with the installed Atom version: ${this.installedAtomVersion}`); + return; + } + + const {tarball} = pack.versions[packageVersion]?.dist != null ? pack.versions[packageVersion]?.dist : {}; + if (!tarball) { + this.logFailure(); + callback(`Package version: ${packageVersion} not found`); + return; + } + + const commands = []; + commands.push(next => this.installModule(options, pack, tarball, next)); + if (installGlobally && (packageName.localeCompare(pack.name, 'en', {sensitivity: 'accent'}) !== 0)) { + commands.push((newPack, next) => { // package was renamed; delete old package folder + fs.removeSync(path.join(this.atomPackagesDirectory, packageName)); + return next(null, newPack); + }); + } + commands.push(function({installPath}, next) { + if (installPath != null) { + metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')); + const json = {installPath, metadata}; + return next(null, json); + } else { + return next(null, {}); + } + }); // installed locally, no install path data + + return async.waterfall(commands, (error, json) => { + if (!installGlobally) { + if (error != null) { + this.logFailure(); + } else { + if (!options.argv.json) { this.logSuccess(); } + } + } + return callback(error, json); + }); + } + }); + } + + // Install the package with the given name and local path + // + // packageName - The name of the package + // packagePath - The local path of the package in the form "file:./packages/package-name" + // options - The installation options object. + // callback - The function to invoke when installation completes with an + // error as the first argument. + installLocalPackage(packageName, packagePath, options, callback) { + if (!options.argv.json) { + process.stdout.write(`Installing ${packageName} from ${packagePath.slice('file:'.length)} `); + const commands = []; + commands.push(next => { + return this.installModule(options, {name: packageName}, packagePath, next); + }); + commands.push(function({installPath}, next) { + if (installPath != null) { + const metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')); + const json = {installPath, metadata}; + return next(null, json); + } else { + return next(null, {}); + } + }); // installed locally, no install path data + + return async.waterfall(commands, (error, json) => { + if (error != null) { + this.logFailure(); + } else { + if (!options.argv.json) { this.logSuccess(); } + } + return callback(error, json); + }); + } + } + + // Install all the package dependencies found in the package.json file. + // + // options - The installation options + // callback - The callback function to invoke when done with an error as the + // first argument. + installPackageDependencies(options, callback) { + options = _.extend({}, options, {installGlobally: false}); + const commands = []; + const object = this.getPackageDependencies(); + for (let name in object) { + const version = object[name]; + ((name, version) => { + return commands.push(next => { + if (this.repoLocalPackagePathRegex.test(version)) { + return this.installLocalPackage(name, version, options, next); + } else { + return this.installRegisteredPackage({name, version}, options, next); + } + }); + })(name, version); + } + + return async.series(commands, callback); + } + + installDependencies(options, callback) { + options.installGlobally = false; + const commands = []; + commands.push(callback => this.installModules(options, callback)); + commands.push(callback => this.installPackageDependencies(options, callback)); + + return async.waterfall(commands, callback); + } + + // Get all package dependency names and versions from the package.json file. + getPackageDependencies() { + try { + let left; + const metadata = fs.readFileSync('package.json', 'utf8'); + const {packageDependencies, dependencies} = (left = JSON.parse(metadata)) != null ? left : {}; + + if (!packageDependencies) { return {}; } + if (!dependencies) { return packageDependencies; } + + // This code filters out any `packageDependencies` that have an equivalent + // normalized repo-local package path entry in the `dependencies` section of + // `package.json`. Versioned `packageDependencies` are always returned. + const filteredPackages = {}; + for (let packageName in packageDependencies) { + const packageSpec = packageDependencies[packageName]; + const dependencyPath = this.getRepoLocalPackagePath(dependencies[packageName]); + const packageDependencyPath = this.getRepoLocalPackagePath(packageSpec); + if (!packageDependencyPath || (dependencyPath !== packageDependencyPath)) { + filteredPackages[packageName] = packageSpec; + } + } + + return filteredPackages; + } catch (error) { + return {}; + } + } + + getRepoLocalPackagePath(packageSpec) { + if (!packageSpec) { return undefined; } + const repoLocalPackageMatch = packageSpec.match(this.repoLocalPackagePathRegex); + if (repoLocalPackageMatch) { + return path.normalize(repoLocalPackageMatch[1]); + } else { + return undefined; + } + } + + createAtomDirectories() { + fs.makeTreeSync(this.atomDirectory); + fs.makeTreeSync(this.atomPackagesDirectory); + return fs.makeTreeSync(this.atomNodeDirectory); + } + + // Compile a sample native module to see if a useable native build toolchain + // is instlalled and successfully detected. This will include both Python + // and a compiler. + checkNativeBuildTools(callback) { + process.stdout.write('Checking for native build tools '); + + const buildArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'build']; + buildArgs.push(path.resolve(__dirname, '..', 'native-module')); + buildArgs.push(...this.getNpmBuildFlags()); + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + const buildOptions = {env}; + if (this.verbose) { buildOptions.streaming = true; } + + fs.removeSync(path.resolve(__dirname, '..', 'native-module', 'build')); + + return this.fork(this.atomNpmPath, buildArgs, buildOptions, (...args) => { + return this.logCommandResults(callback, ...args); + }); + } + + packageNamesFromPath(filePath) { + filePath = path.resolve(filePath); + + if (!fs.isFileSync(filePath)) { + throw new Error(`File '${filePath}' does not exist`); + } + + const packages = fs.readFileSync(filePath, 'utf8'); + return this.sanitizePackageNames(packages.split(/\s/)); + } + + buildModuleCache(packageName, callback) { + const packageDirectory = path.join(this.atomPackagesDirectory, packageName); + const rebuildCacheCommand = new RebuildModuleCache(); + return rebuildCacheCommand.rebuild(packageDirectory, () => // Ignore cache errors and just finish the install + callback()); + } + + warmCompileCache(packageName, callback) { + const packageDirectory = path.join(this.atomPackagesDirectory, packageName); + + return this.getResourcePath(resourcePath => { + try { + const CompileCache = require(path.join(resourcePath, 'src', 'compile-cache')); + + const onDirectory = directoryPath => path.basename(directoryPath) !== 'node_modules'; + + const onFile = filePath => { + try { + return CompileCache.addPathToCache(filePath, this.atomDirectory); + } catch (error) {} + }; + + fs.traverseTreeSync(packageDirectory, onFile, onDirectory); + } catch (error) {} + return callback(null); + }); + } + + isBundledPackage(packageName, callback) { + return this.getResourcePath(function(resourcePath) { + let atomMetadata; + try { + atomMetadata = JSON.parse(fs.readFileSync(path.join(resourcePath, 'package.json'))); + } catch (error) { + return callback(false); + } + + return callback(atomMetadata?.packageDependencies?.hasOwnProperty(packageName)); + }); + } + + getLatestCompatibleVersion(pack) { + if (!this.installedAtomVersion) { + if (isDeprecatedPackage(pack.name, pack.releases.latest)) { + return null; + } else { + return pack.releases.latest; + } + } + + let latestVersion = null; + const object = pack.versions != null ? pack.versions : {}; + for (let version in object) { + const metadata = object[version]; + if (!semver.valid(version)) { continue; } + if (!metadata) { continue; } + if (isDeprecatedPackage(pack.name, version)) { continue; } + + const engine = metadata.engines?.atom != null ? metadata.engines?.atom : '*'; + if (!semver.validRange(engine)) { continue; } + if (!semver.satisfies(this.installedAtomVersion, engine)) { continue; } + + if (latestVersion == null) { latestVersion = version; } + if (semver.gt(version, latestVersion)) { latestVersion = version; } + } + + return latestVersion; + } + + getHostedGitInfo(name) { + return hostedGitInfo.fromUrl(name); + } + + installGitPackage(packageUrl, options, callback) { + const tasks = []; + + const cloneDir = temp.mkdirSync("atom-git-package-clone-"); + + tasks.push((data, next) => { + const urls = this.getNormalizedGitUrls(packageUrl); + return this.cloneFirstValidGitUrl(urls, cloneDir, options, err => next(err, data)); + }); + + tasks.push((data, next) => { + return this.installGitPackageDependencies(cloneDir, options, err => next(err, data)); + }); + + tasks.push((data, next) => { + return this.getRepositoryHeadSha(cloneDir, function(err, sha) { + data.sha = sha; + return next(err, data); + }); + }); + + tasks.push(function(data, next) { + const metadataFilePath = CSON.resolve(path.join(cloneDir, 'package')); + return CSON.readFile(metadataFilePath, function(err, metadata) { + data.metadataFilePath = metadataFilePath; + data.metadata = metadata; + return next(err, data); + }); + }); + + tasks.push(function(data, next) { + data.metadata.apmInstallSource = { + type: "git", + source: packageUrl, + sha: data.sha + }; + return CSON.writeFile(data.metadataFilePath, data.metadata, err => next(err, data)); + }); + + tasks.push((data, next) => { + const {name} = data.metadata; + const targetDir = path.join(this.atomPackagesDirectory, name); + if (!options.argv.json) { process.stdout.write(`Moving ${name} to ${targetDir} `); } + return fs.cp(cloneDir, targetDir, err => { + if (err) { + return next(err); + } else { + if (!options.argv.json) { this.logSuccess(); } + const json = {installPath: targetDir, metadata: data.metadata}; + return next(null, json); + } + }); + }); + + const iteratee = (currentData, task, next) => task(currentData, next); + return async.reduce(tasks, {}, iteratee, callback); + } + + getNormalizedGitUrls(packageUrl) { + const packageInfo = this.getHostedGitInfo(packageUrl); + + if (packageUrl.indexOf('file://') === 0) { + return [packageUrl]; + } else if (packageInfo.default === 'sshurl') { + return [packageInfo.toString()]; + } else if (packageInfo.default === 'https') { + return [packageInfo.https().replace(/^git\+https:/, "https:")]; + } else if (packageInfo.default === 'shortcut') { + return [ + packageInfo.https().replace(/^git\+https:/, "https:"), + packageInfo.sshurl() + ]; + } + } + + cloneFirstValidGitUrl(urls, cloneDir, options, callback) { + return async.detectSeries(urls, (url, next) => { + return this.cloneNormalizedUrl(url, cloneDir, options, error => next(null, !error)); + } + , function(err, result) { + if (err || !result) { + const invalidUrls = `Couldn't clone ${urls.join(' or ')}`; + const invalidUrlsError = new Error(invalidUrls); + return callback(invalidUrlsError); + } else { + return callback(); + } + }); + } + + cloneNormalizedUrl(url, cloneDir, options, callback) { + // Require here to avoid circular dependency + const Develop = require('./develop'); + const develop = new Develop(); + + return develop.cloneRepository(url, cloneDir, options, err => callback(err)); + } + + installGitPackageDependencies(directory, options, callback) { + options.cwd = directory; + return this.installDependencies(options, callback); + } + + getRepositoryHeadSha(repoDir, callback) { + try { + const repo = Git.open(repoDir); + const sha = repo.getReferenceTarget("HEAD"); + return callback(null, sha); + } catch (err) { + return callback(err); + } + } + + run(options) { + let packageNames; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const packagesFilePath = options.argv['packages-file']; + + this.createAtomDirectories(); + + if (options.argv.check) { + config.loadNpm((error, npm) => { + this.npm = npm; + return this.loadInstalledAtomMetadata(() => { + return this.checkNativeBuildTools(callback); + }); + }); + return; + } + + this.verbose = options.argv.verbose; + if (this.verbose) { + request.debug(true); + process.env.NODE_DEBUG = 'request'; + } + + const installPackage = (name, nextInstallStep) => { + const gitPackageInfo = this.getHostedGitInfo(name); + + if (gitPackageInfo || (name.indexOf('file://') === 0)) { + return this.installGitPackage(name, options, nextInstallStep); + } else if (name === '.') { + return this.installDependencies(options, nextInstallStep); + } else { // is registered package + let version; + const atIndex = name.indexOf('@'); + if (atIndex > 0) { + version = name.substring(atIndex + 1); + name = name.substring(0, atIndex); + } + + return this.isBundledPackage(name, isBundledPackage => { + if (isBundledPackage) { + console.error(`\ +The ${name} package is bundled with Atom and should not be explicitly installed. +You can run \`apm uninstall ${name}\` to uninstall it and then the version bundled +with Atom will be used.\ +`.yellow + ); + } + return this.installRegisteredPackage({name, version}, options, nextInstallStep); + }); + } + }; + + if (packagesFilePath) { + try { + packageNames = this.packageNamesFromPath(packagesFilePath); + } catch (error1) { + const error = error1; + return callback(error); + } + } else { + packageNames = this.packageNamesFromArgv(options.argv); + if (packageNames.length === 0) { packageNames.push('.'); } + } + + const commands = []; + commands.push(callback => { return config.loadNpm((error, npm) => { this.npm = npm; return callback(error); }); }); + commands.push(callback => this.loadInstalledAtomMetadata(() => callback())); + packageNames.forEach(packageName => commands.push(callback => installPackage(packageName, callback))); + const iteratee = (item, next) => item(next); + return async.mapSeries(commands, iteratee, function(err, installedPackagesInfo) { + if (err) { return callback(err); } + installedPackagesInfo = _.compact(installedPackagesInfo); + installedPackagesInfo = installedPackagesInfo.filter((item, idx) => packageNames[idx] !== "."); + if (options.argv.json) { console.log(JSON.stringify(installedPackagesInfo, null, " ")); } + return callback(null); + }); + } +}; diff --git a/src/link.coffee b/src/link.coffee deleted file mode 100644 index 1ce7698..0000000 --- a/src/link.coffee +++ /dev/null @@ -1,54 +0,0 @@ -path = require 'path' - -CSON = require 'season' -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -fs = require './fs' - -module.exports = -class Link extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm link [] [--name ] - - Create a symlink for the package in ~/.atom/packages. The package in the - current working directory is linked if no path is given. - - Run `apm links` to view all the currently linked packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('d', 'dev').boolean('dev').describe('dev', 'Link to ~/.atom/dev/packages') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - packagePath = options.argv._[0]?.toString() ? '.' - linkPath = path.resolve(process.cwd(), packagePath) - - packageName = options.argv.name - try - packageName = CSON.readFileSync(CSON.resolve(path.join(linkPath, 'package'))).name unless packageName - packageName = path.basename(linkPath) unless packageName - - if options.argv.dev - targetPath = path.join(config.getAtomDirectory(), 'dev', 'packages', packageName) - else - targetPath = path.join(config.getAtomDirectory(), 'packages', packageName) - - unless fs.existsSync(linkPath) - callback("Package directory does not exist: #{linkPath}") - return - - try - fs.unlinkSync(targetPath) if fs.isSymbolicLinkSync(targetPath) - fs.makeTreeSync path.dirname(targetPath) - fs.symlinkSync(linkPath, targetPath, 'junction') - console.log "#{targetPath} -> #{linkPath}" - callback() - catch error - callback("Linking #{targetPath} to #{linkPath} failed: #{error.message}") diff --git a/src/link.js b/src/link.js new file mode 100644 index 0000000..9567033 --- /dev/null +++ b/src/link.js @@ -0,0 +1,68 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Link; +import path from 'path'; +import CSON from 'season'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; + +export default Link = class Link extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm link [] [--name ] + +Create a symlink for the package in ~/.atom/packages. The package in the +current working directory is linked if no path is given. + +Run \`apm links\` to view all the currently linked packages.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.alias('d', 'dev').boolean('dev').describe('dev', 'Link to ~/.atom/dev/packages'); + } + + run(options) { + let left, targetPath; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + const packagePath = (left = options.argv._[0]?.toString()) != null ? left : '.'; + const linkPath = path.resolve(process.cwd(), packagePath); + + let packageName = options.argv.name; + try { + if (!packageName) { packageName = CSON.readFileSync(CSON.resolve(path.join(linkPath, 'package'))).name; } + } catch (error1) {} + if (!packageName) { packageName = path.basename(linkPath); } + + if (options.argv.dev) { + targetPath = path.join(config.getAtomDirectory(), 'dev', 'packages', packageName); + } else { + targetPath = path.join(config.getAtomDirectory(), 'packages', packageName); + } + + if (!fs.existsSync(linkPath)) { + callback(`Package directory does not exist: ${linkPath}`); + return; + } + + try { + if (fs.isSymbolicLinkSync(targetPath)) { fs.unlinkSync(targetPath); } + fs.makeTreeSync(path.dirname(targetPath)); + fs.symlinkSync(linkPath, targetPath, 'junction'); + console.log(`${targetPath} -> ${linkPath}`); + return callback(); + } catch (error) { + return callback(`Linking ${targetPath} to ${linkPath} failed: ${error.message}`); + } + } +}; diff --git a/src/links.coffee b/src/links.coffee deleted file mode 100644 index 0f330ea..0000000 --- a/src/links.coffee +++ /dev/null @@ -1,54 +0,0 @@ -path = require 'path' - -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -fs = require './fs' -tree = require './tree' - -module.exports = -class Links extends Command - constructor: -> - super() - @devPackagesPath = path.join(config.getAtomDirectory(), 'dev', 'packages') - @packagesPath = path.join(config.getAtomDirectory(), 'packages') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm links - - List all of the symlinked atom packages in ~/.atom/packages and - ~/.atom/dev/packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - getDevPackagePath: (packageName) -> path.join(@devPackagesPath, packageName) - - getPackagePath: (packageName) -> path.join(@packagesPath, packageName) - - getSymlinks: (directoryPath) -> - symlinks = [] - for directory in fs.list(directoryPath) - symlinkPath = path.join(directoryPath, directory) - symlinks.push(symlinkPath) if fs.isSymbolicLinkSync(symlinkPath) - symlinks - - logLinks: (directoryPath) -> - links = @getSymlinks(directoryPath) - console.log "#{directoryPath.cyan} (#{links.length})" - tree links, emptyMessage: '(no links)', (link) -> - try - realpath = fs.realpathSync(link) - catch error - realpath = '???'.red - "#{path.basename(link).yellow} -> #{realpath}" - - run: (options) -> - {callback} = options - - @logLinks(@devPackagesPath) - @logLinks(@packagesPath) - callback() diff --git a/src/links.js b/src/links.js new file mode 100644 index 0000000..4e91af3 --- /dev/null +++ b/src/links.js @@ -0,0 +1,68 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Links; +import path from 'path'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; +import tree from './tree'; + +export default Links = class Links extends Command { + constructor() { + super(); + this.devPackagesPath = path.join(config.getAtomDirectory(), 'dev', 'packages'); + this.packagesPath = path.join(config.getAtomDirectory(), 'packages'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm links + +List all of the symlinked atom packages in ~/.atom/packages and +~/.atom/dev/packages.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + getDevPackagePath(packageName) { return path.join(this.devPackagesPath, packageName); } + + getPackagePath(packageName) { return path.join(this.packagesPath, packageName); } + + getSymlinks(directoryPath) { + const symlinks = []; + for (let directory of fs.list(directoryPath)) { + const symlinkPath = path.join(directoryPath, directory); + if (fs.isSymbolicLinkSync(symlinkPath)) { symlinks.push(symlinkPath); } + } + return symlinks; + } + + logLinks(directoryPath) { + const links = this.getSymlinks(directoryPath); + console.log(`${directoryPath.cyan} (${links.length})`); + return tree(links, {emptyMessage: '(no links)'}, function(link) { + let realpath; + try { + realpath = fs.realpathSync(link); + } catch (error) { + realpath = '???'.red; + } + return `${path.basename(link).yellow} -> ${realpath}`; + }); + } + + run(options) { + const {callback} = options; + + this.logLinks(this.devPackagesPath); + this.logLinks(this.packagesPath); + return callback(); + } +}; diff --git a/src/list.coffee b/src/list.coffee deleted file mode 100644 index 7c585bc..0000000 --- a/src/list.coffee +++ /dev/null @@ -1,192 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -CSON = require 'season' -yargs = require 'yargs' - -Command = require './command' -fs = require './fs' -config = require './apm' -tree = require './tree' -{getRepository} = require "./packages" - -module.exports = -class List extends Command - constructor: -> - super() - @userPackagesDirectory = path.join(config.getAtomDirectory(), 'packages') - @devPackagesDirectory = path.join(config.getAtomDirectory(), 'dev', 'packages') - if configPath = CSON.resolve(path.join(config.getAtomDirectory(), 'config')) - try - @disabledPackages = CSON.readFileSync(configPath)?['*']?.core?.disabledPackages - @disabledPackages ?= [] - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm list - apm list --themes - apm list --packages - apm list --installed - apm list --installed --enabled - apm list --installed --bare > my-packages.txt - apm list --json - - List all the installed packages and also the packages bundled with Atom. - """ - options.alias('b', 'bare').boolean('bare').describe('bare', 'Print packages one per line with no formatting') - options.alias('e', 'enabled').boolean('enabled').describe('enabled', 'Print only enabled packages') - options.alias('d', 'dev').boolean('dev').default('dev', true).describe('dev', 'Include dev packages') - options.boolean('disabled').describe('disabled', 'Print only disabled packages') - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('i', 'installed').boolean('installed').describe('installed', 'Only list installed packages/themes') - options.alias('j', 'json').boolean('json').describe('json', 'Output all packages as a JSON object') - options.alias('l', 'links').boolean('links').default('links', true).describe('links', 'Include linked packages') - options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes') - options.alias('p', 'packages').boolean('packages').describe('packages', 'Only list packages') - options.alias('v', 'versions').boolean('versions').default('versions', true).describe('versions', 'Include version of each package') - - isPackageDisabled: (name) -> - @disabledPackages.indexOf(name) isnt -1 - - logPackages: (packages, options) -> - if options.argv.bare - for pack in packages - packageLine = pack.name - packageLine += "@#{pack.version}" if pack.version? and options.argv.versions - console.log packageLine - else - tree packages, (pack) => - packageLine = pack.name - packageLine += "@#{pack.version}" if pack.version? and options.argv.versions - if pack.apmInstallSource?.type is 'git' - repo = getRepository(pack) - shaLine = "##{pack.apmInstallSource.sha.substr(0, 8)}" - shaLine = repo + shaLine if repo? - packageLine += " (#{shaLine})".grey - packageLine += ' (disabled)' if @isPackageDisabled(pack.name) and not options.argv.disabled - packageLine - console.log() - - checkExclusiveOptions: (options, positive_option, negative_option, value) -> - if options.argv[positive_option] - value - else if options.argv[negative_option] - not value - else - true - - isPackageVisible: (options, manifest) -> - @checkExclusiveOptions(options, 'themes', 'packages', manifest.theme) and - @checkExclusiveOptions(options, 'disabled', 'enabled', @isPackageDisabled(manifest.name)) - - listPackages: (directoryPath, options) -> - packages = [] - for child in fs.list(directoryPath) - continue unless fs.isDirectorySync(path.join(directoryPath, child)) - continue if child.match /^\./ - unless options.argv.links - continue if fs.isSymbolicLinkSync(path.join(directoryPath, child)) - - manifest = null - if manifestPath = CSON.resolve(path.join(directoryPath, child, 'package')) - try - manifest = CSON.readFileSync(manifestPath) - manifest ?= {} - manifest.name = child - - continue unless @isPackageVisible(options, manifest) - packages.push(manifest) - - packages - - listUserPackages: (options, callback) -> - userPackages = @listPackages(@userPackagesDirectory, options) - .filter (pack) -> not pack.apmInstallSource - unless options.argv.bare or options.argv.json - console.log "Community Packages (#{userPackages.length})".cyan, "#{@userPackagesDirectory}" - callback?(null, userPackages) - - listDevPackages: (options, callback) -> - return callback?(null, []) unless options.argv.dev - - devPackages = @listPackages(@devPackagesDirectory, options) - if devPackages.length > 0 - unless options.argv.bare or options.argv.json - console.log "Dev Packages (#{devPackages.length})".cyan, "#{@devPackagesDirectory}" - callback?(null, devPackages) - - listGitPackages: (options, callback) -> - gitPackages = @listPackages(@userPackagesDirectory, options) - .filter (pack) -> pack.apmInstallSource?.type is 'git' - if gitPackages.length > 0 - unless options.argv.bare or options.argv.json - console.log "Git Packages (#{gitPackages.length})".cyan, "#{@userPackagesDirectory}" - callback?(null, gitPackages) - - listBundledPackages: (options, callback) -> - config.getResourcePath (resourcePath) => - try - metadataPath = path.join(resourcePath, 'package.json') - {_atomPackages} = JSON.parse(fs.readFileSync(metadataPath)) - _atomPackages ?= {} - packages = (metadata for packageName, {metadata} of _atomPackages) - - packages = packages.filter (metadata) => - @isPackageVisible(options, metadata) - - unless options.argv.bare or options.argv.json - if options.argv.themes - console.log "#{'Built-in Atom Themes'.cyan} (#{packages.length})" - else - console.log "#{'Built-in Atom Packages'.cyan} (#{packages.length})" - - callback?(null, packages) - - listInstalledPackages: (options) -> - @listDevPackages options, (error, packages) => - @logPackages(packages, options) if packages.length > 0 - - @listUserPackages options, (error, packages) => - @logPackages(packages, options) - - @listGitPackages options, (error, packages) => - @logPackages(packages, options) if packages.length > 0 - - listPackagesAsJson: (options, callback = ->) -> - output = - core: [] - dev: [] - git: [] - user: [] - - @listBundledPackages options, (error, packages) => - return callback(error) if error - output.core = packages - @listDevPackages options, (error, packages) => - return callback(error) if error - output.dev = packages - @listUserPackages options, (error, packages) => - return callback(error) if error - output.user = packages - @listGitPackages options, (error, packages) -> - return callback(error) if error - output.git = packages - console.log JSON.stringify(output) - callback() - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - if options.argv.json - @listPackagesAsJson(options, callback) - else if options.argv.installed - @listInstalledPackages(options) - callback() - else - @listBundledPackages options, (error, packages) => - @logPackages(packages, options) - @listInstalledPackages(options) - callback() diff --git a/src/list.js b/src/list.js new file mode 100644 index 0000000..6548cd1 --- /dev/null +++ b/src/list.js @@ -0,0 +1,259 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let List; +import path from 'path'; +import _ from 'underscore-plus'; +import CSON from 'season'; +import yargs from 'yargs'; +import Command from './command'; +import fs from './fs'; +import config from './apm'; +import tree from './tree'; +import { getRepository } from "./packages"; + +export default List = class List extends Command { + constructor() { + let configPath; + super(); + this.userPackagesDirectory = path.join(config.getAtomDirectory(), 'packages'); + this.devPackagesDirectory = path.join(config.getAtomDirectory(), 'dev', 'packages'); + if (configPath = CSON.resolve(path.join(config.getAtomDirectory(), 'config'))) { + try { + this.disabledPackages = CSON.readFileSync(configPath)?.['*']?.core?.disabledPackages; + } catch (error) {} + } + if (this.disabledPackages == null) { this.disabledPackages = []; } + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm list + apm list --themes + apm list --packages + apm list --installed + apm list --installed --enabled + apm list --installed --bare > my-packages.txt + apm list --json + +List all the installed packages and also the packages bundled with Atom.\ +` + ); + options.alias('b', 'bare').boolean('bare').describe('bare', 'Print packages one per line with no formatting'); + options.alias('e', 'enabled').boolean('enabled').describe('enabled', 'Print only enabled packages'); + options.alias('d', 'dev').boolean('dev').default('dev', true).describe('dev', 'Include dev packages'); + options.boolean('disabled').describe('disabled', 'Print only disabled packages'); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('i', 'installed').boolean('installed').describe('installed', 'Only list installed packages/themes'); + options.alias('j', 'json').boolean('json').describe('json', 'Output all packages as a JSON object'); + options.alias('l', 'links').boolean('links').default('links', true).describe('links', 'Include linked packages'); + options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes'); + options.alias('p', 'packages').boolean('packages').describe('packages', 'Only list packages'); + return options.alias('v', 'versions').boolean('versions').default('versions', true).describe('versions', 'Include version of each package'); + } + + isPackageDisabled(name) { + return this.disabledPackages.indexOf(name) !== -1; + } + + logPackages(packages, options) { + if (options.argv.bare) { + return (() => { + const result = []; + for (let pack of packages) { + let packageLine = pack.name; + if ((pack.version != null) && options.argv.versions) { packageLine += `@${pack.version}`; } + result.push(console.log(packageLine)); + } + return result; + })(); + } else { + tree(packages, pack => { + let packageLine = pack.name; + if ((pack.version != null) && options.argv.versions) { packageLine += `@${pack.version}`; } + if (pack.apmInstallSource?.type === 'git') { + const repo = getRepository(pack); + let shaLine = `#${pack.apmInstallSource.sha.substr(0, 8)}`; + if (repo != null) { shaLine = repo + shaLine; } + packageLine += ` (${shaLine})`.grey; + } + if (this.isPackageDisabled(pack.name) && !options.argv.disabled) { packageLine += ' (disabled)'; } + return packageLine; + }); + return console.log(); + } + } + + checkExclusiveOptions(options, positive_option, negative_option, value) { + if (options.argv[positive_option]) { + return value; + } else if (options.argv[negative_option]) { + return !value; + } else { + return true; + } + } + + isPackageVisible(options, manifest) { + return this.checkExclusiveOptions(options, 'themes', 'packages', manifest.theme) && + this.checkExclusiveOptions(options, 'disabled', 'enabled', this.isPackageDisabled(manifest.name)); + } + + listPackages(directoryPath, options) { + const packages = []; + for (let child of fs.list(directoryPath)) { + var manifestPath; + if (!fs.isDirectorySync(path.join(directoryPath, child))) { continue; } + if (child.match(/^\./)) { continue; } + if (!options.argv.links) { + if (fs.isSymbolicLinkSync(path.join(directoryPath, child))) { continue; } + } + + let manifest = null; + if (manifestPath = CSON.resolve(path.join(directoryPath, child, 'package'))) { + try { + manifest = CSON.readFileSync(manifestPath); + } catch (error) {} + } + if (manifest == null) { manifest = {}; } + manifest.name = child; + + if (!this.isPackageVisible(options, manifest)) { continue; } + packages.push(manifest); + } + + return packages; + } + + listUserPackages(options, callback) { + const userPackages = this.listPackages(this.userPackagesDirectory, options) + .filter(pack => !pack.apmInstallSource); + if (!options.argv.bare && !options.argv.json) { + console.log(`Community Packages (${userPackages.length})`.cyan, `${this.userPackagesDirectory}`); + } + return callback?.(null, userPackages); + } + + listDevPackages(options, callback) { + if (!options.argv.dev) { return callback?.(null, []); } + + const devPackages = this.listPackages(this.devPackagesDirectory, options); + if (devPackages.length > 0) { + if (!options.argv.bare && !options.argv.json) { + console.log(`Dev Packages (${devPackages.length})`.cyan, `${this.devPackagesDirectory}`); + } + } + return callback?.(null, devPackages); + } + + listGitPackages(options, callback) { + const gitPackages = this.listPackages(this.userPackagesDirectory, options) + .filter(pack => pack.apmInstallSource?.type === 'git'); + if (gitPackages.length > 0) { + if (!options.argv.bare && !options.argv.json) { + console.log(`Git Packages (${gitPackages.length})`.cyan, `${this.userPackagesDirectory}`); + } + } + return callback?.(null, gitPackages); + } + + listBundledPackages(options, callback) { + return config.getResourcePath(resourcePath => { + let _atomPackages; + let metadata; + try { + const metadataPath = path.join(resourcePath, 'package.json'); + ({_atomPackages} = JSON.parse(fs.readFileSync(metadataPath))); + } catch (error) {} + if (_atomPackages == null) { _atomPackages = {}; } + let packages = ((() => { + const result = []; + for (let packageName in _atomPackages) { + ({metadata} = _atomPackages[packageName]); + result.push(metadata); + } + return result; + })()); + + packages = packages.filter(metadata => { + return this.isPackageVisible(options, metadata); + }); + + if (!options.argv.bare && !options.argv.json) { + if (options.argv.themes) { + console.log(`${'Built-in Atom Themes'.cyan} (${packages.length})`); + } else { + console.log(`${'Built-in Atom Packages'.cyan} (${packages.length})`); + } + } + + return callback?.(null, packages); + }); + } + + listInstalledPackages(options) { + return this.listDevPackages(options, (error, packages) => { + if (packages.length > 0) { this.logPackages(packages, options); } + + return this.listUserPackages(options, (error, packages) => { + this.logPackages(packages, options); + + return this.listGitPackages(options, (error, packages) => { + if (packages.length > 0) { return this.logPackages(packages, options); } + }); + }); + }); + } + + listPackagesAsJson(options, callback = function() {}) { + const output = { + core: [], + dev: [], + git: [], + user: [] + }; + + return this.listBundledPackages(options, (error, packages) => { + if (error) { return callback(error); } + output.core = packages; + return this.listDevPackages(options, (error, packages) => { + if (error) { return callback(error); } + output.dev = packages; + return this.listUserPackages(options, (error, packages) => { + if (error) { return callback(error); } + output.user = packages; + return this.listGitPackages(options, function(error, packages) { + if (error) { return callback(error); } + output.git = packages; + console.log(JSON.stringify(output)); + return callback(); + }); + }); + }); + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + if (options.argv.json) { + return this.listPackagesAsJson(options, callback); + } else if (options.argv.installed) { + this.listInstalledPackages(options); + return callback(); + } else { + return this.listBundledPackages(options, (error, packages) => { + this.logPackages(packages, options); + this.listInstalledPackages(options); + return callback(); + }); + } + } +}; diff --git a/src/login.coffee b/src/login.coffee deleted file mode 100644 index 2b53510..0000000 --- a/src/login.coffee +++ /dev/null @@ -1,81 +0,0 @@ -_ = require 'underscore-plus' -yargs = require 'yargs' -Q = require 'q' -read = require 'read' -open = require 'open' - -auth = require './auth' -Command = require './command' - -module.exports = -class Login extends Command - @getTokenOrLogin: (callback) -> - auth.getToken (error, token) -> - if error? - new Login().run({callback, commandArgs: []}) - else - callback(null, token) - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: apm login - - Enter your Atom.io API token and save it to the keychain. This token will - be used to identify you when publishing packages to atom.io. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.string('token').describe('token', 'atom.io API token') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - Q(token: options.argv.token) - .then(@welcomeMessage) - .then(@openURL) - .then(@getToken) - .then(@saveToken) - .then (token) -> callback(null, token) - .catch(callback) - - prompt: (options) -> - readPromise = Q.denodeify(read) - readPromise(options) - - welcomeMessage: (state) => - return Q(state) if state.token - - welcome = """ - Welcome to Atom! - - Before you can publish packages, you'll need an API token. - - Visit your account page on Atom.io #{'https://atom.io/account'.underline}, - copy the token and paste it below when prompted. - - """ - console.log welcome - - @prompt({prompt: "Press [Enter] to open your account page on Atom.io."}) - - openURL: (state) -> - return Q(state) if state.token - - open('https://atom.io/account') - - getToken: (state) => - return Q(state) if state.token - - @prompt({prompt: 'Token>', edit: true}) - .spread (token) -> - state.token = token - Q(state) - - saveToken: ({token}) => - throw new Error("Token is required") unless token - - process.stdout.write('Saving token to Keychain ') - auth.saveToken(token) - @logSuccess() - Q(token) diff --git a/src/login.js b/src/login.js new file mode 100644 index 0000000..429c324 --- /dev/null +++ b/src/login.js @@ -0,0 +1,106 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Login; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import Q from 'q'; +import read from 'read'; +import open from 'open'; +import auth from './auth'; +import Command from './command'; + +export default Login = class Login extends Command { + constructor(...args) { + super(...args); + this.welcomeMessage = this.welcomeMessage.bind(this); + this.getToken = this.getToken.bind(this); + this.saveToken = this.saveToken.bind(this); + } + + static getTokenOrLogin(callback) { + return auth.getToken(function(error, token) { + if (error != null) { + return new Login().run({callback, commandArgs: []}); + } else { + return callback(null, token); + } + }); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: apm login + +Enter your Atom.io API token and save it to the keychain. This token will +be used to identify you when publishing packages to atom.io.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.string('token').describe('token', 'atom.io API token'); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + return Q({token: options.argv.token}) + .then(this.welcomeMessage) + .then(this.openURL) + .then(this.getToken) + .then(this.saveToken) + .then(token => callback(null, token)) + .catch(callback); + } + + prompt(options) { + const readPromise = Q.denodeify(read); + return readPromise(options); + } + + welcomeMessage(state) { + if (state.token) { return Q(state); } + + const welcome = `\ +Welcome to Atom! + +Before you can publish packages, you'll need an API token. + +Visit your account page on Atom.io ${'https://atom.io/account'.underline}, +copy the token and paste it below when prompted. +\ +`; + console.log(welcome); + + return this.prompt({prompt: "Press [Enter] to open your account page on Atom.io."}); + } + + openURL(state) { + if (state.token) { return Q(state); } + + return open('https://atom.io/account'); + } + + getToken(state) { + if (state.token) { return Q(state); } + + return this.prompt({prompt: 'Token>', edit: true}) + .spread(function(token) { + state.token = token; + return Q(state); + }); + } + + saveToken({token}) { + if (!token) { throw new Error("Token is required"); } + + process.stdout.write('Saving token to Keychain '); + auth.saveToken(token); + this.logSuccess(); + return Q(token); + } +}; diff --git a/src/package-converter.coffee b/src/package-converter.coffee deleted file mode 100644 index 1eeec5e..0000000 --- a/src/package-converter.coffee +++ /dev/null @@ -1,217 +0,0 @@ -path = require 'path' -url = require 'url' -zlib = require 'zlib' - -_ = require 'underscore-plus' -CSON = require 'season' -plist = require '@atom/plist' -{ScopeSelector} = require 'first-mate' -tar = require 'tar' -temp = require 'temp' - -fs = require './fs' -request = require './request' - -# Convert a TextMate bundle to an Atom package -module.exports = -class PackageConverter - constructor: (@sourcePath, destinationPath) -> - @destinationPath = path.resolve(destinationPath) - - @plistExtensions = [ - '.plist' - '.tmCommand' - '.tmLanguage' - '.tmMacro' - '.tmPreferences' - '.tmSnippet' - ] - - @directoryMappings = { - 'Preferences': 'settings' - 'Snippets': 'snippets' - 'Syntaxes': 'grammars' - } - - convert: (callback) -> - {protocol} = url.parse(@sourcePath) - if protocol is 'http:' or protocol is 'https:' - @downloadBundle(callback) - else - @copyDirectories(@sourcePath, callback) - - getDownloadUrl: -> - downloadUrl = @sourcePath - downloadUrl = downloadUrl.replace(/(\.git)?\/*$/, '') - downloadUrl += '/archive/master.tar.gz' - - downloadBundle: (callback) -> - tempPath = temp.mkdirSync('atom-bundle-') - requestOptions = url: @getDownloadUrl() - request.createReadStream requestOptions, (readStream) => - readStream.on 'response', ({headers, statusCode}) -> - if statusCode isnt 200 - callback("Download failed (#{headers.status})") - - readStream.pipe(zlib.createGunzip()).pipe(tar.extract(cwd: tempPath)) - .on 'error', (error) -> callback(error) - .on 'end', => - sourcePath = path.join(tempPath, fs.readdirSync(tempPath)[0]) - @copyDirectories(sourcePath, callback) - - copyDirectories: (sourcePath, callback) -> - sourcePath = path.resolve(sourcePath) - try - packageName = JSON.parse(fs.readFileSync(path.join(sourcePath, 'package.json')))?.packageName - packageName ?= path.basename(@destinationPath) - - @convertSnippets(packageName, sourcePath) - @convertPreferences(packageName, sourcePath) - @convertGrammars(sourcePath) - callback() - - filterObject: (object) -> - delete object.uuid - delete object.keyEquivalent - - convertSettings: (settings) -> - if settings.shellVariables - shellVariables = {} - for {name, value} in settings.shellVariables - shellVariables[name] = value - settings.shellVariables = shellVariables - - editorProperties = _.compactObject( - commentStart: _.valueForKeyPath(settings, 'shellVariables.TM_COMMENT_START') - commentEnd: _.valueForKeyPath(settings, 'shellVariables.TM_COMMENT_END') - increaseIndentPattern: settings.increaseIndentPattern - decreaseIndentPattern: settings.decreaseIndentPattern - foldEndPattern: settings.foldingStopMarker - completions: settings.completions - ) - {editor: editorProperties} unless _.isEmpty(editorProperties) - - readFileSync: (filePath) -> - if _.contains(@plistExtensions, path.extname(filePath)) - plist.parseFileSync(filePath) - else if _.contains(['.json', '.cson'], path.extname(filePath)) - CSON.readFileSync(filePath) - - writeFileSync: (filePath, object={}) -> - @filterObject(object) - if Object.keys(object).length > 0 - CSON.writeFileSync(filePath, object) - - convertFile: (sourcePath, destinationDir) -> - extension = path.extname(sourcePath) - destinationName = "#{path.basename(sourcePath, extension)}.cson" - destinationName = destinationName.toLowerCase() - destinationPath = path.join(destinationDir, destinationName) - - if _.contains(@plistExtensions, path.extname(sourcePath)) - contents = plist.parseFileSync(sourcePath) - else if _.contains(['.json', '.cson'], path.extname(sourcePath)) - contents = CSON.readFileSync(sourcePath) - - @writeFileSync(destinationPath, contents) - - normalizeFilenames: (directoryPath) -> - return unless fs.isDirectorySync(directoryPath) - - for child in fs.readdirSync(directoryPath) - childPath = path.join(directoryPath, child) - - # Invalid characters taken from http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx - convertedFileName = child.replace(/[|?*<>:"\\\/]+/g, '-') - continue if child is convertedFileName - - convertedFileName = convertedFileName.replace(/[\s-]+/g, '-') - convertedPath = path.join(directoryPath, convertedFileName) - suffix = 1 - while fs.existsSync(convertedPath) or fs.existsSync(convertedPath.toLowerCase()) - extension = path.extname(convertedFileName) - convertedFileName = "#{path.basename(convertedFileName, extension)}-#{suffix}#{extension}" - convertedPath = path.join(directoryPath, convertedFileName) - suffix++ - fs.renameSync(childPath, convertedPath) - - convertSnippets: (packageName, source) -> - sourceSnippets = path.join(source, 'snippets') - unless fs.isDirectorySync(sourceSnippets) - sourceSnippets = path.join(source, 'Snippets') - return unless fs.isDirectorySync(sourceSnippets) - - snippetsBySelector = {} - destination = path.join(@destinationPath, 'snippets') - for child in fs.readdirSync(sourceSnippets) - snippet = @readFileSync(path.join(sourceSnippets, child)) ? {} - {scope, name, content, tabTrigger} = snippet - continue unless tabTrigger and content - - # Replace things like '${TM_C_POINTER: *}' with ' *' - content = content.replace(/\$\{TM_[A-Z_]+:([^}]+)}/g, '$1') - - # Replace things like '${1:${TM_FILENAME/(\\w+)*/(?1:$1:NSObject)/}}' - # with '$1' - content = content.replace(/\$\{(\d)+:\s*\$\{TM_[^}]+\s*\}\s*\}/g, '$$1') - - # Unescape escaped dollar signs $ - content = content.replace(/\\\$/g, '$') - - unless name? - extension = path.extname(child) - name = path.basename(child, extension) - - try - selector = new ScopeSelector(scope).toCssSelector() if scope - catch e - e.message = "In file #{e.fileName} at #{JSON.stringify(scope)}: #{e.message}" - throw e - selector ?= '*' - - snippetsBySelector[selector] ?= {} - snippetsBySelector[selector][name] = {prefix: tabTrigger, body: content} - - @writeFileSync(path.join(destination, "#{packageName}.cson"), snippetsBySelector) - @normalizeFilenames(destination) - - convertPreferences: (packageName, source) -> - sourcePreferences = path.join(source, 'preferences') - unless fs.isDirectorySync(sourcePreferences) - sourcePreferences = path.join(source, 'Preferences') - return unless fs.isDirectorySync(sourcePreferences) - - preferencesBySelector = {} - destination = path.join(@destinationPath, 'settings') - for child in fs.readdirSync(sourcePreferences) - {scope, settings} = @readFileSync(path.join(sourcePreferences, child)) ? {} - continue unless scope and settings - - if properties = @convertSettings(settings) - try - selector = new ScopeSelector(scope).toCssSelector() - catch e - e.message = "In file #{e.fileName} at #{JSON.stringify(scope)}: #{e.message}" - throw e - for key, value of properties - preferencesBySelector[selector] ?= {} - if preferencesBySelector[selector][key]? - preferencesBySelector[selector][key] = _.extend(value, preferencesBySelector[selector][key]) - else - preferencesBySelector[selector][key] = value - - @writeFileSync(path.join(destination, "#{packageName}.cson"), preferencesBySelector) - @normalizeFilenames(destination) - - convertGrammars: (source) -> - sourceSyntaxes = path.join(source, 'syntaxes') - unless fs.isDirectorySync(sourceSyntaxes) - sourceSyntaxes = path.join(source, 'Syntaxes') - return unless fs.isDirectorySync(sourceSyntaxes) - - destination = path.join(@destinationPath, 'grammars') - for child in fs.readdirSync(sourceSyntaxes) - childPath = path.join(sourceSyntaxes, child) - @convertFile(childPath, destination) if fs.isFileSync(childPath) - - @normalizeFilenames(destination) diff --git a/src/package-converter.js b/src/package-converter.js new file mode 100644 index 0000000..a557ef0 --- /dev/null +++ b/src/package-converter.js @@ -0,0 +1,274 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let PackageConverter; +import path from 'path'; +import url from 'url'; +import zlib from 'zlib'; +import _ from 'underscore-plus'; +import CSON from 'season'; +import plist from '@atom/plist'; +import { ScopeSelector } from 'first-mate'; +import tar from 'tar'; +import temp from 'temp'; +import fs from './fs'; +import request from './request'; + +// Convert a TextMate bundle to an Atom package +export default PackageConverter = class PackageConverter { + constructor(sourcePath, destinationPath) { + this.sourcePath = sourcePath; + this.destinationPath = path.resolve(destinationPath); + + this.plistExtensions = [ + '.plist', + '.tmCommand', + '.tmLanguage', + '.tmMacro', + '.tmPreferences', + '.tmSnippet' + ]; + + this.directoryMappings = { + 'Preferences': 'settings', + 'Snippets': 'snippets', + 'Syntaxes': 'grammars' + }; + } + + convert(callback) { + const {protocol} = url.parse(this.sourcePath); + if ((protocol === 'http:') || (protocol === 'https:')) { + return this.downloadBundle(callback); + } else { + return this.copyDirectories(this.sourcePath, callback); + } + } + + getDownloadUrl() { + let downloadUrl = this.sourcePath; + downloadUrl = downloadUrl.replace(/(\.git)?\/*$/, ''); + return downloadUrl += '/archive/master.tar.gz'; + } + + downloadBundle(callback) { + const tempPath = temp.mkdirSync('atom-bundle-'); + const requestOptions = {url: this.getDownloadUrl()}; + return request.createReadStream(requestOptions, readStream => { + readStream.on('response', function({headers, statusCode}) { + if (statusCode !== 200) { + return callback(`Download failed (${headers.status})`); + } + }); + + return readStream.pipe(zlib.createGunzip()).pipe(tar.extract({cwd: tempPath})) + .on('error', error => callback(error)) + .on('end', () => { + const sourcePath = path.join(tempPath, fs.readdirSync(tempPath)[0]); + return this.copyDirectories(sourcePath, callback); + }); + }); + } + + copyDirectories(sourcePath, callback) { + let packageName; + sourcePath = path.resolve(sourcePath); + try { + packageName = JSON.parse(fs.readFileSync(path.join(sourcePath, 'package.json')))?.packageName; + } catch (error) {} + if (packageName == null) { packageName = path.basename(this.destinationPath); } + + this.convertSnippets(packageName, sourcePath); + this.convertPreferences(packageName, sourcePath); + this.convertGrammars(sourcePath); + return callback(); + } + + filterObject(object) { + delete object.uuid; + return delete object.keyEquivalent; + } + + convertSettings(settings) { + if (settings.shellVariables) { + const shellVariables = {}; + for (let {name, value} of settings.shellVariables) { + shellVariables[name] = value; + } + settings.shellVariables = shellVariables; + } + + const editorProperties = _.compactObject({ + commentStart: _.valueForKeyPath(settings, 'shellVariables.TM_COMMENT_START'), + commentEnd: _.valueForKeyPath(settings, 'shellVariables.TM_COMMENT_END'), + increaseIndentPattern: settings.increaseIndentPattern, + decreaseIndentPattern: settings.decreaseIndentPattern, + foldEndPattern: settings.foldingStopMarker, + completions: settings.completions + }); + if (!_.isEmpty(editorProperties)) { return {editor: editorProperties}; } + } + + readFileSync(filePath) { + if (_.contains(this.plistExtensions, path.extname(filePath))) { + return plist.parseFileSync(filePath); + } else if (_.contains(['.json', '.cson'], path.extname(filePath))) { + return CSON.readFileSync(filePath); + } + } + + writeFileSync(filePath, object={}) { + this.filterObject(object); + if (Object.keys(object).length > 0) { + return CSON.writeFileSync(filePath, object); + } + } + + convertFile(sourcePath, destinationDir) { + let contents; + const extension = path.extname(sourcePath); + let destinationName = `${path.basename(sourcePath, extension)}.cson`; + destinationName = destinationName.toLowerCase(); + const destinationPath = path.join(destinationDir, destinationName); + + if (_.contains(this.plistExtensions, path.extname(sourcePath))) { + contents = plist.parseFileSync(sourcePath); + } else if (_.contains(['.json', '.cson'], path.extname(sourcePath))) { + contents = CSON.readFileSync(sourcePath); + } + + return this.writeFileSync(destinationPath, contents); + } + + normalizeFilenames(directoryPath) { + if (!fs.isDirectorySync(directoryPath)) { return; } + + return (() => { + const result = []; + for (let child of fs.readdirSync(directoryPath)) { + const childPath = path.join(directoryPath, child); + + // Invalid characters taken from http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx + let convertedFileName = child.replace(/[|?*<>:"\\\/]+/g, '-'); + if (child === convertedFileName) { continue; } + + convertedFileName = convertedFileName.replace(/[\s-]+/g, '-'); + let convertedPath = path.join(directoryPath, convertedFileName); + let suffix = 1; + while (fs.existsSync(convertedPath) || fs.existsSync(convertedPath.toLowerCase())) { + const extension = path.extname(convertedFileName); + convertedFileName = `${path.basename(convertedFileName, extension)}-${suffix}${extension}`; + convertedPath = path.join(directoryPath, convertedFileName); + suffix++; + } + result.push(fs.renameSync(childPath, convertedPath)); + } + return result; + })(); + } + + convertSnippets(packageName, source) { + let sourceSnippets = path.join(source, 'snippets'); + if (!fs.isDirectorySync(sourceSnippets)) { + sourceSnippets = path.join(source, 'Snippets'); + } + if (!fs.isDirectorySync(sourceSnippets)) { return; } + + const snippetsBySelector = {}; + const destination = path.join(this.destinationPath, 'snippets'); + for (let child of fs.readdirSync(sourceSnippets)) { + var left, selector; + const snippet = (left = this.readFileSync(path.join(sourceSnippets, child))) != null ? left : {}; + let {scope, name, content, tabTrigger} = snippet; + if (!tabTrigger || !content) { continue; } + + // Replace things like '${TM_C_POINTER: *}' with ' *' + content = content.replace(/\$\{TM_[A-Z_]+:([^}]+)}/g, '$1'); + + // Replace things like '${1:${TM_FILENAME/(\\w+)*/(?1:$1:NSObject)/}}' + // with '$1' + content = content.replace(/\$\{(\d)+:\s*\$\{TM_[^}]+\s*\}\s*\}/g, '$$1'); + + // Unescape escaped dollar signs $ + content = content.replace(/\\\$/g, '$'); + + if (name == null) { + const extension = path.extname(child); + name = path.basename(child, extension); + } + + try { + if (scope) { selector = new ScopeSelector(scope).toCssSelector(); } + } catch (e) { + e.message = `In file ${e.fileName} at ${JSON.stringify(scope)}: ${e.message}`; + throw e; + } + if (selector == null) { selector = '*'; } + + if (snippetsBySelector[selector] == null) { snippetsBySelector[selector] = {}; } + snippetsBySelector[selector][name] = {prefix: tabTrigger, body: content}; + } + + this.writeFileSync(path.join(destination, `${packageName}.cson`), snippetsBySelector); + return this.normalizeFilenames(destination); + } + + convertPreferences(packageName, source) { + let sourcePreferences = path.join(source, 'preferences'); + if (!fs.isDirectorySync(sourcePreferences)) { + sourcePreferences = path.join(source, 'Preferences'); + } + if (!fs.isDirectorySync(sourcePreferences)) { return; } + + const preferencesBySelector = {}; + const destination = path.join(this.destinationPath, 'settings'); + for (let child of fs.readdirSync(sourcePreferences)) { + var left, properties; + const {scope, settings} = (left = this.readFileSync(path.join(sourcePreferences, child))) != null ? left : {}; + if (!scope || !settings) { continue; } + + if (properties = this.convertSettings(settings)) { + var selector; + try { + selector = new ScopeSelector(scope).toCssSelector(); + } catch (e) { + e.message = `In file ${e.fileName} at ${JSON.stringify(scope)}: ${e.message}`; + throw e; + } + for (let key in properties) { + const value = properties[key]; + if (preferencesBySelector[selector] == null) { preferencesBySelector[selector] = {}; } + if (preferencesBySelector[selector][key] != null) { + preferencesBySelector[selector][key] = _.extend(value, preferencesBySelector[selector][key]); + } else { + preferencesBySelector[selector][key] = value; + } + } + } + } + + this.writeFileSync(path.join(destination, `${packageName}.cson`), preferencesBySelector); + return this.normalizeFilenames(destination); + } + + convertGrammars(source) { + let sourceSyntaxes = path.join(source, 'syntaxes'); + if (!fs.isDirectorySync(sourceSyntaxes)) { + sourceSyntaxes = path.join(source, 'Syntaxes'); + } + if (!fs.isDirectorySync(sourceSyntaxes)) { return; } + + const destination = path.join(this.destinationPath, 'grammars'); + for (let child of fs.readdirSync(sourceSyntaxes)) { + const childPath = path.join(sourceSyntaxes, child); + if (fs.isFileSync(childPath)) { this.convertFile(childPath, destination); } + } + + return this.normalizeFilenames(destination); + } +}; diff --git a/src/packages.coffee b/src/packages.coffee deleted file mode 100644 index 4dcadcc..0000000 --- a/src/packages.coffee +++ /dev/null @@ -1,22 +0,0 @@ -url = require 'url' - -# Package helpers -module.exports = - # Parse the repository in `name/owner` format from the package metadata. - # - # pack - The package metadata object. - # - # Returns a name/owner string or null if not parseable. - getRepository: (pack={}) -> - if repository = pack.repository?.url ? pack.repository - repoPath = url.parse(repository.replace(/\.git$/, '')).pathname - [name, owner] = repoPath.split('/')[-2..] - return "#{name}/#{owner}" if name and owner - null - - # Determine remote from package metadata. - # - # pack - The package metadata object. - # Returns a the remote or 'origin' if not parseable. - getRemote: (pack={}) -> - pack.repository?.url or pack.repository or 'origin' diff --git a/src/packages.js b/src/packages.js new file mode 100644 index 0000000..8b16765 --- /dev/null +++ b/src/packages.js @@ -0,0 +1,33 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import url from 'url'; + +// Package helpers +export default { + // Parse the repository in `name/owner` format from the package metadata. + // + // pack - The package metadata object. + // + // Returns a name/owner string or null if not parseable. + getRepository(pack={}) { + let repository; + if (repository = pack.repository?.url != null ? pack.repository?.url : pack.repository) { + const repoPath = url.parse(repository.replace(/\.git$/, '')).pathname; + const [name, owner] = repoPath.split('/').slice(-2); + if (name && owner) { return `${name}/${owner}`; } + } + return null; + }, + + // Determine remote from package metadata. + // + // pack - The package metadata object. + // Returns a the remote or 'origin' if not parseable. + getRemote(pack={}) { + return pack.repository?.url || pack.repository || 'origin'; + } +}; diff --git a/src/publish.coffee b/src/publish.coffee deleted file mode 100644 index 22d97a2..0000000 --- a/src/publish.coffee +++ /dev/null @@ -1,379 +0,0 @@ -path = require 'path' -url = require 'url' - -yargs = require 'yargs' -Git = require 'git-utils' -semver = require 'semver' - -fs = require './fs' -config = require './apm' -Command = require './command' -Login = require './login' -Packages = require './packages' -request = require './request' - -module.exports = -class Publish extends Command - constructor: -> - super() - @userConfigPath = config.getUserConfigPath() - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm publish [ | major | minor | patch | build] - apm publish --tag - apm publish --rename - - Publish a new version of the package in the current working directory. - - If a new version or version increment is specified, then a new Git tag is - created and the package.json file is updated with that new version before - it is published to the apm registry. The HEAD branch and the new tag are - pushed up to the remote repository automatically using this option. - - If a tag is provided via the --tag flag, it must have the form `vx.y.z`. - For example, `apm publish -t v1.12.0`. - - If a new name is provided via the --rename flag, the package.json file is - updated with the new name and the package's name is updated on Atom.io. - - Run `apm featured` to see all the featured packages or - `apm view ` to see information about your package after you - have published it. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('t', 'tag').string('tag').describe('tag', 'Specify a tag to publish. Must be of the form vx.y.z') - options.alias('r', 'rename').string('rename').describe('rename', 'Specify a new name for the package') - - # Create a new version and tag use the `npm version` command. - # - # version - The new version or version increment. - # callback - The callback function to invoke with an error as the first - # argument and a the generated tag string as the second argument. - versionPackage: (version, callback) -> - process.stdout.write 'Preparing and tagging a new version ' - versionArgs = ['version', version, '-m', 'Prepare v%s release'] - @fork @atomNpmPath, versionArgs, (code, stderr='', stdout='') => - if code is 0 - @logSuccess() - callback(null, stdout.trim()) - else - @logFailure() - callback("#{stdout}\n#{stderr}".trim()) - - # Push a tag to the remote repository. - # - # tag - The tag to push. - # pack - The package metadata. - # callback - The callback function to invoke with an error as the first - # argument. - pushVersion: (tag, pack, callback) -> - process.stdout.write "Pushing #{tag} tag " - pushArgs = ['push', Packages.getRemote(pack), 'HEAD', tag] - @spawn 'git', pushArgs, (args...) => - @logCommandResults(callback, args...) - - # Check for the tag being available from the GitHub API before notifying - # atom.io about the new version. - # - # The tag is checked for 5 times at 1 second intervals. - # - # pack - The package metadata. - # tag - The tag that was pushed. - # callback - The callback function to invoke when either the tag is available - # or the maximum numbers of requests for the tag have been made. - # No arguments are passed to the callback when it is invoked. - waitForTagToBeAvailable: (pack, tag, callback) -> - retryCount = 5 - interval = 1000 - requestSettings = - url: "https://api.github.com/repos/#{Packages.getRepository(pack)}/tags" - json: true - - requestTags = -> - request.get requestSettings, (error, response, tags=[]) -> - if response?.statusCode is 200 - for {name}, index in tags when name is tag - return callback() - if --retryCount <= 0 - callback() - else - setTimeout(requestTags, interval) - requestTags() - - # Does the given package already exist in the registry? - # - # packageName - The string package name to check. - # callback - The callback function invoke with an error as the first - # argument and true/false as the second argument. - packageExists: (packageName, callback) -> - Login.getTokenOrLogin (error, token) -> - return callback(error) if error? - - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{packageName}" - json: true - headers: - authorization: token - request.get requestSettings, (error, response, body={}) -> - if error? - callback(error) - else - callback(null, response.statusCode is 200) - - # Register the current repository with the package registry. - # - # pack - The package metadata. - # callback - The callback function. - registerPackage: (pack, callback) -> - unless pack.name - callback('Required name field in package.json not found') - return - - @packageExists pack.name, (error, exists) => - return callback(error) if error? - return callback() if exists - - unless repository = Packages.getRepository(pack) - callback('Unable to parse repository name/owner from package.json repository field') - return - - process.stdout.write "Registering #{pack.name} " - Login.getTokenOrLogin (error, token) => - if error? - @logFailure() - callback(error) - return - - requestSettings = - url: config.getAtomPackagesUrl() - json: true - body: - repository: repository - headers: - authorization: token - request.post requestSettings, (error, response, body={}) => - if error? - callback(error) - else if response.statusCode isnt 201 - message = request.getErrorMessage(response, body) - @logFailure() - callback("Registering package in #{repository} repository failed: #{message}") - else - @logSuccess() - callback(null, true) - - # Create a new package version at the given Git tag. - # - # packageName - The string name of the package. - # tag - The string Git tag of the new version. - # callback - The callback function to invoke with an error as the first - # argument. - createPackageVersion: (packageName, tag, options, callback) -> - Login.getTokenOrLogin (error, token) -> - if error? - callback(error) - return - - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{packageName}/versions" - json: true - body: - tag: tag - rename: options.rename - headers: - authorization: token - request.post requestSettings, (error, response, body={}) -> - if error? - callback(error) - else if response.statusCode isnt 201 - message = request.getErrorMessage(response, body) - callback("Creating new version failed: #{message}") - else - callback() - - # Publish the version of the package associated with the given tag. - # - # pack - The package metadata. - # tag - The Git tag string of the package version to publish. - # options - An options Object (optional). - # callback - The callback function to invoke when done with an error as the - # first argument. - publishPackage: (pack, tag, remaining...) -> - options = remaining.shift() if remaining.length >= 2 - options ?= {} - callback = remaining.shift() - - process.stdout.write "Publishing #{options.rename or pack.name}@#{tag} " - @createPackageVersion pack.name, tag, options, (error) => - if error? - @logFailure() - callback(error) - else - @logSuccess() - callback() - - logFirstTimePublishMessage: (pack) -> - process.stdout.write 'Congrats on publishing a new package!'.rainbow - # :+1: :package: :tada: when available - if process.platform is 'darwin' - process.stdout.write ' \uD83D\uDC4D \uD83D\uDCE6 \uD83C\uDF89' - - process.stdout.write "\nCheck it out at https://atom.io/packages/#{pack.name}\n" - - loadMetadata: -> - metadataPath = path.resolve('package.json') - unless fs.isFileSync(metadataPath) - throw new Error("No package.json file found at #{process.cwd()}/package.json") - - try - pack = JSON.parse(fs.readFileSync(metadataPath)) - catch error - throw new Error("Error parsing package.json file: #{error.message}") - - saveMetadata: (pack, callback) -> - metadataPath = path.resolve('package.json') - metadataJson = JSON.stringify(pack, null, 2) - fs.writeFile(metadataPath, "#{metadataJson}\n", callback) - - loadRepository: -> - currentDirectory = process.cwd() - - repo = Git.open(currentDirectory) - unless repo?.isWorkingDirectory(currentDirectory) - throw new Error('Package must be in a Git repository before publishing: https://help.github.com/articles/create-a-repo') - - - if currentBranch = repo.getShortHead() - remoteName = repo.getConfigValue("branch.#{currentBranch}.remote") - remoteName ?= repo.getConfigValue('branch.master.remote') - - upstreamUrl = repo.getConfigValue("remote.#{remoteName}.url") if remoteName - upstreamUrl ?= repo.getConfigValue('remote.origin.url') - - unless upstreamUrl - throw new Error('Package must be pushed up to GitHub before publishing: https://help.github.com/articles/create-a-repo') - - # Rename package if necessary - renamePackage: (pack, name, callback) -> - if name?.length > 0 - return callback('The new package name must be different than the name in the package.json file') if pack.name is name - - message = "Renaming #{pack.name} to #{name} " - process.stdout.write(message) - @setPackageName pack, name, (error) => - if error? - @logFailure() - return callback(error) - - config.getSetting 'git', (gitCommand) => - gitCommand ?= 'git' - @spawn gitCommand, ['add', 'package.json'], (code, stderr='', stdout='') => - unless code is 0 - @logFailure() - addOutput = "#{stdout}\n#{stderr}".trim() - return callback("`git add package.json` failed: #{addOutput}") - - @spawn gitCommand, ['commit', '-m', message], (code, stderr='', stdout='') => - if code is 0 - @logSuccess() - callback() - else - @logFailure() - commitOutput = "#{stdout}\n#{stderr}".trim() - callback("Failed to commit package.json: #{commitOutput}") - else - # Just fall through if the name is empty - callback() - - setPackageName: (pack, name, callback) -> - pack.name = name - @saveMetadata(pack, callback) - - validateSemverRanges: (pack) -> - return unless pack - - isValidRange = (semverRange) -> - return true if semver.validRange(semverRange) - - try - return true if url.parse(semverRange).protocol.length > 0 - - semverRange is 'latest' - - if pack.engines?.atom? - unless semver.validRange(pack.engines.atom) - throw new Error("The Atom engine range in the package.json file is invalid: #{pack.engines.atom}") - - for packageName, semverRange of pack.dependencies - unless isValidRange(semverRange) - throw new Error("The #{packageName} dependency range in the package.json file is invalid: #{semverRange}") - - for packageName, semverRange of pack.devDependencies - unless isValidRange(semverRange) - throw new Error("The #{packageName} dev dependency range in the package.json file is invalid: #{semverRange}") - - return - - # Run the publish command with the given options - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - {tag, rename} = options.argv - [version] = options.argv._ - - try - pack = @loadMetadata() - catch error - return callback(error) - - try - @validateSemverRanges(pack) - catch error - return callback(error) - - try - @loadRepository() - catch error - return callback(error) - - if version?.length > 0 or rename?.length > 0 - version = 'patch' unless version?.length > 0 - originalName = pack.name if rename?.length > 0 - - @registerPackage pack, (error, firstTimePublishing) => - return callback(error) if error? - - @renamePackage pack, rename, (error) => - return callback(error) if error? - - @versionPackage version, (error, tag) => - return callback(error) if error? - - @pushVersion tag, pack, (error) => - return callback(error) if error? - - @waitForTagToBeAvailable pack, tag, => - - if originalName? - # If we're renaming a package, we have to hit the API with the - # current name, not the new one, or it will 404. - rename = pack.name - pack.name = originalName - @publishPackage pack, tag, {rename}, (error) => - if firstTimePublishing and not error? - @logFirstTimePublishMessage(pack) - callback(error) - else if tag?.length > 0 - @registerPackage pack, (error, firstTimePublishing) => - return callback(error) if error? - - @publishPackage pack, tag, (error) => - if firstTimePublishing and not error? - @logFirstTimePublishMessage(pack) - callback(error) - else - callback('A version, tag, or new package name is required') diff --git a/src/publish.js b/src/publish.js new file mode 100644 index 0000000..4b68534 --- /dev/null +++ b/src/publish.js @@ -0,0 +1,484 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Publish; +import path from 'path'; +import url from 'url'; +import yargs from 'yargs'; +import Git from 'git-utils'; +import semver from 'semver'; +import fs from './fs'; +import config from './apm'; +import Command from './command'; +import Login from './login'; +import Packages from './packages'; +import request from './request'; + +export default Publish = class Publish extends Command { + constructor() { + super(); + this.userConfigPath = config.getUserConfigPath(); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm publish [ | major | minor | patch | build] + apm publish --tag + apm publish --rename + +Publish a new version of the package in the current working directory. + +If a new version or version increment is specified, then a new Git tag is +created and the package.json file is updated with that new version before +it is published to the apm registry. The HEAD branch and the new tag are +pushed up to the remote repository automatically using this option. + +If a tag is provided via the --tag flag, it must have the form \`vx.y.z\`. +For example, \`apm publish -t v1.12.0\`. + +If a new name is provided via the --rename flag, the package.json file is +updated with the new name and the package's name is updated on Atom.io. + +Run \`apm featured\` to see all the featured packages or +\`apm view \` to see information about your package after you +have published it.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('t', 'tag').string('tag').describe('tag', 'Specify a tag to publish. Must be of the form vx.y.z'); + return options.alias('r', 'rename').string('rename').describe('rename', 'Specify a new name for the package'); + } + + // Create a new version and tag use the `npm version` command. + // + // version - The new version or version increment. + // callback - The callback function to invoke with an error as the first + // argument and a the generated tag string as the second argument. + versionPackage(version, callback) { + process.stdout.write('Preparing and tagging a new version '); + const versionArgs = ['version', version, '-m', 'Prepare v%s release']; + return this.fork(this.atomNpmPath, versionArgs, (code, stderr='', stdout='') => { + if (code === 0) { + this.logSuccess(); + return callback(null, stdout.trim()); + } else { + this.logFailure(); + return callback(`${stdout}\n${stderr}`.trim()); + } + }); + } + + // Push a tag to the remote repository. + // + // tag - The tag to push. + // pack - The package metadata. + // callback - The callback function to invoke with an error as the first + // argument. + pushVersion(tag, pack, callback) { + process.stdout.write(`Pushing ${tag} tag `); + const pushArgs = ['push', Packages.getRemote(pack), 'HEAD', tag]; + return this.spawn('git', pushArgs, (...args) => { + return this.logCommandResults(callback, ...args); + }); + } + + // Check for the tag being available from the GitHub API before notifying + // atom.io about the new version. + // + // The tag is checked for 5 times at 1 second intervals. + // + // pack - The package metadata. + // tag - The tag that was pushed. + // callback - The callback function to invoke when either the tag is available + // or the maximum numbers of requests for the tag have been made. + // No arguments are passed to the callback when it is invoked. + waitForTagToBeAvailable(pack, tag, callback) { + let retryCount = 5; + const interval = 1000; + const requestSettings = { + url: `https://api.github.com/repos/${Packages.getRepository(pack)}/tags`, + json: true + }; + + var requestTags = () => request.get(requestSettings, function(error, response, tags=[]) { + if (response?.statusCode === 200) { + for (let index = 0; index < tags.length; index++) { + const {name} = tags[index]; + if (name === tag) { + return callback(); + } + } + } + if (--retryCount <= 0) { + return callback(); + } else { + return setTimeout(requestTags, interval); + } + }); + return requestTags(); + } + + // Does the given package already exist in the registry? + // + // packageName - The string package name to check. + // callback - The callback function invoke with an error as the first + // argument and true/false as the second argument. + packageExists(packageName, callback) { + return Login.getTokenOrLogin(function(error, token) { + if (error != null) { return callback(error); } + + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${packageName}`, + json: true, + headers: { + authorization: token + } + }; + return request.get(requestSettings, function(error, response, body={}) { + if (error != null) { + return callback(error); + } else { + return callback(null, response.statusCode === 200); + } + }); + }); + } + + // Register the current repository with the package registry. + // + // pack - The package metadata. + // callback - The callback function. + registerPackage(pack, callback) { + if (!pack.name) { + callback('Required name field in package.json not found'); + return; + } + + return this.packageExists(pack.name, (error, exists) => { + let repository; + if (error != null) { return callback(error); } + if (exists) { return callback(); } + + if (!(repository = Packages.getRepository(pack))) { + callback('Unable to parse repository name/owner from package.json repository field'); + return; + } + + process.stdout.write(`Registering ${pack.name} `); + return Login.getTokenOrLogin((error, token) => { + if (error != null) { + this.logFailure(); + callback(error); + return; + } + + const requestSettings = { + url: config.getAtomPackagesUrl(), + json: true, + body: { + repository + }, + headers: { + authorization: token + } + }; + return request.post(requestSettings, (error, response, body={}) => { + if (error != null) { + return callback(error); + } else if (response.statusCode !== 201) { + const message = request.getErrorMessage(response, body); + this.logFailure(); + return callback(`Registering package in ${repository} repository failed: ${message}`); + } else { + this.logSuccess(); + return callback(null, true); + } + }); + }); + }); + } + + // Create a new package version at the given Git tag. + // + // packageName - The string name of the package. + // tag - The string Git tag of the new version. + // callback - The callback function to invoke with an error as the first + // argument. + createPackageVersion(packageName, tag, options, callback) { + return Login.getTokenOrLogin(function(error, token) { + if (error != null) { + callback(error); + return; + } + + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${packageName}/versions`, + json: true, + body: { + tag, + rename: options.rename + }, + headers: { + authorization: token + } + }; + return request.post(requestSettings, function(error, response, body={}) { + if (error != null) { + return callback(error); + } else if (response.statusCode !== 201) { + const message = request.getErrorMessage(response, body); + return callback(`Creating new version failed: ${message}`); + } else { + return callback(); + } + }); + }); + } + + // Publish the version of the package associated with the given tag. + // + // pack - The package metadata. + // tag - The Git tag string of the package version to publish. + // options - An options Object (optional). + // callback - The callback function to invoke when done with an error as the + // first argument. + publishPackage(pack, tag, ...remaining) { + let options; + if (remaining.length >= 2) { options = remaining.shift(); } + if (options == null) { options = {}; } + const callback = remaining.shift(); + + process.stdout.write(`Publishing ${options.rename || pack.name}@${tag} `); + return this.createPackageVersion(pack.name, tag, options, error => { + if (error != null) { + this.logFailure(); + return callback(error); + } else { + this.logSuccess(); + return callback(); + } + }); + } + + logFirstTimePublishMessage(pack) { + process.stdout.write('Congrats on publishing a new package!'.rainbow); + // :+1: :package: :tada: when available + if (process.platform === 'darwin') { + process.stdout.write(' \uD83D\uDC4D \uD83D\uDCE6 \uD83C\uDF89'); + } + + return process.stdout.write(`\nCheck it out at https://atom.io/packages/${pack.name}\n`); + } + + loadMetadata() { + const metadataPath = path.resolve('package.json'); + if (!fs.isFileSync(metadataPath)) { + throw new Error(`No package.json file found at ${process.cwd()}/package.json`); + } + + try { + let pack; + return pack = JSON.parse(fs.readFileSync(metadataPath)); + } catch (error) { + throw new Error(`Error parsing package.json file: ${error.message}`); + } + } + + saveMetadata(pack, callback) { + const metadataPath = path.resolve('package.json'); + const metadataJson = JSON.stringify(pack, null, 2); + return fs.writeFile(metadataPath, `${metadataJson}\n`, callback); + } + + loadRepository() { + let currentBranch, remoteName, upstreamUrl; + const currentDirectory = process.cwd(); + + const repo = Git.open(currentDirectory); + if (!repo?.isWorkingDirectory(currentDirectory)) { + throw new Error('Package must be in a Git repository before publishing: https://help.github.com/articles/create-a-repo'); + } + + + if (currentBranch = repo.getShortHead()) { + remoteName = repo.getConfigValue(`branch.${currentBranch}.remote`); + } + if (remoteName == null) { remoteName = repo.getConfigValue('branch.master.remote'); } + + if (remoteName) { upstreamUrl = repo.getConfigValue(`remote.${remoteName}.url`); } + if (upstreamUrl == null) { upstreamUrl = repo.getConfigValue('remote.origin.url'); } + + if (!upstreamUrl) { + throw new Error('Package must be pushed up to GitHub before publishing: https://help.github.com/articles/create-a-repo'); + } + } + + // Rename package if necessary + renamePackage(pack, name, callback) { + if (name?.length > 0) { + if (pack.name === name) { return callback('The new package name must be different than the name in the package.json file'); } + + const message = `Renaming ${pack.name} to ${name} `; + process.stdout.write(message); + return this.setPackageName(pack, name, error => { + if (error != null) { + this.logFailure(); + return callback(error); + } + + return config.getSetting('git', gitCommand => { + if (gitCommand == null) { gitCommand = 'git'; } + return this.spawn(gitCommand, ['add', 'package.json'], (code, stderr='', stdout='') => { + if (code !== 0) { + this.logFailure(); + const addOutput = `${stdout}\n${stderr}`.trim(); + return callback(`\`git add package.json\` failed: ${addOutput}`); + } + + return this.spawn(gitCommand, ['commit', '-m', message], (code, stderr='', stdout='') => { + if (code === 0) { + this.logSuccess(); + return callback(); + } else { + this.logFailure(); + const commitOutput = `${stdout}\n${stderr}`.trim(); + return callback(`Failed to commit package.json: ${commitOutput}`); + } + }); + }); + }); + }); + } else { + // Just fall through if the name is empty + return callback(); + } + } + + setPackageName(pack, name, callback) { + pack.name = name; + return this.saveMetadata(pack, callback); + } + + validateSemverRanges(pack) { + let packageName, semverRange; + if (!pack) { return; } + + const isValidRange = function(semverRange) { + if (semver.validRange(semverRange)) { return true; } + + try { + if (url.parse(semverRange).protocol.length > 0) { return true; } + } catch (error) {} + + return semverRange === 'latest'; + }; + + if (pack.engines?.atom != null) { + if (!semver.validRange(pack.engines.atom)) { + throw new Error(`The Atom engine range in the package.json file is invalid: ${pack.engines.atom}`); + } + } + + for (packageName in pack.dependencies) { + semverRange = pack.dependencies[packageName]; + if (!isValidRange(semverRange)) { + throw new Error(`The ${packageName} dependency range in the package.json file is invalid: ${semverRange}`); + } + } + + for (packageName in pack.devDependencies) { + semverRange = pack.devDependencies[packageName]; + if (!isValidRange(semverRange)) { + throw new Error(`The ${packageName} dev dependency range in the package.json file is invalid: ${semverRange}`); + } + } + + } + + // Run the publish command with the given options + run(options) { + let error, pack; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + let {tag, rename} = options.argv; + let [version] = options.argv._; + + try { + pack = this.loadMetadata(); + } catch (error1) { + error = error1; + return callback(error); + } + + try { + this.validateSemverRanges(pack); + } catch (error2) { + error = error2; + return callback(error); + } + + try { + this.loadRepository(); + } catch (error3) { + error = error3; + return callback(error); + } + + if ((version?.length > 0) || (rename?.length > 0)) { + let originalName; + if (version?.length <= 0) { version = 'patch'; } + if (rename?.length > 0) { originalName = pack.name; } + + return this.registerPackage(pack, (error, firstTimePublishing) => { + if (error != null) { return callback(error); } + + return this.renamePackage(pack, rename, error => { + if (error != null) { return callback(error); } + + return this.versionPackage(version, (error, tag) => { + if (error != null) { return callback(error); } + + return this.pushVersion(tag, pack, error => { + if (error != null) { return callback(error); } + + return this.waitForTagToBeAvailable(pack, tag, () => { + + if (originalName != null) { + // If we're renaming a package, we have to hit the API with the + // current name, not the new one, or it will 404. + rename = pack.name; + pack.name = originalName; + } + return this.publishPackage(pack, tag, {rename}, error => { + if (firstTimePublishing && (error == null)) { + this.logFirstTimePublishMessage(pack); + } + return callback(error); + }); + }); + }); + }); + }); + }); + } else if (tag?.length > 0) { + return this.registerPackage(pack, (error, firstTimePublishing) => { + if (error != null) { return callback(error); } + + return this.publishPackage(pack, tag, error => { + if (firstTimePublishing && (error == null)) { + this.logFirstTimePublishMessage(pack); + } + return callback(error); + }); + }); + } else { + return callback('A version, tag, or new package name is required'); + } + } +}; diff --git a/src/rebuild-module-cache.coffee b/src/rebuild-module-cache.coffee deleted file mode 100644 index b8c0a6f..0000000 --- a/src/rebuild-module-cache.coffee +++ /dev/null @@ -1,64 +0,0 @@ -path = require 'path' -async = require 'async' -yargs = require 'yargs' -Command = require './command' -config = require './apm' -fs = require './fs' - -module.exports = -class RebuildModuleCache extends Command - constructor: -> - super() - @atomPackagesDirectory = path.join(config.getAtomDirectory(), 'packages') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm rebuild-module-cache - - Rebuild the module cache for all the packages installed to - ~/.atom/packages - - You can see the state of the module cache for a package by looking - at the _atomModuleCache property in the package's package.json file. - - This command skips all linked packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - getResourcePath: (callback) -> - if @resourcePath - process.nextTick => callback(@resourcePath) - else - config.getResourcePath (@resourcePath) => callback(@resourcePath) - - rebuild: (packageDirectory, callback) -> - @getResourcePath (resourcePath) => - try - @moduleCache ?= require(path.join(resourcePath, 'src', 'module-cache')) - @moduleCache.create(packageDirectory) - catch error - return callback(error) - - callback() - - run: (options) -> - {callback} = options - - commands = [] - fs.list(@atomPackagesDirectory).forEach (packageName) => - packageDirectory = path.join(@atomPackagesDirectory, packageName) - return if fs.isSymbolicLinkSync(packageDirectory) - return unless fs.isFileSync(path.join(packageDirectory, 'package.json')) - - commands.push (callback) => - process.stdout.write "Rebuilding #{packageName} module cache " - @rebuild packageDirectory, (error) => - if error? - @logFailure() - else - @logSuccess() - callback(error) - - async.waterfall(commands, callback) diff --git a/src/rebuild-module-cache.js b/src/rebuild-module-cache.js new file mode 100644 index 0000000..9cfb875 --- /dev/null +++ b/src/rebuild-module-cache.js @@ -0,0 +1,84 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RebuildModuleCache; +import path from 'path'; +import async from 'async'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; + +export default RebuildModuleCache = class RebuildModuleCache extends Command { + constructor() { + super(); + this.atomPackagesDirectory = path.join(config.getAtomDirectory(), 'packages'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm rebuild-module-cache + +Rebuild the module cache for all the packages installed to +~/.atom/packages + +You can see the state of the module cache for a package by looking +at the _atomModuleCache property in the package's package.json file. + +This command skips all linked packages.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + getResourcePath(callback) { + if (this.resourcePath) { + return process.nextTick(() => callback(this.resourcePath)); + } else { + return config.getResourcePath(resourcePath => { this.resourcePath = resourcePath; return callback(this.resourcePath); }); + } + } + + rebuild(packageDirectory, callback) { + return this.getResourcePath(resourcePath => { + try { + if (this.moduleCache == null) { this.moduleCache = require(path.join(resourcePath, 'src', 'module-cache')); } + this.moduleCache.create(packageDirectory); + } catch (error) { + return callback(error); + } + + return callback(); + }); + } + + run(options) { + const {callback} = options; + + const commands = []; + fs.list(this.atomPackagesDirectory).forEach(packageName => { + const packageDirectory = path.join(this.atomPackagesDirectory, packageName); + if (fs.isSymbolicLinkSync(packageDirectory)) { return; } + if (!fs.isFileSync(path.join(packageDirectory, 'package.json'))) { return; } + + return commands.push(callback => { + process.stdout.write(`Rebuilding ${packageName} module cache `); + return this.rebuild(packageDirectory, error => { + if (error != null) { + this.logFailure(); + } else { + this.logSuccess(); + } + return callback(error); + }); + }); + }); + + return async.waterfall(commands, callback); + } +}; diff --git a/src/rebuild.coffee b/src/rebuild.coffee deleted file mode 100644 index 9172ddb..0000000 --- a/src/rebuild.coffee +++ /dev/null @@ -1,58 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -fs = require './fs' -Install = require './install' - -module.exports = -class Rebuild extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomNodeDirectory = path.join(@atomDirectory, '.node-gyp') - @atomNpmPath = require.resolve('npm/bin/npm-cli') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm rebuild [ [ ...]] - - Rebuild the given modules currently installed in the node_modules folder - in the current working directory. - - All the modules will be rebuilt if no module names are specified. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - forkNpmRebuild: (options, callback) -> - process.stdout.write 'Rebuilding modules ' - - rebuildArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'rebuild'] - rebuildArgs.push(@getNpmBuildFlags()...) - rebuildArgs.push(options.argv._...) - - fs.makeTreeSync(@atomDirectory) - - env = _.extend({}, process.env, {HOME: @atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}) - @addBuildEnvVars(env) - - @fork(@atomNpmPath, rebuildArgs, {env}, callback) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - config.loadNpm (error, @npm) => - @loadInstalledAtomMetadata => - @forkNpmRebuild options, (code, stderr='') => - if code is 0 - @logSuccess() - callback() - else - @logFailure() - callback(stderr) diff --git a/src/rebuild.js b/src/rebuild.js new file mode 100644 index 0000000..d32a7ae --- /dev/null +++ b/src/rebuild.js @@ -0,0 +1,72 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Rebuild; +import path from 'path'; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import fs from './fs'; +import Install from './install'; + +export default Rebuild = class Rebuild extends Command { + constructor() { + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomNodeDirectory = path.join(this.atomDirectory, '.node-gyp'); + this.atomNpmPath = require.resolve('npm/bin/npm-cli'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm rebuild [ [ ...]] + +Rebuild the given modules currently installed in the node_modules folder +in the current working directory. + +All the modules will be rebuilt if no module names are specified.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + forkNpmRebuild(options, callback) { + process.stdout.write('Rebuilding modules '); + + const rebuildArgs = ['--globalconfig', config.getGlobalConfigPath(), '--userconfig', config.getUserConfigPath(), 'rebuild']; + rebuildArgs.push(...this.getNpmBuildFlags()); + rebuildArgs.push(...options.argv._); + + fs.makeTreeSync(this.atomDirectory); + + const env = _.extend({}, process.env, {HOME: this.atomNodeDirectory, RUSTUP_HOME: config.getRustupHomeDirPath()}); + this.addBuildEnvVars(env); + + return this.fork(this.atomNpmPath, rebuildArgs, {env}, callback); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + return config.loadNpm((error, npm) => { + this.npm = npm; + return this.loadInstalledAtomMetadata(() => { + return this.forkNpmRebuild(options, (code, stderr='') => { + if (code === 0) { + this.logSuccess(); + return callback(); + } else { + this.logFailure(); + return callback(stderr); + } + }); + }); + }); + } +}; diff --git a/src/request.coffee b/src/request.coffee deleted file mode 100644 index b84a04e..0000000 --- a/src/request.coffee +++ /dev/null @@ -1,59 +0,0 @@ -npm = require 'npm' -request = require 'request' - -config = require './apm' - -loadNpm = (callback) -> - npmOptions = - userconfig: config.getUserConfigPath() - globalconfig: config.getGlobalConfigPath() - npm.load(npmOptions, callback) - -configureRequest = (requestOptions, callback) -> - loadNpm -> - requestOptions.proxy ?= npm.config.get('https-proxy') or npm.config.get('proxy') or process.env.HTTPS_PROXY or process.env.HTTP_PROXY - requestOptions.strictSSL ?= npm.config.get('strict-ssl') - - userAgent = npm.config.get('user-agent') ? "AtomApm/#{require('../package.json').version}" - requestOptions.headers ?= {} - requestOptions.headers['User-Agent'] ?= userAgent - callback() - -module.exports = - get: (requestOptions, callback) -> - configureRequest requestOptions, -> - retryCount = requestOptions.retries ? 0 - requestsMade = 0 - tryRequest = -> - requestsMade++ - request.get requestOptions, (error, response, body) -> - if retryCount > 0 and error?.code in ['ETIMEDOUT', 'ECONNRESET'] - retryCount-- - tryRequest() - else - if error?.message and requestsMade > 1 - error.message += " (#{requestsMade} attempts)" - - callback(error, response, body) - tryRequest() - - del: (requestOptions, callback) -> - configureRequest requestOptions, -> - request.del(requestOptions, callback) - - post: (requestOptions, callback) -> - configureRequest requestOptions, -> - request.post(requestOptions, callback) - - createReadStream: (requestOptions, callback) -> - configureRequest requestOptions, -> - callback(request.get(requestOptions)) - - getErrorMessage: (response, body) -> - if response?.statusCode is 503 - 'atom.io is temporarily unavailable, please try again later.' - else - body?.message ? body?.error ? body - - debug: (debug) -> - request.debug = debug diff --git a/src/request.js b/src/request.js new file mode 100644 index 0000000..bfe68a6 --- /dev/null +++ b/src/request.js @@ -0,0 +1,79 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import npm from 'npm'; +import request from 'request'; +import config from './apm'; + +const loadNpm = function(callback) { + const npmOptions = { + userconfig: config.getUserConfigPath(), + globalconfig: config.getGlobalConfigPath() + }; + return npm.load(npmOptions, callback); +}; + +const configureRequest = (requestOptions, callback) => loadNpm(function() { + let left; + if (requestOptions.proxy == null) { requestOptions.proxy = npm.config.get('https-proxy') || npm.config.get('proxy') || process.env.HTTPS_PROXY || process.env.HTTP_PROXY; } + if (requestOptions.strictSSL == null) { requestOptions.strictSSL = npm.config.get('strict-ssl'); } + + const userAgent = (left = npm.config.get('user-agent')) != null ? left : `AtomApm/${require('../package.json').version}`; + if (requestOptions.headers == null) { requestOptions.headers = {}; } + if (requestOptions.headers['User-Agent'] == null) { requestOptions.headers['User-Agent'] = userAgent; } + return callback(); +}); + +export default { + get(requestOptions, callback) { + return configureRequest(requestOptions, function() { + let retryCount = requestOptions.retries != null ? requestOptions.retries : 0; + let requestsMade = 0; + var tryRequest = function() { + requestsMade++; + return request.get(requestOptions, function(error, response, body) { + if ((retryCount > 0) && ['ETIMEDOUT', 'ECONNRESET'].includes(error?.code)) { + retryCount--; + return tryRequest(); + } else { + if (error?.message && (requestsMade > 1)) { + error.message += ` (${requestsMade} attempts)`; + } + + return callback(error, response, body); + } + }); + }; + return tryRequest(); + }); + }, + + del(requestOptions, callback) { + return configureRequest(requestOptions, () => request.del(requestOptions, callback)); + }, + + post(requestOptions, callback) { + return configureRequest(requestOptions, () => request.post(requestOptions, callback)); + }, + + createReadStream(requestOptions, callback) { + return configureRequest(requestOptions, () => callback(request.get(requestOptions))); + }, + + getErrorMessage(response, body) { + if (response?.statusCode === 503) { + return 'atom.io is temporarily unavailable, please try again later.'; + } else { + let left; + return (left = body?.message != null ? body?.message : body?.error) != null ? left : body; + } + }, + + debug(debug) { + return request.debug = debug; + } +}; diff --git a/src/search.coffee b/src/search.coffee deleted file mode 100644 index 39813e1..0000000 --- a/src/search.coffee +++ /dev/null @@ -1,85 +0,0 @@ -_ = require 'underscore-plus' -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -request = require './request' -tree = require './tree' -{isDeprecatedPackage} = require './deprecated-packages' - -module.exports = -class Search extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm search - - Search for Atom packages/themes on the atom.io registry. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('json').describe('json', 'Output matching packages as JSON array') - options.boolean('packages').describe('packages', 'Search only non-theme packages').alias('p', 'packages') - options.boolean('themes').describe('themes', 'Search only themes').alias('t', 'themes') - - searchPackages: (query, opts, callback) -> - qs = - q: query - - if opts.packages - qs.filter = 'package' - else if opts.themes - qs.filter = 'theme' - - requestSettings = - url: "#{config.getAtomPackagesUrl()}/search" - qs: qs - json: true - - request.get requestSettings, (error, response, body={}) -> - if error? - callback(error) - else if response.statusCode is 200 - packages = body.filter (pack) -> pack.releases?.latest? - packages = packages.map ({readme, metadata, downloads, stargazers_count}) -> _.extend({}, metadata, {readme, downloads, stargazers_count}) - packages = packages.filter ({name, version}) -> not isDeprecatedPackage(name, version) - callback(null, packages) - else - message = request.getErrorMessage(response, body) - callback("Searching packages failed: #{message}") - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - [query] = options.argv._ - - unless query - callback("Missing required search query") - return - - searchOptions = - packages: options.argv.packages - themes: options.argv.themes - - @searchPackages query, searchOptions, (error, packages) -> - if error? - callback(error) - return - - if options.argv.json - console.log(JSON.stringify(packages)) - else - heading = "Search Results For '#{query}'".cyan - console.log "#{heading} (#{packages.length})" - - tree packages, ({name, version, description, downloads, stargazers_count}) -> - label = name.yellow - label += " #{description.replace(/\s+/g, ' ')}" if description - label += " (#{_.pluralize(downloads, 'download')}, #{_.pluralize(stargazers_count, 'star')})".grey if downloads >= 0 and stargazers_count >= 0 - label - - console.log() - console.log "Use `apm install` to install them or visit #{'http://atom.io/packages'.underline} to read more about them." - console.log() - - callback() diff --git a/src/search.js b/src/search.js new file mode 100644 index 0000000..9e378d9 --- /dev/null +++ b/src/search.js @@ -0,0 +1,105 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Search; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import request from './request'; +import tree from './tree'; +import { isDeprecatedPackage } from './deprecated-packages'; + +export default Search = class Search extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm search + +Search for Atom packages/themes on the atom.io registry.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.boolean('json').describe('json', 'Output matching packages as JSON array'); + options.boolean('packages').describe('packages', 'Search only non-theme packages').alias('p', 'packages'); + return options.boolean('themes').describe('themes', 'Search only themes').alias('t', 'themes'); + } + + searchPackages(query, opts, callback) { + const qs = + {q: query}; + + if (opts.packages) { + qs.filter = 'package'; + } else if (opts.themes) { + qs.filter = 'theme'; + } + + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/search`, + qs, + json: true + }; + + return request.get(requestSettings, function(error, response, body={}) { + if (error != null) { + return callback(error); + } else if (response.statusCode === 200) { + let packages = body.filter(pack => pack.releases?.latest != null); + packages = packages.map(({readme, metadata, downloads, stargazers_count}) => _.extend({}, metadata, {readme, downloads, stargazers_count})); + packages = packages.filter(({name, version}) => !isDeprecatedPackage(name, version)); + return callback(null, packages); + } else { + const message = request.getErrorMessage(response, body); + return callback(`Searching packages failed: ${message}`); + } + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const [query] = options.argv._; + + if (!query) { + callback("Missing required search query"); + return; + } + + const searchOptions = { + packages: options.argv.packages, + themes: options.argv.themes + }; + + return this.searchPackages(query, searchOptions, function(error, packages) { + if (error != null) { + callback(error); + return; + } + + if (options.argv.json) { + console.log(JSON.stringify(packages)); + } else { + const heading = `Search Results For '${query}'`.cyan; + console.log(`${heading} (${packages.length})`); + + tree(packages, function({name, version, description, downloads, stargazers_count}) { + let label = name.yellow; + if (description) { label += ` ${description.replace(/\s+/g, ' ')}`; } + if ((downloads >= 0) && (stargazers_count >= 0)) { label += ` (${_.pluralize(downloads, 'download')}, ${_.pluralize(stargazers_count, 'star')})`.grey; } + return label; + }); + + console.log(); + console.log(`Use \`apm install\` to install them or visit ${'http://atom.io/packages'.underline} to read more about them.`); + console.log(); + } + + return callback(); + }); + } +}; diff --git a/src/star.coffee b/src/star.coffee deleted file mode 100644 index 874bf32..0000000 --- a/src/star.coffee +++ /dev/null @@ -1,91 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -async = require 'async' -CSON = require 'season' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -fs = require './fs' -Login = require './login' -Packages = require './packages' -request = require './request' - -module.exports = -class Star extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm star ... - - Star the given packages on https://atom.io - - Run `apm stars` to see all your starred packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('installed').describe('installed', 'Star all packages in ~/.atom/packages') - - starPackage: (packageName, {ignoreUnpublishedPackages, token}={}, callback) -> - process.stdout.write '\u2B50 ' if process.platform is 'darwin' - process.stdout.write "Starring #{packageName} " - requestSettings = - json: true - url: "#{config.getAtomPackagesUrl()}/#{packageName}/star" - headers: - authorization: token - request.post requestSettings, (error, response, body={}) => - if error? - @logFailure() - callback(error) - else if response.statusCode is 404 and ignoreUnpublishedPackages - process.stdout.write 'skipped (not published)\n'.yellow - callback() - else if response.statusCode isnt 200 - @logFailure() - message = request.getErrorMessage(response, body) - callback("Starring package failed: #{message}") - else - @logSuccess() - callback() - - getInstalledPackageNames: -> - installedPackages = [] - userPackagesDirectory = path.join(config.getAtomDirectory(), 'packages') - for child in fs.list(userPackagesDirectory) - continue unless fs.isDirectorySync(path.join(userPackagesDirectory, child)) - - if manifestPath = CSON.resolve(path.join(userPackagesDirectory, child, 'package')) - try - metadata = CSON.readFileSync(manifestPath) ? {} - if metadata.name and Packages.getRepository(metadata) - installedPackages.push metadata.name - - _.uniq(installedPackages) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - if options.argv.installed - packageNames = @getInstalledPackageNames() - if packageNames.length is 0 - callback() - return - else - packageNames = @packageNamesFromArgv(options.argv) - if packageNames.length is 0 - callback("Please specify a package name to star") - return - - Login.getTokenOrLogin (error, token) => - return callback(error) if error? - - starOptions = - ignoreUnpublishedPackages: options.argv.installed - token: token - - commands = packageNames.map (packageName) => - (callback) => @starPackage(packageName, starOptions, callback) - async.waterfall(commands, callback) diff --git a/src/star.js b/src/star.js new file mode 100644 index 0000000..c1dde29 --- /dev/null +++ b/src/star.js @@ -0,0 +1,119 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Star; +import path from 'path'; +import _ from 'underscore-plus'; +import async from 'async'; +import CSON from 'season'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import fs from './fs'; +import Login from './login'; +import Packages from './packages'; +import request from './request'; + +export default Star = class Star extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm star ... + +Star the given packages on https://atom.io + +Run \`apm stars\` to see all your starred packages.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.boolean('installed').describe('installed', 'Star all packages in ~/.atom/packages'); + } + + starPackage(packageName, {ignoreUnpublishedPackages, token}={}, callback) { + if (process.platform === 'darwin') { process.stdout.write('\u2B50 '); } + process.stdout.write(`Starring ${packageName} `); + const requestSettings = { + json: true, + url: `${config.getAtomPackagesUrl()}/${packageName}/star`, + headers: { + authorization: token + } + }; + return request.post(requestSettings, (error, response, body={}) => { + if (error != null) { + this.logFailure(); + return callback(error); + } else if ((response.statusCode === 404) && ignoreUnpublishedPackages) { + process.stdout.write('skipped (not published)\n'.yellow); + return callback(); + } else if (response.statusCode !== 200) { + this.logFailure(); + const message = request.getErrorMessage(response, body); + return callback(`Starring package failed: ${message}`); + } else { + this.logSuccess(); + return callback(); + } + }); + } + + getInstalledPackageNames() { + const installedPackages = []; + const userPackagesDirectory = path.join(config.getAtomDirectory(), 'packages'); + for (let child of fs.list(userPackagesDirectory)) { + var manifestPath; + if (!fs.isDirectorySync(path.join(userPackagesDirectory, child))) { continue; } + + if (manifestPath = CSON.resolve(path.join(userPackagesDirectory, child, 'package'))) { + try { + var left; + const metadata = (left = CSON.readFileSync(manifestPath)) != null ? left : {}; + if (metadata.name && Packages.getRepository(metadata)) { + installedPackages.push(metadata.name); + } + } catch (error) {} + } + } + + return _.uniq(installedPackages); + } + + run(options) { + let packageNames; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + if (options.argv.installed) { + packageNames = this.getInstalledPackageNames(); + if (packageNames.length === 0) { + callback(); + return; + } + } else { + packageNames = this.packageNamesFromArgv(options.argv); + if (packageNames.length === 0) { + callback("Please specify a package name to star"); + return; + } + } + + return Login.getTokenOrLogin((error, token) => { + if (error != null) { return callback(error); } + + const starOptions = { + ignoreUnpublishedPackages: options.argv.installed, + token + }; + + const commands = packageNames.map(packageName => { + return callback => this.starPackage(packageName, starOptions, callback); + }); + return async.waterfall(commands, callback); + }); + } +}; diff --git a/src/stars.coffee b/src/stars.coffee deleted file mode 100644 index 37e70e3..0000000 --- a/src/stars.coffee +++ /dev/null @@ -1,104 +0,0 @@ -_ = require 'underscore-plus' -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -Install = require './install' -Login = require './login' -request = require './request' -tree = require './tree' - -module.exports = -class Stars extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm stars - apm stars --install - apm stars --user thedaniel - apm stars --themes - - List or install starred Atom packages and themes. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('i', 'install').boolean('install').describe('install', 'Install the starred packages') - options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes') - options.alias('u', 'user').string('user').describe('user', 'GitHub username to show starred packages for') - options.boolean('json').describe('json', 'Output packages as a JSON array') - - getStarredPackages: (user, atomVersion, callback) -> - requestSettings = json: true - requestSettings.qs = engine: atomVersion if atomVersion - - if user - requestSettings.url = "#{config.getAtomApiUrl()}/users/#{user}/stars" - @requestStarredPackages(requestSettings, callback) - else - requestSettings.url = "#{config.getAtomApiUrl()}/stars" - Login.getTokenOrLogin (error, token) => - return callback(error) if error? - - requestSettings.headers = authorization: token - @requestStarredPackages(requestSettings, callback) - - requestStarredPackages: (requestSettings, callback) -> - request.get requestSettings, (error, response, body=[]) -> - if error? - callback(error) - else if response.statusCode is 200 - packages = body.filter (pack) -> pack?.releases?.latest? - packages = packages.map ({readme, metadata, downloads, stargazers_count}) -> _.extend({}, metadata, {readme, downloads, stargazers_count}) - packages = _.sortBy(packages, 'name') - callback(null, packages) - else - message = request.getErrorMessage(response, body) - callback("Requesting packages failed: #{message}") - - installPackages: (packages, callback) -> - return callback() if packages.length is 0 - - commandArgs = packages.map ({name}) -> name - new Install().run({commandArgs, callback}) - - logPackagesAsJson: (packages, callback) -> - console.log(JSON.stringify(packages)) - callback() - - logPackagesAsText: (user, packagesAreThemes, packages, callback) -> - userLabel = user ? 'you' - if packagesAreThemes - label = "Themes starred by #{userLabel}" - else - label = "Packages starred by #{userLabel}" - console.log "#{label.cyan} (#{packages.length})" - - tree packages, ({name, version, description, downloads, stargazers_count}) -> - label = name.yellow - label = "\u2B50 #{label}" if process.platform is 'darwin' - label += " #{description.replace(/\s+/g, ' ')}" if description - label += " (#{_.pluralize(downloads, 'download')}, #{_.pluralize(stargazers_count, 'star')})".grey if downloads >= 0 and stargazers_count >= 0 - label - - console.log() - console.log "Use `apm stars --install` to install them all or visit #{'http://atom.io/packages'.underline} to read more about them." - console.log() - callback() - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - user = options.argv.user?.toString().trim() - - @getStarredPackages user, options.argv.compatible, (error, packages) => - return callback(error) if error? - - if options.argv.themes - packages = packages.filter ({theme}) -> theme - - if options.argv.install - @installPackages(packages, callback) - else if options.argv.json - @logPackagesAsJson(packages, callback) - else - @logPackagesAsText(user, options.argv.themes, packages, callback) diff --git a/src/stars.js b/src/stars.js new file mode 100644 index 0000000..4cdf204 --- /dev/null +++ b/src/stars.js @@ -0,0 +1,128 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Stars; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import Install from './install'; +import Login from './login'; +import request from './request'; +import tree from './tree'; + +export default Stars = class Stars extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm stars + apm stars --install + apm stars --user thedaniel + apm stars --themes + +List or install starred Atom packages and themes.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('i', 'install').boolean('install').describe('install', 'Install the starred packages'); + options.alias('t', 'themes').boolean('themes').describe('themes', 'Only list themes'); + options.alias('u', 'user').string('user').describe('user', 'GitHub username to show starred packages for'); + return options.boolean('json').describe('json', 'Output packages as a JSON array'); + } + + getStarredPackages(user, atomVersion, callback) { + const requestSettings = {json: true}; + if (atomVersion) { requestSettings.qs = {engine: atomVersion}; } + + if (user) { + requestSettings.url = `${config.getAtomApiUrl()}/users/${user}/stars`; + return this.requestStarredPackages(requestSettings, callback); + } else { + requestSettings.url = `${config.getAtomApiUrl()}/stars`; + return Login.getTokenOrLogin((error, token) => { + if (error != null) { return callback(error); } + + requestSettings.headers = {authorization: token}; + return this.requestStarredPackages(requestSettings, callback); + }); + } + } + + requestStarredPackages(requestSettings, callback) { + return request.get(requestSettings, function(error, response, body=[]) { + if (error != null) { + return callback(error); + } else if (response.statusCode === 200) { + let packages = body.filter(pack => pack?.releases?.latest != null); + packages = packages.map(({readme, metadata, downloads, stargazers_count}) => _.extend({}, metadata, {readme, downloads, stargazers_count})); + packages = _.sortBy(packages, 'name'); + return callback(null, packages); + } else { + const message = request.getErrorMessage(response, body); + return callback(`Requesting packages failed: ${message}`); + } + }); + } + + installPackages(packages, callback) { + if (packages.length === 0) { return callback(); } + + const commandArgs = packages.map(({name}) => name); + return new Install().run({commandArgs, callback}); + } + + logPackagesAsJson(packages, callback) { + console.log(JSON.stringify(packages)); + return callback(); + } + + logPackagesAsText(user, packagesAreThemes, packages, callback) { + let label; + const userLabel = user != null ? user : 'you'; + if (packagesAreThemes) { + label = `Themes starred by ${userLabel}`; + } else { + label = `Packages starred by ${userLabel}`; + } + console.log(`${label.cyan} (${packages.length})`); + + tree(packages, function({name, version, description, downloads, stargazers_count}) { + label = name.yellow; + if (process.platform === 'darwin') { label = `\u2B50 ${label}`; } + if (description) { label += ` ${description.replace(/\s+/g, ' ')}`; } + if ((downloads >= 0) && (stargazers_count >= 0)) { label += ` (${_.pluralize(downloads, 'download')}, ${_.pluralize(stargazers_count, 'star')})`.grey; } + return label; + }); + + console.log(); + console.log(`Use \`apm stars --install\` to install them all or visit ${'http://atom.io/packages'.underline} to read more about them.`); + console.log(); + return callback(); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const user = options.argv.user?.toString().trim(); + + return this.getStarredPackages(user, options.argv.compatible, (error, packages) => { + if (error != null) { return callback(error); } + + if (options.argv.themes) { + packages = packages.filter(({theme}) => theme); + } + + if (options.argv.install) { + return this.installPackages(packages, callback); + } else if (options.argv.json) { + return this.logPackagesAsJson(packages, callback); + } else { + return this.logPackagesAsText(user, options.argv.themes, packages, callback); + } + }); + } +}; diff --git a/src/test.coffee b/src/test.coffee deleted file mode 100644 index aac5af4..0000000 --- a/src/test.coffee +++ /dev/null @@ -1,63 +0,0 @@ -path = require 'path' - -yargs = require 'yargs' -temp = require 'temp' - -Command = require './command' -fs = require './fs' - -module.exports = -class Test extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: - apm test - - Runs the package's tests contained within the spec directory (relative - to the current working directory). - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('p', 'path').string('path').describe('path', 'Path to atom command') - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - {env} = process - - atomCommand = options.argv.path if options.argv.path - unless fs.existsSync(atomCommand) - atomCommand = 'atom' - atomCommand += '.cmd' if process.platform is 'win32' - - packagePath = process.cwd() - testArgs = ['--dev', '--test', path.join(packagePath, 'spec')] - - if process.platform is 'win32' - logFile = temp.openSync(suffix: '.log', prefix: "#{path.basename(packagePath)}-") - fs.closeSync(logFile.fd) - logFilePath = logFile.path - testArgs.push("--log-file=#{logFilePath}") - - @spawn atomCommand, testArgs, (code) -> - try - loggedOutput = fs.readFileSync(logFilePath, 'utf8') - process.stdout.write("#{loggedOutput}\n") if loggedOutput - - if code is 0 - process.stdout.write 'Tests passed\n'.green - callback() - else if code?.message - callback("Error spawning Atom: #{code.message}") - else - callback('Tests failed') - else - @spawn atomCommand, testArgs, {env, streaming: true}, (code) -> - if code is 0 - process.stdout.write 'Tests passed\n'.green - callback() - else if code?.message - callback("Error spawning #{atomCommand}: #{code.message}") - else - callback('Tests failed') diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000..f7d2a8d --- /dev/null +++ b/src/test.js @@ -0,0 +1,78 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Test; +import path from 'path'; +import yargs from 'yargs'; +import temp from 'temp'; +import Command from './command'; +import fs from './fs'; + +export default Test = class Test extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: + apm test + +Runs the package's tests contained within the spec directory (relative +to the current working directory).\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.alias('p', 'path').string('path').describe('path', 'Path to atom command'); + } + + run(options) { + let atomCommand; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const {env} = process; + + if (options.argv.path) { atomCommand = options.argv.path; } + if (!fs.existsSync(atomCommand)) { + atomCommand = 'atom'; + if (process.platform === 'win32') { atomCommand += '.cmd'; } + } + + const packagePath = process.cwd(); + const testArgs = ['--dev', '--test', path.join(packagePath, 'spec')]; + + if (process.platform === 'win32') { + const logFile = temp.openSync({suffix: '.log', prefix: `${path.basename(packagePath)}-`}); + fs.closeSync(logFile.fd); + const logFilePath = logFile.path; + testArgs.push(`--log-file=${logFilePath}`); + + return this.spawn(atomCommand, testArgs, function(code) { + try { + const loggedOutput = fs.readFileSync(logFilePath, 'utf8'); + if (loggedOutput) { process.stdout.write(`${loggedOutput}\n`); } + } catch (error) {} + + if (code === 0) { + process.stdout.write('Tests passed\n'.green); + return callback(); + } else if (code?.message) { + return callback(`Error spawning Atom: ${code.message}`); + } else { + return callback('Tests failed'); + } + }); + } else { + return this.spawn(atomCommand, testArgs, {env, streaming: true}, function(code) { + if (code === 0) { + process.stdout.write('Tests passed\n'.green); + return callback(); + } else if (code?.message) { + return callback(`Error spawning ${atomCommand}: ${code.message}`); + } else { + return callback('Tests failed'); + } + }); + } + } +}; diff --git a/src/text-mate-theme.coffee b/src/text-mate-theme.coffee deleted file mode 100644 index 43e7ce6..0000000 --- a/src/text-mate-theme.coffee +++ /dev/null @@ -1,195 +0,0 @@ -_ = require 'underscore-plus' -plist = require '@atom/plist' -{ScopeSelector} = require 'first-mate' - -module.exports = -class TextMateTheme - constructor: (@contents) -> - @rulesets = [] - @buildRulesets() - - buildRulesets: -> - {settings} = plist.parseStringSync(@contents) ? {} - settings ?= [] - - for setting in settings - {scope, name} = setting.settings - continue if scope or name - - # Require all of these or invalid LESS will be generated if any required - # variable value is missing - {background, foreground, caret, selection, invisibles, lineHighlight} = setting.settings - if background and foreground and caret and selection and lineHighlight and invisibles - variableSettings = setting.settings - break - - unless variableSettings? - throw new Error """ - Could not find the required color settings in the theme. - - The theme being converted must contain a settings array with all of the following keys: - * background - * caret - * foreground - * invisibles - * lineHighlight - * selection - """ - - @buildSyntaxVariables(variableSettings) - @buildGlobalSettingsRulesets(variableSettings) - @buildScopeSelectorRulesets(settings) - - getStylesheet: -> - lines = [ - '@import "syntax-variables";' - '' - ] - for {selector, properties} in @getRulesets() - lines.push("#{selector} {") - lines.push " #{name}: #{value};" for name, value of properties - lines.push("}\n") - lines.join('\n') - - getRulesets: -> @rulesets - - getSyntaxVariables: -> @syntaxVariables - - buildSyntaxVariables: (settings) -> - @syntaxVariables = SyntaxVariablesTemplate - for key, value of settings - replaceRegex = new RegExp("\\{\\{#{key}\\}\\}", 'g') - @syntaxVariables = @syntaxVariables.replace(replaceRegex, @translateColor(value)) - @syntaxVariables - - buildGlobalSettingsRulesets: (settings) -> - @rulesets.push - selector: 'atom-text-editor' - properties: - 'background-color': '@syntax-background-color' - 'color': '@syntax-text-color' - - @rulesets.push - selector: 'atom-text-editor .gutter' - properties: - 'background-color': '@syntax-gutter-background-color' - 'color': '@syntax-gutter-text-color' - - @rulesets.push - selector: 'atom-text-editor .gutter .line-number.cursor-line' - properties: - 'background-color': '@syntax-gutter-background-color-selected' - 'color': '@syntax-gutter-text-color-selected' - - @rulesets.push - selector: 'atom-text-editor .gutter .line-number.cursor-line-no-selection' - properties: - 'color': '@syntax-gutter-text-color-selected' - - @rulesets.push - selector: 'atom-text-editor .wrap-guide' - properties: - 'color': '@syntax-wrap-guide-color' - - @rulesets.push - selector: 'atom-text-editor .indent-guide' - properties: - 'color': '@syntax-indent-guide-color' - - @rulesets.push - selector: 'atom-text-editor .invisible-character' - properties: - 'color': '@syntax-invisible-character-color' - - @rulesets.push - selector: 'atom-text-editor.is-focused .cursor' - properties: - 'border-color': '@syntax-cursor-color' - - @rulesets.push - selector: 'atom-text-editor.is-focused .selection .region' - properties: - 'background-color': '@syntax-selection-color' - - @rulesets.push - selector: 'atom-text-editor.is-focused .line-number.cursor-line-no-selection, - atom-text-editor.is-focused .line.cursor-line' - properties: - 'background-color': @translateColor(settings.lineHighlight) - - buildScopeSelectorRulesets: (scopeSelectorSettings) -> - for {name, scope, settings} in scopeSelectorSettings - continue unless scope - @rulesets.push - comment: name - selector: @translateScopeSelector(scope) - properties: @translateScopeSelectorSettings(settings) - - translateScopeSelector: (textmateScopeSelector) -> - new ScopeSelector(textmateScopeSelector).toCssSyntaxSelector() - - translateScopeSelectorSettings: ({foreground, background, fontStyle}) -> - properties = {} - - if fontStyle - fontStyles = fontStyle.split(/\s+/) - properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold') - properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic') - properties['text-decoration'] = 'underline' if _.contains(fontStyles, 'underline') - - properties['color'] = @translateColor(foreground) if foreground - properties['background-color'] = @translateColor(background) if background - properties - - translateColor: (textmateColor) -> - textmateColor = "##{textmateColor.replace(/^#+/, '')}" - if textmateColor.length <= 7 - textmateColor - else - r = @parseHexColor(textmateColor[1..2]) - g = @parseHexColor(textmateColor[3..4]) - b = @parseHexColor(textmateColor[5..6]) - a = @parseHexColor(textmateColor[7..8]) - a = Math.round((a / 255.0) * 100) / 100 - - "rgba(#{r}, #{g}, #{b}, #{a})" - - parseHexColor: (color) -> - parsed = Math.min(255, Math.max(0, parseInt(color, 16))) - if isNaN(parsed) - 0 - else - parsed - -SyntaxVariablesTemplate = """ - // This defines all syntax variables that syntax themes must implement when they - // include a syntax-variables.less file. - - // General colors - @syntax-text-color: {{foreground}}; - @syntax-cursor-color: {{caret}}; - @syntax-selection-color: {{selection}}; - @syntax-background-color: {{background}}; - - // Guide colors - @syntax-wrap-guide-color: {{invisibles}}; - @syntax-indent-guide-color: {{invisibles}}; - @syntax-invisible-character-color: {{invisibles}}; - - // For find and replace markers - @syntax-result-marker-color: {{invisibles}}; - @syntax-result-marker-color-selected: {{foreground}}; - - // Gutter colors - @syntax-gutter-text-color: {{foreground}}; - @syntax-gutter-text-color-selected: {{foreground}}; - @syntax-gutter-background-color: {{background}}; - @syntax-gutter-background-color-selected: {{lineHighlight}}; - - // For git diff info. i.e. in the gutter - // These are static and were not extracted from your textmate theme - @syntax-color-renamed: #96CBFE; - @syntax-color-added: #A8FF60; - @syntax-color-modified: #E9C062; - @syntax-color-removed: #CC6666; -""" diff --git a/src/text-mate-theme.js b/src/text-mate-theme.js new file mode 100644 index 0000000..2a7fe13 --- /dev/null +++ b/src/text-mate-theme.js @@ -0,0 +1,252 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TextMateTheme; +import _ from 'underscore-plus'; +import plist from '@atom/plist'; +import { ScopeSelector } from 'first-mate'; + +export default TextMateTheme = class TextMateTheme { + constructor(contents) { + this.contents = contents; + this.rulesets = []; + this.buildRulesets(); + } + + buildRulesets() { + let left, variableSettings; + let {settings} = (left = plist.parseStringSync(this.contents)) != null ? left : {}; + if (settings == null) { settings = []; } + + for (let setting of settings) { + const {scope, name} = setting.settings; + if (scope || name) { continue; } + + // Require all of these or invalid LESS will be generated if any required + // variable value is missing + const {background, foreground, caret, selection, invisibles, lineHighlight} = setting.settings; + if (background && foreground && caret && selection && lineHighlight && invisibles) { + variableSettings = setting.settings; + break; + } + } + + if (variableSettings == null) { + throw new Error(`\ +Could not find the required color settings in the theme. + +The theme being converted must contain a settings array with all of the following keys: + * background + * caret + * foreground + * invisibles + * lineHighlight + * selection\ +` + ); + } + + this.buildSyntaxVariables(variableSettings); + this.buildGlobalSettingsRulesets(variableSettings); + return this.buildScopeSelectorRulesets(settings); + } + + getStylesheet() { + const lines = [ + '@import "syntax-variables";', + '' + ]; + for (let {selector, properties} of this.getRulesets()) { + lines.push(`${selector} {`); + for (let name in properties) { const value = properties[name]; lines.push(` ${name}: ${value};`); } + lines.push("}\n"); + } + return lines.join('\n'); + } + + getRulesets() { return this.rulesets; } + + getSyntaxVariables() { return this.syntaxVariables; } + + buildSyntaxVariables(settings) { + this.syntaxVariables = SyntaxVariablesTemplate; + for (let key in settings) { + const value = settings[key]; + const replaceRegex = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); + this.syntaxVariables = this.syntaxVariables.replace(replaceRegex, this.translateColor(value)); + } + return this.syntaxVariables; + } + + buildGlobalSettingsRulesets(settings) { + this.rulesets.push({ + selector: 'atom-text-editor', + properties: { + 'background-color': '@syntax-background-color', + 'color': '@syntax-text-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .gutter', + properties: { + 'background-color': '@syntax-gutter-background-color', + 'color': '@syntax-gutter-text-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .gutter .line-number.cursor-line', + properties: { + 'background-color': '@syntax-gutter-background-color-selected', + 'color': '@syntax-gutter-text-color-selected' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .gutter .line-number.cursor-line-no-selection', + properties: { + 'color': '@syntax-gutter-text-color-selected' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .wrap-guide', + properties: { + 'color': '@syntax-wrap-guide-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .indent-guide', + properties: { + 'color': '@syntax-indent-guide-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor .invisible-character', + properties: { + 'color': '@syntax-invisible-character-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor.is-focused .cursor', + properties: { + 'border-color': '@syntax-cursor-color' + } + }); + + this.rulesets.push({ + selector: 'atom-text-editor.is-focused .selection .region', + properties: { + 'background-color': '@syntax-selection-color' + } + }); + + return this.rulesets.push({ + selector: `atom-text-editor.is-focused .line-number.cursor-line-no-selection, \ +atom-text-editor.is-focused .line.cursor-line`, + properties: { + 'background-color': this.translateColor(settings.lineHighlight) + } + }); + } + + buildScopeSelectorRulesets(scopeSelectorSettings) { + return (() => { + const result = []; + for (let {name, scope, settings} of scopeSelectorSettings) { + if (!scope) { continue; } + result.push(this.rulesets.push({ + comment: name, + selector: this.translateScopeSelector(scope), + properties: this.translateScopeSelectorSettings(settings) + })); + } + return result; + })(); + } + + translateScopeSelector(textmateScopeSelector) { + return new ScopeSelector(textmateScopeSelector).toCssSyntaxSelector(); + } + + translateScopeSelectorSettings({foreground, background, fontStyle}) { + const properties = {}; + + if (fontStyle) { + const fontStyles = fontStyle.split(/\s+/); + if (_.contains(fontStyles, 'bold')) { properties['font-weight'] = 'bold'; } + if (_.contains(fontStyles, 'italic')) { properties['font-style'] = 'italic'; } + if (_.contains(fontStyles, 'underline')) { properties['text-decoration'] = 'underline'; } + } + + if (foreground) { properties['color'] = this.translateColor(foreground); } + if (background) { properties['background-color'] = this.translateColor(background); } + return properties; + } + + translateColor(textmateColor) { + textmateColor = `#${textmateColor.replace(/^#+/, '')}`; + if (textmateColor.length <= 7) { + return textmateColor; + } else { + const r = this.parseHexColor(textmateColor.slice(1, 3)); + const g = this.parseHexColor(textmateColor.slice(3, 5)); + const b = this.parseHexColor(textmateColor.slice(5, 7)); + let a = this.parseHexColor(textmateColor.slice(7, 9)); + a = Math.round((a / 255.0) * 100) / 100; + + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + } + + parseHexColor(color) { + const parsed = Math.min(255, Math.max(0, parseInt(color, 16))); + if (isNaN(parsed)) { + return 0; + } else { + return parsed; + } + } +}; + +var SyntaxVariablesTemplate = `\ +// This defines all syntax variables that syntax themes must implement when they +// include a syntax-variables.less file. + +// General colors +@syntax-text-color: {{foreground}}; +@syntax-cursor-color: {{caret}}; +@syntax-selection-color: {{selection}}; +@syntax-background-color: {{background}}; + +// Guide colors +@syntax-wrap-guide-color: {{invisibles}}; +@syntax-indent-guide-color: {{invisibles}}; +@syntax-invisible-character-color: {{invisibles}}; + +// For find and replace markers +@syntax-result-marker-color: {{invisibles}}; +@syntax-result-marker-color-selected: {{foreground}}; + +// Gutter colors +@syntax-gutter-text-color: {{foreground}}; +@syntax-gutter-text-color-selected: {{foreground}}; +@syntax-gutter-background-color: {{background}}; +@syntax-gutter-background-color-selected: {{lineHighlight}}; + +// For git diff info. i.e. in the gutter +// These are static and were not extracted from your textmate theme +@syntax-color-renamed: #96CBFE; +@syntax-color-added: #A8FF60; +@syntax-color-modified: #E9C062; +@syntax-color-removed: #CC6666;\ +`; diff --git a/src/theme-converter.coffee b/src/theme-converter.coffee deleted file mode 100644 index 265be81..0000000 --- a/src/theme-converter.coffee +++ /dev/null @@ -1,44 +0,0 @@ -path = require 'path' -url = require 'url' -fs = require './fs' -request = require './request' -TextMateTheme = require './text-mate-theme' - -# Convert a TextMate theme to an Atom theme -module.exports = -class ThemeConverter - constructor: (@sourcePath, destinationPath) -> - @destinationPath = path.resolve(destinationPath) - - readTheme: (callback) -> - {protocol} = url.parse(@sourcePath) - if protocol is 'http:' or protocol is 'https:' - requestOptions = url: @sourcePath - request.get requestOptions, (error, response, body) => - if error? - if error.code is 'ENOTFOUND' - error = "Could not resolve URL: #{@sourcePath}" - callback(error) - else if response.statusCode isnt 200 - callback("Request to #{@sourcePath} failed (#{response.headers.status})") - else - callback(null, body) - else - sourcePath = path.resolve(@sourcePath) - if fs.isFileSync(sourcePath) - callback(null, fs.readFileSync(sourcePath, 'utf8')) - else - callback("TextMate theme file not found: #{sourcePath}") - - convert: (callback) -> - @readTheme (error, themeContents) => - return callback(error) if error? - - try - theme = new TextMateTheme(themeContents) - catch error - return callback(error) - - fs.writeFileSync(path.join(@destinationPath, 'styles', 'base.less'), theme.getStylesheet()) - fs.writeFileSync(path.join(@destinationPath, 'styles', 'syntax-variables.less'), theme.getSyntaxVariables()) - callback() diff --git a/src/theme-converter.js b/src/theme-converter.js new file mode 100644 index 0000000..c064df3 --- /dev/null +++ b/src/theme-converter.js @@ -0,0 +1,64 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ThemeConverter; +import path from 'path'; +import url from 'url'; +import fs from './fs'; +import request from './request'; +import TextMateTheme from './text-mate-theme'; + +// Convert a TextMate theme to an Atom theme +export default ThemeConverter = class ThemeConverter { + constructor(sourcePath, destinationPath) { + this.sourcePath = sourcePath; + this.destinationPath = path.resolve(destinationPath); + } + + readTheme(callback) { + const {protocol} = url.parse(this.sourcePath); + if ((protocol === 'http:') || (protocol === 'https:')) { + const requestOptions = {url: this.sourcePath}; + return request.get(requestOptions, (error, response, body) => { + if (error != null) { + if (error.code === 'ENOTFOUND') { + error = `Could not resolve URL: ${this.sourcePath}`; + } + return callback(error); + } else if (response.statusCode !== 200) { + return callback(`Request to ${this.sourcePath} failed (${response.headers.status})`); + } else { + return callback(null, body); + } + }); + } else { + const sourcePath = path.resolve(this.sourcePath); + if (fs.isFileSync(sourcePath)) { + return callback(null, fs.readFileSync(sourcePath, 'utf8')); + } else { + return callback(`TextMate theme file not found: ${sourcePath}`); + } + } + } + + convert(callback) { + return this.readTheme((error, themeContents) => { + let theme; + if (error != null) { return callback(error); } + + try { + theme = new TextMateTheme(themeContents); + } catch (error1) { + error = error1; + return callback(error); + } + + fs.writeFileSync(path.join(this.destinationPath, 'styles', 'base.less'), theme.getStylesheet()); + fs.writeFileSync(path.join(this.destinationPath, 'styles', 'syntax-variables.less'), theme.getSyntaxVariables()); + return callback(); + }); + } +}; diff --git a/src/tree.coffee b/src/tree.coffee deleted file mode 100644 index 26e585f..0000000 --- a/src/tree.coffee +++ /dev/null @@ -1,18 +0,0 @@ -_ = require 'underscore-plus' - -module.exports = (items, options={}, callback) -> - if _.isFunction(options) - callback = options - options = {} - callback ?= (item) -> item - - if items.length is 0 - emptyMessage = options.emptyMessage ? '(empty)' - console.log "\u2514\u2500\u2500 #{emptyMessage}" - else - for item, index in items - if index is items.length - 1 - itemLine = '\u2514\u2500\u2500 ' - else - itemLine = '\u251C\u2500\u2500 ' - console.log "#{itemLine}#{callback(item)}" diff --git a/src/tree.js b/src/tree.js new file mode 100644 index 0000000..34c625c --- /dev/null +++ b/src/tree.js @@ -0,0 +1,36 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import _ from 'underscore-plus'; + +export default function(items, options={}, callback) { + if (_.isFunction(options)) { + callback = options; + options = {}; + } + if (callback == null) { callback = item => item; } + + if (items.length === 0) { + const emptyMessage = options.emptyMessage != null ? options.emptyMessage : '(empty)'; + return console.log(`\u2514\u2500\u2500 ${emptyMessage}`); + } else { + return (() => { + const result = []; + for (let index = 0; index < items.length; index++) { + var itemLine; + const item = items[index]; + if (index === (items.length - 1)) { + itemLine = '\u2514\u2500\u2500 '; + } else { + itemLine = '\u251C\u2500\u2500 '; + } + result.push(console.log(`${itemLine}${callback(item)}`)); + } + return result; + })(); + } +}; diff --git a/src/uninstall.coffee b/src/uninstall.coffee deleted file mode 100644 index a5f135d..0000000 --- a/src/uninstall.coffee +++ /dev/null @@ -1,92 +0,0 @@ -path = require 'path' - -async = require 'async' -CSON = require 'season' -yargs = require 'yargs' - -auth = require './auth' -Command = require './command' -config = require './apm' -fs = require './fs' -request = require './request' - -module.exports = -class Uninstall extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm uninstall ... - - Delete the installed package(s) from the ~/.atom/packages directory. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('d', 'dev').boolean('dev').describe('dev', 'Uninstall from ~/.atom/dev/packages') - options.boolean('hard').describe('hard', 'Uninstall from ~/.atom/packages and ~/.atom/dev/packages') - - getPackageVersion: (packageDirectory) -> - try - CSON.readFileSync(path.join(packageDirectory, 'package.json'))?.version - catch error - null - - registerUninstall: ({packageName, packageVersion}, callback) -> - return callback() unless packageVersion - - auth.getToken (error, token) -> - return callback() unless token - - requestOptions = - url: "#{config.getAtomPackagesUrl()}/#{packageName}/versions/#{packageVersion}/events/uninstall" - json: true - headers: - authorization: token - - request.post requestOptions, (error, response, body) -> callback() - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - packageNames = @packageNamesFromArgv(options.argv) - - if packageNames.length is 0 - callback("Please specify a package name to uninstall") - return - - packagesDirectory = path.join(config.getAtomDirectory(), 'packages') - devPackagesDirectory = path.join(config.getAtomDirectory(), 'dev', 'packages') - - uninstallsToRegister = [] - uninstallError = null - - for packageName in packageNames - if packageName is '.' - packageName = path.basename(process.cwd()) - process.stdout.write "Uninstalling #{packageName} " - try - unless options.argv.dev - packageDirectory = path.join(packagesDirectory, packageName) - packageManifestPath = path.join(packageDirectory, 'package.json') - if fs.existsSync(packageManifestPath) - packageVersion = @getPackageVersion(packageDirectory) - fs.removeSync(packageDirectory) - if packageVersion - uninstallsToRegister.push({packageName, packageVersion}) - else if not options.argv.hard - throw new Error("No package.json found at #{packageManifestPath}") - - if options.argv.hard or options.argv.dev - packageDirectory = path.join(devPackagesDirectory, packageName) - if fs.existsSync(packageDirectory) - fs.removeSync(packageDirectory) - else if not options.argv.hard - throw new Error("Does not exist") - - @logSuccess() - catch error - @logFailure() - uninstallError = new Error("Failed to delete #{packageName}: #{error.message}") - break - - async.eachSeries uninstallsToRegister, @registerUninstall.bind(this), -> - callback(uninstallError) diff --git a/src/uninstall.js b/src/uninstall.js new file mode 100644 index 0000000..f57e380 --- /dev/null +++ b/src/uninstall.js @@ -0,0 +1,114 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Uninstall; +import path from 'path'; +import async from 'async'; +import CSON from 'season'; +import yargs from 'yargs'; +import auth from './auth'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; +import request from './request'; + +export default Uninstall = class Uninstall extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm uninstall ... + +Delete the installed package(s) from the ~/.atom/packages directory.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('d', 'dev').boolean('dev').describe('dev', 'Uninstall from ~/.atom/dev/packages'); + return options.boolean('hard').describe('hard', 'Uninstall from ~/.atom/packages and ~/.atom/dev/packages'); + } + + getPackageVersion(packageDirectory) { + try { + return CSON.readFileSync(path.join(packageDirectory, 'package.json'))?.version; + } catch (error) { + return null; + } + } + + registerUninstall({packageName, packageVersion}, callback) { + if (!packageVersion) { return callback(); } + + return auth.getToken(function(error, token) { + if (!token) { return callback(); } + + const requestOptions = { + url: `${config.getAtomPackagesUrl()}/${packageName}/versions/${packageVersion}/events/uninstall`, + json: true, + headers: { + authorization: token + } + }; + + return request.post(requestOptions, (error, response, body) => callback()); + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const packageNames = this.packageNamesFromArgv(options.argv); + + if (packageNames.length === 0) { + callback("Please specify a package name to uninstall"); + return; + } + + const packagesDirectory = path.join(config.getAtomDirectory(), 'packages'); + const devPackagesDirectory = path.join(config.getAtomDirectory(), 'dev', 'packages'); + + const uninstallsToRegister = []; + let uninstallError = null; + + for (let packageName of packageNames) { + if (packageName === '.') { + packageName = path.basename(process.cwd()); + } + process.stdout.write(`Uninstalling ${packageName} `); + try { + var packageDirectory; + if (!options.argv.dev) { + packageDirectory = path.join(packagesDirectory, packageName); + const packageManifestPath = path.join(packageDirectory, 'package.json'); + if (fs.existsSync(packageManifestPath)) { + const packageVersion = this.getPackageVersion(packageDirectory); + fs.removeSync(packageDirectory); + if (packageVersion) { + uninstallsToRegister.push({packageName, packageVersion}); + } + } else if (!options.argv.hard) { + throw new Error(`No package.json found at ${packageManifestPath}`); + } + } + + if (options.argv.hard || options.argv.dev) { + packageDirectory = path.join(devPackagesDirectory, packageName); + if (fs.existsSync(packageDirectory)) { + fs.removeSync(packageDirectory); + } else if (!options.argv.hard) { + throw new Error("Does not exist"); + } + } + + this.logSuccess(); + } catch (error) { + this.logFailure(); + uninstallError = new Error(`Failed to delete ${packageName}: ${error.message}`); + break; + } + } + + return async.eachSeries(uninstallsToRegister, this.registerUninstall.bind(this), () => callback(uninstallError)); + } +}; diff --git a/src/unlink.coffee b/src/unlink.coffee deleted file mode 100644 index d25440d..0000000 --- a/src/unlink.coffee +++ /dev/null @@ -1,92 +0,0 @@ -path = require 'path' - -CSON = require 'season' -yargs = require 'yargs' - -Command = require './command' -config = require './apm' -fs = require './fs' - -module.exports = -class Unlink extends Command - constructor: -> - super() - @devPackagesPath = path.join(config.getAtomDirectory(), 'dev', 'packages') - @packagesPath = path.join(config.getAtomDirectory(), 'packages') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm unlink [] - - Delete the symlink in ~/.atom/packages for the package. The package in the - current working directory is unlinked if no path is given. - - Run `apm links` to view all the currently linked packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('d', 'dev').boolean('dev').describe('dev', 'Unlink package from ~/.atom/dev/packages') - options.boolean('hard').describe('hard', 'Unlink package from ~/.atom/packages and ~/.atom/dev/packages') - options.alias('a', 'all').boolean('all').describe('all', 'Unlink all packages in ~/.atom/packages and ~/.atom/dev/packages') - - getDevPackagePath: (packageName) -> path.join(@devPackagesPath, packageName) - - getPackagePath: (packageName) -> path.join(@packagesPath, packageName) - - unlinkPath: (pathToUnlink) -> - try - process.stdout.write "Unlinking #{pathToUnlink} " - fs.unlinkSync(pathToUnlink) - @logSuccess() - catch error - @logFailure() - throw error - - unlinkAll: (options, callback) -> - try - for child in fs.list(@devPackagesPath) - packagePath = path.join(@devPackagesPath, child) - @unlinkPath(packagePath) if fs.isSymbolicLinkSync(packagePath) - unless options.argv.dev - for child in fs.list(@packagesPath) - packagePath = path.join(@packagesPath, child) - @unlinkPath(packagePath) if fs.isSymbolicLinkSync(packagePath) - callback() - catch error - callback(error) - - unlinkPackage: (options, callback) -> - packagePath = options.argv._[0]?.toString() ? '.' - linkPath = path.resolve(process.cwd(), packagePath) - - try - packageName = CSON.readFileSync(CSON.resolve(path.join(linkPath, 'package'))).name - packageName = path.basename(linkPath) unless packageName - - if options.argv.hard - try - @unlinkPath(@getDevPackagePath(packageName)) - @unlinkPath(@getPackagePath(packageName)) - callback() - catch error - callback(error) - else - if options.argv.dev - targetPath = @getDevPackagePath(packageName) - else - targetPath = @getPackagePath(packageName) - try - @unlinkPath(targetPath) - callback() - catch error - callback(error) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - - if options.argv.all - @unlinkAll(options, callback) - else - @unlinkPackage(options, callback) diff --git a/src/unlink.js b/src/unlink.js new file mode 100644 index 0000000..4e96438 --- /dev/null +++ b/src/unlink.js @@ -0,0 +1,121 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Unlink; +import path from 'path'; +import CSON from 'season'; +import yargs from 'yargs'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; + +export default Unlink = class Unlink extends Command { + constructor() { + super(); + this.devPackagesPath = path.join(config.getAtomDirectory(), 'dev', 'packages'); + this.packagesPath = path.join(config.getAtomDirectory(), 'packages'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm unlink [] + +Delete the symlink in ~/.atom/packages for the package. The package in the +current working directory is unlinked if no path is given. + +Run \`apm links\` to view all the currently linked packages.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('d', 'dev').boolean('dev').describe('dev', 'Unlink package from ~/.atom/dev/packages'); + options.boolean('hard').describe('hard', 'Unlink package from ~/.atom/packages and ~/.atom/dev/packages'); + return options.alias('a', 'all').boolean('all').describe('all', 'Unlink all packages in ~/.atom/packages and ~/.atom/dev/packages'); + } + + getDevPackagePath(packageName) { return path.join(this.devPackagesPath, packageName); } + + getPackagePath(packageName) { return path.join(this.packagesPath, packageName); } + + unlinkPath(pathToUnlink) { + try { + process.stdout.write(`Unlinking ${pathToUnlink} `); + fs.unlinkSync(pathToUnlink); + return this.logSuccess(); + } catch (error) { + this.logFailure(); + throw error; + } + } + + unlinkAll(options, callback) { + try { + let child, packagePath; + for (child of fs.list(this.devPackagesPath)) { + packagePath = path.join(this.devPackagesPath, child); + if (fs.isSymbolicLinkSync(packagePath)) { this.unlinkPath(packagePath); } + } + if (!options.argv.dev) { + for (child of fs.list(this.packagesPath)) { + packagePath = path.join(this.packagesPath, child); + if (fs.isSymbolicLinkSync(packagePath)) { this.unlinkPath(packagePath); } + } + } + return callback(); + } catch (error) { + return callback(error); + } + } + + unlinkPackage(options, callback) { + let error, left, packageName; + const packagePath = (left = options.argv._[0]?.toString()) != null ? left : '.'; + const linkPath = path.resolve(process.cwd(), packagePath); + + try { + packageName = CSON.readFileSync(CSON.resolve(path.join(linkPath, 'package'))).name; + } catch (error3) {} + if (!packageName) { packageName = path.basename(linkPath); } + + if (options.argv.hard) { + try { + this.unlinkPath(this.getDevPackagePath(packageName)); + this.unlinkPath(this.getPackagePath(packageName)); + return callback(); + } catch (error1) { + error = error1; + return callback(error); + } + } else { + let targetPath; + if (options.argv.dev) { + targetPath = this.getDevPackagePath(packageName); + } else { + targetPath = this.getPackagePath(packageName); + } + try { + this.unlinkPath(targetPath); + return callback(); + } catch (error2) { + error = error2; + return callback(error); + } + } + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + + if (options.argv.all) { + return this.unlinkAll(options, callback); + } else { + return this.unlinkPackage(options, callback); + } + } +}; diff --git a/src/unpublish.coffee b/src/unpublish.coffee deleted file mode 100644 index 2bf51f8..0000000 --- a/src/unpublish.coffee +++ /dev/null @@ -1,107 +0,0 @@ -path = require 'path' -readline = require 'readline' - -yargs = require 'yargs' - -auth = require './auth' -Command = require './command' -config = require './apm' -fs = require './fs' -request = require './request' - -module.exports = -class Unpublish extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - - options.usage """ - Usage: apm unpublish [] - apm unpublish @ - - Remove a published package or package version from the atom.io registry. - - The package in the current working directory will be used if no package - name is specified. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('f', 'force').boolean('force').describe('force', 'Do not prompt for confirmation') - - unpublishPackage: (packageName, packageVersion, callback) -> - packageLabel = packageName - packageLabel += "@#{packageVersion}" if packageVersion - - process.stdout.write "Unpublishing #{packageLabel} " - - auth.getToken (error, token) => - if error? - @logFailure() - callback(error) - return - - options = - uri: "#{config.getAtomPackagesUrl()}/#{packageName}" - headers: - authorization: token - json: true - - options.uri += "/versions/#{packageVersion}" if packageVersion - - request.del options, (error, response, body={}) => - if error? - @logFailure() - callback(error) - else if response.statusCode isnt 204 - @logFailure() - message = body.message ? body.error ? body - callback("Unpublishing failed: #{message}") - else - @logSuccess() - callback() - - promptForConfirmation: (packageName, packageVersion, callback) -> - packageLabel = packageName - packageLabel += "@#{packageVersion}" if packageVersion - - if packageVersion - question = "Are you sure you want to unpublish '#{packageLabel}'? (no) " - else - question = "Are you sure you want to unpublish ALL VERSIONS of '#{packageLabel}'? " + - "This will remove it from the apm registry, including " + - "download counts and stars, and this action is irreversible. (no)" - - @prompt question, (answer) => - answer = if answer then answer.trim().toLowerCase() else 'no' - if answer in ['y', 'yes'] - @unpublishPackage(packageName, packageVersion, callback) - else - callback("Cancelled unpublishing #{packageLabel}") - - prompt: (question, callback) -> - prompt = readline.createInterface(process.stdin, process.stdout) - - prompt.question question, (answer) -> - prompt.close() - callback(answer) - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - [name] = options.argv._ - - if name?.length > 0 - atIndex = name.indexOf('@') - if atIndex isnt -1 - version = name.substring(atIndex + 1) - name = name.substring(0, atIndex) - - unless name - try - name = JSON.parse(fs.readFileSync('package.json'))?.name - - unless name - name = path.basename(process.cwd()) - - if options.argv.force - @unpublishPackage(name, version, callback) - else - @promptForConfirmation(name, version, callback) diff --git a/src/unpublish.js b/src/unpublish.js new file mode 100644 index 0000000..0da7889 --- /dev/null +++ b/src/unpublish.js @@ -0,0 +1,138 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Unpublish; +import path from 'path'; +import readline from 'readline'; +import yargs from 'yargs'; +import auth from './auth'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; +import request from './request'; + +export default Unpublish = class Unpublish extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + + options.usage(`\ +Usage: apm unpublish [] + apm unpublish @ + +Remove a published package or package version from the atom.io registry. + +The package in the current working directory will be used if no package +name is specified.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + return options.alias('f', 'force').boolean('force').describe('force', 'Do not prompt for confirmation'); + } + + unpublishPackage(packageName, packageVersion, callback) { + let packageLabel = packageName; + if (packageVersion) { packageLabel += `@${packageVersion}`; } + + process.stdout.write(`Unpublishing ${packageLabel} `); + + return auth.getToken((error, token) => { + if (error != null) { + this.logFailure(); + callback(error); + return; + } + + const options = { + uri: `${config.getAtomPackagesUrl()}/${packageName}`, + headers: { + authorization: token + }, + json: true + }; + + if (packageVersion) { options.uri += `/versions/${packageVersion}`; } + + return request.del(options, (error, response, body={}) => { + if (error != null) { + this.logFailure(); + return callback(error); + } else if (response.statusCode !== 204) { + let left; + this.logFailure(); + const message = (left = body.message != null ? body.message : body.error) != null ? left : body; + return callback(`Unpublishing failed: ${message}`); + } else { + this.logSuccess(); + return callback(); + } + }); + }); + } + + promptForConfirmation(packageName, packageVersion, callback) { + let question; + let packageLabel = packageName; + if (packageVersion) { packageLabel += `@${packageVersion}`; } + + if (packageVersion) { + question = `Are you sure you want to unpublish '${packageLabel}'? (no) `; + } else { + question = `Are you sure you want to unpublish ALL VERSIONS of '${packageLabel}'? ` + + "This will remove it from the apm registry, including " + + "download counts and stars, and this action is irreversible. (no)"; + } + + return this.prompt(question, answer => { + answer = answer ? answer.trim().toLowerCase() : 'no'; + if (['y', 'yes'].includes(answer)) { + return this.unpublishPackage(packageName, packageVersion, callback); + } else { + return callback(`Cancelled unpublishing ${packageLabel}`); + } + }); + } + + prompt(question, callback) { + const prompt = readline.createInterface(process.stdin, process.stdout); + + return prompt.question(question, function(answer) { + prompt.close(); + return callback(answer); + }); + } + + run(options) { + let version; + const {callback} = options; + options = this.parseOptions(options.commandArgs); + let [name] = options.argv._; + + if (name?.length > 0) { + const atIndex = name.indexOf('@'); + if (atIndex !== -1) { + version = name.substring(atIndex + 1); + name = name.substring(0, atIndex); + } + } + + if (!name) { + try { + name = JSON.parse(fs.readFileSync('package.json'))?.name; + } catch (error) {} + } + + if (!name) { + name = path.basename(process.cwd()); + } + + if (options.argv.force) { + return this.unpublishPackage(name, version, callback); + } else { + return this.promptForConfirmation(name, version, callback); + } + } +}; diff --git a/src/unstar.coffee b/src/unstar.coffee deleted file mode 100644 index 3a900b5..0000000 --- a/src/unstar.coffee +++ /dev/null @@ -1,57 +0,0 @@ -async = require 'async' -yargs = require 'yargs' - -config = require './apm' -Command = require './command' -Login = require './login' -request = require './request' - -module.exports = -class Unstar extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm unstar ... - - Unstar the given packages on https://atom.io - - Run `apm stars` to see all your starred packages. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - - starPackage: (packageName, token, callback) -> - process.stdout.write '\uD83D\uDC5F \u2B50 ' if process.platform is 'darwin' - process.stdout.write "Unstarring #{packageName} " - requestSettings = - json: true - url: "#{config.getAtomPackagesUrl()}/#{packageName}/star" - headers: - authorization: token - request.del requestSettings, (error, response, body={}) => - if error? - @logFailure() - callback(error) - else if response.statusCode isnt 204 - @logFailure() - message = body.message ? body.error ? body - callback("Unstarring package failed: #{message}") - else - @logSuccess() - callback() - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - packageNames = @packageNamesFromArgv(options.argv) - - if packageNames.length is 0 - callback("Please specify a package name to unstar") - return - - Login.getTokenOrLogin (error, token) => - return callback(error) if error? - - commands = packageNames.map (packageName) => - (callback) => @starPackage(packageName, token, callback) - async.waterfall(commands, callback) diff --git a/src/unstar.js b/src/unstar.js new file mode 100644 index 0000000..ed00d98 --- /dev/null +++ b/src/unstar.js @@ -0,0 +1,76 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Unstar; +import async from 'async'; +import yargs from 'yargs'; +import config from './apm'; +import Command from './command'; +import Login from './login'; +import request from './request'; + +export default Unstar = class Unstar extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm unstar ... + +Unstar the given packages on https://atom.io + +Run \`apm stars\` to see all your starred packages.\ +` + ); + return options.alias('h', 'help').describe('help', 'Print this usage message'); + } + + starPackage(packageName, token, callback) { + if (process.platform === 'darwin') { process.stdout.write('\uD83D\uDC5F \u2B50 '); } + process.stdout.write(`Unstarring ${packageName} `); + const requestSettings = { + json: true, + url: `${config.getAtomPackagesUrl()}/${packageName}/star`, + headers: { + authorization: token + } + }; + return request.del(requestSettings, (error, response, body={}) => { + if (error != null) { + this.logFailure(); + return callback(error); + } else if (response.statusCode !== 204) { + let left; + this.logFailure(); + const message = (left = body.message != null ? body.message : body.error) != null ? left : body; + return callback(`Unstarring package failed: ${message}`); + } else { + this.logSuccess(); + return callback(); + } + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const packageNames = this.packageNamesFromArgv(options.argv); + + if (packageNames.length === 0) { + callback("Please specify a package name to unstar"); + return; + } + + return Login.getTokenOrLogin((error, token) => { + if (error != null) { return callback(error); } + + const commands = packageNames.map(packageName => { + return callback => this.starPackage(packageName, token, callback); + }); + return async.waterfall(commands, callback); + }); + } +}; diff --git a/src/upgrade.coffee b/src/upgrade.coffee deleted file mode 100644 index 96ed7e9..0000000 --- a/src/upgrade.coffee +++ /dev/null @@ -1,220 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -async = require 'async' -yargs = require 'yargs' -read = require 'read' -semver = require 'semver' -Git = require 'git-utils' - -Command = require './command' -config = require './apm' -fs = require './fs' -Install = require './install' -Packages = require './packages' -request = require './request' -tree = require './tree' -git = require './git' - -module.exports = -class Upgrade extends Command - constructor: -> - super() - @atomDirectory = config.getAtomDirectory() - @atomPackagesDirectory = path.join(@atomDirectory, 'packages') - - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm upgrade - apm upgrade --list - apm upgrade [...] - - Upgrade out of date packages installed to ~/.atom/packages - - This command lists the out of date packages and then prompts to install - available updates. - """ - options.alias('c', 'confirm').boolean('confirm').default('confirm', true).describe('confirm', 'Confirm before installing updates') - options.alias('h', 'help').describe('help', 'Print this usage message') - options.alias('l', 'list').boolean('list').describe('list', 'List but don\'t install the outdated packages') - options.boolean('json').describe('json', 'Output outdated packages as a JSON array') - options.string('compatible').describe('compatible', 'Only list packages/themes compatible with this Atom version') - options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information') - - getInstalledPackages: (options) -> - packages = [] - for name in fs.list(@atomPackagesDirectory) - if pack = @getIntalledPackage(name) - packages.push(pack) - - packageNames = @packageNamesFromArgv(options.argv) - if packageNames.length > 0 - packages = packages.filter ({name}) -> packageNames.indexOf(name) isnt -1 - - packages - - getIntalledPackage: (name) -> - packageDirectory = path.join(@atomPackagesDirectory, name) - return if fs.isSymbolicLinkSync(packageDirectory) - try - metadata = JSON.parse(fs.readFileSync(path.join(packageDirectory, 'package.json'))) - return metadata if metadata?.name and metadata?.version - - loadInstalledAtomVersion: (options, callback) -> - if options.argv.compatible - process.nextTick => - version = @normalizeVersion(options.argv.compatible) - @installedAtomVersion = version if semver.valid(version) - callback() - else - @loadInstalledAtomMetadata(callback) - - folderIsRepo: (pack) -> - repoGitFolderPath = path.join(@atomPackagesDirectory, pack.name, '.git') - return fs.existsSync repoGitFolderPath - - getLatestVersion: (pack, callback) -> - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{pack.name}" - json: true - request.get requestSettings, (error, response, body={}) => - if error? - callback("Request for package information failed: #{error.message}") - else if response.statusCode is 404 - callback() - else if response.statusCode isnt 200 - message = body.message ? body.error ? body - callback("Request for package information failed: #{message}") - else - atomVersion = @installedAtomVersion - latestVersion = pack.version - for version, metadata of body.versions ? {} - continue unless semver.valid(version) - continue unless metadata - - engine = metadata.engines?.atom ? '*' - continue unless semver.validRange(engine) - continue unless semver.satisfies(atomVersion, engine) - - latestVersion = version if semver.gt(version, latestVersion) - - if latestVersion isnt pack.version and @hasRepo(pack) - callback(null, latestVersion) - else - callback() - - getLatestSha: (pack, callback) -> - repoPath = path.join(@atomPackagesDirectory, pack.name) - config.getSetting 'git', (command) => - command ?= 'git' - args = ['fetch', 'origin', 'master'] - git.addGitToEnv(process.env) - @spawn command, args, {cwd: repoPath}, (code, stderr='', stdout='') -> - return callback(new Error('Exit code: ' + code + ' - ' + stderr)) unless code is 0 - repo = Git.open(repoPath) - sha = repo.getReferenceTarget(repo.getUpstreamBranch('refs/heads/master')) - if sha isnt pack.apmInstallSource.sha - callback(null, sha) - else - callback() - - hasRepo: (pack) -> - Packages.getRepository(pack)? - - getAvailableUpdates: (packages, callback) -> - getLatestVersionOrSha = (pack, done) => - if @folderIsRepo(pack) and pack.apmInstallSource?.type is 'git' - @getLatestSha pack, (err, sha) -> - done(err, {pack, sha}) - else - @getLatestVersion pack, (err, latestVersion) -> - done(err, {pack, latestVersion}) - - async.mapLimit packages, 10, getLatestVersionOrSha, (error, updates) -> - return callback(error) if error? - - updates = _.filter updates, (update) -> update.latestVersion? or update.sha? - updates.sort (updateA, updateB) -> - updateA.pack.name.localeCompare(updateB.pack.name) - - callback(null, updates) - - promptForConfirmation: (callback) -> - read {prompt: 'Would you like to install these updates? (yes)', edit: true}, (error, answer) -> - answer = if answer then answer.trim().toLowerCase() else 'yes' - callback(error, answer is 'y' or answer is 'yes') - - installUpdates: (updates, callback) -> - installCommands = [] - verbose = @verbose - for {pack, latestVersion} in updates - do (pack, latestVersion) -> - installCommands.push (callback) -> - if pack.apmInstallSource?.type is 'git' - commandArgs = [pack.apmInstallSource.source] - else - commandArgs = ["#{pack.name}@#{latestVersion}"] - commandArgs.unshift('--verbose') if verbose - new Install().run({callback, commandArgs}) - - async.waterfall(installCommands, callback) - - run: (options) -> - {callback, command} = options - options = @parseOptions(options.commandArgs) - options.command = command - - @verbose = options.argv.verbose - if @verbose - request.debug(true) - process.env.NODE_DEBUG = 'request' - - @loadInstalledAtomVersion options, => - if @installedAtomVersion - @upgradePackages(options, callback) - else - callback('Could not determine current Atom version installed') - - upgradePackages: (options, callback) -> - packages = @getInstalledPackages(options) - @getAvailableUpdates packages, (error, updates) => - return callback(error) if error? - - if options.argv.json - packagesWithLatestVersionOrSha = updates.map ({pack, latestVersion, sha}) -> - pack.latestVersion = latestVersion if latestVersion - pack.latestSha = sha if sha - pack - console.log JSON.stringify(packagesWithLatestVersionOrSha) - else - console.log "Package Updates Available".cyan + " (#{updates.length})" - tree updates, ({pack, latestVersion, sha}) -> - {name, apmInstallSource, version} = pack - name = name.yellow - if sha? - version = apmInstallSource.sha.substr(0, 8).red - latestVersion = sha.substr(0, 8).green - else - version = version.red - latestVersion = latestVersion.green - latestVersion = latestVersion?.green or apmInstallSource?.sha?.green - "#{name} #{version} -> #{latestVersion}" - - return callback() if options.command is 'outdated' - return callback() if options.argv.list - return callback() if updates.length is 0 - - console.log() - if options.argv.confirm - @promptForConfirmation (error, confirmed) => - return callback(error) if error? - - if confirmed - console.log() - @installUpdates(updates, callback) - else - callback() - else - @installUpdates(updates, callback) diff --git a/src/upgrade.js b/src/upgrade.js new file mode 100644 index 0000000..69920a8 --- /dev/null +++ b/src/upgrade.js @@ -0,0 +1,277 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Upgrade; +import path from 'path'; +import _ from 'underscore-plus'; +import async from 'async'; +import yargs from 'yargs'; +import read from 'read'; +import semver from 'semver'; +import Git from 'git-utils'; +import Command from './command'; +import config from './apm'; +import fs from './fs'; +import Install from './install'; +import Packages from './packages'; +import request from './request'; +import tree from './tree'; +import git from './git'; + +export default Upgrade = class Upgrade extends Command { + constructor() { + super(); + this.atomDirectory = config.getAtomDirectory(); + this.atomPackagesDirectory = path.join(this.atomDirectory, 'packages'); + } + + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm upgrade + apm upgrade --list + apm upgrade [...] + +Upgrade out of date packages installed to ~/.atom/packages + +This command lists the out of date packages and then prompts to install +available updates.\ +` + ); + options.alias('c', 'confirm').boolean('confirm').default('confirm', true).describe('confirm', 'Confirm before installing updates'); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.alias('l', 'list').boolean('list').describe('list', 'List but don\'t install the outdated packages'); + options.boolean('json').describe('json', 'Output outdated packages as a JSON array'); + options.string('compatible').describe('compatible', 'Only list packages/themes compatible with this Atom version'); + return options.boolean('verbose').default('verbose', false).describe('verbose', 'Show verbose debug information'); + } + + getInstalledPackages(options) { + let packages = []; + for (let name of fs.list(this.atomPackagesDirectory)) { + var pack; + if (pack = this.getIntalledPackage(name)) { + packages.push(pack); + } + } + + const packageNames = this.packageNamesFromArgv(options.argv); + if (packageNames.length > 0) { + packages = packages.filter(({name}) => packageNames.indexOf(name) !== -1); + } + + return packages; + } + + getIntalledPackage(name) { + const packageDirectory = path.join(this.atomPackagesDirectory, name); + if (fs.isSymbolicLinkSync(packageDirectory)) { return; } + try { + const metadata = JSON.parse(fs.readFileSync(path.join(packageDirectory, 'package.json'))); + if (metadata?.name && metadata?.version) { return metadata; } + } catch (error) {} + } + + loadInstalledAtomVersion(options, callback) { + if (options.argv.compatible) { + return process.nextTick(() => { + const version = this.normalizeVersion(options.argv.compatible); + if (semver.valid(version)) { this.installedAtomVersion = version; } + return callback(); + }); + } else { + return this.loadInstalledAtomMetadata(callback); + } + } + + folderIsRepo(pack) { + const repoGitFolderPath = path.join(this.atomPackagesDirectory, pack.name, '.git'); + return fs.existsSync(repoGitFolderPath); + } + + getLatestVersion(pack, callback) { + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${pack.name}`, + json: true + }; + return request.get(requestSettings, (error, response, body={}) => { + if (error != null) { + return callback(`Request for package information failed: ${error.message}`); + } else if (response.statusCode === 404) { + return callback(); + } else if (response.statusCode !== 200) { + let left; + const message = (left = body.message != null ? body.message : body.error) != null ? left : body; + return callback(`Request for package information failed: ${message}`); + } else { + let version; + const atomVersion = this.installedAtomVersion; + let latestVersion = pack.version; + const object = body.versions != null ? body.versions : {}; + for (version in object) { + const metadata = object[version]; + if (!semver.valid(version)) { continue; } + if (!metadata) { continue; } + + const engine = metadata.engines?.atom != null ? metadata.engines?.atom : '*'; + if (!semver.validRange(engine)) { continue; } + if (!semver.satisfies(atomVersion, engine)) { continue; } + + if (semver.gt(version, latestVersion)) { latestVersion = version; } + } + + if ((latestVersion !== pack.version) && this.hasRepo(pack)) { + return callback(null, latestVersion); + } else { + return callback(); + } + } + }); + } + + getLatestSha(pack, callback) { + const repoPath = path.join(this.atomPackagesDirectory, pack.name); + return config.getSetting('git', command => { + if (command == null) { command = 'git'; } + const args = ['fetch', 'origin', 'master']; + git.addGitToEnv(process.env); + return this.spawn(command, args, {cwd: repoPath}, function(code, stderr='', stdout='') { + if (code !== 0) { return callback(new Error('Exit code: ' + code + ' - ' + stderr)); } + const repo = Git.open(repoPath); + const sha = repo.getReferenceTarget(repo.getUpstreamBranch('refs/heads/master')); + if (sha !== pack.apmInstallSource.sha) { + return callback(null, sha); + } else { + return callback(); + } + }); + }); + } + + hasRepo(pack) { + return (Packages.getRepository(pack) != null); + } + + getAvailableUpdates(packages, callback) { + const getLatestVersionOrSha = (pack, done) => { + if (this.folderIsRepo(pack) && (pack.apmInstallSource?.type === 'git')) { + return this.getLatestSha(pack, (err, sha) => done(err, {pack, sha})); + } else { + return this.getLatestVersion(pack, (err, latestVersion) => done(err, {pack, latestVersion})); + } + }; + + return async.mapLimit(packages, 10, getLatestVersionOrSha, function(error, updates) { + if (error != null) { return callback(error); } + + updates = _.filter(updates, update => (update.latestVersion != null) || (update.sha != null)); + updates.sort((updateA, updateB) => updateA.pack.name.localeCompare(updateB.pack.name)); + + return callback(null, updates); + }); + } + + promptForConfirmation(callback) { + return read({prompt: 'Would you like to install these updates? (yes)', edit: true}, function(error, answer) { + answer = answer ? answer.trim().toLowerCase() : 'yes'; + return callback(error, (answer === 'y') || (answer === 'yes')); + }); + } + + installUpdates(updates, callback) { + const installCommands = []; + const { + verbose + } = this; + for (let {pack, latestVersion} of updates) { + (((pack, latestVersion) => installCommands.push(function(callback) { + let commandArgs; + if (pack.apmInstallSource?.type === 'git') { + commandArgs = [pack.apmInstallSource.source]; + } else { + commandArgs = [`${pack.name}@${latestVersion}`]; + } + if (verbose) { commandArgs.unshift('--verbose'); } + return new Install().run({callback, commandArgs}); + })))(pack, latestVersion); + } + + return async.waterfall(installCommands, callback); + } + + run(options) { + const {callback, command} = options; + options = this.parseOptions(options.commandArgs); + options.command = command; + + this.verbose = options.argv.verbose; + if (this.verbose) { + request.debug(true); + process.env.NODE_DEBUG = 'request'; + } + + return this.loadInstalledAtomVersion(options, () => { + if (this.installedAtomVersion) { + return this.upgradePackages(options, callback); + } else { + return callback('Could not determine current Atom version installed'); + } + }); + } + + upgradePackages(options, callback) { + const packages = this.getInstalledPackages(options); + return this.getAvailableUpdates(packages, (error, updates) => { + if (error != null) { return callback(error); } + + if (options.argv.json) { + const packagesWithLatestVersionOrSha = updates.map(function({pack, latestVersion, sha}) { + if (latestVersion) { pack.latestVersion = latestVersion; } + if (sha) { pack.latestSha = sha; } + return pack; + }); + console.log(JSON.stringify(packagesWithLatestVersionOrSha)); + } else { + console.log("Package Updates Available".cyan + ` (${updates.length})`); + tree(updates, function({pack, latestVersion, sha}) { + let {name, apmInstallSource, version} = pack; + name = name.yellow; + if (sha != null) { + version = apmInstallSource.sha.substr(0, 8).red; + latestVersion = sha.substr(0, 8).green; + } else { + version = version.red; + latestVersion = latestVersion.green; + } + latestVersion = latestVersion?.green || apmInstallSource?.sha?.green; + return `${name} ${version} -> ${latestVersion}`; + }); + } + + if (options.command === 'outdated') { return callback(); } + if (options.argv.list) { return callback(); } + if (updates.length === 0) { return callback(); } + + console.log(); + if (options.argv.confirm) { + return this.promptForConfirmation((error, confirmed) => { + if (error != null) { return callback(error); } + + if (confirmed) { + console.log(); + return this.installUpdates(updates, callback); + } else { + return callback(); + } + }); + } else { + return this.installUpdates(updates, callback); + } + }); + } +}; diff --git a/src/view.coffee b/src/view.coffee deleted file mode 100644 index e48fdf4..0000000 --- a/src/view.coffee +++ /dev/null @@ -1,104 +0,0 @@ -_ = require 'underscore-plus' -yargs = require 'yargs' -semver = require 'semver' - -Command = require './command' -config = require './apm' -request = require './request' -tree = require './tree' - -module.exports = -class View extends Command - parseOptions: (argv) -> - options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())) - options.usage """ - - Usage: apm view - - View information about a package/theme in the atom.io registry. - """ - options.alias('h', 'help').describe('help', 'Print this usage message') - options.boolean('json').describe('json', 'Output featured packages as JSON array') - options.string('compatible').describe('compatible', 'Show the latest version compatible with this Atom version') - - loadInstalledAtomVersion: (options, callback) -> - process.nextTick => - if options.argv.compatible - version = @normalizeVersion(options.argv.compatible) - installedAtomVersion = version if semver.valid(version) - callback(installedAtomVersion) - - getLatestCompatibleVersion: (pack, options, callback) -> - @loadInstalledAtomVersion options, (installedAtomVersion) -> - return callback(pack.releases.latest) unless installedAtomVersion - - latestVersion = null - for version, metadata of pack.versions ? {} - continue unless semver.valid(version) - continue unless metadata - - engine = metadata.engines?.atom ? '*' - continue unless semver.validRange(engine) - continue unless semver.satisfies(installedAtomVersion, engine) - - latestVersion ?= version - latestVersion = version if semver.gt(version, latestVersion) - - callback(latestVersion) - - getRepository: (pack) -> - if repository = pack.repository?.url ? pack.repository - repository.replace(/\.git$/, '') - - getPackage: (packageName, options, callback) -> - requestSettings = - url: "#{config.getAtomPackagesUrl()}/#{packageName}" - json: true - request.get requestSettings, (error, response, body={}) => - if error? - callback(error) - else if response.statusCode is 200 - @getLatestCompatibleVersion body, options, (version) -> - {name, readme, downloads, stargazers_count} = body - metadata = body.versions?[version] ? {name} - pack = _.extend({}, metadata, {readme, downloads, stargazers_count}) - callback(null, pack) - else - message = body.message ? body.error ? body - callback("Requesting package failed: #{message}") - - run: (options) -> - {callback} = options - options = @parseOptions(options.commandArgs) - [packageName] = options.argv._ - - unless packageName - callback("Missing required package name") - return - - @getPackage packageName, options, (error, pack) => - if error? - callback(error) - return - - if options.argv.json - console.log(JSON.stringify(pack, null, 2)) - else - console.log "#{pack.name.cyan}" - items = [] - items.push(pack.version.yellow) if pack.version - if repository = @getRepository(pack) - items.push(repository.underline) - items.push(pack.description.replace(/\s+/g, ' ')) if pack.description - if pack.downloads >= 0 - items.push(_.pluralize(pack.downloads, 'download')) - if pack.stargazers_count >= 0 - items.push(_.pluralize(pack.stargazers_count, 'star')) - - tree(items) - - console.log() - console.log "Run `apm install #{pack.name}` to install this package." - console.log() - - callback() diff --git a/src/view.js b/src/view.js new file mode 100644 index 0000000..b084502 --- /dev/null +++ b/src/view.js @@ -0,0 +1,140 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let View; +import _ from 'underscore-plus'; +import yargs from 'yargs'; +import semver from 'semver'; +import Command from './command'; +import config from './apm'; +import request from './request'; +import tree from './tree'; + +export default View = class View extends Command { + parseOptions(argv) { + const options = yargs(argv).wrap(Math.min(100, yargs.terminalWidth())); + options.usage(`\ + +Usage: apm view + +View information about a package/theme in the atom.io registry.\ +` + ); + options.alias('h', 'help').describe('help', 'Print this usage message'); + options.boolean('json').describe('json', 'Output featured packages as JSON array'); + return options.string('compatible').describe('compatible', 'Show the latest version compatible with this Atom version'); + } + + loadInstalledAtomVersion(options, callback) { + return process.nextTick(() => { + let installedAtomVersion; + if (options.argv.compatible) { + const version = this.normalizeVersion(options.argv.compatible); + if (semver.valid(version)) { installedAtomVersion = version; } + } + return callback(installedAtomVersion); + }); + } + + getLatestCompatibleVersion(pack, options, callback) { + return this.loadInstalledAtomVersion(options, function(installedAtomVersion) { + if (!installedAtomVersion) { return callback(pack.releases.latest); } + + let latestVersion = null; + const object = pack.versions != null ? pack.versions : {}; + for (let version in object) { + const metadata = object[version]; + if (!semver.valid(version)) { continue; } + if (!metadata) { continue; } + + const engine = metadata.engines?.atom != null ? metadata.engines?.atom : '*'; + if (!semver.validRange(engine)) { continue; } + if (!semver.satisfies(installedAtomVersion, engine)) { continue; } + + if (latestVersion == null) { latestVersion = version; } + if (semver.gt(version, latestVersion)) { latestVersion = version; } + } + + return callback(latestVersion); + }); + } + + getRepository(pack) { + let repository; + if (repository = pack.repository?.url != null ? pack.repository?.url : pack.repository) { + return repository.replace(/\.git$/, ''); + } + } + + getPackage(packageName, options, callback) { + const requestSettings = { + url: `${config.getAtomPackagesUrl()}/${packageName}`, + json: true + }; + return request.get(requestSettings, (error, response, body={}) => { + if (error != null) { + return callback(error); + } else if (response.statusCode === 200) { + return this.getLatestCompatibleVersion(body, options, function(version) { + const {name, readme, downloads, stargazers_count} = body; + const metadata = body.versions?.[version] != null ? body.versions?.[version] : {name}; + const pack = _.extend({}, metadata, {readme, downloads, stargazers_count}); + return callback(null, pack); + }); + } else { + let left; + const message = (left = body.message != null ? body.message : body.error) != null ? left : body; + return callback(`Requesting package failed: ${message}`); + } + }); + } + + run(options) { + const {callback} = options; + options = this.parseOptions(options.commandArgs); + const [packageName] = options.argv._; + + if (!packageName) { + callback("Missing required package name"); + return; + } + + return this.getPackage(packageName, options, (error, pack) => { + if (error != null) { + callback(error); + return; + } + + if (options.argv.json) { + console.log(JSON.stringify(pack, null, 2)); + } else { + let repository; + console.log(`${pack.name.cyan}`); + const items = []; + if (pack.version) { items.push(pack.version.yellow); } + if (repository = this.getRepository(pack)) { + items.push(repository.underline); + } + if (pack.description) { items.push(pack.description.replace(/\s+/g, ' ')); } + if (pack.downloads >= 0) { + items.push(_.pluralize(pack.downloads, 'download')); + } + if (pack.stargazers_count >= 0) { + items.push(_.pluralize(pack.stargazers_count, 'star')); + } + + tree(items); + + console.log(); + console.log(`Run \`apm install ${pack.name}\` to install this package.`); + console.log(); + } + + return callback(); + }); + } +};