From d3911ee5a3e00eb410321c42d9726b5952c15045 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Sat, 13 Jun 2015 16:58:48 -0700 Subject: [PATCH] Kitematic development version of windows Signed-off-by: Jeffrey Morgan --- .gitignore | 1 + Gruntfile.js | 281 ++++++++++++++++++ package.json | 37 ++- src/browser.js | 50 +++- .../ContainerDetailsSubheader.react.js | 15 +- src/components/ContainerHomeFolders.react.js | 7 +- .../ContainerSettingsVolumes.react.js | 7 +- src/components/Containers.react.js | 1 + src/stores/SetupStore.js | 18 +- src/utils/DockerMachineUtil.js | 15 +- src/utils/MetricsUtil.js | 2 +- src/utils/RegHubUtil.js | 3 +- src/utils/Util.js | 17 +- util/kitematic.ico | Bin 0 -> 10035 bytes util/loading.gif | Bin 0 -> 5610 bytes 15 files changed, 394 insertions(+), 60 deletions(-) create mode 100644 Gruntfile.js create mode 100644 util/kitematic.ico create mode 100644 util/loading.gif diff --git a/.gitignore b/.gitignore index e51ebc5468..8fe91862fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .swp build dist +installer node_modules coverage npm-debug.log diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000000..062ff65461 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,281 @@ +var path = require('path'); +var execFile = require('child_process').execFile; +var packagejson = require('./package.json'); +var electron = require('electron-prebuilt'); + +var WINDOWS_DOCKER_URL = 'https://get.docker.com/builds/Windows/x86_64/docker-1.6.2.exe'; +var DARWIN_DOCKER_URL = 'https://get.docker.com/builds/Darwin/x86_64/docker-' + packagejson['docker-version']; +var WINDOWS_DOCKER_MACHINE_URL = 'https://github.com/docker/machine/releases/download/v' + packagejson['docker-machine-version'] + '/docker-machine_windows-amd64.exe'; +var DARWIN_DOCKER_MACHINE_URL = 'https://github.com/docker/machine/releases/download/v' + packagejson['docker-machine-version'] + '/docker-machine_darwin-amd64'; +var DARWIN_COMPOSE_URL = 'https://github.com/docker/compose/releases/download/' + packagejson['docker-compose-version'] + '/docker-compose-Darwin-x86_64'; +var BOOT2DOCKER_ISO_URL = 'https://github.com/boot2docker/boot2docker/releases/download/v' + packagejson['docker-version'] + '/boot2docker.iso'; + +module.exports = function (grunt) { + require('load-grunt-tasks')(grunt); + var target = grunt.option('target') || 'development'; + var beta = grunt.option('beta') || false; + var env = process.env; + env.NODE_PATH = '..:' + env.NODE_PATH; + env.NODE_ENV = target; + + var version = function (str) { + var match = str.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : null; + }; + + grunt.registerMultiTask('download-binary', 'Downloads binary unless version up to date', function () { + var target = grunt.task.current.target; + var done = this.async(); + var config = grunt.config('download-binary')[target]; + execFile(config.binary, ['--version'], function (err, stdout) { + var currentVersion = version(stdout); + if (!currentVersion || currentVersion !== version(config.version)) { + grunt.task.run('curl:' + target); + grunt.task.run('chmod'); + } + done(); + }); + }); + + var APPNAME = beta ? 'Kitematic (Beta)' : 'Kitematic'; + var OSX_OUT = './dist/osx'; + var OSX_FILENAME = OSX_OUT + '/' + APPNAME + '.app'; + + grunt.initConfig({ + IDENTITY: 'Developer ID Application: Docker Inc', + APPNAME: APPNAME, + OSX_OUT: OSX_OUT, + OSX_FILENAME: OSX_FILENAME, + OSX_FILENAME_ESCAPED: OSX_FILENAME.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'), + + // electron + electron: { + windows: { + options: { + name: '<%= APPNAME %>', + dir: 'build/', + out: 'dist/', + version: packagejson['electron-version'], + platform: 'win32', + arch: 'x64', + asar: true, + icon: 'util/kitematic.ico' + } + }, + osx: { + options: { + name: '<%= APPNAME %>', + dir: 'build/', + out: '<%= OSX_OUT %>', + version: packagejson['electron-version'], + platform: 'darwin', + arch: 'x64', + asar: true, + 'app-bundle-id': 'com.kitematic.kitematic' + } + } + }, + + 'create-windows-installer': { + appDirectory: 'dist/Kitematic-win32/', + authors: 'Docker Inc.', + loadingGif: 'util/loading.gif', + setupIcon: 'util/kitematic.ico' + }, + + // docker binaries + 'download-binary': { + docker: { + version: packagejson['docker-version'], + binary: path.join('resources', 'docker'), + download: 'curl:docker' + }, + 'docker-machine': { + version: packagejson['docker-machine-version'], + binary: path.join('resources', 'docker-machine'), + download: 'curl:docker-machine' + } + }, + + // images + copy: { + dev: { + files: [{ + expand: true, + cwd: '.', + src: ['package.json', 'settings.json', 'index.html'], + dest: 'build/' + }, { + expand: true, + cwd: 'images/', + src: ['**/*'], + dest: 'build/' + }, { + expand: true, + cwd: 'fonts/', + src: ['**/*'], + dest: 'build/' + }, { + cwd: 'node_modules/', + src: Object.keys(packagejson.dependencies).map(function (dep) { return dep + '/**/*';}), + dest: 'build/node_modules/', + expand: true + }] + }, + windows: { + files: [{ + expand: true, + cwd: 'resources', + src: ['docker*'], + dest: 'dist/Kitematic-win32/resources/resources/' + }], + options: { + mode: true + } + }, + osx: { + files: [{ + expand: true, + cwd: 'resources', + src: ['**/*'], + dest: '<%= OSX_FILENAME %>/Contents/Resources/resources/' + }, { + src: 'util/kitematic.icns', + dest: '<%= OSX_FILENAME %>/Contents/Resources/atom.icns' + }], + options: { + mode: true + } + } + }, + + rename: { + installer: { + src: 'installer/Setup.exe', + dest: 'installer/KitematicSetup.exe' + } + }, + + // download binaries + curl: { + docker: { + src: process.platform === 'win32' ? WINDOWS_DOCKER_URL : DARWIN_DOCKER_URL, + dest: process.platform === 'win32' ? path.join('resources', 'docker.exe') : path.join('resources', 'docker') + }, + 'docker-machine': { + src: process.platform === 'win32' ? WINDOWS_DOCKER_MACHINE_URL : DARWIN_DOCKER_MACHINE_URL, + dest: process.platform === 'win32' ? path.join('resources', 'docker-machine.exe') : path.join('resources', 'docker-machine') + }, + 'docker-compose': { + src: DARWIN_COMPOSE_URL, + dest: 'resources/docker-compose' + }, + 'boot2docker-iso': { + src: BOOT2DOCKER_ISO_URL, + dest: path.join('resources', 'boot2docker-' + packagejson['docker-version']) + } + }, + + chmod: { + binaries: { + options: { + mode: '755' + }, + src: ['resources/docker*'] + } + }, + + // styles + less: { + options: { + sourceMapFileInline: true + }, + dist: { + files: { + 'build/main.css': 'styles/main.less' + } + } + }, + + // javascript + babel: { + options: { + sourceMap: 'inline', + blacklist: 'regenerator' + }, + dist: { + files: [{ + expand: true, + cwd: 'src/', + src: ['**/*.js'], + dest: 'build/', + }] + } + }, + + shell: { + electron: { + command: electron + ' ' + 'build', + options: { + async: true, + execOptions: { + env: env + } + } + }, + sign: { + options: { + failOnError: false, + }, + command: [ + 'codesign --deep -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>/Contents/Frameworks/*', + 'codesign -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>', + 'codesign -vvv --display <%= OSX_FILENAME_ESCAPED %>', + 'codesign -v --verify <%= OSX_FILENAME_ESCAPED %>', + ].join(' && '), + }, + zip: { + command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> <%= OSX_OUT %>/Kitematic-' + packagejson.version + '.zip', + } + }, + + clean: { + release: ['build/', 'dist/', 'installer/'], + }, + + // livereload + watchChokidar: { + options: { + spawn: true + }, + livereload: { + options: {livereload: true}, + files: ['build/**/*'] + }, + js: { + files: ['src/**/*.js'], + tasks: ['newer:babel'] + }, + less: { + files: ['styles/**/*.less'], + tasks: ['newer:less'] + }, + copy: { + files: ['images/*', 'index.html', 'fonts/*'], + tasks: ['newer:copy:dev'] + } + } + }); + grunt.registerTask('default', ['download-binary', 'newer:babel', 'newer:less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']); + + if (process.platform === 'win32') { + grunt.registerTask('release', ['clean', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:windows', 'copy:windows', 'create-windows-installer', 'rename:installer']); + } else { + grunt.registerTask('release', ['clean:dist', 'clean:build', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip']); + } + + process.on('SIGINT', function () { + grunt.task.run(['shell:electron:kill']); + process.exit(1); + }); +}; diff --git a/package.json b/package.json index a2ee190495..f77fa685af 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "start": "gulp", "test": "jest -c jest-unit.json", "integration": "jest -c jest-integration.json", - "release": "gulp release", - "release:beta": "gulp release --beta", + "release": "grunt release", + "release:beta": "grunt release --beta=true", "lint": "jsxhint src", "reset": "gulp reset" }, @@ -67,25 +67,30 @@ "devDependencies": { "babel": "^5.1.10", "babel-jest": "^5.2.0", - "gulp": "^3.8.11", - "gulp-babel": "^5.1.0", - "gulp-changed": "^1.2.1", - "gulp-concat": "^2.5.2", - "gulp-cssmin": "^0.1.6", - "gulp-download-electron": "^0.0.5", - "gulp-if": "^1.2.5", - "gulp-insert": "^0.4.0", - "gulp-less": "^3.0.2", - "gulp-livereload": "^3.8.0", - "gulp-plumber": "^1.0.0", - "gulp-shell": "^0.4.1", - "gulp-sourcemaps": "^1.5.2", - "gulp-util": "^3.0.4", + "electron-prebuilt": "^0.27.3", + "grunt": "^0.4.5", + "grunt-babel": "^5.0.1", + "grunt-chmod": "^1.0.3", + "grunt-cli": "^0.1.13", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-copy": "^0.8.0", + "grunt-contrib-less": "^1.0.1", + "grunt-contrib-watch-chokidar": "^1.0.0", + "grunt-curl": "^2.2.0", + "grunt-download-electron": "^2.1.1", + "grunt-electron": "^1.0.0", + "grunt-electron-installer": "^0.33.0", + "grunt-newer": "^1.1.1", + "grunt-rename": "^0.1.4", + "grunt-shell": "^1.1.2", + "grunt-shell-spawn": "^0.3.8", "jest-cli": "^0.4.5", "jsxhint": "^0.14.0", + "load-grunt-tasks": "^3.2.0", "minimist": "^1.1.1", "react-tools": "^0.13.1", "run-sequence": "^1.0.2", + "shell-escape": "^0.2.0", "source-map-support": "^0.2.10" } } diff --git a/src/browser.js b/src/browser.js index a9f087e7c8..bc0979d16b 100644 --- a/src/browser.js +++ b/src/browser.js @@ -5,9 +5,8 @@ var fs = require('fs'); var ipc = require('ipc'); var path = require('path'); -process.env.NODE_PATH = path.join(__dirname, '/../node_modules'); +process.env.NODE_PATH = path.join(__dirname, 'node_modules'); process.env.RESOURCES_PATH = path.join(__dirname, '/../resources'); -process.chdir(path.join(__dirname, '..')); process.env.PATH = '/usr/local/bin:' + process.env.PATH; var size = {}, settingsjson = {}; @@ -15,9 +14,49 @@ try { size = JSON.parse(fs.readFileSync(path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], 'Library', 'Application\ Support', 'Kitematic', 'size'))); } catch (err) {} try { - settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); + settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, 'settings.json'), 'utf8')); } catch (err) {} +var handleStartupEvent = function() { + if (process.platform !== 'win32') { + return false; + } + + var squirrelCommand = process.argv[1]; + switch (squirrelCommand) { + case '--squirrel-install': + case '--squirrel-updated': + + // Optionally do things such as: + // + // - Install desktop and start menu shortcuts + // - Add your .exe to the PATH + // - Write to the registry for things like file associations and + // explorer context menus + + // Always quit when done + app.quit(); + + return true; + case '--squirrel-uninstall': + // Undo anything you did in the --squirrel-install and + // --squirrel-updated handlers + + // Always quit when done + app.quit(); + + return true; + case '--squirrel-obsolete': + // This is called on the outgoing version of your app before + // we update to the new version - it's the opposite of + // --squirrel-updated + app.quit(); + return true; + } +}; + +handleStartupEvent(); + var openURL = null; app.on('open-url', function (event, url) { event.preventDefault(); @@ -36,7 +75,7 @@ app.on('ready', function () { show: false, }); - mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html'))); + mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, 'index.html'))); app.on('activate-with-no-open-windows', function () { if (mainWindow) { @@ -52,7 +91,8 @@ app.on('ready', function () { }); app.on('before-quit', function () { - if (!updating) { + // TODO: make this work for right click + close + if (!updating && mainWindow.webContents) { mainWindow.webContents.send('application:quitting'); } }); diff --git a/src/components/ContainerDetailsSubheader.react.js b/src/components/ContainerDetailsSubheader.react.js index f0cedecc95..e4793d6d5b 100644 --- a/src/components/ContainerDetailsSubheader.react.js +++ b/src/components/ContainerDetailsSubheader.react.js @@ -1,13 +1,18 @@ +var $ = require('jquery'); var _ = require('underscore'); var React = require('react'); var exec = require('exec'); var shell = require('shell'); var metrics = require('../utils/MetricsUtil'); var ContainerUtil = require('../utils/ContainerUtil'); +var util = require('../utils/Util'); var machine = require('../utils/DockerMachineUtil'); +var RetinaImage = require('react-retina-image'); var classNames = require('classnames'); var resources = require('../utils/ResourcesUtil'); +var dockerUtil = require('../utils/DockerUtil'); var containerActions = require('../actions/ContainerActions'); +var dockerMachineUtil = require('../utils/DockerMachineUtil'); var ContainerDetailsSubheader = React.createClass({ contextTypes: { @@ -101,15 +106,7 @@ var ContainerDetailsSubheader = React.createClass({ if(!shell) { shell = 'sh'; } - machine.ip().then(ip => { - var cmd = [resources.terminal(), 'ssh', '-p', '22', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel=quiet', '-o', 'StrictHostKeyChecking=no', '-i', '~/.docker/machine/machines/' + machine.name() + '/id_rsa', 'docker@' + ip, '-t', 'docker', - 'exec', '-i', '-t', container.Name, shell]; - exec(cmd, function (stderr, stdout, code) { - if (code) { - console.log(stderr); - } - }); - }); + dockerMachineUtil.dockerTerminal(`docker exec -it ${this.props.container.Name} ${shell}`); } }, render: function () { diff --git a/src/components/ContainerHomeFolders.react.js b/src/components/ContainerHomeFolders.react.js index e2fbf89e78..f2b9912684 100644 --- a/src/components/ContainerHomeFolders.react.js +++ b/src/components/ContainerHomeFolders.react.js @@ -29,12 +29,7 @@ var ContainerHomeFolder = React.createClass({ volumes[containerVolume] = newHostVolume; var binds = _.pairs(volumes).map(function (pair) { if(util.isWindows()) { - var home = util.home(); - home = home.charAt(0).toLowerCase() + home.slice(1); - home = '/' + home.replace(':', '').replace(/\\/g, '/'); - var fullPath = path.join(home, 'Kitematic', pair[1], pair[0]); - fullPath = fullPath.replace(/\\/g, '/'); - return fullPath + ':' + pair[0]; + return util.windowsToLinuxPath(pair[1]) + ':' + pair[0]; } return pair[1] + ':' + pair[0]; }); diff --git a/src/components/ContainerSettingsVolumes.react.js b/src/components/ContainerSettingsVolumes.react.js index b1b828b686..fe2572eae7 100644 --- a/src/components/ContainerSettingsVolumes.react.js +++ b/src/components/ContainerSettingsVolumes.react.js @@ -3,6 +3,7 @@ var React = require('react/addons'); var remote = require('remote'); var dialog = remote.require('dialog'); var shell = require('shell'); +var util = require('../utils/Util'); var metrics = require('../utils/MetricsUtil'); var containerActions = require('../actions/ContainerActions'); @@ -15,7 +16,10 @@ var ContainerSettingsVolumes = React.createClass({ } var directory = filenames[0]; if (directory) { - metrics.track('Chose Directory for Volume'); + metrics.track('Choose Directory for Volume'); + if(util.isWindows()) { + directory = util.windowsToLinuxPath(directory); + } var volumes = _.clone(self.props.container.Volumes); volumes[dockerVol] = directory; var binds = _.pairs(volumes).map(function (pair) { @@ -47,6 +51,7 @@ var ContainerSettingsVolumes = React.createClass({ if (!this.props.container) { return false; } + var volumes = _.map(this.props.container.Volumes, (val, key) => { if (!val || val.indexOf(process.env.HOME) === -1) { val = ( diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js index 449afb4835..521001629a 100644 --- a/src/components/Containers.react.js +++ b/src/components/Containers.react.js @@ -6,6 +6,7 @@ var containerStore = require('../stores/ContainerStore'); var ContainerList = require('./ContainerList.react'); var Header = require('./Header.react'); var metrics = require('../utils/MetricsUtil'); +var RetinaImage = require('react-retina-image'); var shell = require('shell'); var machine = require('../utils/DockerMachineUtil'); diff --git a/src/stores/SetupStore.js b/src/stores/SetupStore.js index 588d2c07e8..7c99a0ff64 100644 --- a/src/stores/SetupStore.js +++ b/src/stores/SetupStore.js @@ -42,14 +42,14 @@ var _steps = [{ progressCallback(50); // TODO: detect when the installation has started so we can simulate progress try { if (util.isWindows()) { - yield util.exec([path.join(util.supportDir(), virtualBox.filename())]); + yield util.exec([path.join(util.supportDir(), virtualBox.filename()), '-msiparams', 'REBOOT=ReallySuppress', 'LIMITUI=INSTALLUILEVEL_PROGRESSONLY']); } else { yield util.exec(setupUtil.macSudoCmd(setupUtil.installVirtualBoxCmd())); } } catch (err) { throw null; } - } else if (!virtualBox.active()) { + } else if (util.isWindows() && !virtualBox.active()) { yield util.exec(setupUtil.macSudoCmd(util.escapePath('/Library/Application Support/VirtualBox/LaunchDaemons/VirtualBoxStartup.sh') + ' restart')); } }) @@ -59,7 +59,7 @@ var _steps = [{ message: 'To run Docker containers on your computer, Kitematic is starting a Linux virtual machine. This may take a minute...', totalPercent: 60, percent: 0, - seconds: 72, + seconds: 80, run: Promise.coroutine(function* (progressCallback) { setupUtil.simulateProgress(this.seconds, progressCallback); var exists = yield machine.exists(); @@ -68,16 +68,6 @@ var _steps = [{ yield machine.rm(); } yield machine.create(); - if(util.isWindows()) { - let home = util.home(); - let driveLetter = home.charAt(0); - let parts = home.split('\\').slice(0, -1); - let usersDirName = parts[parts.length-1]; - let usersDirPath = parts.join('\\'); - let shareName = driveLetter + '/' + usersDirName; - yield virtualBox.mountSharedDir(machine.name(), shareName, usersDirPath); - yield machine.start(); - } return; } @@ -159,7 +149,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { var vboxNeedsInstall = !virtualBox.installed(); required.download = vboxNeedsInstall && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== virtualBox.checksum()); - required.install = vboxNeedsInstall || !virtualBox.active(); + required.install = vboxNeedsInstall || (util.isWindows() && !virtualBox.active()); required.init = required.install || !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || util.compareVersions(isoversion, packagejson['docker-version']) < 0; var exists = yield machine.exists(); diff --git a/src/utils/DockerMachineUtil.js b/src/utils/DockerMachineUtil.js index 0875882d5c..37532dc690 100644 --- a/src/utils/DockerMachineUtil.js +++ b/src/utils/DockerMachineUtil.js @@ -153,14 +153,23 @@ var DockerMachine = { }); }); }, - dockerTerminal: function () { + dockerTerminal: function (cmd) { if(util.isWindows()) { + cmd = cmd || ''; this.info().then(machine => { - util.execProper(`start cmd.exe /k "SET DOCKER_HOST=${machine.url}&& SET DOCKER_CERT_PATH=${path.join(util.home(), '.docker/machine/machines/' + machine.name)}&& SET DOCKER_TLS_VERIFY=1`); + util.exec('start powershell.exe ' + cmd, + {env: { + 'DOCKER_HOST' : machine.url, + 'DOCKER_CERT_PATH' : path.join(util.home(), '.docker/machine/machines/' + machine.name), + 'DOCKER_TLS_VERIFY': 1, + 'PATH': resources.resourceDir() + } + }); }); } else { + cmd = cmd || '$SHELL'; this.info().then(machine => { - var cmd = [resources.terminal(), `DOCKER_HOST=${machine.url} DOCKER_CERT_PATH=${path.join(util.home(), '.docker/machine/machines/' + machine.name)} DOCKER_TLS_VERIFY=1 $SHELL`]; + var cmd = [resources.terminal(), `DOCKER_HOST=${machine.url} DOCKER_CERT_PATH=${path.join(util.home(), '.docker/machine/machines/' + machine.name)} DOCKER_TLS_VERIFY=1 ${cmd}`]; util.exec(cmd).then(() => {}); }); } diff --git a/src/utils/MetricsUtil.js b/src/utils/MetricsUtil.js index e7083989fd..8b19edfbd0 100644 --- a/src/utils/MetricsUtil.js +++ b/src/utils/MetricsUtil.js @@ -7,7 +7,7 @@ var util = require('./Util'); var settings; try { - settings = JSON.parse(fs.readFileSync(path.join(__dirname, '../..', 'settings.json'), 'utf8')); + settings = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) { settings = {}; } diff --git a/src/utils/RegHubUtil.js b/src/utils/RegHubUtil.js index bb7b1f5734..cf00640249 100644 --- a/src/utils/RegHubUtil.js +++ b/src/utils/RegHubUtil.js @@ -5,6 +5,7 @@ var util = require('../utils/Util'); var hubUtil = require('../utils/HubUtil'); var repositoryServerActions = require('../actions/RepositoryServerActions'); var tagServerActions = require('../actions/TagServerActions'); +var Promise = require('bluebird'); let REGHUB2_ENDPOINT = process.env.REGHUB2_ENDPOINT || 'https://registry.hub.docker.com/v2'; let searchReq = null; @@ -61,7 +62,7 @@ module.exports = { let data = JSON.parse(body); let repos = data.repos; async.map(repos, (repo, cb) => { - var name = repo.repo; + let name = repo.repo; if (util.isOfficialRepo(name)) { name = 'library/' + name; } diff --git a/src/utils/Util.js b/src/utils/Util.js index 47d54b16f4..4566025bf5 100644 --- a/src/utils/Util.js +++ b/src/utils/Util.js @@ -1,4 +1,5 @@ var exec = require('exec'); +var child_process = require('child_process'); var Promise = require('bluebird'); var fs = require('fs'); var path = require('path'); @@ -9,11 +10,12 @@ var app = remote.require('app'); module.exports = { exec: function (args, options) { options = options || {}; + let fn = Array.isArray(args) ? exec : child_process.exec; return new Promise((resolve, reject) => { - exec(args, options, (stderr, stdout, code) => { + fn(args, options, (stderr, stdout, code) => { if (code) { var cmd = Array.isArray(args) ? args.join(' ') : args; - reject(new Error(cmd + ' returned non zero exit code.\n===== Stderr =====\n ' + stderr + '\n===== Stdout =====\n' + stdout)); + reject(new Error(cmd + ' returned non zero exit code. Stderr: ' + stderr)); } else { resolve(stdout); } @@ -59,12 +61,12 @@ module.exports = { .replace(/\/Users\/[^\/]*\//mg, '/Users//'); }, packagejson: function () { - return JSON.parse(fs.readFileSync(path.join(__dirname, '../..', 'package.json'), 'utf8')); + return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); }, settingsjson: function () { var settingsjson = {}; try { - settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../..', 'settings.json'), 'utf8')); + settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) {} return settingsjson; }, @@ -132,5 +134,12 @@ module.exports = { randomId: function () { return crypto.randomBytes(32).toString('hex'); }, + windowsToLinuxPath: function(windowsAbsPath) { + var fullPath = windowsAbsPath.replace(':', '').split(path.sep).join('/'); + if(fullPath.charAt(0) !== '/'){ + fullPath = '/' + fullPath.charAt(0).toLowerCase() + fullPath.substring(1); + } + return fullPath; + }, webPorts: ['80', '8000', '8080', '3000', '5000', '2368', '9200', '8983'] }; diff --git a/util/kitematic.ico b/util/kitematic.ico new file mode 100644 index 0000000000000000000000000000000000000000..ec7dab48abc23221f9700e715a77121e75393778 GIT binary patch literal 10035 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 DiVAmS literal 0 HcmV?d00001 diff --git a/util/loading.gif b/util/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..3f6d3b671a0a861a65a178aee2d1242554422de3 GIT binary patch literal 5610 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 ACjbBd literal 0 HcmV?d00001