├── .gitattributes ├── logo.png ├── commitlint.config.js ├── .gitignore ├── .prettierrc ├── __tests__ ├── packages │ └── test-duplicates │ │ ├── node_modules │ │ ├── b │ │ │ └── package.json │ │ ├── a │ │ │ ├── node_modules │ │ │ │ └── b │ │ │ │ │ └── package.json │ │ │ └── package.json │ │ └── @scoped-name │ │ │ └── test │ │ │ └── package.json │ │ └── package.json ├── __snapshots__ │ ├── duplicates.test.js.snap │ ├── scopedPackages.test.js.snap │ └── envinfo.test.js.snap ├── scopedPackages.test.js ├── duplicates.test.js ├── envinfo.test.js └── utils.test.js ├── src ├── matchers.js ├── helpers │ ├── servers.js │ ├── index.js │ ├── monorepos.js │ ├── databases.js │ ├── virtualization.js │ ├── binaries.js │ ├── system.js │ ├── managers.js │ ├── languages.js │ ├── sdks.js │ ├── utilities.js │ ├── browsers.js │ └── ides.js ├── cli.js ├── presets.js ├── formatters.js ├── envinfo.js ├── utils.js └── packages.js ├── .editorconfig ├── babel.config.js ├── .eslintrc.js ├── .gitpod.yml ├── appveyor.yml ├── .github └── workflows │ ├── release-please.yml │ └── node.js.yml ├── webpack.config.js ├── LICENSE ├── package.json ├── .all-contributorsrc ├── CHANGELOG.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabrindle/envinfo/HEAD/logo.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !__tests__/packages/**/node_modules 3 | .DS_Store 4 | .idea/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/packages/test-duplicates/node_modules/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/packages/test-duplicates/node_modules/a/node_modules/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b", 3 | "version": "1.0.2" 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/packages/test-duplicates/node_modules/@scoped-name/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scoped-name/test", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /__tests__/packages/test-duplicates/node_modules/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "b": "1.0.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/matchers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | androidSystemImages: /system-images;([\S \t]+)/g, 3 | androidAPILevels: /platforms;android-(\d+)[\S\s]/g, 4 | androidBuildTools: /build-tools;([\d|.]+)[\S\s]/g, 5 | }; 6 | -------------------------------------------------------------------------------- /__tests__/packages/test-duplicates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-duplicates", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@scoped-name/test": "1.0.0", 6 | "a": "1.0.0", 7 | "b": "1.0.0" 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | modules: 'commonjs', 9 | targets: { 10 | node: '4.9.1', 11 | }, 12 | useBuiltIns: 'usage', 13 | }, 14 | ], 15 | ], 16 | plugins: ['@babel/plugin-proposal-optional-chaining'], 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | node: true, 5 | es6: true, 6 | jest: true, 7 | }, 8 | extends: ['airbnb-base/legacy', 'prettier'], 9 | parserOptions: { 10 | sourceType: 'module', 11 | }, 12 | plugins: ['prettier'], 13 | rules: { 14 | 'vars-on-top': 0, 15 | 'no-param-reassign': 0, 16 | 'prettier/prettier': ['error'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: yarn install && yarn run build 9 | command: yarn run start 10 | 11 | 12 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2022 3 | 4 | environment: 5 | nodejs_version: '24' 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version x64 9 | - yarn 10 | 11 | test_script: 12 | - node --version 13 | - choco install ffmpeg 14 | - choco install r.project -version 3.6.0 15 | - set RPATH=C:\Program Files\R\R-3.6.0\bin\x64 16 | - set PATH=%RPATH%;%PATH% 17 | - node src/cli.js 18 | - SET ENVINFO_DEBUG=trace 19 | - npm run build 20 | - node dist/cli.js 21 | - SET ENVINFO_DEBUG="" 22 | - npm test 23 | 24 | build: off 25 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/duplicates.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Running the programmatic interface return expected duplicates in json 1`] = ` 4 | Object { 5 | "npmPackages": Object { 6 | "b": Object { 7 | "duplicates": Array [ 8 | "1.0.2", 9 | ], 10 | "installed": "1.0.0", 11 | "wanted": "1.0.0", 12 | }, 13 | }, 14 | } 15 | `; 16 | 17 | exports[`Running the programmatic interface return expected duplicates in yaml 1`] = ` 18 | " 19 | npmPackages: 20 | b: 1.0.0 => 1.0.0 (1.0.2) 21 | " 22 | `; 23 | -------------------------------------------------------------------------------- /__tests__/scopedPackages.test.js: -------------------------------------------------------------------------------- 1 | const envinfo = require('../src/envinfo'); 2 | const path = require('path'); 3 | 4 | describe('envinfo will report on scoped npm packages', () => { 5 | test('return expected packages', async () => { 6 | const cwd = process.cwd(); 7 | process.chdir(path.join(__dirname, 'packages', path.sep, 'test-duplicates')); 8 | try { 9 | const data = await envinfo.run({ npmPackages: true }, { duplicates: true, json: true }); 10 | expect(JSON.parse(data)).toMatchSnapshot(); 11 | } finally { 12 | process.chdir(cwd); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/scopedPackages.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`envinfo will report on scoped npm packages return expected packages 1`] = ` 4 | Object { 5 | "npmPackages": Object { 6 | "@scoped-name/test": Object { 7 | "installed": "1.0.0", 8 | "wanted": "1.0.0", 9 | }, 10 | "a": Object { 11 | "installed": "1.0.0", 12 | "wanted": "1.0.0", 13 | }, 14 | "b": Object { 15 | "duplicates": Array [ 16 | "1.0.2", 17 | ], 18 | "installed": "1.0.0", 19 | "wanted": "1.0.0", 20 | }, 21 | }, 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/helpers/servers.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getApacheInfo: () => { 5 | utils.log('trace', 'getApacheInfo'); 6 | if (utils.isMacOS || utils.isLinux) { 7 | return Promise.all([ 8 | utils.run('apachectl -v').then(utils.findVersion), 9 | utils.which('apachectl'), 10 | ]).then(v => utils.determineFound('Apache', v[0], v[1])); 11 | } 12 | return Promise.resolve(['Apache', 'N/A']); 13 | }, 14 | 15 | getNginxInfo: () => { 16 | utils.log('trace', 'getNginxInfo'); 17 | if (utils.isMacOS || utils.isLinux) { 18 | return Promise.all([ 19 | utils.run('nginx -v 2>&1').then(utils.findVersion), 20 | utils.which('nginx'), 21 | ]).then(v => utils.determineFound('Nginx', v[0], v[1])); 22 | } 23 | return Promise.resolve(['Nginx', 'N/A']); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: release-please 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: GoogleCloudPlatform/release-please-action@v2 12 | id: release 13 | with: 14 | release-type: node 15 | package-name: envinfo 16 | - uses: actions/checkout@v2 17 | if: ${{ steps.release.outputs.release_created }} 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: 16 21 | registry-url: 'https://registry.npmjs.org' 22 | if: ${{ steps.release.outputs.release_created }} 23 | - run: yarn && yarn build 24 | if: ${{ steps.release.outputs.release_created }} 25 | - run: npm publish 26 | env: 27 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 28 | if: ${{ steps.release.outputs.release_created }} -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | const packages = require('../packages'); 2 | const utils = require('../utils'); 3 | 4 | const binaries = require('./binaries'); 5 | const browsers = require('./browsers'); 6 | const databases = require('./databases'); 7 | const ides = require('./ides'); 8 | const languages = require('./languages'); 9 | const managers = require('./managers'); 10 | const monorepos = require('./monorepos'); 11 | const sdks = require('./sdks'); 12 | const servers = require('./servers'); 13 | const system = require('./system'); 14 | const utilities = require('./utilities'); 15 | const virtualization = require('./virtualization'); 16 | 17 | module.exports = Object.assign({}, utils, packages, { 18 | ...binaries, 19 | ...browsers, 20 | ...databases, 21 | ...ides, 22 | ...languages, 23 | ...managers, 24 | ...monorepos, 25 | ...sdks, 26 | ...servers, 27 | ...system, 28 | ...utilities, 29 | ...virtualization, 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/duplicates.test.js: -------------------------------------------------------------------------------- 1 | const envinfo = require('../src/envinfo'); 2 | const path = require('path'); 3 | 4 | describe('Running the programmatic interface', () => { 5 | test('return expected duplicates in json', async () => { 6 | const cwd = process.cwd(); 7 | process.chdir(path.join(__dirname, 'packages', path.sep, 'test-duplicates')); 8 | try { 9 | const data = await envinfo.run({ npmPackages: ['b'] }, { duplicates: true, json: true }); 10 | expect(JSON.parse(data)).toMatchSnapshot(); 11 | } finally { 12 | process.chdir(cwd); 13 | } 14 | }); 15 | 16 | test('return expected duplicates in yaml', async () => { 17 | const cwd = process.cwd(); 18 | process.chdir(path.join(__dirname, 'packages', path.sep, 'test-duplicates')); 19 | try { 20 | const data = await envinfo.run({ npmPackages: ['b'] }, { duplicates: true }); 21 | expect(data).toMatchSnapshot(); 22 | } finally { 23 | process.chdir(cwd); 24 | } 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const packageJson = require('./package.json'); 4 | 5 | module.exports = { 6 | entry: { 7 | envinfo: './src/envinfo.js', 8 | cli: './src/cli.js', 9 | }, 10 | target: 'node', 11 | mode: 'production', 12 | optimization: { 13 | minimize: true, 14 | }, 15 | output: { 16 | libraryTarget: 'commonjs2', 17 | filename: '[name].js', 18 | path: path.join(__dirname, '/dist'), 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | use: 'babel-loader', 24 | exclude: /(node_modules)/, 25 | test: /\.js$/, 26 | }, 27 | ], 28 | }, 29 | externals: [/envinfo$/], 30 | plugins: [ 31 | new webpack.BannerPlugin({ 32 | banner: `#!/usr/bin/env node\n`, 33 | raw: true, 34 | include: 'cli', 35 | }), 36 | new webpack.DefinePlugin({ 37 | 'global.__VERSION__': JSON.stringify(packageJson.version), 38 | }), 39 | ], 40 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Trevor Brindle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers/monorepos.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | getYarnWorkspacesInfo: () => { 6 | utils.log('trace', 'getYarnWorkspacesInfo'); 7 | return Promise.all([ 8 | utils.run('yarn -v'), 9 | utils 10 | .getPackageJsonByPath('package.json') 11 | .then(packageJson => packageJson && 'workspaces' in packageJson), 12 | ]).then(v => { 13 | const name = 'Yarn Workspaces'; 14 | if (v[0] && v[1]) { 15 | return Promise.resolve([name, v[0]]); 16 | } 17 | return Promise.resolve([name, 'Not Found']); 18 | }); 19 | }, 20 | 21 | getLernaInfo: () => { 22 | utils.log('trace', 'getLernaInfo'); 23 | return Promise.all([ 24 | utils.getPackageJsonByName('lerna').then(packageJson => packageJson && packageJson.version), 25 | utils.fileExists(path.join(process.cwd(), 'lerna.json')), 26 | ]).then(v => { 27 | const name = 'Lerna'; 28 | if (v[0] && v[1]) { 29 | return Promise.resolve([name, v[0]]); 30 | } 31 | return Promise.resolve([name, 'Not Found']); 32 | }); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/helpers/databases.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getMongoDBInfo: () => { 5 | utils.log('trace', 'getMongoDBInfo'); 6 | return Promise.all([ 7 | utils.run('mongo --version').then(utils.findVersion), 8 | utils.which('mongo'), 9 | ]).then(v => utils.determineFound('MongoDB', v[0], v[1])); 10 | }, 11 | 12 | getMySQLInfo: () => { 13 | utils.log('trace', 'getMySQLInfo'); 14 | return Promise.all([ 15 | utils 16 | .run('mysql --version') 17 | .then(v => `${utils.findVersion(v, null, 1)}${v.includes('MariaDB') ? ' (MariaDB)' : ''}`), 18 | utils.which('mysql'), 19 | ]).then(v => utils.determineFound('MySQL', v[0], v[1])); 20 | }, 21 | 22 | getPostgreSQLInfo: () => { 23 | utils.log('trace', 'getPostgreSQLInfo'); 24 | return Promise.all([ 25 | utils.run('postgres --version').then(utils.findVersion), 26 | utils.which('postgres'), 27 | ]).then(v => utils.determineFound('PostgreSQL', v[0], v[1])); 28 | }, 29 | 30 | getSQLiteInfo: () => { 31 | utils.log('trace', 'getSQLiteInfo'); 32 | return Promise.all([ 33 | utils.run('sqlite3 --version').then(utils.findVersion), 34 | utils.which('sqlite3'), 35 | ]).then(v => utils.determineFound('SQLite', v[0], v[1])); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build-windows: 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | - run: yarn 22 | - run: node --unhandled-rejections=strict src/cli.js --all 23 | 24 | build-ubuntu: 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | matrix: 29 | node-version: [16.x, 18.x, 20.x] 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | - run: yarn 38 | - run: yarn build 39 | - run: node --unhandled-rejections=strict src/cli.js --all 40 | 41 | build-macOS: 42 | runs-on: macos-14 43 | steps: 44 | - uses: actions/checkout@v3 45 | - name: Use Node.js ${{ matrix.node-version }} 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: 20 49 | - run: yarn 50 | - run: yarn build 51 | - run: node --unhandled-rejections=strict src/cli.js --all 52 | -------------------------------------------------------------------------------- /src/helpers/virtualization.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getDockerInfo: () => { 5 | utils.log('trace', 'getDockerInfo'); 6 | return Promise.all([ 7 | utils.run('docker --version').then(utils.findVersion), 8 | utils.which('docker'), 9 | ]).then(v => utils.determineFound('Docker', v[0], v[1])); 10 | }, 11 | 12 | getDockerComposeInfo: () => { 13 | utils.log('trace', 'getDockerComposeInfo'); 14 | return Promise.all([ 15 | utils.run('docker-compose --version').then(utils.findVersion), 16 | utils.which('docker-compose'), 17 | ]).then(v => utils.determineFound('Docker Compose', v[0], v[1])); 18 | }, 19 | 20 | getParallelsInfo: () => { 21 | utils.log('trace', 'getParallelsInfo'); 22 | return Promise.all([ 23 | utils.run('prlctl --version').then(utils.findVersion), 24 | utils.which('prlctl'), 25 | ]).then(v => utils.determineFound('Parallels', v[0], v[1])); 26 | }, 27 | 28 | getPodmanInfo: () => { 29 | utils.log('trace', 'getPodmanInfo'); 30 | return Promise.all([ 31 | utils.run('podman --version').then(utils.findVersion), 32 | utils.which('podman'), 33 | ]).then(v => utils.determineFound('Podman', v[0], v[1])); 34 | }, 35 | 36 | getVirtualBoxInfo: () => { 37 | utils.log('trace', 'getVirtualBoxInfo'); 38 | return Promise.all([ 39 | utils.run('vboxmanage --version').then(utils.findVersion), 40 | utils.which('vboxmanage'), 41 | ]).then(v => utils.determineFound('VirtualBox', v[0], v[1])); 42 | }, 43 | 44 | getVMwareFusionInfo: () => { 45 | utils.log('trace', 'getVMwareFusionInfo'); 46 | return utils 47 | .getDarwinApplicationVersion('com.vmware.fusion') 48 | .then(v => utils.determineFound('VMWare Fusion', v, 'N/A')); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/helpers/binaries.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getNodeInfo: () => { 5 | utils.log('trace', 'getNodeInfo'); 6 | return Promise.all([ 7 | utils.isWindows 8 | ? utils.run('node -v').then(utils.findVersion) 9 | : utils 10 | .which('node') 11 | .then(nodePath => (nodePath ? utils.run(nodePath + ' -v') : Promise.resolve(''))) 12 | .then(utils.findVersion), 13 | utils.which('node'), 14 | ]).then(v => utils.determineFound('Node', v[0], v[1])); 15 | }, 16 | 17 | getnpmInfo: () => { 18 | utils.log('trace', 'getnpmInfo'); 19 | return Promise.all([utils.run('npm -v'), utils.which('npm')]).then(v => 20 | utils.determineFound('npm', v[0], v[1]) 21 | ); 22 | }, 23 | 24 | getWatchmanInfo: () => { 25 | utils.log('trace', 'getWatchmanInfo'); 26 | return Promise.all([ 27 | utils 28 | .which('watchman') 29 | .then(watchmanPath => (watchmanPath ? utils.run(watchmanPath + ' -v') : undefined)), 30 | utils.which('watchman'), 31 | ]).then(v => utils.determineFound('Watchman', v[0], v[1])); 32 | }, 33 | 34 | getYarnInfo: () => { 35 | utils.log('trace', 'getYarnInfo'); 36 | return Promise.all([utils.run('yarn -v'), utils.which('yarn')]).then(v => 37 | utils.determineFound('Yarn', v[0], v[1]) 38 | ); 39 | }, 40 | 41 | getpnpmInfo: () => { 42 | utils.log('trace', 'getpnpmInfo'); 43 | return Promise.all([utils.run('pnpm -v'), utils.which('pnpm')]).then(v => 44 | utils.determineFound('pnpm', v[0], v[1]) 45 | ); 46 | }, 47 | 48 | getbunInfo: () => { 49 | utils.log('trace', 'getbunInfo'); 50 | return Promise.all([utils.run('bun -v'), utils.which('bun')]).then(v => 51 | utils.determineFound('bun', v[0], v[1]) 52 | ); 53 | }, 54 | 55 | getDenoInfo: () => { 56 | utils.log('trace', 'getDenoInfo'); 57 | return Promise.all([ 58 | utils.isWindows 59 | ? utils.run('deno --version').then(utils.findVersion) 60 | : utils 61 | .which('deno') 62 | .then(denoPath => (denoPath ? utils.run(denoPath + ' --version') : Promise.resolve(''))) 63 | .then(utils.findVersion), 64 | utils.which('deno'), 65 | ]).then(v => utils.determineFound('Deno', v[0], v[1])); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)); 2 | const version = global.__VERSION__ || ''; // eslint-disable-line 3 | 4 | argv.console = true; 5 | 6 | if (argv.help || argv._.indexOf('help') > -1) { 7 | // eslint-disable-next-line 8 | console.log(` 9 | ,,', ,, ,,,,,, ,',, 10 | ,,, ,,, ,,, 11 | ,, ,,,,, ,,,,,, ,,, ,, ,, .,,,,,, ,,,,,,, ,,,,, ,, 12 | ,, ,, ,, ,,, ,,, ,, ,,, ,, ,,, ,,, ,, ,,, ,,, ,, 13 | ,,, ,, .,, ,,, ,, ,,, ,, ,, ,,, ,, ,, ,, ,, ,,, 14 | ,, ,,,,,,,,,, ,,, ,, ,, ,, ,, ,,, ,, ,, ,, ,, ,, 15 | ,, ,,, ,,, ,, ,,,,, ,, ,,, ,, ,, ,,, ,,, ,, 16 | ,, ,,,,,,, ,,, ,, ,,, ,, ,,, ,, ,, ,,,,,,, ,, 17 | ,,, ,,, 18 | ,,,' ',,, 19 | 20 | VERSION: ${version} 21 | 22 | USAGE: 23 | 24 | \`envinfo\` || \`npx envinfo\` 25 | 26 | OPTIONS: 27 | 28 | --system Print general system info such as OS, CPU, Memory and Shell 29 | --browsers Get version numbers of installed web browsers 30 | --SDKs Get platforms, build tools and SDKs of iOS and Android 31 | --IDEs Get version numbers of installed IDEs 32 | --languages Get version numbers of installed languages such as Java, Python, PHP, etc 33 | --managers Get version numbers of installed package/dependency managers 34 | --monorepos Get monorepo tools 35 | --binaries Get version numbers of node, npm, watchman, etc 36 | --npmPackages Get version numbers of locally installed npm packages - glob, string, or comma delimited list 37 | --npmGlobalPackages Get version numbers of globally installed npm packages 38 | --pnpmGlobalPackages Get version numbers of globally installed pnpm packages 39 | 40 | --duplicates Mark duplicate npm packages inside parentheses eg. (2.1.4) 41 | --fullTree Traverse entire node_modules dependency tree, not just top level 42 | 43 | --markdown Print output in markdown format 44 | --json Print output in JSON format 45 | --console Print to console (defaults to on for CLI usage, off for programmatic usage) 46 | --showNotFound Don't filter out values marked 'Not Found' 47 | --title Give your report a top level title ie 'Environment Report' 48 | 49 | --clipboard *Removed - use clipboardy or clipboard-cli directly* 50 | `); 51 | } else if (argv.version || argv.v || argv._.indexOf('version') > -1) { 52 | console.log(version); // eslint-disable-line no-console 53 | } else { 54 | require('./envinfo').cli(argv); // eslint-disable-line global-require 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "envinfo", 3 | "version": "7.21.0", 4 | "description": "Info about your dev environment for debugging purposes", 5 | "repository": "https://github.com/tabrindle/envinfo", 6 | "author": "tabrindle@gmail.com", 7 | "license": "MIT", 8 | "files": [ 9 | "dist/" 10 | ], 11 | "main": "dist/envinfo.js", 12 | "bin": { 13 | "envinfo": "dist/cli.js" 14 | }, 15 | "engines": { 16 | "node": ">=4" 17 | }, 18 | "scripts": { 19 | "build": "webpack --progress", 20 | "check:format": "prettier -l src/**/*.js --verbose", 21 | "compress": "gzexe envinfo-* && upx envinfo-win.exe", 22 | "contributors:add": "all-contributors add", 23 | "contributors:generate": "all-contributors generate", 24 | "postcompress": "tar -czvf envinfo-linux.tar.gz envinfo-linux && tar -czvf envinfo-macos.tar.gz envinfo-macos && zip -r -X envinfo-win.zip envinfo-win.exe", 25 | "executable": "pkg package.json", 26 | "format": "prettier --write src/**/*.js", 27 | "lint": "eslint src", 28 | "lint-fix": "eslint src --fix", 29 | "preversion": "npm run test && webpack && git add .", 30 | "postversion": "npm run executable && npm run compress && npm run release", 31 | "release": "github-release upload --owner=tabrindle --repo=envinfo --tag=${npm_package_version} 'envinfo-linux.tar.gz' 'envinfo-macos.tar.gz' 'envinfo-win.zip'", 32 | "start": "node src/cli.js", 33 | "test": "jest --env=node && eslint src && prettier -l src/**/*.js" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS && npm run lint" 38 | } 39 | }, 40 | "keywords": [ 41 | "development", 42 | "env", 43 | "environment", 44 | "info", 45 | "issues", 46 | "reporting", 47 | "diagnostics" 48 | ], 49 | "pkg": { 50 | "scripts": "dist/*.js", 51 | "targets": [ 52 | "linux", 53 | "macos", 54 | "win" 55 | ] 56 | }, 57 | "jest": { 58 | "testEnvironment": "node" 59 | }, 60 | "dependencies": {}, 61 | "devDependencies": { 62 | "@babel/core": "^7.2.2", 63 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 64 | "@babel/polyfill": "^7.2.5", 65 | "@babel/preset-env": "^7.3.1", 66 | "@commitlint/cli": "^8.3.5", 67 | "@commitlint/config-conventional": "^8.3.4", 68 | "all-contributors-cli": "^4.11.1", 69 | "babel-core": "7.0.0-bridge.0", 70 | "babel-eslint": "^10.0.1", 71 | "babel-jest": "23.6.0", 72 | "babel-loader": "^8.0.5", 73 | "eslint": "^5.13.0", 74 | "eslint-config-airbnb-base": "^12.1.0", 75 | "eslint-config-prettier": "^2.7.0", 76 | "eslint-plugin-import": "^2.8.0", 77 | "eslint-plugin-prettier": "^2.3.1", 78 | "github-release-cli": "^0.4.1", 79 | "glob": "^7.1.6", 80 | "husky": "^4.2.5", 81 | "jest": "^22.4.3", 82 | "minimist": "^1.2.5", 83 | "os-name": "^3.1.0", 84 | "pkg": "^4.5.1", 85 | "prettier": "^1.19.1", 86 | "prettier-eslint-cli": "^4.1.1", 87 | "webpack": "^5.90.1", 88 | "webpack-cli": "^5.1.4", 89 | "which": "^1.2.14", 90 | "yamlify-object": "^0.5.1" 91 | }, 92 | "resolutions": { 93 | "lodash": "4.17.15" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/helpers/system.js: -------------------------------------------------------------------------------- 1 | const osName = require('os-name'); 2 | const utils = require('../utils'); 3 | const os = require('os'); 4 | 5 | module.exports = { 6 | getContainerInfo: () => { 7 | utils.log('trace', 'getContainerInfo'); 8 | if (utils.isLinux) 9 | return Promise.all([utils.fileExists('/.dockerenv'), utils.readFile('/proc/self/cgroup')]) 10 | .then(results => { 11 | utils.log('trace', 'getContainerInfoThen', results); 12 | return Promise.resolve(['Container', results[0] || results[1] ? 'Yes' : 'N/A']); 13 | }) 14 | .catch(err => utils.log('trace', 'getContainerInfoCatch', err)); 15 | return Promise.resolve(['Container', 'N/A']); 16 | }, 17 | 18 | getCPUInfo: () => { 19 | utils.log('trace', 'getCPUInfo'); 20 | let info; 21 | try { 22 | const cpus = os.cpus(); 23 | info = '(' + cpus.length + ') ' + os.arch() + ' ' + cpus[0].model; 24 | } catch (err) { 25 | info = 'Unknown'; 26 | } 27 | return Promise.all(['CPU', info]); 28 | }, 29 | 30 | getMemoryInfo: () => { 31 | utils.log('trace', 'getMemoryInfo'); 32 | return Promise.all([ 33 | 'Memory', 34 | `${utils.toReadableBytes(os.freemem())} / ${utils.toReadableBytes(os.totalmem())}`, 35 | ]); 36 | }, 37 | 38 | getOSInfo: () => { 39 | utils.log('trace', 'getOSInfo'); 40 | let version; 41 | let info; 42 | if (utils.isMacOS) { 43 | version = utils.run('sw_vers -productVersion '); 44 | } else if (utils.isLinux) { 45 | version = utils.run('cat /etc/os-release').then(v => { 46 | const distro = (v || '').match(/NAME="(.+)"/) || ''; 47 | const versionInfo = (v || '').match(/VERSION="(.+)"/) || ['', '']; 48 | const versionStr = versionInfo !== null ? versionInfo[1] : ''; 49 | return `${distro[1]} ${versionStr}`.trim() || ''; 50 | }); 51 | } else if (utils.isWindows) { 52 | version = Promise.resolve(os.release()); 53 | const release = os.release().split('.'); 54 | if (release[0] === '10' && release[1] === '0' && release[2] >= 22000) { 55 | info = 'Windows 11'; 56 | } 57 | } else { 58 | version = Promise.resolve(); 59 | } 60 | return version.then(v => { 61 | info = info || osName(os.platform(), os.release()); 62 | if (v) info += ` ${v}`; 63 | return ['OS', info]; 64 | }); 65 | }, 66 | 67 | getShellInfo: () => { 68 | utils.log('trace', 'getShellInfo', process.env); 69 | if (utils.isMacOS || utils.isLinux) { 70 | const shell = 71 | process.env.SHELL || utils.runSync('getent passwd $LOGNAME | cut -d: -f7 | head -1'); 72 | 73 | let command = `${shell} --version 2>&1`; 74 | if (shell.match('/bin/ash')) command = `${shell} --help 2>&1`; 75 | 76 | return Promise.all([utils.run(command).then(utils.findVersion), utils.which(shell)]).then(v => 77 | utils.determineFound('Shell', v[0] || 'Unknown', v[1]) 78 | ); 79 | } 80 | return Promise.resolve(['Shell', 'N/A']); 81 | }, 82 | 83 | getGLibcInfo: () => { 84 | utils.log('trace', 'getGLibc'); 85 | if (utils.isLinux) { 86 | return Promise.all([utils.run(`ldd --version`).then(utils.findVersion)]).then(v => 87 | utils.determineFound('GLibc', v[0] || 'Unknown') 88 | ); 89 | } 90 | return Promise.resolve(['GLibc', 'N/A']); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/envinfo.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Running the cli interface returns expected formatted yaml value 1`] = ` 4 | " 5 | Binaries: 6 | Node: 10.0.0 - /usr/local/bin/node 7 | Yarn: 10.0.0 - /usr/local/bin/yarn 8 | npm: 10.0.0 - /usr/local/bin/npm 9 | pnpm: 10.0.0 - /usr/local/bin/pnpm 10 | bun: 10.0.0 - /usr/local/bin/bun 11 | Deno: 10.0.0 - /usr/local/bin/deno 12 | Watchman: 10.0.0 - /usr/local/bin/watchman 13 | " 14 | `; 15 | 16 | exports[`Running the cli interface returns expected unformatted yaml value 1`] = ` 17 | " 18 | Binaries: 19 | Node: 10.0.0 - /usr/local/bin/node 20 | Yarn: 10.0.0 - /usr/local/bin/yarn 21 | npm: 10.0.0 - /usr/local/bin/npm 22 | pnpm: 10.0.0 - /usr/local/bin/pnpm 23 | bun: 10.0.0 - /usr/local/bin/bun 24 | Deno: 10.0.0 - /usr/local/bin/deno 25 | Watchman: 10.0.0 - /usr/local/bin/watchman 26 | " 27 | `; 28 | 29 | exports[`Running the programmatic interface filters out returned values with N/A 1`] = ` 30 | Object { 31 | "Browsers": Object { 32 | "Firefox": Object { 33 | "path": "/usr/local/bin/firefox", 34 | "version": "10.0.0", 35 | }, 36 | }, 37 | } 38 | `; 39 | 40 | exports[`Running the programmatic interface returns expected json value 1`] = ` 41 | Object { 42 | "Binaries": Object { 43 | "Node": Object { 44 | "path": "/usr/local/bin/node", 45 | "version": "10.0.0", 46 | }, 47 | }, 48 | } 49 | `; 50 | 51 | exports[`Running the programmatic interface returns expected json value with multiple categories 1`] = ` 52 | Object { 53 | "Binaries": Object { 54 | "Node": Object { 55 | "path": "/usr/local/bin/node", 56 | "version": "10.0.0", 57 | }, 58 | }, 59 | "Languages": Object { 60 | "Bash": Object { 61 | "path": "/usr/local/bin/bash", 62 | "version": "10.0.0", 63 | }, 64 | }, 65 | } 66 | `; 67 | 68 | exports[`Running the programmatic interface returns expected json value with multiple values 1`] = ` 69 | Object { 70 | "Binaries": Object { 71 | "Node": Object { 72 | "path": "/usr/local/bin/node", 73 | "version": "10.0.0", 74 | }, 75 | "Yarn": Object { 76 | "path": "/usr/local/bin/yarn", 77 | "version": "10.0.0", 78 | }, 79 | "npm": Object { 80 | "path": "/usr/local/bin/npm", 81 | "version": "10.0.0", 82 | }, 83 | }, 84 | } 85 | `; 86 | 87 | exports[`Running the programmatic interface returns expected markdown value 1`] = ` 88 | " 89 | ## Binaries: 90 | - npm: 10.0.0 - /usr/local/bin/npm 91 | " 92 | `; 93 | 94 | exports[`Running the programmatic interface returns expected title in json 1`] = ` 95 | Object { 96 | "envinfo rocks!": Object { 97 | "Binaries": Object { 98 | "Node": Object { 99 | "path": "/usr/local/bin/node", 100 | "version": "10.0.0", 101 | }, 102 | }, 103 | }, 104 | } 105 | `; 106 | 107 | exports[`Running the programmatic interface returns expected title in yaml 1`] = ` 108 | " 109 | envinfo rocks!: 110 | Binaries: 111 | Node: 10.0.0 - /usr/local/bin/node 112 | " 113 | `; 114 | 115 | exports[`Running the programmatic interface returns expected yaml value 1`] = ` 116 | " 117 | Binaries: 118 | Node: 10.0.0 - /usr/local/bin/node 119 | " 120 | `; 121 | -------------------------------------------------------------------------------- /src/helpers/managers.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getAptInfo: () => { 5 | utils.log('trace', 'getAptInfo'); 6 | if (utils.isLinux) 7 | return Promise.all([ 8 | utils.run('apt --version').then(utils.findVersion), 9 | utils.which('apt'), 10 | ]).then(v => utils.determineFound('Apt', v[0], v[1])); 11 | return Promise.all(['Apt', 'N/A']); 12 | }, 13 | 14 | getCargoInfo: () => { 15 | utils.log('trace', 'getCargoInfo'); 16 | return Promise.all([ 17 | utils.run('cargo --version').then(utils.findVersion), 18 | utils.which('cargo'), 19 | ]).then(v => utils.determineFound('Cargo', v[0], v[1])); 20 | }, 21 | 22 | getCocoaPodsInfo: () => { 23 | utils.log('trace', 'getCocoaPodsInfo'); 24 | if (utils.isMacOS) 25 | return Promise.all([ 26 | utils.run('pod --version').then(utils.findVersion), 27 | utils.which('pod'), 28 | ]).then(v => utils.determineFound('CocoaPods', v[0], v[1])); 29 | return Promise.all(['CocoaPods', 'N/A']); 30 | }, 31 | 32 | getComposerInfo: () => { 33 | utils.log('trace', 'getComposerInfo'); 34 | return Promise.all([ 35 | utils.run('composer --version').then(utils.findVersion), 36 | utils.which('composer').then(utils.condensePath), 37 | ]).then(v => utils.determineFound('Composer', v[0], v[1])); 38 | }, 39 | 40 | getGradleInfo: () => { 41 | utils.log('trace', 'getGradleInfo'); 42 | return Promise.all([ 43 | utils.run('gradle --version').then(utils.findVersion), 44 | utils.which('gradle').then(utils.condensePath), 45 | ]).then(v => utils.determineFound('Gradle', v[0], v[1])); 46 | }, 47 | 48 | getHomebrewInfo: () => { 49 | utils.log('trace', 'getHomebrewInfo'); 50 | if (utils.isMacOS || utils.isLinux) 51 | return Promise.all([ 52 | utils.run('brew --version').then(utils.findVersion), 53 | utils.which('brew'), 54 | ]).then(v => utils.determineFound('Homebrew', v[0], v[1])); 55 | return Promise.all(['Homebrew', 'N/A']); 56 | }, 57 | 58 | getMavenInfo: () => { 59 | utils.log('trace', 'getMavenInfo'); 60 | return Promise.all([ 61 | utils.run('mvn --version').then(utils.findVersion), 62 | utils.which('mvn').then(utils.condensePath), 63 | ]).then(v => utils.determineFound('Maven', v[0], v[1])); 64 | }, 65 | 66 | getpip2Info: () => { 67 | utils.log('trace', 'getpip2Info'); 68 | return Promise.all([ 69 | utils.run('pip2 --version').then(utils.findVersion), 70 | utils.which('pip2').then(utils.condensePath), 71 | ]).then(v => utils.determineFound('pip2', v[0], v[1])); 72 | }, 73 | 74 | getpip3Info: () => { 75 | utils.log('trace', 'getpip3Info'); 76 | return Promise.all([ 77 | utils.run('pip3 --version').then(utils.findVersion), 78 | utils.which('pip3').then(utils.condensePath), 79 | ]).then(v => utils.determineFound('pip3', v[0], v[1])); 80 | }, 81 | 82 | getRubyGemsInfo: () => { 83 | utils.log('trace', 'getRubyGemsInfo'); 84 | return Promise.all([ 85 | utils.run('gem --version').then(utils.findVersion), 86 | utils.which('gem'), 87 | ]).then(v => utils.determineFound('RubyGems', v[0], v[1])); 88 | }, 89 | 90 | getYumInfo: () => { 91 | utils.log('trace', 'getYumInfo'); 92 | if (utils.isLinux) 93 | return Promise.all([ 94 | utils.run('yum --version').then(utils.findVersion), 95 | utils.which('yum'), 96 | ]).then(v => utils.determineFound('Yum', v[0], v[1])); 97 | return Promise.all(['Yum', 'N/A']); 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "envinfo", 3 | "projectOwner": "tabrindle", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "contributors": [ 12 | { 13 | "login": "tabrindle", 14 | "name": "Trevor Brindle", 15 | "avatar_url": "https://avatars1.githubusercontent.com/u/2925048?v=4", 16 | "profile": "http://trevorbrindle.com", 17 | "contributions": [ 18 | "question", 19 | "blog", 20 | "bug", 21 | "code", 22 | "doc", 23 | "example", 24 | "ideas", 25 | "review", 26 | "talk", 27 | "test" 28 | ] 29 | }, 30 | { 31 | "login": "GantMan", 32 | "name": "Gant Laborde", 33 | "avatar_url": "https://avatars0.githubusercontent.com/u/997157?v=4", 34 | "profile": "http://gantlaborde.com/", 35 | "contributions": [ 36 | "blog", 37 | "bug", 38 | "code", 39 | "ideas" 40 | ] 41 | }, 42 | { 43 | "login": "antonfisher", 44 | "name": "Anton Fisher", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/599352?v=4", 46 | "profile": "http://antonfisher.com", 47 | "contributions": [ 48 | "bug", 49 | "code" 50 | ] 51 | }, 52 | { 53 | "login": "ahmadawais", 54 | "name": "Ahmad Awais ⚡️", 55 | "avatar_url": "https://avatars1.githubusercontent.com/u/960133?v=4", 56 | "profile": "https://AhmadAwais.com/", 57 | "contributions": [ 58 | "bug", 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "LEQADA", 64 | "name": "Hasan", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/9251453?v=4", 66 | "profile": "https://github.com/LEQADA", 67 | "contributions": [ 68 | "bug", 69 | "code" 70 | ] 71 | }, 72 | { 73 | "login": "ErnestoR", 74 | "name": "Ernesto Ramírez", 75 | "avatar_url": "https://avatars3.githubusercontent.com/u/1232725?v=4", 76 | "profile": "http://twitter.com/_ErnestoR", 77 | "contributions": [ 78 | "bug", 79 | "code" 80 | ] 81 | }, 82 | { 83 | "login": "gengjiawen", 84 | "name": "Jiawen Geng", 85 | "avatar_url": "https://avatars1.githubusercontent.com/u/3759816?v=4", 86 | "profile": "https://www.gengjiawen.com", 87 | "contributions": [ 88 | "bug", 89 | "code", 90 | "ideas", 91 | "test" 92 | ] 93 | }, 94 | { 95 | "login": "zacanger", 96 | "name": "Zac Anger", 97 | "avatar_url": "https://avatars3.githubusercontent.com/u/12520493?v=4", 98 | "profile": "https://zacanger.com", 99 | "contributions": [ 100 | "code", 101 | "bug" 102 | ] 103 | }, 104 | { 105 | "login": "fson", 106 | "name": "Ville Immonen", 107 | "avatar_url": "https://avatars3.githubusercontent.com/u/497214?v=4", 108 | "profile": "https://twitter.com/VilleImmonen", 109 | "contributions": [ 110 | "bug", 111 | "code" 112 | ] 113 | }, 114 | { 115 | "login": "ibolmo", 116 | "name": "Olmo Maldonado", 117 | "avatar_url": "https://avatars2.githubusercontent.com/u/27246?v=4", 118 | "profile": "http://ibolmo.com", 119 | "contributions": [ 120 | "bug", 121 | "code" 122 | ] 123 | }, 124 | { 125 | "login": "rishabh3112", 126 | "name": "Rishabh Chawla", 127 | "avatar_url": "https://avatars.githubusercontent.com/u/15812317?v=4", 128 | "profile": "https://rishabhchawla.co", 129 | "contributions": [ 130 | "bug", 131 | "code" 132 | ] 133 | }, 134 | { 135 | "login": "Nthalk", 136 | "name": "Carl Taylor", 137 | "avatar_url": "https://avatars.githubusercontent.com/u/174297?v=4", 138 | "profile": "https://github.com/Nthalk", 139 | "contributions": [ 140 | "code" 141 | ] 142 | } 143 | ] 144 | } 145 | -------------------------------------------------------------------------------- /__tests__/envinfo.test.js: -------------------------------------------------------------------------------- 1 | const envinfo = require('../src/envinfo'); 2 | const helpers = require('../src/helpers'); 3 | 4 | jest.mock('../src/helpers'); 5 | 6 | // cycle through the helperFns and mock them according to their name 7 | Object.keys(helpers).forEach(helperFn => { 8 | const match = helperFn.match(/get(.*)Info/); 9 | if (match) { 10 | const name = match[1]; 11 | helpers[helperFn].mockImplementation(() => 12 | Promise.resolve([name, '10.0.0', `/usr/local/bin/${name.toLowerCase()}`]) 13 | ); 14 | } 15 | }); 16 | 17 | describe('Running the programmatic interface', () => { 18 | test('returns expected json value', () => { 19 | return envinfo.run({ Binaries: ['Node'] }, { json: true }).then(data => { 20 | expect(JSON.parse(data)).toMatchSnapshot(); 21 | }); 22 | }); 23 | 24 | test('returns expected yaml value', () => { 25 | return envinfo.run({ Binaries: ['Node'] }).then(data => { 26 | expect(data).toMatchSnapshot(); 27 | }); 28 | }); 29 | 30 | test('returns expected markdown value', () => { 31 | return envinfo.run({ Binaries: ['npm'] }, { markdown: true }).then(data => { 32 | expect(data).toMatchSnapshot(); 33 | }); 34 | }); 35 | 36 | test('returns expected json value with multiple values', () => { 37 | return envinfo 38 | .run( 39 | { 40 | Binaries: ['Node', 'Yarn', 'npm'], 41 | }, 42 | { json: true } 43 | ) 44 | .then(data => { 45 | expect(JSON.parse(data)).toMatchSnapshot(); 46 | }); 47 | }); 48 | 49 | test('returns expected json value with multiple categories', () => { 50 | return envinfo 51 | .run( 52 | { 53 | Binaries: ['Node'], 54 | Languages: ['Bash'], 55 | }, 56 | { json: true } 57 | ) 58 | .then(data => { 59 | expect(JSON.parse(data)).toMatchSnapshot(); 60 | }); 61 | }); 62 | 63 | test('filters out returned values with N/A', () => { 64 | helpers.getChromeInfo.mockImplementation(() => Promise.resolve(['Chrome', 'N/A', 'N/A'])); 65 | 66 | return envinfo.run({ Browsers: ['Chrome', 'Firefox'] }, { json: true }).then(data => { 67 | expect(JSON.parse(data)).toMatchSnapshot(); 68 | }); 69 | }); 70 | 71 | test('filters out returned path values with N/A', () => { 72 | helpers.getChromeInfo.mockImplementation(() => 73 | Promise.resolve(['Chrome', '65.0.3325.181', 'N/A']) 74 | ); 75 | 76 | return envinfo.run({ Browsers: ['Chrome'] }, { json: true }).then(data => { 77 | return expect(JSON.parse(data)).toEqual({ 78 | Browsers: { 79 | Chrome: { version: '65.0.3325.181' }, 80 | }, 81 | }); 82 | }); 83 | }); 84 | 85 | test('returns expected title in json', () => { 86 | return envinfo 87 | .run({ Binaries: ['Node'] }, { title: 'envinfo rocks!', json: true }) 88 | .then(data => { 89 | expect(JSON.parse(data)).toMatchSnapshot(); 90 | }); 91 | }); 92 | 93 | test('returns expected title in yaml', () => { 94 | return envinfo.run({ Binaries: ['Node'] }, { title: 'envinfo rocks!' }).then(data => { 95 | expect(data).toMatchSnapshot(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('Running the cli interface', () => { 101 | test('returns expected formatted yaml value', () => { 102 | const oldIsTTY = process.stdout.isTTY; 103 | process.stdout.isTTY = true; 104 | 105 | const consoleLogSpy = jest.spyOn(global.console, 'log'); 106 | consoleLogSpy.mockImplementation((data) => expect(data).toMatchSnapshot()); 107 | 108 | return envinfo.cli({ binaries: true, console: true }).then(() => { 109 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 110 | }).finally(() => { 111 | process.stdout.isTTY = oldIsTTY; 112 | consoleLogSpy.mockClear(); 113 | }); 114 | }); 115 | 116 | test('returns expected unformatted yaml value', () => { 117 | const oldIsTTY = process.stdout.isTTY; 118 | process.stdout.isTTY = false; 119 | 120 | const consoleLogSpy = jest.spyOn(global.console, 'log'); 121 | consoleLogSpy.mockImplementation((data) => expect(data).toMatchSnapshot()); 122 | 123 | return envinfo.cli({ binaries: true, console: true }).then(() => { 124 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 125 | }).finally(() => { 126 | process.stdout.isTTY = oldIsTTY; 127 | consoleLogSpy.mockClear(); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/helpers/languages.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getBashInfo: () => { 5 | utils.log('trace', 'getBashInfo'); 6 | return Promise.all([ 7 | utils.run('bash --version').then(utils.findVersion), 8 | utils.which('bash'), 9 | ]).then(v => utils.determineFound('Bash', v[0], v[1])); 10 | }, 11 | 12 | getElixirInfo: () => { 13 | utils.log('trace', 'getElixirInfo'); 14 | return Promise.all([ 15 | utils 16 | .run('elixir --version') 17 | .then(v => utils.findVersion(v, /[Elixir]+\s([\d+.[\d+|.]+)/, 1)), 18 | utils.which('elixir'), 19 | ]).then(v => Promise.resolve(utils.determineFound('Elixir', v[0], v[1]))); 20 | }, 21 | 22 | getErlangInfo: () => { 23 | utils.log('trace', 'getErlangInfo'); 24 | return Promise.all([ 25 | utils 26 | // https://stackoverflow.com/a/34326368 27 | .run( 28 | `erl -eval "{ok, Version} = file:read_file(filename:join([code:root_dir(), 'releases', erlang:system_info(otp_release), 'OTP_VERSION'])), io:fwrite(Version), halt()." -noshell` 29 | ) 30 | .then(utils.findVersion), 31 | utils.which('erl'), 32 | ]).then(v => Promise.resolve(utils.determineFound('Erlang', v[0], v[1]))); 33 | }, 34 | 35 | getGoInfo: () => { 36 | utils.log('trace', 'getGoInfo'); 37 | return Promise.all([ 38 | utils.run('go version').then(utils.findVersion), 39 | utils.which('go'), 40 | ]).then(v => utils.determineFound('Go', v[0], v[1])); 41 | }, 42 | 43 | getJavaInfo: () => { 44 | utils.log('trace', 'getJavaInfo'); 45 | return Promise.all([ 46 | utils 47 | .run('javac -version', { unify: true }) 48 | .then(v => utils.findVersion(v, /\d+\.[\w+|.|_|-]+/)), 49 | utils.which('javac'), 50 | ]).then(v => utils.determineFound('Java', v[0], v[1])); 51 | }, 52 | 53 | getPerlInfo: () => { 54 | utils.log('trace', 'getPerlInfo'); 55 | return Promise.all([ 56 | utils.run('perl -v').then(utils.findVersion), 57 | utils.which('perl'), 58 | ]).then(v => utils.determineFound('Perl', v[0], v[1])); 59 | }, 60 | 61 | getPHPInfo: () => { 62 | utils.log('trace', 'getPHPInfo'); 63 | return Promise.all([utils.run('php -v').then(utils.findVersion), utils.which('php')]).then(v => 64 | utils.determineFound('PHP', v[0], v[1]) 65 | ); 66 | }, 67 | 68 | getProtocInfo: () => { 69 | utils.log('trace', 'getProtocInfo'); 70 | return Promise.all([ 71 | utils.run('protoc --version').then(utils.findVersion), 72 | utils.which('protoc'), 73 | ]).then(v => utils.determineFound('Protoc', v[0], v[1])); 74 | }, 75 | 76 | getPythonInfo: () => { 77 | utils.log('trace', 'getPythonInfo'); 78 | return Promise.all([ 79 | utils.run('python -V 2>&1').then(utils.findVersion), 80 | utils.which('python'), 81 | ]).then(v => utils.determineFound('Python', v[0], v[1])); 82 | }, 83 | 84 | getPython3Info: () => { 85 | utils.log('trace', 'getPython3Info'); 86 | return Promise.all([ 87 | utils.run('python3 -V 2>&1').then(utils.findVersion), 88 | utils.which('python3'), 89 | ]).then(v => utils.determineFound('Python3', v[0], v[1])); 90 | }, 91 | 92 | getRInfo: () => { 93 | utils.log('trace', 'getRInfo'); 94 | return Promise.all([ 95 | utils.run('R --version', { unify: true }).then(utils.findVersion), 96 | utils.which('R'), 97 | ]).then(v => utils.determineFound('R', v[0], v[1])); 98 | }, 99 | 100 | getRubyInfo: () => { 101 | utils.log('trace', 'getRubyInfo'); 102 | return Promise.all([ 103 | utils.run('ruby -v').then(utils.findVersion), 104 | utils.which('ruby'), 105 | ]).then(v => utils.determineFound('Ruby', v[0], v[1])); 106 | }, 107 | 108 | getRustInfo: () => { 109 | utils.log('trace', 'getRustInfo'); 110 | return Promise.all([ 111 | utils.run('rustc --version').then(utils.findVersion), 112 | utils.which('rustc'), 113 | ]).then(v => utils.determineFound('Rust', v[0], v[1])); 114 | }, 115 | 116 | getZigInfo: () => { 117 | utils.log('trace', 'getZigInfo'); 118 | return Promise.all([ 119 | utils.run('zig version').then(utils.findVersion), 120 | utils.which('zig'), 121 | ]).then(v => utils.determineFound('Zig', v[0], v[1])); 122 | }, 123 | 124 | getScalaInfo: () => { 125 | utils.log('trace', 'getScalaInfo'); 126 | if (utils.isMacOS || utils.isLinux) { 127 | return Promise.all([ 128 | utils.run('scalac -version').then(utils.findVersion), 129 | utils.which('scalac'), 130 | ]).then(v => utils.determineFound('Scala', v[0], v[1])); 131 | } 132 | return Promise.resolve(['Scala', 'N/A']); 133 | }, 134 | }; 135 | -------------------------------------------------------------------------------- /src/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defaults: { 3 | System: ['OS', 'CPU', 'Memory', 'Container', 'Shell'], 4 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno', 'Watchman'], 5 | Managers: [ 6 | 'Apt', 7 | 'Cargo', 8 | 'CocoaPods', 9 | 'Composer', 10 | 'Gradle', 11 | 'Homebrew', 12 | 'Maven', 13 | 'pip2', 14 | 'pip3', 15 | 'RubyGems', 16 | 'Yum', 17 | ], 18 | Utilities: [ 19 | '7z', 20 | 'Bazel', 21 | 'CMake', 22 | 'Make', 23 | 'GCC', 24 | 'Git', 25 | 'Git LFS', 26 | 'Clang', 27 | 'Ninja', 28 | 'Mercurial', 29 | 'Subversion', 30 | 'FFmpeg', 31 | 'Curl', 32 | 'OpenSSL', 33 | 'ccache', 34 | 'Calibre', 35 | 'Clash Meta', 36 | ], 37 | Servers: ['Apache', 'Nginx'], 38 | Virtualization: ['Docker', 'Docker Compose', 'Parallels', 'VirtualBox', 'VMware Fusion'], 39 | SDKs: ['iOS SDK', 'Android SDK', 'Windows SDK'], 40 | IDEs: [ 41 | 'Android Studio', 42 | 'Atom', 43 | 'Emacs', 44 | 'IntelliJ', 45 | 'NVim', 46 | 'Nano', 47 | 'PhpStorm', 48 | 'Sublime Text', 49 | 'VSCode', 50 | 'Cursor', 51 | 'Claude Code', 52 | 'Codex', 53 | 'opencode', 54 | 'Visual Studio', 55 | 'Vim', 56 | 'WebStorm', 57 | 'Xcode', 58 | ], 59 | Languages: [ 60 | 'Bash', 61 | 'Go', 62 | 'Elixir', 63 | 'Erlang', 64 | 'Java', 65 | 'Perl', 66 | 'PHP', 67 | 'Protoc', 68 | 'Python', 69 | 'Python3', 70 | 'R', 71 | 'Ruby', 72 | 'Rust', 73 | 'Scala', 74 | 'Zig', 75 | ], 76 | Databases: ['MongoDB', 'MySQL', 'PostgreSQL', 'SQLite'], 77 | Browsers: [ 78 | 'Brave Browser', 79 | 'Chrome', 80 | 'Chrome Canary', 81 | 'Chromium', 82 | 'Edge', 83 | 'Firefox', 84 | 'Firefox Developer Edition', 85 | 'Firefox Nightly', 86 | 'Internet Explorer', 87 | 'Safari', 88 | 'Safari Technology Preview', 89 | ], 90 | Monorepos: ['Yarn Workspaces', 'Lerna'], 91 | npmPackages: null, 92 | npmGlobalPackages: null, 93 | pnpmGlobalPackages: null, 94 | }, 95 | cssnano: { 96 | System: ['OS', 'CPU'], 97 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 98 | npmPackages: ['cssnano', 'postcss'], 99 | options: { 100 | duplicates: true, 101 | }, 102 | }, 103 | jest: { 104 | System: ['OS', 'CPU'], 105 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 106 | npmPackages: ['jest'], 107 | }, 108 | 'react-native': { 109 | System: ['OS', 'CPU'], 110 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Watchman'], 111 | SDKs: ['iOS SDK', 'Android SDK', 'Windows SDK'], 112 | IDEs: ['Android Studio', 'Xcode', 'Visual Studio'], 113 | npmPackages: ['react', 'react-native'], 114 | npmGlobalPackages: ['react-native-cli'], 115 | }, 116 | nyc: { 117 | System: ['OS', 'CPU', 'Memory'], 118 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 119 | npmPackages: '/**/{*babel*,@babel/*/,*istanbul*,nyc,source-map-support,typescript,ts-node}', 120 | }, 121 | webpack: { 122 | System: ['OS', 'CPU'], 123 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 124 | npmPackages: '*webpack*', 125 | npmGlobalPackages: ['webpack', 'webpack-cli'], 126 | }, 127 | 'styled-components': { 128 | System: ['OS', 'CPU'], 129 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 130 | Browsers: ['Chrome', 'Firefox', 'Safari'], 131 | npmPackages: '*styled-components*', 132 | }, 133 | 'create-react-app': { 134 | System: ['OS', 'CPU'], 135 | Binaries: ['Node', 'npm', 'Yarn', 'pnpm', 'bun'], 136 | Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'], 137 | npmPackages: ['react', 'react-dom', 'react-scripts'], 138 | npmGlobalPackages: ['create-react-app'], 139 | options: { 140 | duplicates: true, 141 | showNotFound: true, 142 | }, 143 | }, 144 | apollo: { 145 | System: ['OS'], 146 | Binaries: ['Node', 'npm', 'Yarn', 'pnpm', 'bun'], 147 | Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'], 148 | npmPackages: '{*apollo*,@apollo/*}', 149 | npmGlobalPackages: '{*apollo*,@apollo/*}', 150 | }, 151 | 'react-native-web': { 152 | System: ['OS', 'CPU'], 153 | Binaries: ['Node', 'npm', 'Yarn', 'pnpm', 'bun'], 154 | Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'], 155 | npmPackages: ['react', 'react-native-web'], 156 | options: { 157 | showNotFound: true, 158 | }, 159 | }, 160 | babel: { 161 | System: ['OS'], 162 | Binaries: ['Node', 'npm', 'Yarn', 'pnpm', 'bun'], 163 | Monorepos: ['Yarn Workspaces', 'Lerna'], 164 | npmPackages: 165 | '{*babel*,@babel/*,eslint,webpack,create-react-app,react-native,lerna,jest,next,rollup}', 166 | }, 167 | playwright: { 168 | System: ['OS', 'CPU', 'Memory', 'Container'], 169 | Binaries: ['Node', 'Yarn', 'npm', 'pnpm', 'bun', 'Deno'], 170 | Languages: ['Bash'], 171 | IDEs: ['VSCode', 'Cursor', 'Claude Code', 'Codex', 'opencode'], 172 | npmPackages: '{playwright*,@playwright/*}', 173 | }, 174 | }; 175 | -------------------------------------------------------------------------------- /src/helpers/sdks.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const utils = require('../utils'); 5 | 6 | module.exports = { 7 | getAndroidSDKInfo: () => { 8 | return utils 9 | .run('sdkmanager --list') 10 | .then(output => { 11 | if (!output && process.env.ANDROID_HOME) 12 | return utils.run(`${process.env.ANDROID_HOME}/tools/bin/sdkmanager --list`); 13 | return output; 14 | }) 15 | .then(output => { 16 | if (!output && process.env.ANDROID_HOME) 17 | return utils.run( 18 | `${process.env.ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --list` 19 | ); 20 | return output; 21 | }) 22 | .then(output => { 23 | if (!output && utils.isMacOS) 24 | return utils.run('~/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager --list'); 25 | return output; 26 | }) 27 | .then(output => { 28 | if (!output && utils.isMacOS) 29 | return utils.run('~/Library/Android/sdk/tools/bin/sdkmanager --list'); 30 | return output; 31 | }) 32 | .then(output => { 33 | if (!output && utils.isWindows) { 34 | const sdkPath = path.join( 35 | os.homedir(), 36 | 'AppData/Local/Android/Sdk/cmdline-tools/latest/bin/sdkmanager' 37 | ); 38 | return utils.run(`${sdkPath} --list`); 39 | } 40 | return output; 41 | }) 42 | .then(output => { 43 | const sdkmanager = utils.parseSDKManagerOutput(output); 44 | const getNdkVersionFromPath = ndkDir => { 45 | const metaPath = path.join(ndkDir, 'source.properties'); 46 | let contents; 47 | try { 48 | contents = fs.readFileSync(metaPath, 'utf8'); 49 | } catch (err) { 50 | if (err.code === 'ENOENT') { 51 | return undefined; 52 | } 53 | throw err; 54 | } 55 | 56 | const split = contents.split('\n'); 57 | for (let i = 0; i < split.length; i += 1) { 58 | const splits = split[i].split('='); 59 | if (splits.length === 2) { 60 | if (splits[0].trim() === 'Pkg.Revision') { 61 | return splits[1].trim(); 62 | } 63 | } 64 | } 65 | return undefined; 66 | }; 67 | 68 | const getNdk = () => { 69 | if (process.env.ANDROID_NDK) { 70 | return getNdkVersionFromPath(process.env.ANDROID_NDK); 71 | } 72 | 73 | if (process.env.ANDROID_NDK_HOME) { 74 | return getNdkVersionFromPath(process.env.ANDROID_NDK_HOME); 75 | } 76 | 77 | if (process.env.ANDROID_HOME) { 78 | return getNdkVersionFromPath(path.join(process.env.ANDROID_HOME, 'ndk-bundle')); 79 | } 80 | 81 | return undefined; 82 | }; 83 | const ndkVersion = getNdk(); 84 | 85 | if ( 86 | sdkmanager.buildTools.length || 87 | sdkmanager.apiLevels.length || 88 | sdkmanager.systemImages.length || 89 | ndkVersion 90 | ) 91 | return Promise.resolve([ 92 | 'Android SDK', 93 | { 94 | 'API Levels': sdkmanager.apiLevels || utils.NotFound, 95 | 'Build Tools': sdkmanager.buildTools || utils.NotFound, 96 | 'System Images': sdkmanager.systemImages || utils.NotFound, 97 | 'Android NDK': ndkVersion || utils.NotFound, 98 | }, 99 | ]); 100 | return Promise.resolve(['Android SDK', utils.NotFound]); 101 | }); 102 | }, 103 | 104 | getiOSSDKInfo: () => { 105 | if (utils.isMacOS) { 106 | return utils 107 | .run('xcodebuild -showsdks') 108 | .then(sdks => sdks.match(/[\w]+\s[\d|.]+/g)) 109 | .then(utils.uniq) 110 | .then(platforms => 111 | platforms.length ? ['iOS SDK', { Platforms: platforms }] : ['iOS SDK', utils.NotFound] 112 | ); 113 | } 114 | return Promise.resolve(['iOS SDK', 'N/A']); 115 | }, 116 | 117 | getWindowsSDKInfo: () => { 118 | utils.log('trace', 'getWindowsSDKInfo'); 119 | if (utils.isWindows) { 120 | let info = utils.NotFound; 121 | // Query the registry under AppModelUnlock, 122 | // parse out the output into an object where the object properties are the registry values, 123 | // and the values of those properties are the corresponding registry data. 124 | return utils 125 | .run('reg query HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock') 126 | .then(out => { 127 | info = out 128 | .split(/[\r\n]/g) 129 | .slice(1) 130 | .filter(x => x !== '') 131 | .reduce((m, o) => { 132 | let values = o.match(/[^\s]+/g); 133 | if (values[2] === '0x0' || values[2] === '0x1') { 134 | // coerce to a string bool value instead of a boolean because 135 | // clean() will strip out non-Object values like Boolean. 136 | values[2] = values[2] === '0x1' ? 'Enabled' : 'Disabled'; 137 | } 138 | m[values[0]] = values[2]; 139 | return m; 140 | }, {}); 141 | if (Object.keys(info).length === 0) { 142 | info = utils.NotFound; 143 | } 144 | try { 145 | const versions = fs.readdirSync( 146 | `${process.env['ProgramFiles(x86)']}/Windows Kits/10/Platforms/UAP` 147 | ); 148 | info.Versions = versions; 149 | } catch (_) { 150 | // None found 151 | } 152 | return Promise.resolve(['Windows SDK', info]); 153 | }); 154 | } 155 | return Promise.resolve(['Windows SDK', utils.NA]); 156 | }, 157 | }; 158 | -------------------------------------------------------------------------------- /src/formatters.js: -------------------------------------------------------------------------------- 1 | const yamlify = require('yamlify-object'); 2 | const utils = require('./utils'); 3 | 4 | function clean(data, options) { 5 | utils.log('trace', 'clean', data); 6 | return Object.keys(data).reduce((acc, prop) => { 7 | if ( 8 | (!options.showNotFound && data[prop] === 'Not Found') || 9 | data[prop] === 'N/A' || 10 | data[prop] === undefined || 11 | Object.keys(data[prop]).length === 0 12 | ) 13 | return acc; 14 | if (utils.isObject(data[prop])) { 15 | if ( 16 | Object.values(data[prop]).every( 17 | v => v === 'N/A' || (!options.showNotFound && v === 'Not Found') 18 | ) 19 | ) 20 | return acc; 21 | return Object.assign(acc, { [prop]: clean(data[prop], options) }); 22 | } 23 | return Object.assign(acc, { [prop]: data[prop] }); 24 | }, {}); 25 | } 26 | 27 | function formatHeaders(data, options) { 28 | utils.log('trace', 'formatHeaders'); 29 | if (!options) options = { type: 'underline' }; 30 | const formats = { 31 | underline: ['\x1b[4m', '\x1b[0m'], 32 | }; 33 | return data 34 | .slice() 35 | .split('\n') 36 | .map(line => { 37 | const isHeading = line.slice('-1') === ':'; 38 | if (isHeading) { 39 | const indent = line.match(/^[\s]*/g)[0]; 40 | return `${indent}${formats[options.type][0]}${line.slice(indent.length)}${ 41 | formats[options.type][1] 42 | }`; 43 | } 44 | return line; 45 | }) 46 | .join('\n'); 47 | } 48 | 49 | function formatPackages(data) { 50 | utils.log('trace', 'formatPackages'); 51 | if (!data.npmPackages) return data; 52 | return Object.assign(data, { 53 | npmPackages: Object.entries(data.npmPackages || {}).reduce((acc, entry) => { 54 | const key = entry[0]; 55 | const value = entry[1]; 56 | if (value === 'Not Found') 57 | return Object.assign(acc, { 58 | [key]: value, 59 | }); 60 | const wanted = value.wanted ? `${value.wanted} =>` : ''; 61 | const installed = Array.isArray(value.installed) 62 | ? value.installed.join(', ') 63 | : value.installed; 64 | const duplicates = value.duplicates ? `(${value.duplicates.join(', ')})` : ''; 65 | return Object.assign(acc, { 66 | [key]: `${wanted} ${installed} ${duplicates}`, 67 | }); 68 | }, {}), 69 | }); 70 | } 71 | 72 | function joinArray(key, value, options) { 73 | if (!options) options = { emptyMessage: 'None' }; 74 | if (Array.isArray(value)) { 75 | value = value.length > 0 ? value.join(', ') : options.emptyMessage; 76 | } 77 | return { 78 | [key]: value, 79 | }; 80 | } 81 | 82 | function recursiveTransform(data, fn) { 83 | return Object.entries(data).reduce((acc, entry) => { 84 | const key = entry[0]; 85 | const value = entry[1]; 86 | if (utils.isObject(value)) { 87 | return Object.assign(acc, { [key]: recursiveTransform(value, fn) }); 88 | } 89 | return Object.assign(acc, fn(key, value)); 90 | }, {}); 91 | } 92 | 93 | function serializeArrays(data) { 94 | utils.log('trace', 'serializeArrays'); 95 | return recursiveTransform(data, joinArray); 96 | } 97 | 98 | function serializeVersionsAndPaths(data) { 99 | utils.log('trace', 'serializeVersionsAndPaths'); 100 | return Object.entries(data).reduce( 101 | (Dacc, Dentry) => 102 | Object.assign( 103 | Dacc, 104 | { 105 | [Dentry[0]]: Object.entries(Dentry[1]).reduce((acc, entry) => { 106 | const key = entry[0]; 107 | const value = entry[1]; 108 | if (value.version) { 109 | return Object.assign(acc, { 110 | [key]: [value.version, value.path].filter(Boolean).join(' - '), 111 | }); 112 | } 113 | return Object.assign(acc, { 114 | [key]: [value][0], 115 | }); 116 | }, {}), 117 | }, 118 | {} 119 | ), 120 | {} 121 | ); 122 | } 123 | 124 | function yaml(data) { 125 | return yamlify(data, { 126 | indent: ' ', 127 | prefix: '\n', 128 | postfix: '\n', 129 | }); 130 | } 131 | 132 | function markdown(data) { 133 | return data 134 | .slice() 135 | .split('\n') 136 | .map(line => { 137 | if (line !== '') { 138 | const isHeading = line.slice('-1') === ':'; 139 | const indent = line.search(/\S|$/); 140 | if (isHeading) { 141 | return `${'#'.repeat(indent / 2 + 1)} ` + line.slice(indent); 142 | } 143 | return ' - ' + line.slice(indent); 144 | } 145 | return ''; 146 | }) 147 | .join('\n'); 148 | } 149 | 150 | function json(data, options) { 151 | if (!options) 152 | options = { 153 | indent: ' ', 154 | }; 155 | return JSON.stringify(data, null, options.indent); 156 | } 157 | 158 | function formatToYaml(data, options) { 159 | utils.log('trace', 'formatToYaml', options); 160 | return utils.pipe([ 161 | () => clean(data, options), 162 | formatPackages, 163 | serializeArrays, 164 | serializeVersionsAndPaths, 165 | options.title ? d => ({ [options.title]: d }) : utils.noop, 166 | yaml, 167 | options.console ? formatHeaders : utils.noop, 168 | ])(data, options); 169 | } 170 | 171 | function formatToMarkdown(data, options) { 172 | utils.log('trace', 'formatToMarkdown'); 173 | return utils.pipe([ 174 | () => clean(data, options), // I'm either too lazy too stupid to fix this. #didYouMeanRecursion? 175 | formatPackages, 176 | serializeArrays, 177 | serializeVersionsAndPaths, 178 | yaml, 179 | markdown, 180 | options.title ? d => `\n# ${options.title}${d}` : utils.noop, 181 | ])(data, options); 182 | } 183 | 184 | function formatToJson(data, options) { 185 | utils.log('trace', 'formatToJson'); 186 | if (!options) options = {}; 187 | 188 | data = utils.pipe([ 189 | () => clean(data, options), 190 | options.title ? d => ({ [options.title]: d }) : utils.noop, 191 | json, 192 | ])(data); 193 | data = options.console ? `\n${data}\n` : data; 194 | 195 | return data; 196 | } 197 | 198 | module.exports = { 199 | json: formatToJson, 200 | markdown: formatToMarkdown, 201 | yaml: formatToYaml, 202 | }; 203 | -------------------------------------------------------------------------------- /src/envinfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const helpers = require('./helpers'); 4 | const formatters = require('./formatters'); 5 | const presets = require('./presets'); 6 | const utils = require('./utils'); 7 | 8 | function format(data, options) { 9 | // set the default formatter (yaml is default, similar to old table) 10 | const formatter = (() => { 11 | if (options.json) return formatters.json; 12 | if (options.markdown) return formatters.markdown; 13 | return formatters.yaml; 14 | })(); 15 | 16 | if (options.console) { 17 | let enableConsole = false; 18 | if (process.stdout.isTTY) { 19 | enableConsole = true; 20 | } 21 | 22 | console.log(formatter(data, Object.assign({}, options, { console: enableConsole }))); // eslint-disable-line no-console 23 | } 24 | 25 | // call the formatter with console option off first to return 26 | const formatted = formatter(data, Object.assign({}, options, { console: false })); 27 | 28 | return formatted; 29 | } 30 | 31 | function main(props, options) { 32 | options = options || {}; 33 | if (options.clipboard) 34 | console.log('\n*** Clipboard option removed - use clipboardy or clipboard-cli directly ***\n'); // eslint-disable-line no-console 35 | // set props to passed in props or default to presets.defaults 36 | const defaults = Object.keys(props).length > 0 ? props : presets.defaults; 37 | // collect a list of promises of helper functions 38 | const promises = Object.entries(defaults).reduce((acc, entries) => { 39 | const category = entries[0]; 40 | const value = entries[1]; 41 | const categoryFn = helpers[`get${category}`]; 42 | // check to see if a category level function exists 43 | if (categoryFn) { 44 | if (value) acc.push(categoryFn(value, options)); 45 | // if the value on the category is falsy, don't run it 46 | return acc; 47 | } 48 | // map over a categories helper functions, call and add to the stack 49 | acc = acc.concat( 50 | (value || []).map(v => { 51 | const helperFn = helpers[`get${v.replace(/\s/g, '')}Info`]; 52 | return helperFn ? helperFn() : Promise.resolve(['Unknown']); 53 | }) 54 | ); 55 | return acc; 56 | }, []); 57 | // once all tasks are done, map all the data back to the shape of the original config obj 58 | return Promise.all(promises).then(data => { 59 | // reduce promise results to object for addressability. 60 | const dataObj = data.reduce((acc, dataValue) => { 61 | if (dataValue && dataValue[0]) Object.assign(acc, { [dataValue[0]]: dataValue }); 62 | return acc; 63 | }, {}); 64 | // use defaults object for mapping 65 | const reduced = Object.entries(presets.defaults).reduce((acc, entries) => { 66 | const category = entries[0]; 67 | const values = entries[1]; 68 | if (dataObj[category]) return Object.assign(acc, { [category]: dataObj[category][1] }); 69 | return Object.assign(acc, { 70 | [category]: (values || []).reduce((helperAcc, v) => { 71 | if (dataObj[v]) { 72 | dataObj[v].shift(); 73 | if (dataObj[v].length === 1) return Object.assign(helperAcc, { [v]: dataObj[v][0] }); 74 | return Object.assign(helperAcc, { 75 | [v]: { version: dataObj[v][0], path: dataObj[v][1] }, 76 | }); 77 | } 78 | return helperAcc; 79 | }, {}), 80 | }); 81 | }, {}); 82 | return format(reduced, options); 83 | }); 84 | } 85 | 86 | // Example usage: 87 | // $ envinfo --system --npmPackages 88 | function cli(options) { 89 | // if all option is passed, do not pass go, do not collect 200 dollars, go straight to main 90 | if (options.all) { 91 | return main( 92 | Object.assign({}, presets.defaults, { 93 | npmPackages: true, 94 | npmGlobalPackages: true, 95 | pnpmGlobalPackages: true, 96 | }), 97 | options 98 | ); 99 | } 100 | // if raw, parse the row options and skip to main 101 | if (options.raw) return main(JSON.parse(options.raw), options); 102 | // if helper flag, run just that helper then log the results 103 | if (options.helper) { 104 | const helper = 105 | helpers[`get${options.helper}`] || 106 | helpers[`get${options.helper}Info`] || 107 | helpers[options.helper]; 108 | return helper ? helper().then(console.log) : console.error('Not Found'); // eslint-disable-line no-console 109 | } 110 | // generic function to make sure passed option exists in presets.defaults list 111 | // TODO: This will eventually be replaced with a better fuzzy finder. 112 | const matches = (list, opt) => list.toLowerCase().includes(opt.toLowerCase()); 113 | // check cli options to see if any args are top level categories 114 | const categories = Object.keys(options).filter(o => 115 | Object.keys(presets.defaults).some(c => matches(c, o)) 116 | ); 117 | // build the props object for filtering presets.defaults 118 | const props = Object.entries(presets.defaults).reduce((acc, entry) => { 119 | if (categories.some(c => matches(c, entry[0]))) { 120 | return Object.assign(acc, { [entry[0]]: entry[1] || options[entry[0]] }); 121 | } 122 | return acc; 123 | }, {}); 124 | // if there is a preset, merge that with the parsed props and options 125 | if (options.preset) { 126 | if (!presets[options.preset]) return console.error(`\nNo "${options.preset}" preset found.`); // eslint-disable-line no-console 127 | return main( 128 | Object.assign({}, utils.omit(presets[options.preset], ['options']), props), 129 | Object.assign( 130 | {}, 131 | presets[options.preset].options, 132 | utils.pick(options, ['duplicates', 'fullTree', 'json', 'markdown', 'console']) 133 | ) 134 | ); 135 | } 136 | // call the main function with the filtered props, and cli options 137 | return main(props, options); 138 | } 139 | 140 | // require('envinfo); 141 | // envinfo.run({ system: [os, cpu]}, {fullTree: true }) 142 | function run(args, options) { 143 | if (typeof args.preset === 'string') return main(presets[args.preset], options); 144 | return main(args, options); 145 | } 146 | 147 | module.exports = { 148 | cli: cli, 149 | helpers: helpers, 150 | main: main, 151 | run: run, 152 | }; 153 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [7.21.0](https://www.github.com/tabrindle/envinfo/compare/v7.20.0...v7.21.0) (2025-11-26) 4 | 5 | 6 | ### Features 7 | 8 | * add Calibre support for version detection ([#290](https://www.github.com/tabrindle/envinfo/issues/290)) ([bec2168](https://www.github.com/tabrindle/envinfo/commit/bec2168ab8f271b79051ae1e6c4c0b95274098eb)) 9 | 10 | ## [7.20.0](https://www.github.com/tabrindle/envinfo/compare/v7.19.0...v7.20.0) (2025-11-02) 11 | 12 | 13 | ### Features 14 | 15 | * add support for Clash Meta detection ([#289](https://www.github.com/tabrindle/envinfo/issues/289)) ([eab1dbf](https://www.github.com/tabrindle/envinfo/commit/eab1dbf87651cfdc3b309e9fc242c64f8512873f)) 16 | * add zig support ([#287](https://www.github.com/tabrindle/envinfo/issues/287)) ([c3769aa](https://www.github.com/tabrindle/envinfo/commit/c3769aa8f58ce158504cbae5cbfa2a15298ae2a4)) 17 | 18 | ## [7.19.0](https://www.github.com/tabrindle/envinfo/compare/v7.18.0...v7.19.0) (2025-10-14) 19 | 20 | 21 | ### Features 22 | 23 | * support ai agent ([#285](https://www.github.com/tabrindle/envinfo/issues/285)) ([6e3b48d](https://www.github.com/tabrindle/envinfo/commit/6e3b48dbdcdf83872ccdb7d280bd5f31b70fdd7d)) 24 | 25 | ## [7.18.0](https://www.github.com/tabrindle/envinfo/compare/v7.17.0...v7.18.0) (2025-10-12) 26 | 27 | 28 | ### Features 29 | 30 | * add 7z support ([#283](https://www.github.com/tabrindle/envinfo/issues/283)) ([84b9d01](https://www.github.com/tabrindle/envinfo/commit/84b9d0181cdecb9c64d73bf78089a1f2ad305a9f)) 31 | 32 | ## [7.17.0](https://www.github.com/tabrindle/envinfo/compare/v7.16.1...v7.17.0) (2025-10-06) 33 | 34 | 35 | ### Features 36 | 37 | * add git lfs support ([#281](https://www.github.com/tabrindle/envinfo/issues/281)) ([636461f](https://www.github.com/tabrindle/envinfo/commit/636461ff549273dad09bd2d1793d9d4aabf399b2)) 38 | 39 | ### [7.16.1](https://www.github.com/tabrindle/envinfo/compare/v7.16.0...v7.16.1) (2025-10-05) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * add fallback command for SDK manager on macOS ([#275](https://www.github.com/tabrindle/envinfo/issues/275)) ([4bfda67](https://www.github.com/tabrindle/envinfo/commit/4bfda67325d519a83d42e5cc76777a18380c4f2f)) 45 | * make windows android sdk detect more robust ([#278](https://www.github.com/tabrindle/envinfo/issues/278)) ([990362e](https://www.github.com/tabrindle/envinfo/commit/990362e0311fee1c00845cc15d4648bd0973affe)) 46 | * windows chrome and firefox detection ([#279](https://www.github.com/tabrindle/envinfo/issues/279)) ([b089e3e](https://www.github.com/tabrindle/envinfo/commit/b089e3ea258cafa549733975080e4a39c249c5a6)) 47 | * windows path issue ([#280](https://www.github.com/tabrindle/envinfo/issues/280)) ([05c9c29](https://www.github.com/tabrindle/envinfo/commit/05c9c29a22d8682aa218d953beb33aeb70550084)) 48 | 49 | ## [7.16.0](https://www.github.com/tabrindle/envinfo/compare/v7.15.0...v7.16.0) (2025-10-04) 50 | 51 | 52 | ### Features 53 | 54 | * add ccache ([#271](https://www.github.com/tabrindle/envinfo/issues/271)) ([8941eb9](https://www.github.com/tabrindle/envinfo/commit/8941eb93629c3da2efd303bf3826b44ab1985f48)) 55 | * add pnpm global packages ([#273](https://www.github.com/tabrindle/envinfo/issues/273)) ([d32cb16](https://www.github.com/tabrindle/envinfo/commit/d32cb1635b36e0b13355e1ad9e45992a9bc6ec28)) 56 | 57 | ## [7.15.0](https://www.github.com/tabrindle/envinfo/compare/v7.14.0...v7.15.0) (2025-09-27) 58 | 59 | 60 | ### Features 61 | 62 | * support deno ([#269](https://www.github.com/tabrindle/envinfo/issues/269)) ([d03690a](https://www.github.com/tabrindle/envinfo/commit/d03690a399f2aa276222bb9bd64cf92916d23524)) 63 | 64 | ## [7.14.0](https://www.github.com/tabrindle/envinfo/compare/v7.13.0...v7.14.0) (2024-09-13) 65 | 66 | 67 | ### Features 68 | 69 | * add preset for cssnano ([#208](https://www.github.com/tabrindle/envinfo/issues/208)) ([d4f73e9](https://www.github.com/tabrindle/envinfo/commit/d4f73e985b12c3087134088baaa28c2fc0356101)) 70 | * Update Babel preset ([#263](https://www.github.com/tabrindle/envinfo/issues/263)) ([e57dcad](https://www.github.com/tabrindle/envinfo/commit/e57dcadf7d31a6790593e8df0b755aa75872ccfe)) 71 | 72 | ## [7.13.0](https://www.github.com/tabrindle/envinfo/compare/v7.12.0...v7.13.0) (2024-04-29) 73 | 74 | 75 | ### Features 76 | 77 | * add openssl support ([#255](https://www.github.com/tabrindle/envinfo/issues/255)) ([299bd3f](https://www.github.com/tabrindle/envinfo/commit/299bd3f6b365103651e2323351247d3fed583162)) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * be more tolerant when detecting Android Studio IDE on OSX ([#252](https://www.github.com/tabrindle/envinfo/issues/252)) ([eb9e5de](https://www.github.com/tabrindle/envinfo/commit/eb9e5de59310a78ccb24daa088e2695b32e48c28)) 83 | 84 | ## [7.12.0](https://www.github.com/tabrindle/envinfo/compare/v7.11.1...v7.12.0) (2024-04-06) 85 | 86 | 87 | ### Features 88 | 89 | * add docker-compose ([#253](https://www.github.com/tabrindle/envinfo/issues/253)) ([d1861f0](https://www.github.com/tabrindle/envinfo/commit/d1861f07991e8c4a154e9e0d4dc200be9ff36f79)) 90 | * update to webpack5 ([#248](https://www.github.com/tabrindle/envinfo/issues/248)) ([4715340](https://www.github.com/tabrindle/envinfo/commit/4715340388023be122c52a395def8b01815e5e4f)) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * delete old CI ([#247](https://www.github.com/tabrindle/envinfo/issues/247)) ([6ae3cda](https://www.github.com/tabrindle/envinfo/commit/6ae3cda9744342182f4e74fd7284bcc0d5536d20)) 96 | 97 | ### [7.11.1](https://www.github.com/tabrindle/envinfo/compare/v7.11.0...v7.11.1) (2024-02-02) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * add @playwright/ to playwright preset ([#245](https://www.github.com/tabrindle/envinfo/issues/245)) ([08802a0](https://www.github.com/tabrindle/envinfo/commit/08802a080675d33f45d7a9f7712d348600f8e257)) 103 | 104 | ## [7.11.0](https://www.github.com/tabrindle/envinfo/compare/v7.10.0...v7.11.0) (2023-11-01) 105 | 106 | 107 | ### Features 108 | 109 | * add bun in binaries ([#240](https://www.github.com/tabrindle/envinfo/issues/240)) ([d7db2a3](https://www.github.com/tabrindle/envinfo/commit/d7db2a3504d4e0fa66c9fda028e895c088cb864b)) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * Windows 11 information ([#242](https://www.github.com/tabrindle/envinfo/issues/242)) ([5330fb2](https://www.github.com/tabrindle/envinfo/commit/5330fb2b970e52de8875824fcd739da1be5e2b11)) 115 | 116 | ## [7.10.0](https://www.github.com/tabrindle/envinfo/compare/v7.9.0...v7.10.0) (2023-06-27) 117 | 118 | 119 | ### Features 120 | 121 | * add auto release ([#207](https://www.github.com/tabrindle/envinfo/issues/207)) ([fc7585c](https://www.github.com/tabrindle/envinfo/commit/fc7585cf8c7046a03efc056c931ca13561801544)) 122 | * add pnpm function in binaries ([#224](https://www.github.com/tabrindle/envinfo/issues/224)) ([60c8db7](https://www.github.com/tabrindle/envinfo/commit/60c8db76804226a057ba3541d31d275c9ac35474)) 123 | * add podman to virtualization ([#236](https://www.github.com/tabrindle/envinfo/issues/236)) ([b744694](https://www.github.com/tabrindle/envinfo/commit/b74469472883451dd466a892d92842f3ade5528c)) 124 | -------------------------------------------------------------------------------- /src/helpers/utilities.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | 3 | module.exports = { 4 | getBazelInfo: () => { 5 | utils.log('trace', 'getBazelInfo'); 6 | return Promise.all([ 7 | utils.run('bazel --version').then(utils.findVersion), 8 | utils.which('bazel'), 9 | ]).then(v => utils.determineFound('Bazel', v[0], v[1])); 10 | }, 11 | 12 | getCMakeInfo: () => { 13 | utils.log('trace', 'getCMakeInfo'); 14 | return Promise.all([ 15 | utils.run('cmake --version').then(utils.findVersion), 16 | utils.which('cmake'), 17 | ]).then(v => utils.determineFound('CMake', v[0], v[1])); 18 | }, 19 | 20 | getGCCInfo: () => { 21 | utils.log('trace', 'getGCCInfo'); 22 | if (utils.isMacOS || utils.isLinux) { 23 | return Promise.all([ 24 | utils.run('gcc -v 2>&1').then(utils.findVersion), 25 | utils.which('gcc'), 26 | ]).then(v => utils.determineFound('GCC', v[0], v[1])); 27 | } 28 | return Promise.resolve(['GCC', 'N/A']); 29 | }, 30 | 31 | getClangInfo: () => { 32 | utils.log('trace', 'getClangInfo'); 33 | return Promise.all([ 34 | utils.run('clang --version').then(utils.findVersion), 35 | utils.which('clang'), 36 | ]).then(v => utils.determineFound('Clang', v[0], v[1])); 37 | }, 38 | 39 | getGitInfo: () => { 40 | utils.log('trace', 'getGitInfo'); 41 | return Promise.all([ 42 | utils.run('git --version').then(utils.findVersion), 43 | utils.which('git'), 44 | ]).then(v => utils.determineFound('Git', v[0], v[1])); 45 | }, 46 | 47 | getGitLFSInfo: () => { 48 | utils.log('trace', 'getGitLFSInfo'); 49 | return Promise.all([ 50 | utils.run('git lfs version').then(utils.findVersion), 51 | utils.which('git-lfs'), 52 | ]).then(v => utils.determineFound('Git LFS', v[0], v[1])); 53 | }, 54 | 55 | getMakeInfo: () => { 56 | utils.log('trace', 'getMakeInfo'); 57 | if (utils.isMacOS || utils.isLinux) { 58 | return Promise.all([ 59 | utils.run('make --version').then(utils.findVersion), 60 | utils.which('make'), 61 | ]).then(v => utils.determineFound('Make', v[0], v[1])); 62 | } 63 | return Promise.resolve(['Make', 'N/A']); 64 | }, 65 | 66 | getNinjaInfo: () => { 67 | utils.log('trace', 'getNinjaInfo'); 68 | return Promise.all([ 69 | utils.run('ninja --version').then(utils.findVersion), 70 | utils.which('ninja'), 71 | ]).then(v => utils.determineFound('Ninja', v[0], v[1])); 72 | }, 73 | 74 | getMercurialInfo: () => { 75 | utils.log('trace', 'getMercurialInfo'); 76 | if (utils.isMacOS || utils.isLinux) { 77 | return Promise.all([ 78 | utils.run('hg --version').then(utils.findVersion), 79 | utils.which('hg'), 80 | ]).then(v => utils.determineFound('Mercurial', v[0], v[1])); 81 | } 82 | return Promise.resolve(['Mercurial', 'N/A']); 83 | }, 84 | 85 | getSubversionInfo: () => { 86 | utils.log('trace', 'getSubversionInfo'); 87 | if (utils.isMacOS || utils.isLinux) { 88 | return Promise.all([ 89 | utils.run('svn --version').then(utils.findVersion), 90 | utils.which('svn'), 91 | ]).then(v => utils.determineFound('Subversion', v[0], v[1])); 92 | } 93 | return Promise.resolve(['Subversion', 'N/A']); 94 | }, 95 | 96 | getFFmpegInfo: () => { 97 | utils.log('trace', 'getFFmpegInfo'); 98 | return Promise.all([ 99 | utils.run('ffmpeg -version').then(utils.findVersion), 100 | utils.which('ffmpeg'), 101 | ]).then(v => utils.determineFound('FFmpeg', v[0], v[1])); 102 | }, 103 | 104 | getCurlInfo: () => { 105 | utils.log('trace', 'getCurlInfo'); 106 | return Promise.all([ 107 | utils.run('curl --version').then(utils.findVersion), 108 | utils.which('curl'), 109 | ]).then(v => utils.determineFound('Curl', v[0], v[1])); 110 | }, 111 | 112 | getOpenSSLInfo: () => { 113 | utils.log('trace', 'getOpenSSLInfo'); 114 | return Promise.all([ 115 | utils.run('openssl version').then(utils.findVersion), 116 | utils.which('openssl'), 117 | ]).then(v => utils.determineFound('OpenSSL', v[0], v[1])); 118 | }, 119 | 120 | getccacheInfo: () => { 121 | utils.log('trace', 'getccacheInfo'); 122 | return Promise.all([ 123 | utils.run('ccache -V').then(utils.findVersion), 124 | utils.which('ccache'), 125 | ]).then(v => utils.determineFound('ccache', v[0], v[1])); 126 | }, 127 | 128 | get7zInfo: () => { 129 | utils.log('trace', 'get7zInfo'); 130 | const candidates = ['7z', '7zz']; 131 | 132 | const findFirstWhich = () => 133 | Promise.all(candidates.map(bin => utils.which(bin))).then(paths => { 134 | const idx = paths.findIndex(Boolean); 135 | return idx >= 0 ? { bin: candidates[idx], path: paths[idx] } : null; 136 | }); 137 | 138 | if (utils.isWindows) { 139 | // Try default install location first 140 | return utils.windowsExeExists('7-Zip/7z.exe').then(filePath => { 141 | if (filePath) { 142 | return Promise.all([ 143 | utils.run(`powershell "& '${filePath}' i | Write-Output"`).then(utils.findVersion), 144 | Promise.resolve(filePath), 145 | ]).then(v => utils.determineFound('7z', v[0], v[1])); 146 | } 147 | // Fallback to PATH candidates 148 | return findFirstWhich().then(found => { 149 | if (!found) return utils.determineFound('7z', '', undefined); 150 | return Promise.all([ 151 | utils.run(`${found.bin} i`).then(utils.findVersion), 152 | Promise.resolve(found.path), 153 | ]).then(v => utils.determineFound('7z', v[0], v[1])); 154 | }); 155 | }); 156 | } 157 | 158 | // macOS/Linux: find on PATH among common names 159 | return findFirstWhich().then(found => { 160 | if (!found) return utils.determineFound('7z', '', undefined); 161 | return Promise.all([ 162 | utils.run(`${found.bin} i`).then(utils.findVersion), 163 | Promise.resolve(found.path), 164 | ]).then(v => utils.determineFound('7z', v[0], v[1])); 165 | }); 166 | }, 167 | 168 | getClashMetaInfo: () => { 169 | utils.log('trace', 'getClashMetaInfo'); 170 | const candidates = ['mihomo']; 171 | 172 | const findFirstWhich = () => 173 | Promise.all(candidates.map(bin => utils.which(bin))).then(paths => { 174 | const idx = paths.findIndex(Boolean); 175 | return idx >= 0 ? { bin: candidates[idx], path: paths[idx] } : null; 176 | }); 177 | 178 | return findFirstWhich().then(found => { 179 | if (!found) return utils.determineFound('Clash Meta', '', undefined); 180 | return Promise.all([ 181 | utils.run(`${found.bin} -v`).then(utils.findVersion), 182 | Promise.resolve(found.path), 183 | ]).then(v => utils.determineFound('Clash Meta', v[0], v[1])); 184 | }); 185 | }, 186 | 187 | getCalibreInfo: () => { 188 | utils.log('trace', 'getCalibreInfo'); 189 | return Promise.all([ 190 | utils.run('ebook-convert --version').then(utils.findVersion), 191 | utils.which('calibre'), 192 | ]).then(v => utils.determineFound('Calibre', v[0], v[1])); 193 | }, 194 | }; 195 | -------------------------------------------------------------------------------- /src/helpers/browsers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const utils = require('../utils'); 4 | const path = require('path'); 5 | 6 | function getFirefoxVersion(name, darvinId, winProgPath) { 7 | let firefoxVersion; 8 | let appPath; 9 | if (utils.isLinux) { 10 | firefoxVersion = utils.run('firefox --version').then(v => v.replace(/^.* ([^ ]*)/g, '$1')); 11 | } else if (utils.isMacOS && typeof darvinId === 'string' && darvinId) { 12 | firefoxVersion = utils.getDarwinApplicationVersion(darvinId); 13 | } else if (utils.isWindows && typeof winProgPath === 'string' && winProgPath) { 14 | firefoxVersion = utils.windowsExeExists(winProgPath).then(filePath => { 15 | appPath = filePath; 16 | return filePath 17 | ? utils 18 | .run(`powershell "& '${filePath}' -v | Write-Output"`) 19 | .then(out => utils.findVersion(out)) 20 | : utils.NA; 21 | }); 22 | } else { 23 | firefoxVersion = Promise.resolve(utils.NA); 24 | } 25 | return firefoxVersion.then(v => utils.determineFound(name, v, appPath || utils.NA)); 26 | } 27 | 28 | module.exports = { 29 | getBraveBrowserInfo: () => { 30 | utils.log('trace', 'getBraveBrowser'); 31 | let braveVersion; 32 | if (utils.isLinux) { 33 | braveVersion = utils 34 | .run('brave --version || brave-browser --version') 35 | .then(v => v.replace(/^.* ([^ ]*)/g, '$1')); 36 | } else if (utils.isMacOS) { 37 | braveVersion = utils 38 | .getDarwinApplicationVersion(utils.browserBundleIdentifiers['Brave Browser']) 39 | .then(utils.findVersion); 40 | } else { 41 | braveVersion = Promise.resolve('N/A'); 42 | } 43 | return braveVersion.then(v => utils.determineFound('Brave Browser', v, 'N/A')); 44 | }, 45 | 46 | getChromeInfo: () => { 47 | utils.log('trace', 'getChromeInfo'); 48 | let chromeVersion; 49 | if (utils.isLinux) { 50 | chromeVersion = utils 51 | .run('google-chrome --version') 52 | .then(v => v.replace(' dev', '').replace(/^.* ([^ ]*)/g, '$1')); 53 | } else if (utils.isMacOS) { 54 | chromeVersion = utils 55 | .getDarwinApplicationVersion(utils.browserBundleIdentifiers.Chrome) 56 | .then(utils.findVersion); 57 | } else if (utils.isWindows) { 58 | let version; 59 | try { 60 | const bases = [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(Boolean); 61 | const contents = bases 62 | .map(base => { 63 | try { 64 | return fs.readdirSync(path.join(base, 'Google/Chrome/Application')); 65 | } catch (e) { 66 | return []; 67 | } 68 | }) 69 | .reduce((acc, arr) => acc.concat(arr), []); 70 | version = contents.length ? utils.findVersion(contents.join('\n')) : utils.NotFound; 71 | } catch (e) { 72 | version = utils.NotFound; 73 | } 74 | chromeVersion = Promise.resolve(version); 75 | } else { 76 | chromeVersion = Promise.resolve('N/A'); 77 | } 78 | return chromeVersion.then(v => utils.determineFound('Chrome', v, 'N/A')); 79 | }, 80 | 81 | getChromeCanaryInfo: () => { 82 | utils.log('trace', 'getChromeCanaryInfo'); 83 | const chromeCanaryVersion = utils.getDarwinApplicationVersion( 84 | utils.browserBundleIdentifiers['Chrome Canary'] 85 | ); 86 | return chromeCanaryVersion.then(v => utils.determineFound('Chrome Canary', v, 'N/A')); 87 | }, 88 | 89 | getChromiumInfo: () => { 90 | utils.log('trace', 'getChromiumInfo'); 91 | let chromiumVersion; 92 | if (utils.isLinux) { 93 | chromiumVersion = utils.run('chromium --version').then(utils.findVersion); 94 | } else { 95 | chromiumVersion = Promise.resolve('N/A'); 96 | } 97 | return chromiumVersion.then(v => utils.determineFound('Chromium', v, 'N/A')); 98 | }, 99 | 100 | getEdgeInfo: () => { 101 | utils.log('trace', 'getEdgeInfo'); 102 | let edgeVersion; 103 | if (utils.isWindows && os.release().split('.')[0] === '10') { 104 | const getPackageInfo = (pkgName, nickname) => { 105 | return utils.run(`powershell get-appxpackage ${pkgName}`).then(out => { 106 | const version = utils.findVersion(out); 107 | if (version !== '') { 108 | return `${nickname} (${utils.findVersion(out)})`; 109 | } 110 | return undefined; 111 | }); 112 | }; 113 | const edgePackages = { 114 | Spartan: 'Microsoft.MicrosoftEdge', 115 | Chromium: 'Microsoft.MicrosoftEdge.Stable', 116 | ChromiumDev: 'Microsoft.MicrosoftEdge.Dev', 117 | }; 118 | edgeVersion = Promise.all( 119 | Object.keys(edgePackages) 120 | .map(nickname => getPackageInfo(edgePackages[nickname], nickname)) 121 | .filter(x => x !== undefined) 122 | ); 123 | } else if (utils.isMacOS) { 124 | edgeVersion = utils.getDarwinApplicationVersion( 125 | utils.browserBundleIdentifiers['Microsoft Edge'] 126 | ); 127 | } else { 128 | return Promise.resolve('N/A'); 129 | } 130 | return edgeVersion.then(v => 131 | utils.determineFound('Edge', Array.isArray(v) ? v.filter(x => x !== undefined) : v, utils.NA) 132 | ); 133 | }, 134 | 135 | getFirefoxInfo: () => { 136 | utils.log('trace', 'getFirefoxInfo'); 137 | return getFirefoxVersion( 138 | 'Firefox', 139 | utils.browserBundleIdentifiers.Firefox, 140 | 'Mozilla Firefox/firefox.exe' 141 | ); 142 | }, 143 | 144 | getFirefoxDeveloperEditionInfo: () => { 145 | utils.log('trace', 'getFirefoxDeveloperEditionInfo'); 146 | return getFirefoxVersion( 147 | 'Firefox Developer Edition', 148 | utils.browserBundleIdentifiers['Firefox Developer Edition'], 149 | 'Firefox Developer Edition/firefox.exe' 150 | ); 151 | }, 152 | 153 | getFirefoxNightlyInfo: () => { 154 | utils.log('trace', 'getFirefoxNightlyInfo'); 155 | let firefoxNightlyVersion; 156 | if (utils.isLinux) { 157 | firefoxNightlyVersion = utils 158 | .run('firefox-trunk --version') 159 | .then(v => v.replace(/^.* ([^ ]*)/g, '$1')); 160 | } else if (utils.isMacOS) { 161 | firefoxNightlyVersion = utils.getDarwinApplicationVersion( 162 | utils.browserBundleIdentifiers['Firefox Nightly'] 163 | ); 164 | } else { 165 | firefoxNightlyVersion = Promise.resolve('N/A'); 166 | } 167 | 168 | return firefoxNightlyVersion.then(v => utils.determineFound('Firefox Nightly', v, 'N/A')); 169 | }, 170 | 171 | getInternetExplorerInfo: () => { 172 | utils.log('trace', 'getInternetExplorerInfo'); 173 | let explorerVersion; 174 | if (utils.isWindows) { 175 | const explorerPath = [ 176 | process.env.SYSTEMDRIVE || 'C:', 177 | 'Program Files', 178 | 'Internet Explorer', 179 | 'iexplore.exe', 180 | ].join('\\\\'); 181 | explorerVersion = utils 182 | .run(`wmic datafile where "name='${explorerPath}'" get Version`) 183 | .then(utils.findVersion); 184 | } else { 185 | explorerVersion = Promise.resolve('N/A'); 186 | } 187 | return explorerVersion.then(v => utils.determineFound('Internet Explorer', v, 'N/A')); 188 | }, 189 | 190 | getSafariTechnologyPreviewInfo: () => { 191 | utils.log('trace', 'getSafariTechnologyPreviewInfo'); 192 | const safariTechnologyPreview = utils.getDarwinApplicationVersion( 193 | utils.browserBundleIdentifiers['Safari Technology Preview'] 194 | ); 195 | return safariTechnologyPreview.then(v => 196 | utils.determineFound('Safari Technology Preview', v, 'N/A') 197 | ); 198 | }, 199 | 200 | getSafariInfo: () => { 201 | utils.log('trace', 'getSafariInfo'); 202 | const safariVersion = utils.getDarwinApplicationVersion(utils.browserBundleIdentifiers.Safari); 203 | return safariVersion.then(v => utils.determineFound('Safari', v, 'N/A')); 204 | }, 205 | }; 206 | -------------------------------------------------------------------------------- /__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | const utils = require('../src/utils'); 2 | const cases = { 3 | apt: { 4 | string: 'apt 1.4.9 (amd64)', 5 | version: '1.4.9', 6 | }, 7 | cargo: { 8 | string: 'cargo 1.31.0 (339d9f9c8 2018-11-16)', 9 | version: '1.31.0', 10 | }, 11 | composer: { 12 | string: '\u001b[32mComposer\u001b[39m version \u001b[33m1.8.6\u001b[39m 2019-06-11 15:03:05', 13 | version: '1.8.6', 14 | }, 15 | gradle: { 16 | string: '---------------------\nGradle 5.5\n---------------------', 17 | version: '5.5', 18 | }, 19 | homebrew: { 20 | string: 21 | 'Homebrew 2.1.7\nHomebrew/homebrew-core (git revision 3507d; last commit 2019-07-08)\nHomebrew/homebrew-cask (git revision 983c4; last commit 2019-07-08)', 22 | version: '2.1.7', 23 | }, 24 | maven: { 25 | string: 26 | 'Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-04T15:00:29-04:00)', 27 | version: '3.6.1', 28 | }, 29 | pip: { 30 | string: 'pip 19.0.3 from /usr/local/lib/python2.7/site-packages/pip (python 2.7)', 31 | version: '19.0.3', 32 | }, 33 | yum: { 34 | string: '3.4.3\n Installed: rpm-4.11.3-35.el7.x86_64 at 2019-03-05 17:35\n', 35 | version: '3.4.3', 36 | }, 37 | ash: { 38 | string: ` 39 | BusyBox v1.31.1 () multi-call binary. 40 | 41 | Usage: ash [-/+OPTIONS] [-/+o OPT]... [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS] / -s [ARGS]]`, 42 | version: '1.31.1', 43 | }, 44 | bash: { 45 | string: 'GNU bash, version 4.4.12(1)-release (x86_64-apple-darwin17.0.0)', 46 | version: '4.4.12', 47 | }, 48 | php: { 49 | string: 'PHP 7.1.7 (cli) (built: Jul 15 2017 18:08:09) ( NTS )', 50 | version: '7.1.7', 51 | }, 52 | docker: { 53 | string: 'Docker version 18.03.0-ce, build 0520e24', 54 | version: '18.03.0', 55 | }, 56 | edge: { 57 | string: ` 58 | Name : Microsoft.MicrosoftEdge 59 | Version : 20.10240.17146.0 60 | PackageFullName : Microsoft.MicrosoftEdge_20.10240.17146.0_neutral__8wekyb3d8bbwe 61 | InstallLocation : C:\Windows\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe 62 | PackageFamilyName : Microsoft.MicrosoftEdge_8wekyb3d8bbwe 63 | PublisherId : 8wekyb3d8bbwe`, 64 | version: '20.10240.17146.0', 65 | }, 66 | elixir: { 67 | regex: /[Elixir]+\s([\d+.[\d+|.]+)/, 68 | index: 1, 69 | string: `Erlang/OTP 20 [erts-9.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] 70 | Elixir 1.6.2 (compiled with OTP 20)`, 71 | version: '1.6.2', 72 | }, 73 | explorer: { 74 | string: `Version 75 | 11.0.10240.17443`, 76 | version: '11.0.10240.17443', 77 | }, 78 | ffmpeg: { 79 | string: `ffmpeg version 4.2 Copyright (c) 2000-2019 the FFmpeg developers 80 | built with gcc 9.1.1 (GCC) 20190807`, 81 | version: '4.2', 82 | }, 83 | go: { 84 | string: 'go version go1.9.3 darwin/amd64', 85 | version: '1.9.3', 86 | }, 87 | java: { 88 | regex: /\d+\.[\w+|.|_|-]+/, 89 | string: 'javac 1.8.0_192-b12', 90 | version: '1.8.0_192-b12', 91 | }, 92 | R: { 93 | string: `R version 3.0.2 (2013-09-25) -- "Frisbee Sailing" 94 | Copyright (C) 2013 The R Foundation for Statistical Computing 95 | Platform: x86_64-pc-linux-gnu (64-bit) 96 | 97 | R is free software and comes with ABSOLUTELY NO WARRANTY. 98 | You are welcome to redistribute it under the terms of the 99 | GNU General Public License versions 2 or 3. 100 | For more information about these matters see 101 | http://www.gnu.org/licenses/. 102 | `, 103 | version: '3.0.2', 104 | }, 105 | mariadb: { 106 | index: 1, 107 | string: 'mysql Ver 15.1 Distrib 10.2.14-MariaDB, for osx10.13 (x86_64) using readline 5.1', 108 | version: '10.2.14', 109 | }, 110 | mongodb: { 111 | string: `MongoDB shell version v3.6.4 112 | git version: d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856 113 | OpenSSL version: OpenSSL 1.0.2o 27 Mar 2018`, 114 | version: '3.6.4', 115 | }, 116 | mysql: { 117 | index: 1, 118 | string: 'mysql Ver 14.14 Distrib 5.7.21, for osx10.13 (x86_64) using EditLine wrapper', 119 | version: '5.7.21', 120 | }, 121 | postgres: { 122 | string: 'postgres (PostgreSQL) 10.3', 123 | version: '10.3', 124 | }, 125 | ruby: { 126 | regex: /\d+\.[\d+|.|p]+/, 127 | string: 'ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]', 128 | version: '2.3.7p456', 129 | }, 130 | rust: { 131 | string: 'rustc 1.31.1 (b6c32da9b 2018-12-18)', 132 | version: '1.31.1', 133 | }, 134 | sqlite: { 135 | string: 136 | '3.19.4 2017-08-18 19:28:12 605907e73adb4533b12d22be8422f17a8dc125b5c37bb391756a11fc3a8c4d10', 137 | version: '3.19.4', 138 | }, 139 | clang: { 140 | string: `clang version 7.0.0-3 (tags/RELEASE_700/final) 141 | Target: x86_64-pc-linux-gnu 142 | Thread model: posix 143 | InstalledDir: /usr/bin`, 144 | regex: /([0-9].*) /, 145 | index: 1, 146 | version: '7.0.0-3', 147 | }, 148 | sublime: { 149 | regex: /\d+/, 150 | string: 'Sublime Text Build 3143', 151 | version: '3143', 152 | }, 153 | virtualbox: { 154 | string: '5.2.8r121009', 155 | version: '5.2.8', 156 | }, 157 | xcodebuild: { 158 | string: `Xcode 9.0 159 | Build version 9A235`, 160 | version: '9.0', 161 | }, 162 | glibc: { 163 | string: `ldd (Ubuntu GLIBC 2.30-0ubuntu2) 2.30`, 164 | version: '2.30' 165 | } 166 | }; 167 | 168 | describe('findVersion - Matching version strings against real world cases', () => { 169 | Object.keys(cases).map(c => 170 | test(`${c} returns expected version`, () => 171 | expect(utils.findVersion(cases[c].string, cases[c].regex, cases[c].index)).toEqual( 172 | cases[c].version 173 | )) 174 | ); 175 | }); 176 | 177 | describe('parseSDKManagerOutput - Extracting info from sdkmanager', () => { 178 | const output = ` 179 | Path | Version | Description | Location 180 | ------- | ------- | ------- | ------- 181 | build-tools;28.0.3 | 28.0.3 | Android SDK Build-Tools 28.0.3 | build-tools/28.0.3/ 182 | platform-tools | 28.0.1 | Android SDK Platform-Tools | platform-tools/ 183 | platforms;android-28 | 6 | Android SDK Platform 28 | platforms/android-28/ 184 | sources;android-28 | 1 | Sources for Android 28 | sources/android-28/ 185 | system-images;android-28;google_apis_playstore;x86 | 5 | Google Play Intel x86 Atom System Image | system-images/android-28/google_apis_playstore/x86/ 186 | tools | 26.1.1 | Android SDK Tools | tools/ 187 | 188 | Available Packages: 189 | `; 190 | const sdkmanager = utils.parseSDKManagerOutput(output); 191 | 192 | expect(sdkmanager.apiLevels).toEqual(['28']); 193 | expect(sdkmanager.buildTools).toEqual(['28.0.3']); 194 | expect(sdkmanager.systemImages).toEqual(['android-28 | Google Play Intel x86 Atom']); 195 | }); 196 | 197 | describe('generatePlistBuddyCommand', () => { 198 | test('default options array', () => { 199 | expect(utils.generatePlistBuddyCommand('/Applications/Firefox.app')).toBe( 200 | '/usr/libexec/PlistBuddy -c Print:CFBundleShortVersionString /Applications/Firefox.app' 201 | ); 202 | }); 203 | 204 | test('options array argument', () => { 205 | expect( 206 | utils.generatePlistBuddyCommand('/Applications/Firefox.app', [ 207 | 'CFBundleShortVersionString', 208 | 'CFBundleVersion', 209 | ]) 210 | ).toBe( 211 | '/usr/libexec/PlistBuddy -c Print:CFBundleShortVersionString -c Print:CFBundleVersion /Applications/Firefox.app' 212 | ); 213 | }); 214 | }); 215 | 216 | describe('toReadableBytes', () => { 217 | test('formats bytes correctly', () => { 218 | expect(utils.toReadableBytes(1337)).toBe('1.31 KB'); 219 | }); 220 | 221 | test('handles falsy value for bytes', () => { 222 | expect(utils.toReadableBytes()).toBe('0 Bytes'); 223 | }); 224 | }); 225 | 226 | describe('omit', () => { 227 | test('emits properties from object', () => { 228 | expect(utils.omit({ one: true, two: true }, ['two'])).toEqual({ one: true }); 229 | }); 230 | }); 231 | 232 | describe('pick', () => { 233 | test('picks properties from object', () => { 234 | expect(utils.pick({ one: true, two: true }, ['two'])).toEqual({ two: true }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const os = require('os'); 4 | const childProcess = require('child_process'); 5 | const libWhich = require('which'); 6 | const glob = require('glob'); 7 | const matchers = require('./matchers'); 8 | 9 | const run = (cmd, { unify = false } = {}) => { 10 | return new Promise(resolve => { 11 | childProcess.exec( 12 | cmd, 13 | { 14 | stdio: [0, 'pipe', 'ignore'], 15 | }, 16 | (err, stdout, stderr) => { 17 | let output = ``; 18 | if (unify) { 19 | output = stdout.toString() + stderr.toString(); 20 | } else { 21 | output = stdout.toString(); 22 | } 23 | resolve((err ? '' : output).trim()); 24 | } 25 | ); 26 | }); 27 | }; 28 | 29 | const log = function log(level) { 30 | const args = Object.values(Array.prototype.slice.call(arguments).slice(1)); 31 | if ((process.env.ENVINFO_DEBUG || '').toLowerCase() === level) 32 | console.log(level, JSON.stringify(args)); // eslint-disable-line no-console 33 | }; 34 | 35 | const fileExists = filePath => { 36 | return new Promise(resolve => { 37 | fs.stat(filePath, err => (err ? resolve(null) : resolve(filePath))); 38 | }); 39 | }; 40 | 41 | const windowsExeExists = relExeFilePath => { 42 | return new Promise(resolve => { 43 | let absPath; 44 | fs.access( 45 | (absPath = path.join(process.env.ProgramFiles, `${relExeFilePath}`)), 46 | fs.constants.R_OK, 47 | err1 => { 48 | if (err1) { 49 | fs.access( 50 | (absPath = path.join(process.env['ProgramFiles(x86)'], `${relExeFilePath}`)), 51 | fs.constants.X_OK, 52 | err2 => { 53 | resolve(err2 ? null : absPath); 54 | } 55 | ); 56 | } else resolve(absPath); 57 | } 58 | ); 59 | }); 60 | }; 61 | 62 | const readFile = filePath => { 63 | return new Promise(fileResolved => { 64 | fs.readFile(filePath, 'utf8', (err, file) => (file ? fileResolved(file) : fileResolved(null))); 65 | }); 66 | }; 67 | 68 | const requireJson = filePath => { 69 | return readFile(filePath).then(file => (file ? JSON.parse(file) : null)); 70 | }; 71 | 72 | const versionRegex = /\d+\.[\d+|.]+/g; 73 | 74 | const findDarwinApplication = id => { 75 | log('trace', 'findDarwinApplication', id); 76 | const command = `mdfind "kMDItemCFBundleIdentifier=='${id}'"`; 77 | log('trace', command); 78 | return run(command).then(v => v.replace(/(\s)/g, '\\ ')); 79 | }; 80 | 81 | const generatePlistBuddyCommand = (appPath, options) => { 82 | var optionsArray = (options || ['CFBundleShortVersionString']).map(function optionsMap(option) { 83 | return '-c Print:' + option; 84 | }); 85 | return ['/usr/libexec/PlistBuddy'] 86 | .concat(optionsArray) 87 | .concat([appPath]) 88 | .join(' '); 89 | }; 90 | 91 | const matchAll = (regex, text) => { 92 | const matches = []; 93 | let match = null; 94 | // eslint-disable-next-line no-cond-assign 95 | while ((match = regex.exec(text)) !== null) { 96 | matches.push(match); 97 | } 98 | return matches; 99 | }; 100 | 101 | const parseSDKManagerOutput = output => { 102 | const installed = output.split('Available')[0]; 103 | const apiLevels = matchAll(matchers.androidAPILevels, installed).map(m => m[1]); 104 | const buildTools = matchAll(matchers.androidBuildTools, installed).map(m => m[1]); 105 | const systemImages = matchAll(matchers.androidSystemImages, installed) 106 | .map(m => m[1].split('|').map(s => s.trim())) 107 | .map(image => image[0].split(';')[0] + ' | ' + image[2].split(' System Image')[0]); 108 | 109 | return { 110 | apiLevels: apiLevels, 111 | buildTools: buildTools, 112 | systemImages: systemImages, 113 | }; 114 | }; 115 | 116 | module.exports = { 117 | run: run, 118 | log: log, 119 | fileExists: fileExists, 120 | windowsExeExists: windowsExeExists, 121 | readFile: readFile, 122 | requireJson: requireJson, 123 | versionRegex: versionRegex, 124 | findDarwinApplication: findDarwinApplication, 125 | generatePlistBuddyCommand: generatePlistBuddyCommand, 126 | matchAll: matchAll, 127 | parseSDKManagerOutput: parseSDKManagerOutput, 128 | 129 | isLinux: process.platform === 'linux', 130 | isMacOS: process.platform === 'darwin', 131 | NA: 'N/A', 132 | NotFound: 'Not Found', 133 | isWindows: process.platform.startsWith('win'), 134 | 135 | isObject: val => typeof val === 'object' && !Array.isArray(val), 136 | noop: d => d, 137 | pipe: fns => x => fns.reduce((v, f) => f(v), x), 138 | 139 | browserBundleIdentifiers: { 140 | 'Brave Browser': 'com.brave.Browser', 141 | Chrome: 'com.google.Chrome', 142 | 'Chrome Canary': 'com.google.Chrome.canary', 143 | Firefox: 'org.mozilla.firefox', 144 | 'Firefox Developer Edition': 'org.mozilla.firefoxdeveloperedition', 145 | 'Firefox Nightly': 'org.mozilla.nightly', 146 | 'Microsoft Edge': 'com.microsoft.edgemac', 147 | Safari: 'com.apple.Safari', 148 | 'Safari Technology Preview': 'com.apple.SafariTechnologyPreview', 149 | }, 150 | 151 | ideBundleIdentifiers: { 152 | Atom: 'com.github.atom', 153 | IntelliJ: 'com.jetbrains.intellij', 154 | PhpStorm: 'com.jetbrains.PhpStorm', 155 | 'Sublime Text': 'com.sublimetext.3', 156 | WebStorm: 'com.jetbrains.WebStorm', 157 | }, 158 | 159 | runSync: cmd => { 160 | return ( 161 | childProcess 162 | .execSync(cmd, { 163 | stdio: [0, 'pipe', 'ignore'], 164 | }) 165 | .toString() || '' 166 | ).trim(); 167 | }, 168 | 169 | which: binary => { 170 | return new Promise(resolve => libWhich(binary, (err, binaryPath) => resolve(binaryPath))); 171 | }, 172 | 173 | getDarwinApplicationVersion: bundleIdentifier => { 174 | log('trace', 'getDarwinApplicationVersion', bundleIdentifier); 175 | var version; 176 | if (process.platform !== 'darwin') { 177 | version = 'N/A'; 178 | } else { 179 | version = findDarwinApplication(bundleIdentifier).then(appPath => 180 | run( 181 | generatePlistBuddyCommand(path.join(appPath, 'Contents', 'Info.plist'), [ 182 | 'CFBundleShortVersionString', 183 | ]) 184 | ) 185 | ); 186 | } 187 | return Promise.resolve(version); 188 | }, 189 | 190 | uniq: arr => { 191 | return Array.from(new Set(arr)); // eslint-disable-line no-undef 192 | }, 193 | 194 | toReadableBytes: bytes => { 195 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 196 | return ( 197 | (!bytes && '0 Bytes') || 198 | (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB', 'PB'][i] 199 | ); 200 | }, 201 | 202 | omit: (obj, props) => { 203 | return Object.keys(obj) 204 | .filter(key => props.indexOf(key) < 0) 205 | .reduce((acc, key) => Object.assign(acc, { [key]: obj[key] }), {}); 206 | }, 207 | 208 | pick: (obj, props) => { 209 | return Object.keys(obj) 210 | .filter(key => props.indexOf(key) >= 0) 211 | .reduce((acc, key) => Object.assign(acc, { [key]: obj[key] }), {}); 212 | }, 213 | 214 | getPackageJsonByName: name => { 215 | return requireJson(path.join(process.cwd(), 'node_modules', name, 'package.json')); 216 | }, 217 | 218 | getPackageJsonByPath: filePath => { 219 | return requireJson(path.join(process.cwd(), filePath)); 220 | }, 221 | 222 | getPackageJsonByFullPath: fullPath => { 223 | log('trace', 'getPackageJsonByFullPath', fullPath); 224 | return requireJson(fullPath); 225 | }, 226 | 227 | getAllPackageJsonPaths: packageGlob => { 228 | log('trace', 'getAllPackageJsonPaths', packageGlob); 229 | return new Promise(resolve => { 230 | const cb = (err, res) => resolve(res.map(path.normalize) || []); 231 | if (packageGlob) return glob(path.join('node_modules', packageGlob, 'package.json'), cb); 232 | return glob(path.join('node_modules', '**', 'package.json'), cb); 233 | }); 234 | }, 235 | 236 | sortObject: obj => { 237 | return Object.keys(obj) 238 | .sort() 239 | .reduce((acc, val) => { 240 | acc[val] = obj[val]; 241 | return acc; 242 | }, {}); 243 | }, 244 | 245 | findVersion: (versionString, regex, index) => { 246 | log('trace', 'findVersion', versionString, regex, index); 247 | const idx = index || 0; 248 | const matcher = regex || versionRegex; 249 | const matched = versionString.match(matcher); 250 | return matched ? matched[idx] : versionString; 251 | }, 252 | 253 | condensePath: pathString => { 254 | return (pathString || '').replace(os.homedir(), '~'); 255 | }, 256 | 257 | determineFound: (name, version, appPath) => { 258 | log('trace', 'determineFound', name, version, appPath); 259 | if (version === 'N/A') { 260 | return Promise.resolve([name, 'N/A']); 261 | } 262 | if (!version || Object.keys(version).length === 0) return Promise.resolve([name, 'Not Found']); 263 | if (!appPath) return Promise.resolve([name, version]); 264 | return Promise.resolve([name, version, appPath]); 265 | }, 266 | }; 267 | -------------------------------------------------------------------------------- /src/helpers/ides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const utils = require('../utils'); 3 | 4 | module.exports = { 5 | getAndroidStudioInfo: () => { 6 | let androidStudioVersion = Promise.resolve('N/A'); 7 | if (utils.isMacOS) { 8 | const paths = [ 9 | path.join('/', 'Applications', 'Android Studio.app', 'Contents', 'Info.plist'), 10 | path.join(process.env.HOME, 'Applications', 'Android Studio.app', 'Contents', 'Info.plist'), 11 | path.join( 12 | '/', 13 | 'Applications', 14 | 'JetBrains Toolbox', 15 | 'Android Studio.app', 16 | 'Contents', 17 | 'Info.plist' 18 | ), 19 | path.join( 20 | process.env.HOME, 21 | 'Applications', 22 | 'JetBrains Toolbox', 23 | 'Android Studio.app', 24 | 'Contents', 25 | 'Info.plist' 26 | ), 27 | ]; 28 | androidStudioVersion = Promise.all( 29 | paths.map(p => { 30 | return utils.fileExists(p).then(exists => { 31 | if (!exists) { 32 | return null; 33 | } 34 | const command = utils.generatePlistBuddyCommand(p.replace(/ /g, '\\ '), [ 35 | 'CFBundleShortVersionString', 36 | 'CFBundleVersion', 37 | ]); 38 | return utils.run(command).then(version => { 39 | return version.split('\n').join(' '); 40 | }); 41 | }); 42 | }) 43 | ).then(versions => { 44 | return versions.find(version => version !== null) || utils.NotFound; 45 | }); 46 | } else if (utils.isLinux) { 47 | androidStudioVersion = Promise.all([ 48 | utils 49 | .run('cat /opt/android-studio/bin/studio.sh | grep "$Home/.AndroidStudio" | head -1') 50 | .then(utils.findVersion), 51 | utils.run('cat /opt/android-studio/build.txt'), 52 | ]).then(tasks => { 53 | const linuxVersion = tasks[0]; 54 | const linuxBuildNumber = tasks[1]; 55 | return `${linuxVersion} ${linuxBuildNumber}`.trim() || utils.NotFound; 56 | }); 57 | } else if (utils.isWindows) { 58 | androidStudioVersion = Promise.all([ 59 | utils 60 | .run( 61 | 'wmic datafile where name="C:\\\\Program Files\\\\Android\\\\Android Studio\\\\bin\\\\studio.exe" get Version' 62 | ) 63 | .then(version => version.replace(/(\r\n|\n|\r)/gm, '')), 64 | utils 65 | .run('type "C:\\\\Program Files\\\\Android\\\\Android Studio\\\\build.txt"') 66 | .then(version => version.replace(/(\r\n|\n|\r)/gm, '')), 67 | ]).then(tasks => { 68 | const windowsVersion = tasks[0]; 69 | const windowsBuildNumber = tasks[1]; 70 | return `${windowsVersion} ${windowsBuildNumber}`.trim() || utils.NotFound; 71 | }); 72 | } 73 | return androidStudioVersion.then(v => utils.determineFound('Android Studio', v)); 74 | }, 75 | 76 | getAtomInfo: () => { 77 | utils.log('trace', 'getAtomInfo'); 78 | return Promise.all([ 79 | utils.getDarwinApplicationVersion(utils.ideBundleIdentifiers.Atom), 80 | 'N/A', 81 | ]).then(v => utils.determineFound('Atom', v[0], v[1])); 82 | }, 83 | 84 | getEmacsInfo: () => { 85 | utils.log('trace', 'getEmacsInfo'); 86 | if (utils.isMacOS || utils.isLinux) { 87 | return Promise.all([ 88 | utils.run('emacs --version').then(utils.findVersion), 89 | utils.which('emacs'), 90 | ]).then(v => utils.determineFound('Emacs', v[0], v[1])); 91 | } 92 | return Promise.resolve(['Emacs', 'N/A']); 93 | }, 94 | 95 | getIntelliJInfo: () => { 96 | utils.log('trace', 'getIntelliJInfo'); 97 | return utils 98 | .getDarwinApplicationVersion(utils.ideBundleIdentifiers.IntelliJ) 99 | .then(v => utils.determineFound('IntelliJ', v)); 100 | }, 101 | 102 | getNanoInfo: () => { 103 | utils.log('trace', 'getNanoInfo'); 104 | if (utils.isLinux) { 105 | return Promise.all([ 106 | utils.run('nano --version').then(utils.findVersion), 107 | utils.which('nano'), 108 | ]).then(v => utils.determineFound('Nano', v[0], v[1])); 109 | } 110 | return Promise.resolve(['Nano', 'N/A']); 111 | }, 112 | 113 | getNvimInfo: () => { 114 | utils.log('trace', 'getNvimInfo'); 115 | if (utils.isMacOS || utils.isLinux) { 116 | return Promise.all([ 117 | utils.run('nvim --version').then(utils.findVersion), 118 | utils.which('nvim'), 119 | ]).then(v => utils.determineFound('Nvim', v[0], v[1])); 120 | } 121 | return Promise.resolve(['Vim', 'N/A']); 122 | }, 123 | 124 | getPhpStormInfo: () => { 125 | utils.log('trace', 'getPhpStormInfo'); 126 | return utils 127 | .getDarwinApplicationVersion(utils.ideBundleIdentifiers.PhpStorm) 128 | .then(v => utils.determineFound('PhpStorm', v)); 129 | }, 130 | 131 | getSublimeTextInfo: () => { 132 | utils.log('trace', 'getSublimeTextInfo'); 133 | return Promise.all([ 134 | utils.run('subl --version').then(version => utils.findVersion(version, /\d+/)), 135 | utils.which('subl'), 136 | ]) 137 | .then(v => { 138 | if (v[0] === '' && utils.isMacOS) { 139 | utils.log('trace', 'getSublimeTextInfo using plist'); 140 | return Promise.all([ 141 | utils.getDarwinApplicationVersion(utils.ideBundleIdentifiers['Sublime Text']), 142 | 'N/A', 143 | ]); 144 | } 145 | return v; 146 | }) 147 | .then(v => utils.determineFound('Sublime Text', v[0], v[1])); 148 | }, 149 | 150 | getVimInfo: () => { 151 | utils.log('trace', 'getVimInfo'); 152 | if (utils.isMacOS || utils.isLinux) { 153 | return Promise.all([ 154 | utils.run('vim --version').then(utils.findVersion), 155 | utils.which('vim'), 156 | ]).then(v => utils.determineFound('Vim', v[0], v[1])); 157 | } 158 | return Promise.resolve(['Vim', 'N/A']); 159 | }, 160 | 161 | getVSCodeInfo: () => { 162 | utils.log('trace', 'getVSCodeInfo'); 163 | return Promise.all([ 164 | utils.run('code --version').then(utils.findVersion), 165 | utils.which('code'), 166 | ]).then(v => utils.determineFound('VSCode', v[0], v[1])); 167 | }, 168 | 169 | getCursorInfo: () => { 170 | utils.log('trace', 'getCursorInfo'); 171 | return Promise.all([ 172 | utils.run('cursor --version').then(utils.findVersion), 173 | utils.which('cursor'), 174 | ]).then(v => utils.determineFound('Cursor', v[0], v[1])); 175 | }, 176 | 177 | getClaudeCodeInfo: () => { 178 | utils.log('trace', 'getClaudeCodeInfo'); 179 | return Promise.all([ 180 | utils.run('claude --version').then(utils.findVersion), 181 | utils.which('claude'), 182 | ]).then(v => utils.determineFound('Claude Code', v[0], v[1])); 183 | }, 184 | 185 | getopencodeInfo: () => { 186 | utils.log('trace', 'getopencodeInfo'); 187 | return Promise.all([ 188 | utils.run('opencode --version').then(utils.findVersion), 189 | utils.which('opencode'), 190 | ]).then(v => utils.determineFound('opencode', v[0], v[1])); 191 | }, 192 | 193 | getCodexInfo: () => { 194 | utils.log('trace', 'getCodexInfo'); 195 | return Promise.all([ 196 | utils.run('codex --version').then(utils.findVersion), 197 | utils.which('codex'), 198 | ]).then(v => utils.determineFound('Codex', v[0], v[1])); 199 | }, 200 | 201 | getVisualStudioInfo: () => { 202 | utils.log('trace', 'getVisualStudioInfo'); 203 | if (utils.isWindows) { 204 | return utils 205 | .run( 206 | `"${process.env['ProgramFiles(x86)']}/Microsoft Visual Studio/Installer/vswhere.exe" -format json -prerelease` 207 | ) 208 | .then(jsonText => { 209 | const instances = JSON.parse(jsonText).map(vsInstance => { 210 | return { 211 | Version: vsInstance.installationVersion, 212 | DisplayName: vsInstance.displayName, 213 | }; 214 | }); 215 | return utils.determineFound( 216 | 'Visual Studio', 217 | instances.map(v => `${v.Version} (${v.DisplayName})`) 218 | ); 219 | }) 220 | .catch(() => { 221 | return Promise.resolve(['Visual Studio', utils.NotFound]); 222 | }); 223 | } 224 | return Promise.resolve(['Visual Studio', utils.NA]); 225 | }, 226 | 227 | getWebStormInfo: () => { 228 | utils.log('trace', 'getWebStormInfo'); 229 | return utils 230 | .getDarwinApplicationVersion(utils.ideBundleIdentifiers.WebStorm) 231 | .then(v => utils.determineFound('WebStorm', v)); 232 | }, 233 | 234 | getXcodeInfo: () => { 235 | utils.log('trace', 'getXcodeInfo'); 236 | if (utils.isMacOS) { 237 | return Promise.all([ 238 | utils 239 | .which('xcodebuild') 240 | .then(xcodePath => utils.run(xcodePath + ' -version')) 241 | .then(version => `${utils.findVersion(version)}/${version.split('Build version ')[1]}`), 242 | utils.which('xcodebuild'), 243 | ]).then(v => utils.determineFound('Xcode', v[0], v[1])); 244 | } 245 | return Promise.resolve(['Xcode', 'N/A']); 246 | }, 247 | }; 248 | -------------------------------------------------------------------------------- /src/packages.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const path = require('path'); 3 | const utils = require('./utils'); 4 | 5 | const parsePackagePath = packagePath => { 6 | const split = packagePath.split('node_modules' + path.sep); 7 | const tree = split[split.length - 1]; 8 | if (tree.charAt(0) === '@') { 9 | return [tree.split(path.sep)[0], tree.split(path.sep)[1]].join('/'); 10 | } 11 | 12 | return tree.split(path.sep)[0]; 13 | }; 14 | 15 | function getnpmPackages(packages, options) { 16 | utils.log('trace', 'getnpmPackages'); 17 | if (!options) options = {}; 18 | 19 | let packageGlob = null; 20 | let tld = null; 21 | 22 | if (typeof packages === 'string') { 23 | if ( 24 | // detect characters that are not allowed in npm names, but are in globs 25 | // wont work if the exactly once glob @ is used by itself, because of npm namespacing 26 | packages.includes('*') || 27 | packages.includes('?') || 28 | packages.includes('+') || 29 | packages.includes('!') 30 | ) { 31 | packageGlob = packages; 32 | } else { 33 | packages = packages.split(','); 34 | } 35 | } 36 | 37 | return Promise.all([ 38 | 'npmPackages', 39 | utils 40 | .getPackageJsonByPath('package.json') 41 | .then(packageJson => 42 | Object.assign( 43 | {}, 44 | (packageJson || {}).devDependencies || {}, 45 | (packageJson || {}).dependencies || {} 46 | ) 47 | ) 48 | // determine which paths to get 49 | .then(packageJsonDependencies => { 50 | tld = packageJsonDependencies; 51 | if (options.fullTree || options.duplicates || packageGlob) { 52 | return utils.getAllPackageJsonPaths(packageGlob); 53 | } 54 | return Promise.resolve( 55 | Object.keys(packageJsonDependencies || []).map(dep => 56 | path.join('node_modules', dep, 'package.json') 57 | ) 58 | ); 59 | }) 60 | // filter by glob or selection 61 | .then(packageJsonPaths => { 62 | if ((packageGlob || typeof packages === 'boolean') && !options.fullTree) { 63 | return Promise.resolve( 64 | (packageJsonPaths || []).filter(p => 65 | Object.keys(tld || []).includes(parsePackagePath(p)) 66 | ) 67 | ); 68 | } 69 | if (Array.isArray(packages)) { 70 | return Promise.resolve( 71 | (packageJsonPaths || []).filter(p => packages.includes(parsePackagePath(p))) 72 | ); 73 | } 74 | return Promise.resolve(packageJsonPaths); 75 | }) 76 | .then(paths => 77 | Promise.all([ 78 | paths, 79 | Promise.all(paths.map(filePath => utils.getPackageJsonByPath(filePath))), 80 | ]) 81 | ) 82 | // conglomerate the data 83 | .then(result => { 84 | const paths = result[0]; 85 | const files = result[1]; 86 | 87 | const versions = files.reduce((acc, d, idx) => { 88 | // if the file is a test stub, or doesn't have a name, ignore it. 89 | if (!d || !d.name) return acc; 90 | // create object if its not already created 91 | if (!acc[d.name]) acc[d.name] = {}; 92 | // set duplicates if flag set, if version not already there 93 | if (options.duplicates) { 94 | acc[d.name].duplicates = utils.uniq((acc[d.name].duplicates || []).concat(d.version)); 95 | } 96 | // set the installed version, if its installed top level 97 | if ((paths[idx].match(/node_modules/g) || []).length === 1) 98 | acc[d.name].installed = d.version; 99 | return acc; 100 | }, {}); 101 | 102 | Object.keys(versions).forEach(name => { 103 | // update duplicates, only !== installed 104 | if (versions[name].duplicates && versions[name].installed) { 105 | versions[name].duplicates = versions[name].duplicates.filter( 106 | v => v !== versions[name].installed 107 | ); 108 | } 109 | // if it is a top level dependency, get the wanted version 110 | if (tld[name]) versions[name].wanted = tld[name]; 111 | }); 112 | 113 | return versions; 114 | }) 115 | .then(versions => { 116 | if (options.showNotFound && Array.isArray(packages)) { 117 | packages.forEach(p => { 118 | if (!versions[p]) { 119 | versions[p] = 'Not Found'; 120 | } 121 | }); 122 | } 123 | return versions; 124 | }) 125 | .then(versions => utils.sortObject(versions)), 126 | ]); 127 | } 128 | 129 | function getnpmGlobalPackages(packages, options) { 130 | utils.log('trace', 'getnpmGlobalPackages', packages); 131 | 132 | let packageGlob = null; 133 | 134 | if (typeof packages === 'string') { 135 | if ( 136 | // detect characters that are not allowed in npm names, but are in globs 137 | // wont work if the exactly once glob @ is used by itself, because of npm namespacing 138 | packages.includes('*') || 139 | packages.includes('?') || 140 | packages.includes('+') || 141 | packages.includes('!') 142 | ) { 143 | packageGlob = packages; 144 | } else { 145 | packages = packages.split(','); 146 | } 147 | } else if (!Array.isArray(packages)) { 148 | packages = true; 149 | } 150 | 151 | return Promise.all([ 152 | 'npmGlobalPackages', 153 | utils 154 | // get the location of the npm global node_modules 155 | .run('npm get prefix --global') 156 | // glob all of the package.json files in that directory 157 | .then( 158 | prefix => 159 | new Promise((resolve, reject) => 160 | glob( 161 | // sub packageGlob in to only get globbed packages if not null 162 | path.join( 163 | prefix, 164 | utils.isWindows ? '' : 'lib', 165 | 'node_modules', 166 | packageGlob || '{*,@*/*}', 167 | 'package.json' 168 | ), 169 | (err, files) => { 170 | if (!err) resolve(files); 171 | reject(err); 172 | } 173 | ) 174 | ) 175 | ) 176 | .then(globResults => 177 | Promise.all( 178 | globResults 179 | // filter out package paths not in list provided in options 180 | .filter( 181 | globbedPath => 182 | typeof packages === 'boolean' || 183 | packageGlob !== null || 184 | packages.includes(parsePackagePath(globbedPath)) 185 | ) 186 | // get all the package.json by path, return promises 187 | .map(packageJson => utils.getPackageJsonByFullPath(packageJson)) 188 | ) 189 | ) 190 | // accumulate all the package info in one object. 191 | .then(allPackages => 192 | allPackages.reduce( 193 | (acc, json) => (json ? Object.assign(acc, { [json.name]: json.version }) : acc), 194 | {} 195 | ) 196 | ) 197 | .then(versions => { 198 | if (options.showNotFound && Array.isArray(packages)) { 199 | packages.forEach(p => { 200 | if (!versions[p]) { 201 | versions[p] = 'Not Found'; 202 | } 203 | }); 204 | } 205 | return versions; 206 | }), 207 | ]); 208 | } 209 | 210 | function getpnpmGlobalPackages(packages, options) { 211 | utils.log('trace', 'getpnpmGlobalPackages', packages); 212 | 213 | let packageGlob = null; 214 | 215 | // Normalize packages arg: boolean | array | glob-string | comma-list 216 | if (typeof packages === 'string') { 217 | if ( 218 | packages.includes('*') || 219 | packages.includes('?') || 220 | packages.includes('+') || 221 | packages.includes('!') 222 | ) { 223 | packageGlob = packages; 224 | } else { 225 | packages = packages.split(','); 226 | } 227 | } else if (!Array.isArray(packages)) { 228 | packages = true; 229 | } 230 | 231 | // Parse JSON output from `pnpm list --global --depth=0 --json` 232 | const parseJson = jsonText => { 233 | const versions = {}; 234 | try { 235 | const parsed = JSON.parse(jsonText || '[]'); 236 | const nodes = Array.isArray(parsed) ? parsed : [parsed]; 237 | nodes.forEach(node => { 238 | if (!node) return; 239 | const deps = node.dependencies || {}; 240 | Object.keys(deps).forEach(name => { 241 | const meta = deps[name]; 242 | if (meta && typeof meta === 'object' && meta.version) versions[name] = meta.version; 243 | else if (typeof meta === 'string') versions[name] = meta; 244 | }); 245 | if (node.name && node.version) versions[node.name] = node.version; 246 | }); 247 | } catch (e) { 248 | utils.log('trace', 'pnpm parseJson error', (e && e.message) || String(e)); 249 | } 250 | return versions; 251 | }; 252 | 253 | const applyFilters = versions => { 254 | let out = versions || {}; 255 | if (packageGlob !== null) { 256 | const globRegex = new RegExp( 257 | '^' + 258 | packageGlob 259 | .replace(/[.+^${}()|[\]\\]/g, '\\$&') 260 | .replace(/\*/g, '.*') 261 | .replace(/\?/g, '.') + 262 | '$' 263 | ); 264 | out = Object.keys(out) 265 | .filter(name => globRegex.test(name)) 266 | .reduce((acc, name) => Object.assign(acc, { [name]: out[name] }), {}); 267 | } else if (Array.isArray(packages)) { 268 | out = Object.keys(out) 269 | .filter(name => packages.includes(name)) 270 | .reduce((acc, name) => Object.assign(acc, { [name]: out[name] }), {}); 271 | } 272 | 273 | if (options && options.showNotFound && Array.isArray(packages)) { 274 | packages.forEach(p => { 275 | if (!out[p]) out[p] = 'Not Found'; 276 | }); 277 | } 278 | return utils.sortObject(out); 279 | }; 280 | 281 | return Promise.all([ 282 | 'pnpmGlobalPackages', 283 | utils 284 | .run('pnpm list --global --depth=0 --json') 285 | .then(parseJson) 286 | .then(applyFilters), 287 | ]); 288 | } 289 | 290 | module.exports = { 291 | getnpmPackages: getnpmPackages, 292 | getnpmGlobalPackages: getnpmGlobalPackages, 293 | getpnpmGlobalPackages: getpnpmGlobalPackages, 294 | }; 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

