diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d3337dc --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [ "@babel/preset-env" ] +} diff --git a/.editorconfig b/.editorconfig index a9e47cf..8a2b8e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,13 +4,15 @@ root = true [*] -charset = "utf8" +charset = "utf-8" +indent_style = space +indent_size = 4 +end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true -[*.coffee] -indent_style = space -indent_size = 4 +[{package.json,package-lock.json}] +indent_size = 2 [*.js] indent_style = space diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..76ec1e8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "extends": "airbnb-base", + "env": { + "browser": true, + "mocha": true + }, + "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false, + "sourceType": "module" + }, + "rules": { + "brace-style": "warn", + "class-methods-use-this": 0, + "comma-dangle": ["warn", "only-multiline"], + "indent": ["warn", 4, { "SwitchCase": 1 }], + "lines-between-class-members": "off", + "linebreak-style": "off", + "max-len": ["warn", 128], + "no-promise-executor-return": "warn", + "no-var": "error", + "no-useless-constructor": "off", + "object-curly-newline": "warn", + "object-curly-spacing": "warn", + "prefer-arrow-callback": "off", + "prefer-const": "warn", + "prefer-destructuring": "off" + } +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 72d689c..01d68d6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,15 +6,15 @@ jobs: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] - node-version: ["12", "14", "16", "18", "20"] + node-version: ["14", "16", "18", "20"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} diff --git a/.npmignore b/.npmignore index cd8b52d..fc4b35d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,5 @@ * -!dist/* +!src/* !README.md !LICENSE.md !package.json \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dbccae1..2392fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "auto-launch", - "version": "5.1.0", - "lockfileVersion": 3, + "version": "6.0.0-rc.2", + "lockfileVersion": 1, "requires": true, "packages": { "": { diff --git a/package.json b/package.json index 8ed46d3..fb038a5 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,26 @@ { "name": "auto-launch", - "version": "5.1.0", + "version": "6.0.0-rc.2", "description": "Launch node applications or executables at login (Mac, Windows, Linux and FreeBSD)", - "main": "dist/index.js", + "type": "module", + "main": "./src/index.js", + "exports": "./src/index.js", "scripts": { - "test": "mocha --compilers coffee:coffeescript/register tests/*.coffee", - "lint": "coffeelint -f ./node_modules/teamwork-coffeelint-rules/coffeelint.json src/ tests/", - "build": "coffee -c -o dist/ src/" + "lint": "eslint --ext .js src tests", + "lint:fix": "pnpm lint --fix", + "test": "mocha tests/test.js --reporter spec" + }, + "pre-commit": { + "run": [ + "test" + ] }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "repository": { "type": "git", - "url": "https://github.com/4ver/node-auto-launch" + "url": "https://github.com/Teamwork/node-auto-launch" }, "keywords": [ "login", @@ -27,25 +34,24 @@ "contributors": [ "Adam Lynch " ], + "homepage": "https://github.com/Teamwork/node-auto-launch", "license": "MIT", "bugs": { - "url": "https://github.com/4ver/node-auto-launch/issues" + "url": "https://github.com/Teamwork/node-auto-launch/issues" }, "devDependencies": { - "@coffeelint/cli": "^5.2.11", - "chai": "^3.5.0", - "coffeescript": "^2.7.0", - "mocha": "^3.0.0", - "teamwork-coffeelint-rules": "0.0.1" + "@babel/core": "^7.23.6", + "@babel/eslint-parser": "^7.23.0", + "@babel/preset-env": "^7.23.6", + "chai": "^4.3.0", + "eslint": "^8.55.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.29.0", + "mocha": "^10.2.0" }, - "homepage": "https://github.com/4ver/node-auto-launch", "dependencies": { "applescript": "^1.0.0", - "mkdirp": "^0.5.1", - "untildify": "^3.0.2", + "untildify": "^5.0.0", "winreg": "1.2.4" - }, - "overrides": { - "graceful-fs": "^4.2.11" } } diff --git a/src/AutoLaunchLinux.coffee b/src/AutoLaunchLinux.coffee deleted file mode 100644 index 6973456..0000000 --- a/src/AutoLaunchLinux.coffee +++ /dev/null @@ -1,49 +0,0 @@ -untildify = require 'untildify' -fileBasedUtilities = require './fileBasedUtilities' - -module.exports = - - ### Public ### - - # options - {Object} - # :appName - {String} - # :appPath - {String} - # :isHiddenOnLaunch - {Boolean} - # Returns a Promise - enable: ({appName, appPath, isHiddenOnLaunch}) -> - hiddenArg = if isHiddenOnLaunch then '--hidden' else '' - - data = """[Desktop Entry] - Type=Application - Version=1.0 - Name=#{appName} - Comment=#{appName} startup script - Exec=#{appPath} #{hiddenArg} - StartupNotify=false - Terminal=false""" - - return fileBasedUtilities.createFile { - directory: @getDirectory() - filePath: @getFilePath appName - data: data - } - - - # appName - {String} - # Returns a Promise - disable: (appName) -> fileBasedUtilities.removeFile @getFilePath appName - - - # appName - {String} - # Returns a Promise which resolves to a {Boolean} - isEnabled: (appName) -> fileBasedUtilities.isEnabled @getFilePath appName - - - ### Private ### - - # Returns a {String} - getDirectory: -> untildify '~/.config/autostart/' - - # appName - {String} - # Returns a {String} - getFilePath: (appName) -> "#{@getDirectory()}#{appName}.desktop" \ No newline at end of file diff --git a/src/AutoLaunchMac.coffee b/src/AutoLaunchMac.coffee deleted file mode 100644 index 1049a4b..0000000 --- a/src/AutoLaunchMac.coffee +++ /dev/null @@ -1,99 +0,0 @@ -applescript = require 'applescript' -untildify = require 'untildify' -fileBasedUtilities = require './fileBasedUtilities' - - -module.exports = - - ### Public ### - - # options - {Object} - # :appName - {String} - # :appPath - {String} - # :isHiddenOnLaunch - {Boolean} - # :mac - (Optional) {Object} - # :useLaunchAgent - (Optional) {Boolean} - # Returns a Promise - enable: ({appName, appPath, isHiddenOnLaunch, mac}) -> - - # Add the file if we're using a Launch Agent - if mac.useLaunchAgent - programArguments = [appPath] - programArguments.push '--hidden' if isHiddenOnLaunch - programArgumentsSection = programArguments - .map((argument) -> " #{argument}") - .join('\n') - - data = """ - - - - Label - #{appName} - ProgramArguments - - #{programArgumentsSection} - - RunAtLoad - - - """ - - return fileBasedUtilities.createFile { - directory: @getDirectory() - filePath: @getFilePath appName - data: data - } - - # Otherwise, use default method; use AppleScript to tell System Events to add a Login Item - - isHiddenValue = if isHiddenOnLaunch then 'true' else 'false' - properties = "{path:\"#{appPath}\", hidden:#{isHiddenValue}, name:\"#{appName}\"}" - - return @execApplescriptCommand "make login item at end with properties #{properties}" - - - # appName - {String} - # mac - {Object} - # :useLaunchAgent - {Object} - # Returns a Promise - disable: (appName, mac) -> - # Delete the file if we're using a Launch Agent - return fileBasedUtilities.removeFile @getFilePath appName if mac.useLaunchAgent - - # Otherwise remove the Login Item - return @execApplescriptCommand "delete login item \"#{appName}\"" - - - # appName - {String} - # mac - {Object} - # :useLaunchAgent - {Object} - # Returns a Promise which resolves to a {Boolean} - isEnabled: (appName, mac) -> - # Check if the Launch Agent file exists - return fileBasedUtilities.isEnabled @getFilePath appName if mac.useLaunchAgent - - # Otherwise check if a Login Item exists for our app - return @execApplescriptCommand('get the name of every login item').then (loginItems) -> - return loginItems? and appName in loginItems - - - ### Private ### - - - # commandSuffix - {String} - # Returns a Promise - execApplescriptCommand: (commandSuffix) -> - return new Promise (resolve, reject) -> - applescript.execString "tell application \"System Events\" to #{commandSuffix}", (err, result) -> - return reject err if err? - resolve result - - - # Returns a {String} - getDirectory: -> untildify '~/Library/LaunchAgents/' - - - # appName - {String} - # Returns a {String} - getFilePath: (appName) -> "#{@getDirectory()}#{appName}.plist" \ No newline at end of file diff --git a/src/AutoLaunchWindows.coffee b/src/AutoLaunchWindows.coffee deleted file mode 100644 index 1702d1f..0000000 --- a/src/AutoLaunchWindows.coffee +++ /dev/null @@ -1,65 +0,0 @@ -fs = require 'fs' -path = require 'path' -Winreg = require 'winreg' - - -regKey = new Winreg - hive: Winreg.HKCU - key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' - - -module.exports = - - ### Public ### - - # options - {Object} - # :appName - {String} - # :appPath - {String} - # :isHiddenOnLaunch - {Boolean} - # Returns a Promise - enable: ({appName, appPath, isHiddenOnLaunch}) -> - return new Promise (resolve, reject) -> - # If they're using Electron and Squirrel.Windows, point to its Update.exe instead of the actual appPath - # Otherwise, we'll auto-launch an old version after the app has updated - updateDotExe = path.join(path.dirname(process.execPath), '..', 'update.exe') - - if process.versions?.electron? and fs.existsSync updateDotExe - pathToAutoLaunchedApp = "\"#{updateDotExe}\"" - args = " --processStart \"#{path.basename(process.execPath)}\"" - args += ' --process-start-args "--hidden"' if isHiddenOnLaunch - else - # If this is an AppX (from Microsoft Store), the path doesn't point to a directory per se, - # but it's made of "DEV_ID.APP_ID!PACKAGE_NAME". It's used to identify the app in the AppsFolder. - # To launch the app, explorer.exe must be call in combination with its path relative to AppsFolder - if process.windowsStore? - pathToAutoLaunchedApp = "\"explorer.exe\" shell:AppsFolder\\#{appPath}" - else - pathToAutoLaunchedApp = "\"#{appPath}\"" - args = ' --hidden' if isHiddenOnLaunch - - regKey.set appName, Winreg.REG_SZ, "#{pathToAutoLaunchedApp}#{args}", (err) -> - return reject(err) if err? - resolve() - - - # appName - {String} - # Returns a Promise - disable: (appName) -> - return new Promise (resolve, reject) -> - regKey.remove appName, (err) -> - if err? - # The registry key should exist but, in case it fails because it doesn't exist, - # resolve false instead of rejecting with an error - if err.message.indexOf('The system was unable to find the specified registry key or value') isnt -1 - return resolve false - return reject err - resolve() - - - # appName - {String} - # Returns a Promise which resolves to a {Boolean} - isEnabled: (appName) -> - return new Promise (resolve, reject) -> - regKey.get appName, (err, item) -> - return resolve false if err? - resolve(item?) diff --git a/src/fileBasedUtilities.coffee b/src/fileBasedUtilities.coffee deleted file mode 100644 index 8bb7850..0000000 --- a/src/fileBasedUtilities.coffee +++ /dev/null @@ -1,43 +0,0 @@ -fs = require 'fs' -mkdirp = require 'mkdirp' - -# Public: a few utils for file-based auto-launching -module.exports = - - ### Public ### - - # This is essentially enabling auto-launching - # options - {Object} - # :directory - {String} - # :filePath - {String} - # :data - {String} - # Returns a Promise - createFile: ({directory, filePath, data}) -> - return new Promise (resolve, reject) -> - mkdirp directory, (mkdirErr) -> - return reject mkdirErr if mkdirErr? - fs.writeFile filePath, data, (writeErr) -> - return reject(writeErr) if writeErr? - resolve() - - - # filePath - {String} - isEnabled: (filePath) -> - return new Promise (resolve, reject) => - fs.stat filePath, (err, stat) -> - return resolve false if err? - resolve(stat?) - - - # This is essentially disabling auto-launching - # filePath - {String} - # Returns a Promise - removeFile: (filePath) -> - return new Promise (resolve, reject) => - fs.stat filePath, (statErr) -> - # If it doesn't exist, this is good so resolve - return resolve() if statErr? - - fs.unlink filePath, (unlinkErr) -> - return reject(unlinkErr) if unlinkErr? - resolve() \ No newline at end of file diff --git a/src/index.coffee b/src/index.coffee deleted file mode 100644 index 0d17d57..0000000 --- a/src/index.coffee +++ /dev/null @@ -1,118 +0,0 @@ -pathTools = require 'path' - -# Public: The main auto-launch class -module.exports = class AutoLaunch - - ### Public ### - - # options - {Object} - # :name - {String} - # :isHidden - (Optional) {Boolean} - # :mac - (Optional) {Object} - # :useLaunchAgent - (Optional) {Boolean}. If `true`, use filed-based Launch Agent. Otherwise use AppleScript - # to add Login Item - # :path - (Optional) {String} - constructor: ({name, isHidden, mac, path}) -> - throw new Error 'You must specify a name' unless name? - - @opts = - appName: name - isHiddenOnLaunch: if isHidden? then isHidden else false - mac: mac ? {} - - versions = process?.versions - if path? - # Verify that the path is absolute - throw new Error 'path must be absolute' unless (pathTools.isAbsolute path) or process.windowsStore - @opts.appPath = path - - else if versions? and (versions.nw? or versions['node-webkit']? or versions.electron?) - # This appPath will need to be fixed later depending of the OS used - @opts.appPath = process.execPath - - else - throw new Error 'You must give a path (this is only auto-detected for NW.js and Electron apps)' - - @fixOpts() - - @api = null - if /^win/.test process.platform - @api = require './AutoLaunchWindows' - else if /darwin/.test process.platform - @api = require './AutoLaunchMac' - else if (/linux/.test process.platform) or (/freebsd/.test process.platform) - @api = require './AutoLaunchLinux' - else - throw new Error 'Unsupported platform' - - - enable: => @api.enable @opts - - - disable: => @api.disable @opts.appName, @opts.mac - - - # Returns a Promise which resolves to a {Boolean} - isEnabled: => @api.isEnabled @opts.appName, @opts.mac - - - ### Private ### - - - # Corrects the path to point to the outer .app - # path - {String} - # macOptions - {Object} - # Returns a {String} - fixMacExecPath: (path, macOptions) -> - # This will match apps whose inner app and executable's basename is the outer app's basename plus "Helper" - # (the default Electron app structure for example) - # It will also match apps whose outer app's basename is different to the rest but the inner app and executable's - # basenames are matching (a typical distributed NW.js app for example) - # Does not match when the three are different - # Also matches when the path is pointing not to the exectuable in the inner app at all but to the Electron - # executable in the outer app - path = path.replace /(^.+?[^\/]+?\.app)\/Contents\/(Frameworks\/((\1|[^\/]+?) Helper)\.app\/Contents\/MacOS\/\3|MacOS\/Electron)/, '$1' - # When using a launch agent, it needs the inner executable path - path = path.replace /\.app\/Contents\/MacOS\/[^\/]*$/, '.app' unless macOptions.useLaunchAgent - return path - - # Under Linux and FreeBSD, fix the ExecPath when packaged as AppImage and escape the spaces correctly - # path - {String} - # Returns a {String} - fixLinuxExecPath: (path) -> - # If this is an AppImage, the actual AppImage's file path must be used, otherwise the mount path will be used. - # This will fail on the next launch, since AppImages are mount temporarily when executed in an everchanging mount folder. - if process.env.APPIMAGE? - path = process.env.APPIMAGE - console.log "Using real AppImage path at: %s", process.env.APPIMAGE - - # As stated in the .desktop spec, Exec key's value must be properly escaped with reserved characters. - path = path.replace(/(\s+)/g, '\\$1') - - return path - - - fixOpts: => - @opts.appPath = @opts.appPath.replace /\/$/, '' - - if /darwin/.test process.platform - @opts.appPath = @fixMacExecPath(@opts.appPath, @opts.mac) - - if (/linux/.test process.platform) or (/freebsd/.test process.platform) - @opts.appPath = @fixLinuxExecPath(@opts.appPath) - - # Comment: why are we fiddling with the appName while this is a mandatory when calling the constructor. - # Shouldn't we honor the provided name? Windows use the name as a descriptor, macOS uses - # it for naming the .plist file and Linux/FreeBSD use it to name the .desktop file. - if @opts.appPath.indexOf('\\') isnt -1 - - tempPath = @opts.appPath.split '\\' - @opts.appName = tempPath[tempPath.length - 1] - @opts.appName = @opts.appName.substr(0, @opts.appName.length - '.exe'.length) - - if /darwin/.test process.platform - tempPath = @opts.appPath.split '/' - @opts.appName = tempPath[tempPath.length - 1] - # Remove ".app" from the appName if it exists - if @opts.appName.indexOf('.app', @opts.appName.length - '.app'.length) isnt -1 - @opts.appName = @opts.appName.substr(0, @opts.appName.length - '.app'.length) \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..bf89728 --- /dev/null +++ b/src/index.js @@ -0,0 +1,61 @@ +import pathTools from 'path'; +import autoLaunchHandler from './library/autoLaunchHandler.js' + +// Public: The main auto-launch class +export default class AutoLaunch { + /* Public */ + + // {Object} + // :name - {String} + // :path - (Optional) {String} + // :options - (Optional) {Object} + // :launchInBackground, - (Optional) {String}. If set, either use default --hidden arg or specified one. + // :mac - (Optional) {Object} + // :useLaunchAgent - (Optional) {Boolean}. If `true`, use filed-based Launch Agent. Otherwise use AppleScript + // to add Login Item + // :extraArgs - (Optional) {Array} + constructor({ name, path, options }) { + // Name is the only mandatory parameter and must neither be null nor empty + if (!name) { throw new Error('You must specify a name'); } + + let opts = { + appName: name, + options: { + launchInBackground: (options && (options.launchInBackground != null)) ? options.launchInBackground : false, + mac: (options && (options.mac != null)) ? options.mac : {}, + extraArguments: (options && (options.extraArguments != null)) ? options.extraArgs : [] + } + }; + + const versions = typeof process !== 'undefined' && process !== null ? process.versions : undefined; + if (path != null) { + // Verify that the path is absolute or is an AppX path + if ((!pathTools.isAbsolute(path)) && !process.windowsStore) { + throw new Error('path must be absolute'); + } + opts.appPath = path; + } else if ((versions != null) && ((versions.nw != null) || (versions['node-webkit'] != null) || (versions.electron != null))) { + // Autodetecting the appPath from the execPath. + // This appPath will need to be fixed later depending of the OS used + // TODO: is this the reason behind issue 92: https://github.com/Teamwork/node-auto-launch/issues/92 + opts.appPath = process.execPath; + } else { + throw new Error('You must give a path (this is only auto-detected for NW.js and Electron apps)'); + } + + this.api = autoLaunchHandler(opts); + } + + enable() { + return this.api.enable(); + } + + disable() { + return this.api.disable(); + } + + // Returns a Promise which resolves to a {Boolean} + isEnabled() { + return this.api.isEnabled(); + } +} diff --git a/src/library/autoLaunchAPI/autoLaunchAPI.js b/src/library/autoLaunchAPI/autoLaunchAPI.js new file mode 100644 index 0000000..7f0c5a3 --- /dev/null +++ b/src/library/autoLaunchAPI/autoLaunchAPI.js @@ -0,0 +1,33 @@ +export default class AutoLaunchAPI { + /* Public */ + + // init - {Object} + // :appName - {String} + // :appPath - {String} + // :options - {Object} + // :launchInBackground - (Optional) {String} If set, either use default --hidden arg or specified one. + // :mac - (Optional) {Object} + // :useLaunchAgent - (Optional) {Boolean} If `true`, use filed-based Launch Agent. Otherwise use AppleScript + // to add Login Item + // :extraArguments - (Optional) {Array} + constructor(init) { + this.appName = init.appName; + this.appPath = init.appPath; + this.options = init.options; + } + + // Returns a Promise + enable() { + throw new Error('enable() not implemented'); + } + + // Returns a Promise + disable() { + throw new Error('disable() not implemented'); + } + + // Returns a Promise which resolves to a {Boolean} + isEnabled() { + throw new Error('isEnable() not implemented'); + } +} diff --git a/src/library/autoLaunchAPI/autoLaunchAPILinux.js b/src/library/autoLaunchAPI/autoLaunchAPILinux.js new file mode 100644 index 0000000..ad6e8b5 --- /dev/null +++ b/src/library/autoLaunchAPI/autoLaunchAPILinux.js @@ -0,0 +1,91 @@ +import path from 'node:path'; +import untildify from 'untildify'; +import * as fileBasedUtilities from '../fileBasedUtilities.js'; +import AutoLaunchAPI from './autoLaunchAPI.js' + +const LINUX_AUTOSTART_DIR = '~/.config/autostart'; +const LINUX_DESKTOP = ` +[Desktop Entry] +Type=Application +Version=1.0 +Name={{APP_NAME}} +Comment={{APP_NAME}} startup script +Exec={{APP_PATH}} {{ARGS}} +StartupNotify=false +Terminal=false +`; + +export default class AutoLaunchAPILinux extends AutoLaunchAPI { + /* Public */ + + constructor(init) { + super(init); + this.appPath = this.#fixAppPath(); + } + + // Returns a Promise + enable() { + const hiddenArg = this.options.launchInBackground; + const extraArgs = this.options.extraArguments; + const programArguments = []; + + // Manage arguments + if (hiddenArg) { + programArguments.push((hiddenArg !== true) ? hiddenArg : '--hidden'); + } + if (extraArgs) { + programArguments.push(extraArgs); + } + const args = programArguments.join(' '); + + const desktop = LINUX_DESKTOP.trim() + .replace(/{{APP_NAME}}/g, this.appName) + .replace(/{{APP_PATH}}/g, this.appPath) + .replace(/{{ARGS}}/g, args); + + return fileBasedUtilities.createFile({ + directory: this.#getAutostartDirectory(), + filePath: this.#getDesktopFilePath(this.appName), + data: desktop + }); + } + + // Returns a Promise + disable() { + return fileBasedUtilities.removeFile(this.#getDesktopFilePath()); + } + + // Returns a Promise which resolves to a {Boolean} + isEnabled() { + return fileBasedUtilities.fileExists(this.#getDesktopFilePath()); + } + + /* Private */ + + // Returns a {String} + #getAutostartDirectory() { + return untildify(LINUX_AUTOSTART_DIR); + } + + // Returns a {String} + #getDesktopFilePath() { + return path.join(this.#getAutostartDirectory(), `${this.appName}.desktop`); + } + + // Returns a {String} + #fixAppPath() { + let execPath = this.appPath; + + // If this is an AppImage, the actual AppImage's file path must be used, otherwise the mount path will be used. + // This will fail on the next launch, since AppImages are mount temporarily when executed + // in an everchanging mount folder. + if (process.env?.APPIMAGE != null) { + execPath = process.env.APPIMAGE; + } + + // As stated in the .desktop entry spec, Exec key's value must be properly escaped with reserved characters. + execPath = fileBasedUtilities.escapeFilePath(execPath); + + return execPath; + } +} diff --git a/src/library/autoLaunchAPI/autoLaunchAPIMac.js b/src/library/autoLaunchAPI/autoLaunchAPIMac.js new file mode 100644 index 0000000..1c9b2db --- /dev/null +++ b/src/library/autoLaunchAPI/autoLaunchAPIMac.js @@ -0,0 +1,156 @@ +import applescript from 'applescript'; +import path from 'node:path'; +import untildify from 'untildify'; +import * as fileBasedUtilities from '../fileBasedUtilities.js'; +import AutoLaunchAPI from './autoLaunchAPI.js' + +const MAC_LAUNCHAGENTS_DIR = '~/Library/LaunchAgents/'; +const MAC_PLIST_DATA = ` + + + +Label +{{APP_NAME}} +ProgramArguments + +{{PROGRAM_ARGUMENTS_SECTION}} + +RunAtLoad + + +`; + +export default class AutoLaunchAPIMac extends AutoLaunchAPI { + /* Public */ + + constructor(init) { + super(init); + this.appName = this.#fixAppName(); + this.appPath = this.#fixAppPath(); + } + + // Returns a Promise + enable() { + const hiddenArg = this.options.launchInBackground; + const extraArgs = this.options.extraArguments; + + // Add the file if we're using a Launch Agent + if (this.options.mac.useLaunchAgent) { + const programArguments = [this.appPath]; + + // Manage arguments + if (hiddenArg) { + programArguments.push((hiddenArg !== true) ? hiddenArg : '--hidden'); + } + if (extraArgs) { + programArguments.push(extraArgs); + } + const programArgumentsSection = programArguments + .map((argument) => ` ${argument}`) + .join('\n'); + const plistData = MAC_PLIST_DATA.trim() + .replace(/{{APP_NAME}}/g, this.appName) + .replace(/{{PROGRAM_ARGUMENTS_SECTION}}/g, programArgumentsSection); + + return fileBasedUtilities.createFile({ + directory: this.#getLaunchAgentsDirectory(), + filePath: this.#getPlistFilePath(), + data: plistData + }); + } + + // Otherwise, use default method; use AppleScript to tell System Events to add a Login Item + + const isHidden = hiddenArg ? 'true' : 'false'; + // TODO: Manage extra arguments + const properties = `{path:"${this.appPath}", hidden:${isHidden}, name:"${this.appName}"}`; + + return this.#execApplescriptCommand(`make login item at end with properties ${properties}`); + } + + // Returns a Promise + disable() { + // Delete the file if we're using a Launch Agent + if (this.options.mac.useLaunchAgent) { + return fileBasedUtilities.removeFile(this.#getPlistFilePath()); + } + + // Otherwise remove the Login Item + return this.#execApplescriptCommand(`delete login item "${this.appName}"`); + } + + // Returns a Promise which resolves to a {Boolean} + isEnabled() { + // Check if the Launch Agent file exists + if (this.options.mac.useLaunchAgent) { + return fileBasedUtilities.fileExists(this.#getPlistFilePath()); + } + + // Otherwise check if a Login Item exists for our app + return this.#execApplescriptCommand('get the name of every login item') + .then((loginItems) => (loginItems != null) && Array.from(loginItems).includes(this.appName)); + } + + /* Private */ + + // commandSuffix - {String} + // Returns a Promise + #execApplescriptCommand(commandSuffix) { + return new Promise((resolve, reject) => { + applescript.execString(`tell application "System Events" to ${commandSuffix}`, (err, result) => { + if (err != null) { + return reject(err); + } + return resolve(result); + }) + }); + } + + // Returns a {String} + #getLaunchAgentsDirectory() { + return untildify(MAC_LAUNCHAGENTS_DIR); + } + + // Returns a {String} + #getPlistFilePath() { + return path.join(this.#getLaunchAgentsDirectory(), `${this.appName}.plist`); + } + + // Corrects the path to point to the outer .app + // Returns a {String} + #fixAppPath() { + let execPath = this.appPath; + + // This will match apps whose inner app and executable's basename is the outer app's basename plus "Helper" + // (the default Electron app structure for example) + // It will also match apps whose outer app's basename is different to the rest but the inner app and executable's + // basenames are matching (a typical distributed NW.js app for example) + // Does not match when the three are different + // Also matches when the path is pointing not to the exectuable in the inner app at all but to the Electron + // executable in the outer app + execPath = execPath.replace(/(^.+?[^\/]+?\.app)\/Contents\/(Frameworks\/((\1|[^\/]+?) Helper)\.app\/Contents\/MacOS\/\3|MacOS\/Electron)/, '$1'); + + // When using a launch agent, it needs the inner executable path + if (!this.options.mac.useLaunchAgent) { + execPath = execPath.replace(/\.app\/Contents\/MacOS\/[^\/]*$/, '.app'); + } + + return execPath; + } + + // Kept from Coffeescript, but should we honor the name given to autoLaunch or should we change it specifically for macOS? + // No explanation, see issue 92: https://github.com/Teamwork/node-auto-launch/issues/92 + #fixAppName() { + let fixedName; + + const tempPath = this.appPath.split('/'); + + fixedName = tempPath[tempPath.length - 1]; + // Remove ".app" from the appName if it exists + if (fixedName.indexOf('.app', fixedName.length - '.app'.length) !== -1) { + fixedName = fixedName.substr(0, fixedName.length - '.app'.length); + } + + return fixedName; + } +} diff --git a/src/library/autoLaunchAPI/autoLaunchAPIWindows.js b/src/library/autoLaunchAPI/autoLaunchAPIWindows.js new file mode 100644 index 0000000..082657c --- /dev/null +++ b/src/library/autoLaunchAPI/autoLaunchAPIWindows.js @@ -0,0 +1,110 @@ +import fs from 'fs'; +import path from 'path'; +import Winreg from 'winreg'; +import AutoLaunchAPI from './autoLaunchAPI.js' + +const regKey = new Winreg({ + hive: Winreg.HKCU, + key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' +}); + +export default class AutoLaunchAPIWindows extends AutoLaunchAPI { + /* Public */ + + constructor(init) { + super(init); + } + + // Returns a Promise + enable() { + return new Promise((resolve, reject) => { + let args = ''; + let pathToAutoLaunchedApp; + const hiddenArg = this.options.launchInBackground; + const extraArgs = this.options.extraArguments; + const updateDotExe = path.join(path.dirname(process.execPath), '..', 'update.exe'); + + // If they're using Electron and Squirrel.Windows, point to its Update.exe instead of the actual appPath + // Otherwise, we'll auto-launch an old version after the app has updated + if (((process.versions != null ? process.versions.electron : undefined) != null) && fs.existsSync(updateDotExe)) { + pathToAutoLaunchedApp = `"${updateDotExe}"`; + console.log('This application is built on Electron and is launched through: ', pathToAutoLaunchedApp); + args = ` --processStart "${path.basename(process.execPath)}"`; + + // Manage arguments + if (hiddenArg || extraArgs) { + args += ' --process-start-args'; + if (hiddenArg) { + args += ` "${(hiddenArg !== true) ? hiddenArg : '--hidden'}"`; + } + // Add any extra arguments + if (extraArgs) { + args += ' "'; + args += extraArgs.join('" "'); + args += '"'; + } + } + } else { + // If this is an AppX (from Microsoft Store), the path doesn't point to a directory per se, + // but it's made of "DEV_ID.APP_ID!PACKAGE_NAME". It's used to identify the app in the AppsFolder. + // To launch the app, explorer.exe must be call in combination with its path relative to AppsFolder + if (process.windowsStore) { + pathToAutoLaunchedApp = `"explorer.exe" shell:AppsFolder\\${this.appPath}`; + console.log('This application is an AppX and is launched through: ', pathToAutoLaunchedApp); + } else { + pathToAutoLaunchedApp = `"${this.appPath}"`; + console.log('This application is launched through: ', pathToAutoLaunchedApp); + } + + // Manage arguments + if (hiddenArg) { + args = [(hiddenArg !== true) ? hiddenArg : ' --hidden']; + } + // Add any extra arguments + if (extraArgs) { + args += ' '; + args += extraArgs.join(' '); + } + } + + regKey.set(this.appName, Winreg.REG_SZ, `"${pathToAutoLaunchedApp}"${args}`, (err) => { + if (err != null) { + console.log(err.message); + return reject(err); + } + return resolve(); + }); + }); + } + + // Returns a Promise + disable() { + return new Promise((resolve, reject) => { + regKey.remove(this.appName, (err) => { + if (err != null) { + // The registry key should exist but, in case it fails because it doesn't exist, + // resolve false instead of rejecting with an error + if (err.message.indexOf('The system was unable to find the specified registry key or value') !== -1) { + return resolve(false); + } + console.log(err.message); + return reject(err); + } + return resolve(); + }); + }); + } + + // Returns a Promise which resolves to a {Boolean} + isEnabled() { + return new Promise((resolve, reject) => { + regKey.valueExists(this.appName, (err, exists) => { + if (err != null) { + console.log(err.message); + return resolve(false); + } + return resolve(exists); + }); + }); + } +} diff --git a/src/library/autoLaunchHandler.js b/src/library/autoLaunchHandler.js new file mode 100644 index 0000000..090dad8 --- /dev/null +++ b/src/library/autoLaunchHandler.js @@ -0,0 +1,21 @@ +import AutoLaunchAPILinux from './autoLaunchAPI/autoLaunchAPILinux.js'; +import AutoLaunchAPIMac from './autoLaunchAPI/autoLaunchAPIMac.js'; +import AutoLaunchAPIWindows from './autoLaunchAPI/autoLaunchAPIWindows.js'; + +/* This allows to select the AutoLaunch implementation specific to a */ +// +// Returns a AutoLaunchAPI object + +export default function autoLaunchHandler(options) { + if (/^win/.test(process.platform)) { + return new AutoLaunchAPIWindows(options); + } + if (/darwin/.test(process.platform)) { + return new AutoLaunchAPIMac(options); + } + if ((/linux/.test(process.platform)) || (/freebsd/.test(process.platform))) { + return new AutoLaunchAPILinux(options); + } + + throw new Error('Unsupported platform'); +} diff --git a/src/library/fileBasedUtilities.js b/src/library/fileBasedUtilities.js new file mode 100644 index 0000000..3bc76a3 --- /dev/null +++ b/src/library/fileBasedUtilities.js @@ -0,0 +1,68 @@ +import fs from 'fs'; + +// Public: a few utils for file-based auto-launching + +// This is essentially enabling auto-launching +// options - {Object} +// :directory - {String} +// :filePath - {String} +// :data - {String} +// Returns a Promise +export function createFile({directory, filePath, data}) { + return new Promise((resolve, reject) => { + fs.mkdir(directory, { recursive: true }, (mkdirErr, path) => { + if (mkdirErr != null) { + return reject(mkdirErr); + } + return fs.writeFile(filePath, data, (writeErr) => { + if (writeErr != null) { + return reject(writeErr); + } + return resolve(); + }); + }); + }); +} + +// Verify auto-launch file exists or not +// filePath - {String} +// Returns a Promise +export function fileExists(filePath) { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err, stat) => { + if (err != null) { + return resolve(false); + } + return resolve(stat != null); + }); + }); +} + +// This is essentially disabling auto-launching +// filePath - {String} +// Returns a Promise +export function removeFile(filePath) { + return new Promise((resolve, reject) => { + fs.stat(filePath, (statErr) => { + // If it doesn't exist, this is good so resolve + if (statErr != null) { + return resolve(); + } + + return fs.unlink(filePath, (unlinkErr) => { + if (unlinkErr != null) { + return reject(unlinkErr); + } + return resolve(); + }); + }); + }); +} + +// Escape reserved characters in path +// filePath - {String} +// Returns {String} +export function escapeFilePath(filePath) { + return filePath.replace(/(\s+)/g, '\\$1'); +// return filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // https://github.com/tc39/proposal-regex-escaping +} diff --git a/tests/helper.coffee b/tests/helper.coffee deleted file mode 100644 index 1fa6d02..0000000 --- a/tests/helper.coffee +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = class AutoLaunchHelper - constructor: (autoLaunch) -> - @autoLaunch = autoLaunch - - ensureEnabled: -> - @autoLaunch.isEnabled().then (enabled) => - @autoLaunch.enable() unless enabled - - ensureDisabled: -> - @autoLaunch.isEnabled().then (enabled) => - @autoLaunch.disable() if enabled - - mockApi: (stubs) -> - @autoLaunch.api = stubs diff --git a/tests/helper.js b/tests/helper.js new file mode 100644 index 0000000..eded865 --- /dev/null +++ b/tests/helper.js @@ -0,0 +1,27 @@ +export default class AutoLaunchHelper { + constructor(autoLaunch) { + this.autoLaunch = autoLaunch; + } + + ensureEnabled() { + return this.autoLaunch.isEnabled().then((enabled) => { + if (!enabled) { + return this.autoLaunch.enable(); + } + return enabled; + }); + } + + ensureDisabled() { + return this.autoLaunch.isEnabled().then((enabled) => { + if (enabled) { + return this.autoLaunch.disable(); + } + return enabled; + }); + } + + mockApi(stub) { + this.autoLaunch.api = stub; + } +} diff --git a/tests/index.coffee b/tests/index.coffee deleted file mode 100644 index 408236d..0000000 --- a/tests/index.coffee +++ /dev/null @@ -1,170 +0,0 @@ -chai = require 'chai' -fs = require 'fs' -path = require 'path' -untildify = require 'untildify' -expect = chai.expect -AutoLaunch = require '../src/' -AutoLaunchHelper = require './helper' - -isMac = false -followsXDG = false - -if /^win/.test process.platform - executablePath = path.resolve path.join './tests/executables', 'GitHubSetup.exe' -else if /darwin/.test process.platform - isMac = true - executablePath = '/Applications/Calculator.app' -else if (/linux/.test process.platform) or (/freebsd/.test process.platform) - followsXDG = true - executablePath = path.resolve path.join './tests/executables', 'hv3-linux-x86' - -console.log "Executable being used for tests:", executablePath - -# General tests for all platforms -describe 'node-auto-launch', -> - autoLaunch = null - autoLaunchHelper = null - - beforeEach -> - autoLaunch = new AutoLaunch - name: 'node-auto-launch test' - path: executablePath - mac: - useLaunchAgent: if isMac then true - autoLaunchHelper = new AutoLaunchHelper(autoLaunch) - - describe '.isEnabled', -> - beforeEach -> - autoLaunchHelper.ensureDisabled() - - it 'should be disabled', (done) -> - autoLaunch.isEnabled().then (enabled) -> - expect(enabled).to.equal false - done() - .catch done - return - - it 'should catch errors', (done) -> - autoLaunchHelper.mockApi - isEnabled: -> - Promise.reject() - - autoLaunch.isEnabled().catch done - return - - describe '.enable', -> - beforeEach -> - autoLaunchHelper.ensureDisabled() - - it 'should enable auto launch', (done) -> - autoLaunch.enable() - .then () -> - autoLaunch.isEnabled() - .then (enabled) -> - expect(enabled).to.equal true - done() - .catch done - return - - it 'should catch errors', (done) -> - autoLaunchHelper.mockApi - enable: -> Promise.reject() - - autoLaunch.enable().catch done - return - - describe '.disable', -> - beforeEach -> - autoLaunchHelper.ensureEnabled() - - it 'should disable auto launch', (done) -> - autoLaunch.disable() - .then -> autoLaunch.isEnabled() - .then (enabled) -> - expect(enabled).to.equal false - done() - .catch done - return - - it 'should catch errors', (done) -> - autoLaunchHelper.mockApi - disable: -> - Promise.reject() - - autoLaunch.disable().catch done - return - - return unless followsXDG - - describe 'testing .appName', -> - beforeEach -> - autoLaunchLinux = null - autoLaunchHelper = null - - it 'without space', (done) -> - autoLaunchLinux = new AutoLaunch - name: 'node-auto-launch' - path: executablePath - autoLaunchHelper = new AutoLaunchHelper(autoLaunchLinux) - - autoLaunchLinux.enable() - .then () -> - desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunchLinux.opts.appName + '.desktop')) - fs.stat desktopEntryPath, (err, stats) => - if err - done err - expect(stats.isFile()).to.equal true - done() - .catch done - return - - it 'with space', (done) -> - autoLaunchLinux = new AutoLaunch - name: 'node-auto-launch test' - path: executablePath - autoLaunchHelper = new AutoLaunchHelper(autoLaunchLinux) - - autoLaunchLinux.enable() - .then () -> - desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunchLinux.opts.appName + '.desktop')) - fs.stat desktopEntryPath, (err, stats) => - if err - done err - expect(stats.isFile()).to.equal true - done() - .catch done - return - - it 'with capital letters', (done) -> - autoLaunchLinux = new AutoLaunch - name: 'Node-Auto-Launch' - path: executablePath - autoLaunchHelper = new AutoLaunchHelper(autoLaunchLinux) - - autoLaunchLinux.enable() - .then () -> - desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunchLinux.opts.appName + '.desktop')) - fs.stat desktopEntryPath, (err, stats) => - if err - done err - expect(stats.isFile()).to.equal true - done() - .catch done - return - - afterEach -> - autoLaunchHelper.ensureDisabled() - - describe 'testing path name', -> - executablePathLinux = path.resolve './path with spaces/' - autoLaunchLinux = new AutoLaunch - name: 'node-auto-launch test' - path: executablePathLinux - autoLaunchHelper = new AutoLaunchHelper(autoLaunchLinux) - - it 'should properly escape reserved caracters', (done) -> - expect(autoLaunchLinux.opts.appPath).not.to.equal executablePathLinux - done() - return - - return diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 0000000..e83af15 --- /dev/null +++ b/tests/test.js @@ -0,0 +1,283 @@ +import { expect, should } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import untildify from 'untildify'; +import AutoLaunch from '../src/index.js'; +import AutoLaunchHelper from './helper.js'; + +let executablePath = ''; +let followsXDG = false; +let isPOSIX = false; +let isMac = false; + +if (/^win/.test(process.platform)) { + executablePath = path.resolve(path.join('./tests/executables', 'GitHubSetup.exe')); +} else if (/darwin/.test(process.platform)) { + isMac = true; + executablePath = '/Applications/Calculator.app'; +} else if ((/linux/.test(process.platform)) || (/freebsd/.test(process.platform))) { + followsXDG = true; + isPOSIX = true; + executablePath = path.resolve(path.join('./tests/executables', 'hv3-linux-x86')); +} + +console.log('Executable being used for tests:', executablePath); + +// General tests for all platforms +describe('node-auto-launch', () => { + let autoLaunch = null; + let autoLaunchHelper = null; + + beforeEach(() => { + autoLaunch = new AutoLaunch({ + name: 'node-auto-launch test', + path: executablePath, + options: { + mac: isMac ? { useLaunchAgent: true } : {} + } + }); + autoLaunchHelper = new AutoLaunchHelper(autoLaunch); + }); + + describe('AutoLaunch constructor', () => { + it('should fail without a name', function (done) { + try { + autoLaunch = new AutoLaunch({ name: null }); + // Force the test to fail since error wasn't thrown + should.fail('It should have failed...'); + } catch (error) { + // Constructor threw Error, so test succeeded. + done(); + } + }); + + it('should fail with an empty name', function (done) { + try { + autoLaunch = new AutoLaunch({ name: '' }); + // Force the test to fail since error wasn't thrown + should.fail('It should have failed...'); + } catch (error) { + // Constructor threw Error, so test succeeded. + done(); + } + }); + }); + + describe('.isEnabled', () => { + before(() => { + autoLaunchHelper.ensureDisabled(); + }); + + it('should be disabled', function (done) { + autoLaunch.isEnabled() + .then((enabled) => { + expect(enabled).to.equal(false); + done(); + }) + .catch(done); + }); + + after(() => { + autoLaunchHelper.ensureDisabled(); + }); + }); + + describe('.enable', () => { + before(() => { + autoLaunchHelper.ensureDisabled(); + }); + + it('should enable auto launch', function (done) { + autoLaunch.enable() + .then(() => { + autoLaunch.isEnabled() + .then((enabled) => { + expect(enabled).to.equal(true); + done(); + }) + .catch(done); + }); + }); + + after(() => { + autoLaunchHelper.ensureDisabled(); + }); + }); + + describe('.disable', () => { + beforeEach(() => { + autoLaunchHelper.ensureEnabled(); + }); + + it('should disable auto launch', function (done) { + autoLaunch.disable() + .then(() => { + autoLaunch.isEnabled() + .then((enabled) => { + expect(enabled).to.equal(false); + done(); + }) + .catch(done); + }); + }); + + afterEach(() => { + autoLaunchHelper.ensureDisabled(); + }); + }); + + describe('Let\'s catch errors', () => { + it('should catch isEnable() errors', function (done) { + autoLaunchHelper.mockApi({ + isEnabled() { + return Promise.reject(); + } + }); + + autoLaunch.isEnabled().catch(done); + }); + + it('should catch enable() errors', function (done) { + autoLaunchHelper.mockApi({ + enable() { + return Promise.reject(); + } + }); + autoLaunch.enable().catch(done); + }); + + it('should catch disable() errors', function (done) { + autoLaunchHelper.ensureEnabled(); + + autoLaunchHelper.mockApi({ + disable() { + return Promise.reject(); + } + }); + autoLaunch.disable().catch(done); + }); + }); + + /* On macOS, we modify the appName (leftover from Coffeescript that had no explaination) */ + // See issue 92: https://github.com/Teamwork/node-auto-launch/issues/92 + if (!isMac) { + describe('.appName', () => { + it('should honor name parameter', function (done) { + expect(autoLaunch.api.appName).to.equal('node-auto-launch test'); + done(); + }); + }); + } +}); + +// Let's test some POSIX/Linux/FreeBSD options +// They rely on reading and write files on POSIX based filesystems and +if (isPOSIX) { + describe('POSIX/Linux/FreeBSD tests', () => { + let autoLaunch = null; + let autoLaunchHelper = null; + const executablePathPosix = path.resolve('./path with spaces/'); + + // OSes/window managers that follow XDG (cross desktop group) specifications + if (followsXDG) { + describe('testing .appName', () => { + beforeEach(() => { + autoLaunch = null; + autoLaunchHelper = null; + }); + + it('without space', function (done) { + autoLaunch = new AutoLaunch({ + name: 'node-auto-launch', + path: executablePathPosix + }); + autoLaunchHelper = new AutoLaunchHelper(autoLaunch); + autoLaunchHelper.ensureDisabled(); + + autoLaunch.enable() + .then(() => { + const desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunch.api.appName + '.desktop')); + fs.stat(desktopEntryPath, (err, stats) => { + if (err) { + done(err); + } + expect(stats.isFile()).to.equal(true); + done(); + }); + }) + .catch(done); + }); + + it('with space', function (done) { + autoLaunch = new AutoLaunch({ + name: 'node-auto-launch test', + path: executablePathPosix + }); + autoLaunchHelper = new AutoLaunchHelper(autoLaunch); + autoLaunchHelper.ensureDisabled(); + + autoLaunch.enable() + .then(() => { + const desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunch.api.appName + '.desktop')); + fs.stat(desktopEntryPath, (err, stats) => { + if (err) { + done(err); + } + expect(stats.isFile()).to.equal(true); + done(); + }); + }) + .catch(done); + }); + + it('with capital letters', function (done) { + autoLaunch = new AutoLaunch({ + name: 'Node-Auto-Launch', + path: executablePathPosix + }); + autoLaunchHelper = new AutoLaunchHelper(autoLaunch); + autoLaunchHelper.ensureDisabled(); + + autoLaunch.enable() + .then(() => { + const desktopEntryPath = untildify(path.join('~/.config/autostart/', autoLaunch.api.appName + '.desktop')); + fs.stat(desktopEntryPath, (err, stats) => { + if (err) { + done(err); + } + expect(stats.isFile()).to.equal(true); + done(); + }); + }) + .catch(done); + }); + + afterEach(() => { + autoLaunchHelper.ensureDisabled(); + }); + }); + } + + describe('testing path name', () => { + beforeEach(() => { + autoLaunch = null; + autoLaunchHelper = null; + }); + + it('should properly escape reserved caracters', function (done) { + autoLaunch = new AutoLaunch({ + name: 'node-auto-launch test', + path: executablePathPosix + }); + autoLaunchHelper = new AutoLaunchHelper(autoLaunch); + + expect(autoLaunch.api.appPath).to.equal(executablePathPosix.replace(/(\s+)/g, '\\$1')); + done(); + }); + + afterEach(() => { + autoLaunchHelper.ensureDisabled(); + }); + }); + }); +}