envinfo generates a report of the common details needed when troubleshooting software issues, such as your operating system, binary versions, browsers, installed languages, and more

4 |
5 |

6 | 7 | [![npm version](https://badge.fury.io/js/envinfo.svg)](https://badge.fury.io/js/envinfo) [![npm downloads per month](https://img.shields.io/npm/dm/envinfo.svg?maxAge=86400)](https://www.npmjs.com/package/envinfo) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | [![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors) 9 | 10 | ## The problem 11 | 12 | - It works on my computer 13 | - "command not found" 14 | - what version of "command" are you running? 15 | - what version of "different command" are you running? 16 | - do you have "insert obscure android sdk version"? 17 | - every github issue reporting template ever: 18 | 19 | **Please mention other relevant information such as the browser version, Node.js version, Operating System and programming language.** 20 | 21 | ## This solution 22 | 23 | - Gather all of this information in one spot, quickly, and painlessly. 24 | 25 | ## Installation 26 | 27 | To use as a CLI tool, install this package globally: 28 | 29 | ```sh 30 | npm install -g envinfo || yarn global add envinfo 31 | ``` 32 | 33 | Or, use without installing with npx: 34 | 35 | `npx envinfo` 36 | 37 | To use as a library in another project: 38 | 39 | ```sh 40 | npm install envinfo || yarn add envinfo 41 | ``` 42 | 43 | ## CLI Usage 44 | 45 | `envinfo` || `npx envinfo` 46 | 47 | ```bash 48 | System: 49 | OS: macOS Mojave 10.14.5 50 | CPU: (8) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz 51 | Memory: 2.97 GB / 16.00 GB 52 | Shell: 5.3 - /bin/zsh 53 | Binaries: 54 | Node: 8.16.0 - ~/.nvm/versions/node/v8.16.0/bin/node 55 | Yarn: 1.15.2 - ~/.yarn/bin/yarn 56 | npm: 6.9.0 - ~/.nvm/versions/node/v8.16.0/bin/npm 57 | pnpm: 8.7.6 - /usr/local/bin/pnpm 58 | bun: 1.0.2 - /usr/local/bin/bun 59 | Watchman: 4.9.0 - /usr/local/bin/watchman 60 | Managers: 61 | Cargo: 1.31.0 - ~/.cargo/bin/cargo 62 | CocoaPods: 1.7.3 - /usr/local/bin/pod 63 | Composer: 1.8.6 - /usr/local/bin/composer 64 | Gradle: 5.5 - /usr/local/bin/gradle 65 | Homebrew: 2.1.7 - /usr/local/bin/brew 66 | Maven: 3.6.1 - /usr/local/bin/mvn 67 | pip2: 19.0.3 - /usr/local/bin/pip2 68 | pip3: 19.0.2 - /usr/local/bin/pip3 69 | RubyGems: 2.5.2.3 - /usr/bin/gem 70 | Utilities: 71 | CMake: 3.13.3 - /usr/local/bin/cmake 72 | Make: 3.81 - /usr/bin/make 73 | GCC: 10.14. - /usr/bin/gcc 74 | Git: 2.20.0 - /usr/local/bin/git 75 | Mercurial: 4.5.3 - /usr/bin/hg 76 | Clang: 1001.0.46.4 - /usr/bin/clang 77 | Subversion: 1.10.3 - /usr/bin/svn 78 | Servers: 79 | Apache: 2.4.34 - /usr/sbin/apachectl 80 | Nginx: 1.13.12 - /usr/local/bin/nginx 81 | Virtualization: 82 | Docker: 18.09.1 - /usr/local/bin/docker 83 | Parallels: 13.3.0 - /usr/local/bin/prlctl 84 | VirtualBox: 5.2.20 - /usr/local/bin/vboxmanage 85 | SDKs: 86 | iOS SDK: 87 | Platforms: iOS 12.2, macOS 10.14, tvOS 12.2, watchOS 5.2 88 | Android SDK: 89 | API Levels: 28 90 | Build Tools: 28.0.3 91 | System Images: android-28 | Google Play Intel x86 Atom 92 | IDEs: 93 | Android Studio: 3.2 AI-181.5540.7.32.5056338 94 | Atom: 1.23.3 95 | Emacs: 22.1.1 - /usr/bin/emacs 96 | Nano: 2.0.6 - /usr/bin/nano 97 | VSCode: 1.36.0 - /usr/local/bin/code 98 | Vim: 8.0 - /usr/bin/vim 99 | Xcode: 10.2.1/10E1001 - /usr/bin/xcodebuild 100 | Languages: 101 | Bash: 4.4.23 - /usr/local/bin/bash 102 | Elixir: 1.6.2 - /usr/local/bin/elixir 103 | Go: 1.11.1 - /usr/local/bin/go 104 | Java: 1.8.0_192 - /usr/bin/javac 105 | Perl: 5.18.4 - /usr/bin/perl 106 | PHP: 7.1.23 - /usr/bin/php 107 | Python: 2.7.16 - /usr/local/bin/python 108 | Python3: 3.7.2 - /usr/local/bin/python3 109 | R: 3.6.0 - /usr/local/bin/R 110 | Ruby: 2.3.7 - /usr/bin/ruby 111 | Rust: 1.16.0 - /Users/tabrindle/.cargo/bin/rustup 112 | Databases: 113 | MongoDB: 3.6.4 - /usr/local/bin/mongo 114 | MySQL: 10.3.10 (MariaDB) - /usr/local/bin/mysql 115 | PostgreSQL: 10.3 - /usr/local/bin/postgres 116 | SQLite: 3.24.0 - /usr/bin/sqlite3 117 | Browsers: 118 | Chrome: 75.0.3770.100 119 | Chrome Canary: 77.0.3847.0 120 | Firefox: 68.0 121 | Firefox Developer Edition: 69.0 122 | Firefox Nightly: 69.0a1 123 | Safari: 12.1.1 124 | Safari Technology Preview: 13.0 125 | npmPackages: 126 | apollo-client: ^2.3.1 => 2.3.1 127 | jest: ^22.2.1 => 22.2.1 128 | ... 129 | react: ^16.3.2 => 16.3.2 130 | react-apollo: ^2.1.4 => 2.1.4 131 | run4staged: ^1.1.1 => 1.1.1 132 | solidarity: 2.0.5 => 2.0.5 133 | styled-components: ^3.1.6 => 3.1.6 134 | npmGlobalPackages: 135 | create-react-app: 1.5.2 136 | create-react-native-app: 1.0.0 137 | envinfo: 5.10.0 138 | exp: 49.2.2 139 | gatsby-cli: 1.1.52 140 | npm: 5.6.0 141 | react-native-cli: 2.0.1 142 | solidarity: 2.1.0 143 | typescript: 2.8.1 144 | ``` 145 | 146 | ## Programmatic Usage 147 | 148 | Envinfo takes a configuration object and returns a Promise that resolves a string (optionally yaml, json or markdown) 149 | 150 | ```javascript 151 | import envinfo from 'envinfo'; 152 | 153 | envinfo.run( 154 | { 155 | System: ['OS', 'CPU'], 156 | Binaries: ['Node', 'Yarn', 'npm'], 157 | Browsers: ['Chrome', 'Firefox', 'Safari'], 158 | npmPackages: ['styled-components', 'babel-plugin-styled-components'], 159 | }, 160 | { json: true, showNotFound: true } 161 | ).then(env => console.log(env)); 162 | ``` 163 | 164 | logs: 165 | 166 | ```json 167 | { 168 | "System": { 169 | "OS": "macOS High Sierra 10.13", 170 | "CPU": "x64 Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz" 171 | }, 172 | "Binaries": { 173 | "Node": { 174 | "version": "8.11.0", 175 | "path": "~/.nvm/versions/node/v8.11.0/bin/node" 176 | }, 177 | "Yarn": { 178 | "version": "1.5.1", 179 | "path": "~/.yarn/bin/yarn" 180 | }, 181 | "npm": { 182 | "version": "5.6.0", 183 | "path": "~/.nvm/versions/node/v8.11.0/bin/npm" 184 | } 185 | }, 186 | "Browsers": { 187 | "Chrome": { 188 | "version": "67.0.3396.62" 189 | }, 190 | "Firefox": { 191 | "version": "59.0.2" 192 | }, 193 | "Safari": { 194 | "version": "11.0" 195 | } 196 | }, 197 | "npmPackages": { 198 | "styled-components": { 199 | "wanted": "^3.2.1", 200 | "installed": "3.2.1" 201 | }, 202 | "babel-plugin-styled-components": "Not Found" 203 | } 204 | } 205 | ``` 206 | 207 | All of envinfo's helpers are also exported for use. You can use envinfo as a whole, or just the parts that you need, like this: 208 | 209 | ```javascript 210 | const envinfo = require('envinfo'); 211 | 212 | // each helper returns a promise 213 | const node = await envinfo.helpers.getNodeInfo(); 214 | 215 | // The promises resolve to an array of values: ["Name", "Version", "Path"] 216 | // e.g. ["Node", "10.9.0", "/usr/local/bin/node"] 217 | 218 | console.log(`Node: ${node[1]} - ${node[2]}`); // "Node: 10.9.0 - ~/.nvm/versions/node/v8.14.0/bin/node" 219 | ``` 220 | 221 | ## CLI Options 222 | 223 | ``` 224 | --system Print general system info such as OS, CPU, Memory and Shell 225 | --browsers Get version numbers of installed web browsers 226 | --SDKs Get platforms, build tools and SDKs of iOS and Android 227 | --IDEs Get version numbers of installed IDEs 228 | --languages Get version numbers of installed languages such as Java, Python, PHP, etc 229 | --binaries Get version numbers of node, npm, watchman, etc 230 | --npmPackages Get version numbers of locally installed npm packages - glob, string, or comma delimited list 231 | --npmGlobalPackages Get version numbers of globally installed npm packages 232 | 233 | --duplicates Mark duplicate npm packages inside parentheses eg. (2.1.4) 234 | --fullTree Traverse entire node_modules dependency tree, not just top level 235 | 236 | --markdown Print output in markdown format 237 | --json Print output in JSON format 238 | --console Print to console (defaults to on for CLI usage, off for programmatic usage) 239 | ``` 240 | 241 | ## Integration 242 | 243 | envinfo is live in: 244 | 245 | - [React Native](https://github.com/facebook/react-native) (`react-native info`) 246 | - [Create React App](https://github.com/facebook/create-react-app) (`create-react-app --info`) 247 | - [Expo Environment Info](https://github.com/expo/expo-cli/tree/main/packages/expo-env-info) (`npx expo-env-info`) 248 | - [Webpack](https://github.com/webpack/webpack-cli) (`webpack-cli info`) 249 | - [Solidarity](https://github.com/infinitered/solidarity) (`solidarity report`) 250 | - [Gatsby](https://github.com/gatsbyjs/gatsby) (`gatsby info`) 251 | 252 | envinfo is used in the ISSUE_TEMPLATE of: 253 | 254 | - [styled-components](https://github.com/styled-components/styled-components) 255 | - [Jest](https://github.com/facebook/jest) 256 | - [Apollo Client](https://github.com/apollographql/apollo-client) 257 | - [FakerJS](https://github.com/faker-js/faker) 258 | 259 | ## Alternatives 260 | 261 | - type `command -v` until you smash your computer 262 | - [screenfetch](https://github.com/KittyKatt/screenFetch) - fetch system and terminal information, and display a pretty ascii logo 263 | - [Solidarity](https://github.com/infinitered/solidarity) - a project based environment checker 264 | - write your own 265 | 266 | ## License 267 | 268 | MIT 269 | 270 | ## Contributing 271 | 272 | PRs for additional features are welcome! Run `npm run lint && npm run format` before committing. 273 | 274 | This project came out of a [PR](https://github.com/facebook/react-native/pull/14428) to the React Native CLI tool - issues are reported frequently without important environment information, like Node/npm versions. 275 | 276 | ## Contributors 277 | 278 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 279 | 280 | 281 | 282 | | [
Trevor Brindle](http://trevorbrindle.com)
[💬](#question-tabrindle "Answering Questions") [📝](#blog-tabrindle "Blogposts") [🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Atabrindle "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=tabrindle "Code") [📖](https://github.com/tabrindle/envinfo/commits?author=tabrindle "Documentation") [💡](#example-tabrindle "Examples") [🤔](#ideas-tabrindle "Ideas, Planning, & Feedback") [👀](#review-tabrindle "Reviewed Pull Requests") [📢](#talk-tabrindle "Talks") [⚠️](https://github.com/tabrindle/envinfo/commits?author=tabrindle "Tests") | [
Gant Laborde](http://gantlaborde.com/)
[📝](#blog-GantMan "Blogposts") [🐛](https://github.com/tabrindle/envinfo/issues?q=author%3AGantMan "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=GantMan "Code") [🤔](#ideas-GantMan "Ideas, Planning, & Feedback") | [
Anton Fisher](http://antonfisher.com)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Aantonfisher "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=antonfisher "Code") | [
Ahmad Awais ⚡️](https://AhmadAwais.com/)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Aahmadawais "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=ahmadawais "Code") | [
Hasan](https://github.com/LEQADA)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3ALEQADA "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=LEQADA "Code") | [
Ernesto Ramírez](http://twitter.com/_ErnestoR)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3AErnestoR "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=ErnestoR "Code") | [
Jiawen Geng](https://www.gengjiawen.com)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Agengjiawen "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=gengjiawen "Code") [🤔](#ideas-gengjiawen "Ideas, Planning, & Feedback") [⚠️](https://github.com/tabrindle/envinfo/commits?author=gengjiawen "Tests") | 283 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 284 | | [
Zac Anger](https://zacanger.com)
[💻](https://github.com/tabrindle/envinfo/commits?author=zacanger "Code") [🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Azacanger "Bug reports") | [
Ville Immonen](https://twitter.com/VilleImmonen)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Afson "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=fson "Code") | [
Olmo Maldonado](http://ibolmo.com)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Aibolmo "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=ibolmo "Code") | [
Rishabh Chawla](https://rishabhchawla.co)
[🐛](https://github.com/tabrindle/envinfo/issues?q=author%3Arishabh3112 "Bug reports") [💻](https://github.com/tabrindle/envinfo/commits?author=rishabh3112 "Code") | [
Carl Taylor](https://github.com/Nthalk)
[💻](https://github.com/tabrindle/envinfo/commits?author=Nthalk "Code") | 285 | 286 | 287 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 288 | --------------------------------------------------------------------------------