├── docs ├── .nojekyll ├── _media │ └── favicon.ico ├── pluginsList.md ├── advancedPlugin.md ├── _sidebar.md ├── index.html ├── plugins.md ├── solidarityFromCode.md ├── customRuleCheck.md ├── customRuleSnapshot.md ├── cliOptions.md ├── contributorsGuide.md ├── simplePlugin.md ├── customRuleReport.md └── vendor │ └── search.min.js ├── .prettierignore ├── __tests__ ├── sandbox │ ├── solidarity_broken │ │ └── .solidarity │ ├── solidarity_json │ │ └── .solidarity.json │ └── fake_project │ │ └── node_modules │ │ ├── mock_module │ │ └── .solidarity │ │ └── mock_second_module │ │ └── .solidarity.json ├── __snapshots__ │ └── index.ts.snap ├── command_helpers │ ├── __snapshots__ │ │ ├── getSolidaritySettings.ts.snap │ │ ├── updateVersions.ts.snap │ │ ├── setSolidaritySettings.ts.snap │ │ ├── reviewRule.ts.snap │ │ ├── findPluginInfo.ts.snap │ │ ├── updateRequirement.ts.snap │ │ ├── solidarityReport.ts.snap │ │ └── createPlugin.ts.snap │ ├── binaryExists.ts │ ├── checkSTDERR.ts │ ├── solidarityReport.ts │ ├── setSolidaritySettings.ts │ ├── checkENV.ts │ ├── checkDir.ts │ ├── setOutputMode.ts │ ├── findPluginInfo.ts │ ├── updateVersions.ts │ ├── getLineWithVersion.ts │ ├── removeNonVersionCharacters.ts │ ├── printResults.ts │ ├── quirksNodeModules.ts │ ├── checkFile.ts │ ├── getVersion.ts │ ├── checkCLI.ts │ ├── skipRule.ts │ ├── createPlugin.ts │ ├── checkCLIForUpdates.ts │ ├── getSolidarityHelpers.ts │ ├── checkShell.ts │ ├── appendSolidaritySettings.ts │ ├── checkRequirementPlugins.ts │ ├── reviewRule.ts │ └── getSolidaritySettings.ts ├── setup.ts ├── commands │ ├── __snapshots__ │ │ ├── create.ts.snap │ │ ├── help.ts.snap │ │ ├── report.ts.snap │ │ └── solidarity.ts.snap │ ├── report.ts │ ├── help.ts │ ├── create.ts │ └── solidarity.ts ├── __mocks__ │ ├── listr.ts │ ├── mockContext.ts │ └── examplePlugin.ts ├── schema │ └── validateExampleSchema.ts ├── docs │ └── testDocs.ts ├── integration │ ├── solidarity-check │ │ └── check-invalid.ts │ ├── solidarity-snapshot │ │ └── snapshot-nada.ts │ ├── solidarity-report │ │ └── report-basic.ts │ └── solidarity-create │ │ └── create-nada.ts ├── index.ts └── extensions │ ├── extensionCheck.ts │ └── __snapshots__ │ └── extensionCheck.ts.snap ├── _art ├── combo.jpg ├── plugin.jpg ├── reports.gif ├── action-shot.png ├── custom_menu.png ├── solidarity-logo.png ├── custom_cli_report.png ├── solidarity-logo@2x.png └── solidarity_explainer.gif ├── spellcheck.json ├── src ├── templates │ ├── .gitignore.ejs │ ├── rules-template.json.ejs │ ├── package.json.ejs │ ├── addOptionalRules.js.ejs │ ├── README.md.ejs │ ├── simple-plugin.js.ejs │ └── helpful-plugin.js.ejs ├── extensions │ ├── functions │ │ ├── onboard │ │ │ ├── kickoffs │ │ │ │ ├── kickoffCLI.ts │ │ │ │ ├── kickoffShell.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kickoffDir.ts │ │ │ │ ├── kickoffFile.ts │ │ │ │ ├── kickoffEnv.ts │ │ │ │ └── kickoffAllRules.ts │ │ │ ├── printWizard.ts │ │ │ ├── index.ts │ │ │ ├── addMore.ts │ │ │ ├── reviewAndSave.ts │ │ │ ├── onboardAdd.ts │ │ │ └── executeAddRule.ts │ │ ├── binaryExists.ts │ │ ├── checkENV.ts │ │ ├── removeNonVersionCharacters.ts │ │ ├── setSolidaritySettings.ts │ │ ├── quirksNodeModules.ts │ │ ├── checkSTDERR.ts │ │ ├── checkDir.ts │ │ ├── checkFile.ts │ │ ├── getLineWithVersion.ts │ │ ├── setOutputMode.ts │ │ ├── checkShell.ts │ │ ├── findPluginInfo.ts │ │ ├── skipRule.ts │ │ ├── printResults.ts │ │ ├── checkCLIForUpdates.ts │ │ ├── solidarityReport.ts │ │ ├── updateVersions.ts │ │ ├── checkCLI.ts │ │ ├── getVersion.ts │ │ ├── getSolidaritySettings.ts │ │ ├── appendSolidaritySettings.ts │ │ ├── createPlugin.ts │ │ ├── updateRequirement.ts │ │ ├── getSolidarityHelpers.ts │ │ ├── ruleHandlers.ts │ │ ├── reviewRule.ts │ │ ├── buildSpecificRequirement.ts │ │ └── checkRequirement.ts │ └── solidarity-extension.ts ├── commands │ ├── create.ts │ ├── help.ts │ ├── report.ts │ ├── onboard.ts │ ├── fix.ts │ ├── solidarity.ts │ └── snapshot.ts └── index.ts ├── tslint.json ├── .npmignore ├── .gitignore ├── appveyor.yml ├── .editorconfig ├── bin └── solidarity ├── .travis.yml ├── .solidarity ├── tsconfig.json ├── LICENSE ├── dangerfile.ts ├── .solidarity.example.json ├── .vscode └── cSpell.json ├── config.yml ├── CODE_OF_CONDUCT.md ├── package.json └── solidaritySchema.json /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | readme.md 3 | dist/ 4 | -------------------------------------------------------------------------------- /__tests__/sandbox/solidarity_broken/.solidarity: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /_art/combo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/combo.jpg -------------------------------------------------------------------------------- /_art/plugin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/plugin.jpg -------------------------------------------------------------------------------- /_art/reports.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/reports.gif -------------------------------------------------------------------------------- /_art/action-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/action-shot.png -------------------------------------------------------------------------------- /_art/custom_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/custom_menu.png -------------------------------------------------------------------------------- /_art/solidarity-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/solidarity-logo.png -------------------------------------------------------------------------------- /docs/_media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/docs/_media/favicon.ico -------------------------------------------------------------------------------- /_art/custom_cli_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/custom_cli_report.png -------------------------------------------------------------------------------- /_art/solidarity-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/solidarity-logo@2x.png -------------------------------------------------------------------------------- /_art/solidarity_explainer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/solidarity/HEAD/_art/solidarity_explainer.gif -------------------------------------------------------------------------------- /spellcheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["orta", "artsy", "github", "/danger-*.", "/plugin[s]?", "Node.js"], 3 | "whitelistFiles": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/templates/.gitignore.ejs: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | .node-version 4 | coverage 5 | .nyc_output 6 | .idea 7 | dist/ 8 | .vscode/* 9 | !.vscode/cSpell.json 10 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Verify Runtime AND Configuration 1`] = `[Function]`; 4 | 5 | exports[`ensure build 1`] = `undefined`; 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "trailing-comma": [true], 5 | "no-string-throw": false, 6 | "space-before-function-paren": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _art/ 2 | .solidarity.example.json 3 | __tests__/ 4 | src/ 5 | .node-version 6 | coverage/ 7 | yarn.lock 8 | *.log 9 | config.yml 10 | pretty.yml 11 | render*.gif 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/getSolidaritySettings.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic getSolidaritySettings w/ success getSolidaritySettings exists 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/templates/rules-template.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/solidaritySchema", 3 | "requirements": { 4 | "Node Example": [{ "rule": "cli", "binary": "node", "semver": "0.0.0"}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | // Stops warnings in CI mode 2 | // MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 unhandledRejection listeners added. Use emitter.setMaxListeners() 3 | process.setMaxListeners(0) 4 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/updateVersions.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`updateVersions exists 1`] = `[Function]`; 4 | 5 | exports[`updateVersions pulls solidarity settings 1`] = `undefined`; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | .node-version 4 | coverage 5 | .nyc_output 6 | .idea 7 | dist/ 8 | .vscode/ 9 | !.vscode/cSpell.json 10 | npm-debug.log 11 | .DS_Store 12 | render*.gif 13 | pretty.yml 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/setSolidaritySettings.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`check good setSolidarity path 1`] = `undefined`; 4 | 5 | exports[`setSolidaritySettings exists 1`] = `[Function]`; 6 | -------------------------------------------------------------------------------- /__tests__/commands/__snapshots__/create.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot create command 1`] = ` 4 | Object { 5 | "alias": "c", 6 | "description": "Displays this help", 7 | "run": [Function], 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /__tests__/commands/__snapshots__/help.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot check help command 1`] = ` 4 | Object { 5 | "alias": "h", 6 | "description": "Displays this help", 7 | "run": [Function], 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /__tests__/sandbox/solidarity_json/.solidarity.json: -------------------------------------------------------------------------------- 1 | { 2 | "requirements": { 3 | "NPM": [{ "rule": "cli", "binary": "npm" }], 4 | "Node": [{ "rule": "cli", "binary": "node", "semver": ">=4.6.0", "error": "Upgrade to latest node >= 4.6 please."}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffCLI.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | 3 | export default async (context: SolidarityRunContext): Promise => { 4 | return { rule: 'cli', binary: '' } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/sandbox/fake_project/node_modules/mock_module/.solidarity: -------------------------------------------------------------------------------- 1 | { 2 | "requirements": { 3 | "NPM": [{ "rule": "cli", "binary": "npm" }], 4 | "Node": [{ "rule": "cli", "binary": "node", "semver": ">=4.6.0", "error": "Upgrade to latest node >= 4.6 please."}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffShell.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | 3 | export default async (context: SolidarityRunContext): Promise => { 4 | return { rule: 'shell', command: '', match: '' } 5 | } 6 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/printWizard.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../../types' 2 | // TODO: Make this fancier 3 | export default (context: SolidarityRunContext): void => { 4 | const { print } = context 5 | print.success('Welcome to Solidarity Wizard') 6 | } 7 | -------------------------------------------------------------------------------- /src/extensions/functions/binaryExists.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../types' 2 | module.exports = (binary: string, context: SolidarityRunContext): boolean => { 3 | const { system } = context 4 | 5 | // Check if binary exists 6 | return Boolean(system.which(binary)) 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/commands/__snapshots__/report.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot check help command 1`] = ` 4 | Object { 5 | "alias": "r", 6 | "description": "Report solidarity info about the current machine", 7 | "run": [Function], 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /__tests__/sandbox/fake_project/node_modules/mock_second_module/.solidarity.json: -------------------------------------------------------------------------------- 1 | { 2 | "requirements": { 3 | "NPM": [{ "rule": "cli", "binary": "npm" }], 4 | "Node": [{ "rule": "cli", "binary": "node", "semver": ">=4.6.0", "error": "Upgrade to latest node >= 4.6 please."}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/__mocks__/listr.ts: -------------------------------------------------------------------------------- 1 | let mockListr = jest.genMockFromModule('listr') 2 | 3 | class ComplexListr extends mockListr { 4 | constructor(taskObj) { 5 | super(taskObj) 6 | this.storedInit = taskObj 7 | } 8 | run = jest.fn(() => Promise.resolve()) 9 | } 10 | 11 | module.exports = ComplexListr 12 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | 3 | environment: 4 | nodejs_version: "8.11.1" 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i -g yarn 9 | - yarn 10 | - yarn tsc 11 | 12 | before_test: 13 | - node --version 14 | - npm --version 15 | 16 | test_script: 17 | - yarn test:ci 18 | -------------------------------------------------------------------------------- /src/extensions/functions/checkENV.ts: -------------------------------------------------------------------------------- 1 | import { ENVRule, SolidarityRunContext } from '../../types' 2 | module.exports = (rule: ENVRule, context: SolidarityRunContext): void => { 3 | const envVar = rule.variable || '' 4 | if (!process.env[envVar]) throw new Error(rule.error || `Environment variable ${envVar} not found`) 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/reviewRule.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reviewRule when rule: custom Errors when plugin doesn not exist 1`] = `[Error: Plugin not found 'FAKE']`; 4 | 5 | exports[`reviewRule when rule: unknown rule gets added 1`] = `[Error: Encountered unknown rule]`; 6 | -------------------------------------------------------------------------------- /__tests__/commands/__snapshots__/solidarity.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot check default command 1`] = ` 4 | Object { 5 | "description": "Check environment against solidarity rules", 6 | "run": [Function], 7 | } 8 | `; 9 | 10 | exports[`check base solidarity run 1`] = `undefined`; 11 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/findPluginInfo.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`findPluginInfo Function can find plugins 1`] = ` 4 | Object { 5 | "plugin": Object { 6 | "check": [Function], 7 | "report": [Function], 8 | "snapshot": [Function], 9 | }, 10 | "success": true, 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/templates/package.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= props.pluginName %>", 3 | "version": "1.0.0", 4 | "repository": "https://github.com//<%= props.pluginName %>", 5 | "description": "<%= props.description %>", 6 | "author": "Super Awesome Coder", 7 | "license": "MIT", 8 | "private": false, 9 | "scripts": { 10 | "test": "echo not yet" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/index.ts: -------------------------------------------------------------------------------- 1 | import onboardAdd from './onboardAdd' 2 | import printWizard from './printWizard' 3 | import addMore from './addMore' 4 | import executeAddRule from './executeAddRule' 5 | import reviewAndSave from './reviewAndSave' 6 | module.exports = { 7 | onboardAdd, 8 | printWizard, 9 | addMore, 10 | executeAddRule, 11 | reviewAndSave, 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css,ts,tsx}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /__tests__/schema/validateExampleSchema.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | 3 | test('solidarity.example.json fits local schema', () => { 4 | const ajv = new Ajv() 5 | const localSchema = require('../../solidaritySchema.json') 6 | expect(localSchema).toBeTruthy() 7 | 8 | const solidarityExample = require('../../.solidarity.example.json') 9 | const valid = ajv.validate(localSchema, solidarityExample) 10 | expect(valid).toBe(true) 11 | }) 12 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/index.ts: -------------------------------------------------------------------------------- 1 | import kickoffEnv from './kickoffEnv' 2 | import kickoffCLI from './kickoffCLI' 3 | import kickoffShell from './kickoffShell' 4 | import kickoffFile from './kickoffFile' 5 | import kickoffDir from './kickoffDir' 6 | import kickoffAllRules from './kickoffAllRules' 7 | 8 | export default { 9 | kickoffEnv, 10 | kickoffCLI, 11 | kickoffShell, 12 | kickoffFile, 13 | kickoffDir, 14 | kickoffAllRules, 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/addOptionalRules.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = (context, requirements) => { 2 | const { solidarity, filesystem } = context 3 | const { binaryExists } = solidarity 4 | 5 | // Conditionally add requirements and rules 6 | 7 | // Example: 8 | // if (binaryExists('yarn', context) && filesystem.exists('./yarn.lock') === 'file') { 9 | // requirements['Yarn'] = [{rule: 'cli', binary: 'yarn', version: '--version', semver: '0.0.0'}] 10 | // } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/extensions/functions/removeNonVersionCharacters.ts: -------------------------------------------------------------------------------- 1 | import { CLIRule } from '../../types' 2 | module.exports = (rule: CLIRule, line: string): string => { 3 | const foundVersions = line.match(/(\d+\.)?(\d+\.)?(\d+)([^\sa-zA-Z0-9|_]+\w+)?/g) 4 | 5 | if (Array.isArray(foundVersions)) { 6 | const matchIndex = rule.matchIndex || 0 7 | return foundVersions[matchIndex] 8 | } else { 9 | throw `No version was detected from the output of the binary '${rule.binary}'` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bin/solidarity: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var devMode = require('fs').existsSync(`${__dirname}/../src`) 3 | // used for integration tests 4 | var useCompiled = process.argv.indexOf('--compiled') >= 0 5 | 6 | // devMode does in-memory TS translation 7 | if (devMode && !useCompiled) { 8 | require('ts-node').register({ project: `${__dirname}/../tsconfig.json` }) 9 | require(`${__dirname}/../src/index.ts`)(process.argv) 10 | } else { 11 | require(`${__dirname}/../dist/index.js`)(process.argv) 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/command_helpers/binaryExists.ts: -------------------------------------------------------------------------------- 1 | import binaryExists from '../../src/extensions/functions/binaryExists' 2 | import * as context from 'gluegun/toolbox' 3 | 4 | const doesNotExistCLI = 'no_way_this_should_be_real' 5 | const alwaysExistCLI = 'node' 6 | 7 | test('error on missing binary', async () => { 8 | expect(binaryExists(doesNotExistCLI, context)).toBeFalsy() 9 | }) 10 | 11 | test('true on existing binary', () => { 12 | expect(binaryExists(alwaysExistCLI, context)).toBe(true) 13 | }) 14 | -------------------------------------------------------------------------------- /src/extensions/functions/setSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidaritySettings } from '../../types' 2 | module.exports = (settings: SolidaritySettings, context: SolidarityRunContext): void => { 3 | const { filesystem } = context 4 | 5 | if (settings.requirements) { 6 | // Write file 7 | filesystem.write('.solidarity', JSON.stringify(settings, null, 2), { atomic: true }) 8 | } else { 9 | throw 'You must have a requirements key to be a valid solidarity file' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/addMore.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../../types' 2 | // TODO: Make this fancier 3 | export default async (context: SolidarityRunContext): Promise => { 4 | // return false // Dev speed shortcut 5 | const { prompt } = context 6 | // print.success('ADD MORE') 7 | const userAnswer = await prompt.ask({ 8 | name: 'continue', 9 | type: 'confirm', 10 | message: 'Would you like to add another rule?', 11 | }) 12 | 13 | return Boolean(userAnswer.continue) 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/updateRequirement.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`updateRequirement exists 1`] = `[Function]`; 4 | 5 | exports[`updateRequirement given a rule rule is custom custom combo - play nicely with others 1`] = ` 6 | Array [ 7 | "Setting checkThing 'semver' to '12.0.0'", 8 | "Setting checkSecondThing 'semver' to '12.0.0', 'nachos' to 'true'", 9 | Array [], 10 | ] 11 | `; 12 | 13 | exports[`updateRequirement updateRequirement empty still spins 1`] = `Array []`; 14 | -------------------------------------------------------------------------------- /docs/pluginsList.md: -------------------------------------------------------------------------------- 1 | # Solidarity Plugins 2 | 3 | * [Ember CLI](https://github.com/willrax/solidarity-ember-cli) - rules for Ember projects 4 | * [envinfo](https://github.com/GantMan/solidarity-envinfo) - for reporting advanced environment items (virtualization, modules etc.) 5 | * [React-Native](https://github.com/infinitered/solidarity-react-native) - dynamic rules for React Native projects 6 | * Elixir - Coming Soon... 7 | 8 | _Be sure to add your plugins here in alphabetical order_ 9 | 10 | [How to write a Solidarity Plugin](plugins.md) 11 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkSTDERR.ts: -------------------------------------------------------------------------------- 1 | const checkSTDERR = require('../../src/extensions/functions/checkSTDERR') 2 | const context = require('mockContext') 3 | 4 | describe('checkSTDERR', () => { 5 | test('returns augmented string', async () => { 6 | const rule = { rule: 'cli', binary: 'yarn', version: '--version' } 7 | const normal = `${rule.binary} ${rule.version}` 8 | const output = await checkSTDERR(rule, context) 9 | 10 | expect(typeof output).toBe('string') 11 | expect(output.length).toBeGreaterThan(normal.length) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/reviewAndSave.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../types' 2 | // TODO: Make this fancier 3 | export default (context: SolidarityRunContext, rules: Array): void => { 4 | const { print, solidarity: { setSolidaritySettings } } = context 5 | 6 | setSolidaritySettings({ requirements: rules }, context) 7 | print.success('Review and Save') 8 | print.success('For full custom rules documentation visit: https://infinitered.github.io/solidarity/#/docs/options?id=solidarity-rules') 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - '10' 5 | before_install: 6 | - 'npm i -g typescript' 7 | before_script: 8 | - tsc 9 | script: 10 | - 'npm run test:ci' 11 | # - yarn danger ci 12 | after_success: 13 | - './node_modules/.bin/jest --coverage --runInBand && cat ./coverage/lcov.info | ./node_modules/.bin/codecov' 14 | notifications: 15 | webhooks: 16 | urls: 17 | - 'https://www.travisbuddy.com/' 18 | on_success: never 19 | on_failure: never 20 | on_start: never 21 | on_cancel: never 22 | on_error: never 23 | -------------------------------------------------------------------------------- /src/extensions/functions/quirksNodeModules.ts: -------------------------------------------------------------------------------- 1 | import { reject, contains, concat, difference } from 'ramda' 2 | const path = require('path') 3 | const delineator = process.platform === 'win32' ? ';' : ':' 4 | 5 | // Node mutates path by adding to the front, move that to the back if it exists 6 | const originalPath = process.env.PATH || '' 7 | const originalArray = originalPath.split(delineator) 8 | const cleanArray = reject(contains('node_modules' + path.sep), originalArray) 9 | process.env.PATH = concat(cleanArray, difference(originalArray, cleanArray)).join(delineator) 10 | -------------------------------------------------------------------------------- /__tests__/docs/testDocs.ts: -------------------------------------------------------------------------------- 1 | // import jetpack from 'fs-jetpack' 2 | // import path from 'path' 3 | 4 | // outgrown 5 | // test('Verify each markdown file has a link in sidebar', () => { 6 | // const markdownFiles = jetpack.find('docs', { matching: ['*.md', '!_*.md'] }) 7 | // const sidebarContents = jetpack.read(`docs${path.sep}_sidebar.md`) 8 | // markdownFiles.map(fileName => { 9 | // expect(sidebarContents.includes(fileName.split(path.sep).join('/'))).toBe(true) 10 | // }) 11 | // }) 12 | 13 | test('stub testDocs', () => { 14 | expect(true).toBe(true) 15 | }) 16 | -------------------------------------------------------------------------------- /src/extensions/functions/checkSTDERR.ts: -------------------------------------------------------------------------------- 1 | import { CLIRule } from '../../types' 2 | 3 | // Creates STDERR catching string 4 | module.exports = (rule: CLIRule): string => { 5 | const currentPlatform = process.platform 6 | let grabErrorOutput: string 7 | if (currentPlatform === 'win32') { 8 | const tempFile = `solidarityWinFix${rule.binary}.tmp` 9 | grabErrorOutput = `1>${tempFile} 2>&1 & type ${tempFile} & del ${tempFile}` 10 | } else { 11 | grabErrorOutput = '2>&1 | cat' 12 | } 13 | 14 | return `${rule.binary} ${rule.version} ${grabErrorOutput}` 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/README.md.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | # <%= props.pluginName %> 4 | ## <%= props.description %> 5 | Much longer description 6 | 7 | ## Install: 8 | `npm i <%= props.pluginName %>` or `yarn add <%= props.pluginName %>` 9 | This plugin will automatically be picked up by Solidarity (which should already be installed). 10 | 11 | ## What is Solidarity? 12 | #### [:newspaper: Read More About Solidarity Here](https://github.com/infinitered/solidarity) 13 | -------------------------------------------------------------------------------- /.solidarity: -------------------------------------------------------------------------------- 1 | { 2 | // I'm JSON5 so I support comments 3 | "$schema": "./solidaritySchema.json", 4 | "requirements": { 5 | "Yarn": [{ "rule": "cli", "binary": "yarn", "semver": "^1.0.0", "version": "--version" }], 6 | "Node": [{ "rule": "cli", "binary": "node", "semver": ">=8.7.0", "error": "Upgrade to latest node >= 7.6 please."}], 7 | "TypeScript Active": [ 8 | { "rule": "dir", "location": "./src", "error": "Did you get this code from npm? Try GitHub!" }, 9 | { "rule": "dir", "location": "./dist", "error": "You haven't compiled. Run the build script!" } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/extensions/functions/checkDir.ts: -------------------------------------------------------------------------------- 1 | import { FSRule, SolidarityRunContext } from '../../types' 2 | import * as path from 'path' 3 | module.exports = (rule: FSRule, context: SolidarityRunContext): void => { 4 | const { filesystem } = context 5 | if (rule.location) { 6 | const dirPath = path.isAbsolute(rule.location) ? rule.location : path.join(process.cwd(), rule.location) 7 | if (filesystem.exists(dirPath) !== 'dir') { 8 | throw new Error(rule.error || `Location '${rule.location}' is not a directory`) 9 | } 10 | } else { 11 | throw new Error(`No location for directory rule`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/extensions/functions/checkFile.ts: -------------------------------------------------------------------------------- 1 | import { FSRule, SolidarityRunContext } from '../../types' 2 | import * as path from 'path' 3 | 4 | const resolve = require('resolve-dir') 5 | 6 | module.exports = (rule: FSRule, context: SolidarityRunContext): void => { 7 | const { filesystem } = context 8 | if (rule.location) { 9 | const filePath = path.isAbsolute(rule.location) ? rule.location : resolve(rule.location) 10 | if (filesystem.exists(filePath) !== 'file') { 11 | throw new Error(rule.error || `Location '${rule.location}' is not a file`) 12 | } 13 | } else { 14 | throw new Error(`No location for file rule`) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/commands/report.ts: -------------------------------------------------------------------------------- 1 | jest.mock('envinfo') 2 | 3 | import reportCommand from '../../src/commands/report' 4 | const mockContext = require('mockContext') 5 | 6 | test('Snapshot check help command', () => { 7 | expect(reportCommand).toMatchSnapshot() 8 | }) 9 | 10 | it('enforces required properties', () => { 11 | expect(reportCommand.description).toBeTruthy() 12 | expect(reportCommand.run).toBeTruthy() 13 | expect(typeof reportCommand.run).toBe('function') 14 | }) 15 | 16 | test('check solidarity report', async () => { 17 | await reportCommand.run(mockContext) 18 | expect(mockContext.print.spin.mock.calls).toEqual([['Building Report']]) 19 | }) 20 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/onboardAdd.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../../types' 2 | // TODO: Make this fancier 3 | export default async (context: SolidarityRunContext): Promise => { 4 | const { print, prompt } = context 5 | print.info(` 6 | * cli = Enforce a CLI existence and version 7 | * env = Enforce existence of an environment variable 8 | * file = Enforce existence of a file 9 | * dir = Enforce existence of a directory 10 | * shell = Enforce output of a command to match 11 | `) 12 | const userAnswer = await prompt.ask({ 13 | name: 'addRule', 14 | type: 'select', 15 | message: 'What kind of rule would you like to add?', 16 | choices: ['cli', 'env', 'file', 'dir', 'shell'], 17 | }) 18 | 19 | return userAnswer.addRule 20 | } 21 | -------------------------------------------------------------------------------- /__tests__/command_helpers/solidarityReport.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityReportResults } from '../../src/types' 2 | import { createReport } from '../../src/extensions/functions/solidarityReport' 3 | const context = require('mockContext') 4 | 5 | describe('solidarityReport structure', () => { 6 | test('the basic function generates the Result object', async () => { 7 | let report = await createReport(context) 8 | // Check everything but system stuff against snapshots 9 | expect(report.addCLI).toMatchSnapshot() 10 | expect(report.cliRules).toMatchSnapshot() 11 | expect(report.customRules).toMatchSnapshot() 12 | expect(report.envRules).toMatchSnapshot() 13 | expect(report.filesystemRules).toMatchSnapshot() 14 | expect(report.shellRules).toMatchSnapshot() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/extensions/functions/getLineWithVersion.ts: -------------------------------------------------------------------------------- 1 | import { CLIRule } from '../../types' 2 | module.exports = (rule: CLIRule, versionOutput: string): string => { 3 | let result 4 | if (typeof rule.line === 'number') { 5 | result = versionOutput.split('\n')[rule.line - 1] 6 | } else if (typeof rule.line === 'string') { 7 | const findString = `.*${rule.line}.*` 8 | const findRegex = RegExp(findString, 'g') 9 | const foundLines = versionOutput.match(findRegex) 10 | if (Array.isArray(foundLines)) { 11 | // Always first instance 12 | result = foundLines[0] 13 | } else { 14 | throw `rule.line string '${rule.line}' was not found` 15 | } 16 | } else { 17 | // pass it through if rules don't provide a line 18 | result = versionOutput 19 | } 20 | return result 21 | } 22 | -------------------------------------------------------------------------------- /src/extensions/functions/setOutputMode.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityOutputMode, SolidaritySettings } from '../../types' 2 | 3 | module.exports = (parameters, settings: SolidaritySettings): SolidarityOutputMode => { 4 | const { options } = parameters 5 | // CLI flags override config 6 | if (options.verbose || options.a) { 7 | return SolidarityOutputMode.VERBOSE 8 | } else if (options.silent || options.s) { 9 | return SolidarityOutputMode.SILENT 10 | } else if (options.moderate || options.m) { 11 | return SolidarityOutputMode.MODERATE 12 | } 13 | 14 | // Set output mode, set to default on invalid value 15 | let outputModeString = settings.config ? String(settings.config.output).toUpperCase() : 'MODERATE' 16 | return SolidarityOutputMode[outputModeString] || SolidarityOutputMode.MODERATE 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import { SolidarityRunContext } from '../types' 3 | 4 | const createables = ['plugin'] 5 | 6 | module.exports = { 7 | alias: 'c', 8 | description: 'Displays this help', 9 | run: async (context: SolidarityRunContext) => { 10 | const { print, solidarity, parameters } = context 11 | switch (parameters.first && parameters.first.toLowerCase()) { 12 | case 'plugin': 13 | // Handle errors like grown-ups 14 | try { 15 | await solidarity.createPlugin(context) 16 | } catch (e) { 17 | print.error(e) 18 | } 19 | break 20 | default: 21 | print.error('Missing what to create') 22 | print.error('$ solidarity create ') 23 | print.info(`Things you can create: ${createables}`) 24 | } 25 | }, 26 | } as GluegunCommand 27 | -------------------------------------------------------------------------------- /src/extensions/functions/checkShell.ts: -------------------------------------------------------------------------------- 1 | import { ShellRule, SolidarityRunContext } from '../../types' 2 | 3 | module.exports = async (rule: ShellRule, context: SolidarityRunContext): Promise => { 4 | const { system, strings } = context 5 | try { 6 | // execute the command 7 | const exec = await system.spawn(rule.command, { shell: true }) 8 | const { stdout = '' } = exec 9 | 10 | // clean it up 11 | const output = strings.trimEnd(stdout.toString()) 12 | 13 | // look for matches 14 | let isMatch: boolean = false 15 | const match = rule.match && new RegExp(rule.match) 16 | if (match) { 17 | isMatch = match.test(output) 18 | } 19 | 20 | const standardError = `Shell rule '${rule.command}' output did not contain match: ${match}` 21 | if (!isMatch) throw new Error(rule.error || standardError) 22 | } catch (e) { 23 | throw new Error(rule.error || e.message) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/command_helpers/setSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import setSolidaritySettings from '../../src/extensions/functions/setSolidaritySettings' 2 | 3 | const goodSettings = { 4 | requirements: [], 5 | } 6 | const badSettings = {} 7 | const mockContext = { 8 | print: { 9 | error: jest.fn(), 10 | }, 11 | filesystem: { 12 | write: jest.fn(), 13 | }, 14 | } 15 | 16 | test('setSolidaritySettings exists', () => expect(setSolidaritySettings).toMatchSnapshot()) 17 | 18 | test('check good setSolidarity path', () => { 19 | const resultVoid = setSolidaritySettings(goodSettings, mockContext) 20 | expect(resultVoid).toMatchSnapshot() 21 | expect(mockContext.filesystem.write.mock.calls.length).toBe(1) 22 | }) 23 | 24 | test('check failed setSolidarity path', () => { 25 | expect(() => { 26 | setSolidaritySettings(badSettings, mockContext) 27 | }).toThrowError('You must have a requirements key to be a valid solidarity file') 28 | }) 29 | -------------------------------------------------------------------------------- /src/extensions/functions/findPluginInfo.ts: -------------------------------------------------------------------------------- 1 | import { CustomRule, SolidarityRunContext, PluginFind } from '../../types' 2 | 3 | module.exports = (rule: CustomRule, context: SolidarityRunContext): PluginFind => { 4 | const { head, filter } = require('ramda') 5 | 6 | // find correct rule function 7 | const correctPlugin = head(filter(plugin => plugin.name === rule.plugin, context._pluginsList)) 8 | if (correctPlugin === undefined) { 9 | return { 10 | success: false, 11 | message: `Plugin not found '${rule.plugin}'`, 12 | } 13 | } else { 14 | const customRule = correctPlugin.rules && correctPlugin.rules[rule.name] 15 | if (customRule) { 16 | return { 17 | success: true, 18 | plugin: customRule, 19 | } 20 | } else { 21 | return { 22 | success: false, 23 | message: `NOT FOUND: Custom rule from '${rule.plugin}' plugin with check function '${rule.name}'`, 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/integration/solidarity-check/check-invalid.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import tempy from 'tempy' 3 | 4 | const path = require('path') 5 | const SOLIDARITY = `node ${process.cwd()}${path.sep}bin${path.sep}solidarity` 6 | const origCwd = process.cwd() 7 | let originalTimeout 8 | 9 | beforeAll(() => { 10 | // These can be slow on CI 11 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000 13 | // Tempy! 14 | const tempDir = tempy.directory() 15 | process.chdir(tempDir) 16 | }) 17 | 18 | afterAll(function() { 19 | // Fix timeout change 20 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 21 | }) 22 | 23 | test('default looks for .solidarity file', async done => { 24 | try { 25 | execa.shellSync(`${SOLIDARITY} --compiled`) 26 | done.fail() 27 | } catch (err) { 28 | expect(err.code).not.toBe(0) 29 | done() 30 | } 31 | }) 32 | 33 | afterAll(() => { 34 | process.chdir(origCwd) 35 | }) 36 | -------------------------------------------------------------------------------- /src/templates/simple-plugin.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = (context) => { 2 | // Register this plugin 3 | context.addPlugin({ 4 | name: '<%= props.pluginName %>', 5 | description: '<%= props.description %>'<% if(!props.noRuleTemplate){ %>, 6 | snapshot: '<%= props.pluginName %>-template.json'<% } %><% if(props.customRules){ %>, 7 | rules: { 8 | ruleName: { 9 | check: async (rule, context) => { 10 | return { 11 | pass: true, 12 | message: 'Check always passes' 13 | } 14 | }, 15 | snapshot: async (rule, context) => [ 16 | { 17 | prop: 'addedProp', 18 | value: '12.0.0' 19 | }, 20 | ], 21 | report: async (rule, context, report) => { 22 | report.addCLI({ 23 | binary: 'imaginaryCLI', 24 | version: '10', 25 | desired: '12' 26 | }) 27 | } 28 | } 29 | }<% } %> 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /docs/advancedPlugin.md: -------------------------------------------------------------------------------- 1 | ```json 2 | "CustomRequirement": [ 3 | { 4 | rule: "custom", 5 | plugin: "My Plugin Name", 6 | name: "checkThing", 7 | otherStuff: "I like nachos" 8 | } 9 | ] 10 | ``` 11 | 12 | plugin 13 | ```js 14 | const addOptionalRules = require('./helpers/addOptionalRules') 15 | 16 | module.exports = (context) => { 17 | // Register this plugin 18 | context.addPlugin({ 19 | name: 'My Plugin Name', 20 | description: 'I do amazing things', 21 | customChecks: { 22 | checkThing: async (rule, context) => { 23 | const successObject = { 24 | pass: true, 25 | message: 'Custom check succeeded!' 26 | } 27 | const failureObject = { 28 | pass: false, 29 | message: 'Custom check failed!' 30 | } 31 | 32 | // Randomly succeed or fail 33 | return !!Math.floor(Math.random() * 2) ? successObject : failureObject 34 | } 35 | } 36 | }) 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Brings in Promise and all es2015 extensions to TS 4 | // TODO: Bringing in dom to quell URL issue, please remove if possible 5 | "lib": [ 6 | "es2015", 7 | "dom", 8 | "es7" 9 | ], 10 | "outDir": "dist", 11 | // Emits types for others to consume 12 | "declaration": true, 13 | "declarationDir": "dist/types", 14 | // Using commonjs here to play nice with node (imports not welcome here) 15 | "module": "commonjs", 16 | "target": "es6", 17 | "moduleResolution": "node", 18 | // Help TS understand node globals 19 | "types": [ 20 | "node", 21 | "jasmine" 22 | ], 23 | // Let's allow `import` 24 | "allowSyntheticDefaultImports": true, 25 | // Don't let sneaky null/undefined through 26 | "strictNullChecks": true, 27 | // Yes plz 28 | "sourceMap": true, 29 | // Don't update JS dist with failing TS src 30 | "noEmitOnError": true 31 | }, 32 | "include": [ 33 | "src" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/extensions/functions/skipRule.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRule } from '../../types' 2 | 3 | // Return true if we should skip 4 | module.exports = (rule: SolidarityRule): boolean => { 5 | let platform = rule.platform 6 | 7 | // check Skip CI shortcut first 8 | if (process.env.CI && rule.ci === false) { 9 | return true 10 | } 11 | 12 | if (typeof platform === 'string') { 13 | platform = platform.toLowerCase() 14 | platform = platform === 'windows' ? 'win32' : platform 15 | platform = platform === 'macos' ? 'darwin' : platform 16 | return platform !== process.platform 17 | } else if (Array.isArray(platform)) { 18 | platform.map(p => p.toLowerCase()) 19 | const winIndex = platform.indexOf('windows') 20 | const macIndex = platform.indexOf('macos') 21 | if (winIndex >= 0) { 22 | platform[winIndex] = 'win32' 23 | } 24 | if (macIndex >= 0) { 25 | platform[macIndex] = 'darwin' 26 | } 27 | return !platform.includes(process.platform) 28 | } else { 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [Why Solidarity?](/README.md#why-does-solidarity-existquestion) 2 | - Readme Basics 3 | - [Install](/README.md#install) 4 | - [How do I use it?](/README.md#how-do-I-use-it) 5 | - [How do I update it?](/README.md#how-do-i-update-it-to-my-machine-specs) 6 | - [How do I create my first snapshot?](/README.md#how-do-I-create-my-first-snapshot) 7 | - [How do I update my snapshots?](#how-do-I-update-my-snapshots) 8 | - [Supported Systems](/README.md#supported-systems) 9 | - [Additional Support](/README.md#support) 10 | - [Additional Info](/README.md#additional-info) 11 | - Comprehensive Docs 12 | - [Solidarity Rules Options](/docs/options.md) 13 | - [Solidarity CLI Options](/docs/cliOptions.md) 14 | - [Writing Plugins](/docs/plugins.md) 15 | - [**All Contributors**](/docs/existingContributors.md) 16 | - Blog Posts 17 | - [Introducing Solidarity](https://shift.infinite.red/solidarity-the-cli-for-developer-sanity-672fa81b98e9) 18 | - [Environment Reports](https://shift.infinite.red/effortless-environment-reports-d129d53eb405) 19 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffDir.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | const checkDir = require('../../checkDir') 3 | 4 | export default async (context: SolidarityRunContext): Promise => { 5 | const { print, prompt } = context 6 | 7 | let repeat = true 8 | let dirPath = { value: '' } 9 | while (repeat) { 10 | dirPath = (await prompt.ask({ 11 | name: 'value', 12 | type: 'input', 13 | message: "Enter the path to the directory you'd like to enforce", 14 | })) as any 15 | 16 | try { 17 | // Check dir for existence 18 | checkDir({ location: dirPath.value }, context) 19 | repeat = false 20 | } catch (e) { 21 | print.error('Directory not found on this machine.') 22 | const tryAgain = await prompt.confirm('Would you like to try a different path?') 23 | 24 | repeat = tryAgain 25 | } 26 | } 27 | 28 | print.success(`Enforcing DIR for ${dirPath.value}`) 29 | return { rule: 'dir', location: dirPath.value } 30 | } 31 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffFile.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | const checkFile = require('../../checkFile') 3 | 4 | export default async (context: SolidarityRunContext): Promise => { 5 | const { print, prompt } = context 6 | 7 | let repeat = true 8 | let filePath = { value: '' } 9 | while (repeat) { 10 | filePath = (await prompt.ask({ 11 | name: 'value', 12 | type: 'input', 13 | message: "Enter the path to the file you'd like to enforce", 14 | })) as any 15 | 16 | try { 17 | // Check file for existence 18 | checkFile({ location: filePath.value }, context) 19 | repeat = false 20 | } catch (e) { 21 | print.error('File not found on this machine.') 22 | const tryAgain = await prompt.confirm('Would you like to try a different path?') 23 | 24 | repeat = tryAgain 25 | } 26 | } 27 | 28 | print.success(`Enforcing DIR for ${filePath.value}`) 29 | return { rule: 'file', location: filePath.value } 30 | } 31 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkENV.ts: -------------------------------------------------------------------------------- 1 | import checkENV from '../../src/extensions/functions/checkENV' 2 | 3 | test('checkENV detects set ENV', async () => { 4 | // get the first environment variable 5 | const environmentKeys = Object.keys(process.env) 6 | if (environmentKeys.length > 0) { 7 | let someRealEnvVar 8 | let i = 0 9 | // some environment vars aren't truthy 10 | while (someRealEnvVar === undefined) { 11 | if (process.env[environmentKeys[i]]) { 12 | someRealEnvVar = environmentKeys[i] 13 | } 14 | i++ 15 | } 16 | // Use checkENV to make sure it exists 17 | expect(await checkENV({ variable: someRealEnvVar })).toBe(undefined) 18 | } 19 | }) 20 | 21 | test('checkENV can fail', async () => { 22 | // Use checkENV to make sure it exists 23 | expect(() => { 24 | checkENV({ variable: 'THIS_SHOULD_NOT_EXIST_VERIFIER' }) 25 | }).toThrow() 26 | }) 27 | 28 | test('checkENV fails if no variable is set', async () => { 29 | expect(() => { 30 | expect(checkENV({})).toBeFalsy() 31 | }).toThrow() 32 | }) 33 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/executeAddRule.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../../types' 2 | import Kickoffs from './kickoffs' 3 | 4 | // TODO: Make this fancier 5 | export default async (context: SolidarityRunContext, answer: string): Promise => { 6 | const { print } = context 7 | print.success('ADD RULE ' + answer) 8 | let rule 9 | switch (answer) { 10 | case 'cli': 11 | rule = await Kickoffs.kickoffCLI(context) 12 | break 13 | case 'env': 14 | rule = await Kickoffs.kickoffEnv(context) 15 | break 16 | case 'file': 17 | rule = await Kickoffs.kickoffFile(context) 18 | break 19 | case 'dir': 20 | rule = await Kickoffs.kickoffDir(context) 21 | break 22 | case 'shell': 23 | rule = await Kickoffs.kickoffShell(context) 24 | break 25 | default: 26 | print.info('This should never happen') 27 | throw 'unknown rule type' 28 | } 29 | 30 | // Now ask questions for ALL rules 31 | rule = await Kickoffs.kickoffAllRules(context, rule) 32 | // Now add rule to requirement 33 | return rule 34 | } 35 | -------------------------------------------------------------------------------- /src/extensions/functions/printResults.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityReportResults } from '../../types' 2 | module.exports = (results: SolidarityReportResults, context: SolidarityRunContext): void => { 3 | const { print, printSeparator } = context 4 | const { info } = print 5 | const printSpacedTable = (table, header) => { 6 | info(`### ${header}\n`) 7 | print.table(table, { format: 'markdown' }) 8 | info('\n') 9 | } 10 | const printIfData = (reportItem: Array>, header: string) => 11 | reportItem.length > 1 && printSpacedTable(reportItem, header) 12 | 13 | info('# ⚠️ Environment Report:') 14 | printSeparator() 15 | printIfData(results.basicInfo, 'System') 16 | printIfData(results.cliRules, 'Commands') 17 | printIfData(results.envRules, 'Environment Variables') 18 | printIfData(results.filesystemRules, 'Filesystem') 19 | printIfData(results.shellRules, 'Shell Checks') 20 | results.customRules && 21 | results.customRules.map(customTable => { 22 | printIfData(customTable.table, customTable.title) 23 | }) 24 | printSeparator() 25 | } 26 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Solidarity is an environment checker for project dependencies across multiple machines. 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Infinite Red, Inc. 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 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/solidarityReport.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`solidarityReport structure the basic function generates the Result object 1`] = `[Function]`; 4 | 5 | exports[`solidarityReport structure the basic function generates the Result object 2`] = ` 6 | Array [ 7 | Array [ 8 | "Binary", 9 | "Location", 10 | "Version", 11 | "Desired", 12 | ], 13 | ] 14 | `; 15 | 16 | exports[`solidarityReport structure the basic function generates the Result object 3`] = `Array []`; 17 | 18 | exports[`solidarityReport structure the basic function generates the Result object 4`] = ` 19 | Array [ 20 | Array [ 21 | "Environment Var", 22 | "Value", 23 | ], 24 | ] 25 | `; 26 | 27 | exports[`solidarityReport structure the basic function generates the Result object 5`] = ` 28 | Array [ 29 | Array [ 30 | "Location", 31 | "Type", 32 | "Exists", 33 | ], 34 | ] 35 | `; 36 | 37 | exports[`solidarityReport structure the basic function generates the Result object 6`] = ` 38 | Array [ 39 | Array [ 40 | "Command", 41 | "Pattern", 42 | "Matches", 43 | ], 44 | ] 45 | `; 46 | -------------------------------------------------------------------------------- /src/extensions/functions/checkCLIForUpdates.ts: -------------------------------------------------------------------------------- 1 | import { CLIRule, SolidarityRunContext } from '../../types' 2 | 3 | module.exports = async (rule: CLIRule, context: SolidarityRunContext): Promise => { 4 | const { system, semver, solidarity, print } = context 5 | 6 | // If binary is set but not found 7 | if (rule.binary) { 8 | if (Boolean(system.which(rule.binary)) === false) { 9 | throw new Error(`Binary '${rule.binary}' not found`) 10 | } 11 | } 12 | 13 | const binaryVersion = await solidarity.getVersion(rule, context) 14 | 15 | // pad zeros for any non-semver version systems (rules still work) 16 | let binarySemantic = binaryVersion 17 | while (binarySemantic.split('.').length < 3) { 18 | binarySemantic += '.0' 19 | } 20 | 21 | // if it doesn't satisfy, upgrade, and retain semver symbol 22 | if (rule.semver && !semver.satisfies(binarySemantic, rule.semver)) { 23 | rule.semver = `${/\^|\~/.test(rule.semver) ? rule.semver.charAt(0) : ''}${binaryVersion}` 24 | const lineMessage = rule.line ? ` line ${rule.line}` : '' 25 | return print.colors.green(`Setting ${rule.binary}${lineMessage} to '${rule.semver}'`) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/commands/help.ts: -------------------------------------------------------------------------------- 1 | import helpCommand from '../../src/commands/help' 2 | 3 | const context = { 4 | print: { 5 | info: jest.fn(), 6 | printCommands: jest.fn(), 7 | success: jest.fn(), 8 | colors: { 9 | magenta: jest.fn(), 10 | }, 11 | }, 12 | } 13 | 14 | test('Snapshot check help command', () => { 15 | expect(helpCommand).toMatchSnapshot() 16 | }) 17 | 18 | test('Enforce required properties', () => { 19 | expect(helpCommand.description).toBeTruthy() 20 | expect(helpCommand.run).toBeTruthy() 21 | expect(typeof helpCommand.run).toBe('function') 22 | }) 23 | 24 | test('Calls print items several times', () => { 25 | expect(context.print.info.mock.calls.length).toBe(0) 26 | expect(context.print.printCommands.mock.calls.length).toBe(0) 27 | expect(context.print.success.mock.calls.length).toBe(0) 28 | expect(context.print.colors.magenta.mock.calls.length).toBe(0) 29 | helpCommand.run(context) 30 | expect(context.print.info.mock.calls.length).toBe(9) 31 | expect(context.print.printCommands.mock.calls.length).toBe(1) 32 | expect(context.print.success.mock.calls.length).toBe(2) 33 | expect(context.print.colors.magenta.mock.calls.length).toBe(2) 34 | }) 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'gluegun' 2 | 3 | module.exports = async (): Promise => { 4 | const os = require('os') 5 | // setup the runtime 6 | const cli = build() 7 | .brand('solidarity') 8 | .src(__dirname) 9 | // local installs 10 | .plugins('./node_modules', { matching: 'solidarity-*', hidden: true }) 11 | // global installs 12 | .plugins('/usr/local/lib/node_modules', { matching: 'solidarity-*', hidden: true }) // Darwin 13 | .plugins(`${os.homedir()}/.config/yarn/global/node_modules`, { matching: 'solidarity-*', hidden: true }) // Yarn/Darwin 14 | .plugins(`${process.env.appdata}/npm/node_modules`, { matching: 'solidarity-*', hidden: true }) // Windows 15 | // for testing - force load a local plugin 16 | // .plugin('../solidarity-react-native') 17 | 18 | // when a module parameter is passed we take the plugins from there, too 19 | const parsedArgs = require('yargs-parser')(process.argv.slice(2)) 20 | const moduleName = parsedArgs.m || parsedArgs.module 21 | if (moduleName) cli.plugins(`./node_modules/${moduleName}/node_modules`, { matching: 'solidarity-*', hidden: true }) 22 | 23 | await cli.create().run() 24 | } 25 | 26 | export * from './types' 27 | -------------------------------------------------------------------------------- /__tests__/index.ts: -------------------------------------------------------------------------------- 1 | jest.mock('gluegun', () => { 2 | const buildContext = {} 3 | buildContext.build = jest.fn(() => buildContext) 4 | buildContext.brand = jest.fn(() => buildContext) 5 | buildContext.src = jest.fn(() => buildContext) 6 | buildContext.plugins = jest.fn(() => buildContext) 7 | buildContext.create = jest.fn(() => buildContext) 8 | buildContext.run = jest.fn(() => buildContext) 9 | return buildContext 10 | }) 11 | import baseRuntimeConfiguration from '../src/index' 12 | import { build } from 'gluegun' 13 | 14 | test('Verify Runtime AND Configuration', () => { 15 | expect(baseRuntimeConfiguration).toMatchSnapshot() 16 | }) 17 | 18 | test('ensure build', async () => { 19 | const result = await baseRuntimeConfiguration() 20 | expect(result).toMatchSnapshot() 21 | expect(build.mock.calls.length).toBe(1) 22 | expect(build().brand.mock.calls.length).toBe(1) 23 | expect(build().brand.mock.calls[0][0]).toBe('solidarity') 24 | expect(build().src.mock.calls.length).toBe(1) 25 | // Check local and globals for Windows/Darwin + Yarn === 4 checks 26 | expect(build().plugins.mock.calls.length).toBe(4) 27 | expect(build().create.mock.calls.length).toBe(1) 28 | expect(build().run.mock.calls.length).toBe(1) 29 | }) 30 | -------------------------------------------------------------------------------- /__tests__/integration/solidarity-snapshot/snapshot-nada.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import tempy from 'tempy' 3 | 4 | const path = require('path') 5 | const SOLIDARITY = `node ${process.cwd()}${path.sep}bin${path.sep}solidarity` 6 | const origCwd = process.cwd() 7 | let originalTimeout 8 | 9 | beforeAll(() => { 10 | // These can be slow on CI 11 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000 13 | const tempDir = tempy.directory() 14 | process.chdir(tempDir) 15 | }) 16 | 17 | afterAll(function() { 18 | // Fix timeout change 19 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 20 | }) 21 | 22 | test('solidarity report works', async done => { 23 | try { 24 | execa 25 | .shell(`echo n | ${SOLIDARITY} snapshot --compiled`) 26 | .then(result => { 27 | // do not snapshot stdout bc windows bitches 28 | expect(result.stdout.includes('Nothing to do')).toBeTruthy() 29 | expect(result.code).toBe(0) 30 | done() 31 | }) 32 | .catch(err => { 33 | console.error(err) 34 | done.fail() 35 | }) 36 | } catch (err) { 37 | done.fail() 38 | } 39 | }) 40 | 41 | afterEach(() => { 42 | process.chdir(origCwd) 43 | }) 44 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkDir.ts: -------------------------------------------------------------------------------- 1 | import checkDir from '../../src/extensions/functions/checkDir' 2 | import * as context from 'gluegun/toolbox' 3 | 4 | test('checkDir detects an existing dir', () => { 5 | // Check for a known directory 6 | const location = './src' 7 | // Use checkDir to make sure it exists 8 | expect(checkDir({ location }, context)).toBe(undefined) 9 | }) 10 | 11 | test('checkDir can fail', () => { 12 | // Use checkDir to make sure a non-existant directory returns false 13 | expect(() => { 14 | checkDir({ location: 'DOES_NOT_EXIST' }, context) 15 | }).toThrow() 16 | }) 17 | 18 | test('checkDir returns throws for a file that exists', () => { 19 | // Use checkDir to make sure a known file returns false since it's not a directory 20 | expect(() => { 21 | checkDir({ location: './package.json' }, context) 22 | }).toThrow() 23 | }) 24 | 25 | test('checkDir throws if no location is set', () => { 26 | expect(() => { 27 | checkDir({}, context) 28 | }).toThrow() 29 | }) 30 | 31 | test('checkDir throws custom error if set', () => { 32 | const customError = 'customError' 33 | expect(() => { 34 | checkDir({ location: 'DOES_NOT_EXIST', error: customError }, context) 35 | }).toThrowError(customError) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/extensions/extensionCheck.ts: -------------------------------------------------------------------------------- 1 | import solidarityExtension from '../../src/extensions/solidarity-extension' 2 | const context = require('mockContext') 3 | 4 | const newContext = context 5 | solidarityExtension(newContext) 6 | 7 | test('Assure solidarity object', () => { 8 | expect(typeof newContext.solidarity).toBe('object') 9 | }) 10 | 11 | test('Assure addPlugin function', () => { 12 | expect(typeof newContext.addPlugin).toBe('function') 13 | }) 14 | 15 | test('Verify addPlugin function', () => { 16 | // plugin list is empty 17 | expect(newContext._pluginsList.length).toBe(0) 18 | newContext.addPlugin({}) 19 | expect(newContext._pluginsList.length).toBe(1) 20 | // No existing path means empty template directory 21 | expect(newContext._pluginsList[0].templateDirectory).toBe(null) 22 | // TODO: Create temporary templates folder and make sure it works 23 | // newContext.addPlugin({}) 24 | // expect(newContext._pluginsList[0].templateDirectory).toBe(path.join(__dirname, '../templates/')) 25 | }) 26 | 27 | test('Assure printSeparator function', () => { 28 | expect(typeof newContext.printSeparator).toBe('function') 29 | }) 30 | 31 | test('Snapshot of solidarity extension', () => { 32 | expect(newContext.solidarity).toMatchSnapshot() 33 | }) 34 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import { SolidarityRunContext } from '../types' 3 | 4 | module.exports = { 5 | alias: 'h', 6 | description: 'Displays this help', 7 | run: (context: SolidarityRunContext) => { 8 | const { print } = context 9 | const { colors } = print 10 | print.success('\nSolidarity') 11 | print.info(' Commands') 12 | print.printCommands(context) 13 | print.info('\n Flags\n') 14 | print.info(' --verbose\t\t (-a) Prints all detected info during solidarity check') 15 | print.info(' --moderate\t\t (-m) Prints failures in check or single success message') 16 | print.info(' --silent\t\t (-s) No output, just a return code of success/failure') 17 | print.info(' --solidarityFile\t (-f) Use given path to solidarity file for settings') 18 | print.info(' --module\t\t (-d) Search for a solidarity file in the given npm package') 19 | print.info(' --stack\t\t (-t) Use a known technology stack, and not the local file') 20 | 21 | print.success(colors.magenta('\nSolidarity is open source - https://github.com/infinitered/solidarity')) 22 | print.info(colors.magenta('If you need additional help, join our Slack at http://community.infinite.red')) 23 | }, 24 | } as GluegunCommand 25 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Writing your own plugins 2 | If you're using a technology that doesn't have a plugin, or if you'd just like build your own custom rules to use in solidarity, we've made creating plugins extremely simple. 3 | 4 | The following docs will show you all the features of how to create a plugin. If you'd like, you can generate a plugin-base by typing `solidarity create plugin`. This starts a walkthrough that will ask you questions to help you get started writing your plugin. 5 | 6 | ## Plugin Docs 7 | * [Write the Simplest Plugin](/docs/simplePlugin.md) 8 | * [Plugins that write Solidarity Files from Code](/docs/solidarityFromCode.md) 9 | * **Creating Custom Rules** 10 | * [Custom Rule Check](/docs/customRuleCheck.md) 11 | * [Custom Rule Snapshot](/docs/customRuleSnapshot.md) 12 | * [Custom Rule Report](/docs/customRuleReport.md) 13 | * [Known Plugin List](/docs/pluginsList.md) 14 | 15 | ## Got questions? 16 | We're available on [Infinite Red Community Slack](http://community.infinite.red), so you can hop in and chat with us. Lots of our open source is discussed throughout this slack. If you end up needing advanced attention, we are a consulting company, so we offer Premium support, too. Email us at hello@infinite.red to get that ball rolling with your project. 17 | -------------------------------------------------------------------------------- /src/extensions/functions/solidarityReport.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityReportResults, CLIReportConfig } from '../../types' 2 | 3 | export const createReport = async (context: SolidarityRunContext): Promise => { 4 | const { print, system, envinfo } = context 5 | const { colors } = print 6 | const OS = await envinfo.getOSInfo() 7 | const CPU = await envinfo.getCPUInfo() 8 | 9 | return { 10 | basicInfo: [ 11 | ['System Basics', 'Value'], 12 | ['OS', OS], 13 | ['CPU', CPU], 14 | ], 15 | cliRules: [['Binary', 'Location', 'Version', 'Desired']], 16 | envRules: [['Environment Var', 'Value']], 17 | filesystemRules: [['Location', 'Type', 'Exists']], 18 | shellRules: [['Command', 'Pattern', 'Matches']], 19 | customRules: [], 20 | // helper for adding CLI rules 21 | addCLI: function(cliReportConfig: CLIReportConfig) { 22 | const desired = cliReportConfig.desired ? cliReportConfig.desired : colors.green('*ANY*') 23 | let location = system.which(cliReportConfig.binary) 24 | if (Boolean(location) === false) { 25 | location = colors.red('*MISSING*') 26 | } 27 | this.cliRules.push([cliReportConfig.binary, location, cliReportConfig.version, desired]) 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/extensions/functions/updateVersions.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext } from '../../types' 2 | module.exports = async (context: SolidarityRunContext): Promise => { 3 | const { map, toPairs, isEmpty, flatten } = require('ramda') 4 | const { solidarity, print, strings } = context 5 | const { pluralize } = strings 6 | const { getSolidaritySettings, setSolidaritySettings, updateRequirement } = solidarity 7 | 8 | // load current solidarity file 9 | const solidaritySettings = await getSolidaritySettings(context) 10 | 11 | // Map over requirements with option to mutate settings 12 | const checks = await map( 13 | async requirement => updateRequirement(requirement, solidaritySettings.requirements, context), 14 | toPairs(solidaritySettings.requirements) 15 | ) 16 | 17 | // run the array of promises you just created 18 | try { 19 | const results = await Promise.all(checks) 20 | const updates = flatten(results) 21 | if (isEmpty(updates)) { 22 | print.success('\n No Changes') 23 | } else { 24 | setSolidaritySettings(solidaritySettings, context) 25 | const ruleMessage = pluralize('Rule', updates.length, true) 26 | print.success(`\n ${ruleMessage} updated`) 27 | } 28 | } catch (err) { 29 | print.error(err) 30 | process.exit(2) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dangerfile.ts: -------------------------------------------------------------------------------- 1 | import { danger, warn, schedule } from 'danger' 2 | // can't use import in JS 3 | const spellcheck = require('danger-plugin-spellcheck').default 4 | const JSON5 = require('json5') 5 | const fs = require('fs') 6 | 7 | const whitelistWords = JSON5.parse(fs.readFileSync('./.vscode/cSpell.json')).words 8 | // let's spellcheck 9 | schedule( 10 | spellcheck({ 11 | ignore: whitelistWords.map(word => word.toLowerCase()), 12 | whitelistFiles: ['docs/existingContributors.md'], 13 | }) 14 | ) 15 | 16 | // Enforce yarn.lock updates 17 | const packageChanged = danger.git.modified_files.includes('package.json') 18 | const yarnLockfileChanged = danger.git.modified_files.includes('yarn.lock') 19 | const npmLockfileChanged = danger.git.modified_files.includes('package-lock.json') 20 | if (packageChanged && !yarnLockfileChanged) { 21 | const message = 'Changes were made to package.json, but not to yarn.lock' 22 | const idea = 'Perhaps you need to run `yarn install`?' 23 | warn(`${message} - ${idea}`) 24 | } 25 | // // Enforce package-lock.json 26 | // if (packageChanged && !npmLockfileChanged) { 27 | // const message = 'Changes were made to package.json, but not to package-lock.json' 28 | // const idea = 'Perhaps you need to run `npm install`?' 29 | // warn(`${message} - ${idea}`) 30 | // } 31 | -------------------------------------------------------------------------------- /__tests__/commands/create.ts: -------------------------------------------------------------------------------- 1 | import createCommand from '../../src/commands/create' 2 | const mockContext = require('mockContext') 3 | 4 | test('Snapshot create command', () => { 5 | expect(createCommand).toMatchSnapshot() 6 | }) 7 | 8 | it('enforces required properties', () => { 9 | expect(createCommand.description).toBeTruthy() 10 | expect(createCommand.run).toBeTruthy() 11 | expect(typeof createCommand.run).toBe('function') 12 | }) 13 | 14 | test('check solidarity create with no parameter', async () => { 15 | await createCommand.run(mockContext) 16 | expect(mockContext.print.error.mock.calls).toEqual([['Missing what to create'], ['$ solidarity create ']]) 17 | expect(mockContext.print.info.mock.calls.length).toBe(1) 18 | }) 19 | 20 | test('check solidarity create with plugin', async () => { 21 | const goodContext = { 22 | ...mockContext, 23 | parameters: { first: 'plugin' }, 24 | solidarity: { createPlugin: jest.fn() }, 25 | } 26 | await createCommand.run(goodContext) 27 | expect(goodContext.solidarity.createPlugin).toBeCalled() 28 | 29 | // now make it fail 30 | const errorString = 'ER MA GERD ARRAWRS' 31 | goodContext.solidarity.createPlugin = jest.fn(() => { 32 | throw Error(errorString) 33 | }) 34 | await createCommand.run(goodContext) 35 | expect(goodContext.print.error.mock.calls.slice(-1).toString()).toEqual(`Error: ${errorString}`) 36 | }) 37 | -------------------------------------------------------------------------------- /src/extensions/solidarity-extension.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, solidarity, SolidarityPlugin } from '../types' 2 | // Adding our goodies to the context 3 | module.exports = (context: SolidarityRunContext): void => { 4 | const { helpers } = require('envinfo') 5 | const callsite = require('callsite') 6 | const path = require('path') 7 | 8 | const { filesystem } = context 9 | context.solidarity = solidarity 10 | // place for plugins 11 | context._pluginsList = [] 12 | // the add plugin function 13 | context.addPlugin = (pluginConfig: SolidarityPlugin) => { 14 | // I'll fiinnnnd youuuu... calling function 15 | const stack = callsite() 16 | const requester = stack[1].getFileName() 17 | const templateDirPotential = path.join(path.dirname(requester), '../templates/') 18 | // Don't store directories that aren't there! 19 | const templateDirectory = filesystem.exists(templateDirPotential) ? templateDirPotential : null 20 | 21 | context._pluginsList.push({ 22 | templateDirectory, 23 | ...pluginConfig, 24 | }) 25 | } 26 | 27 | // Add helpers 28 | context.envinfo = helpers 29 | 30 | // Flavored separator 31 | context.printSeparator = (): void => 32 | context.print.info( 33 | context.print.colors.america( 34 | '-----------------------------------------------------------------------------------' 35 | ) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /__tests__/command_helpers/setOutputMode.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityOutputMode } from '../../src/types' 2 | import setOutputMode from '../../src/extensions/functions/setOutputMode' 3 | 4 | test('default output mode', () => { 5 | const params = { options: {} } 6 | const settings = {} 7 | const result = setOutputMode(params, settings) 8 | 9 | expect(result).toEqual(SolidarityOutputMode.MODERATE) 10 | }) 11 | 12 | test('settings output mode', () => { 13 | const params = { options: {} } 14 | const settings = { config: { output: 'SILENT' } } 15 | const result = setOutputMode(params, settings) 16 | 17 | expect(result).toEqual(SolidarityOutputMode.SILENT) 18 | }) 19 | 20 | test('verbose output mode', () => { 21 | const params = { options: { verbose: true } } 22 | const settings = {} 23 | const result = setOutputMode(params, settings) 24 | 25 | expect(result).toEqual(SolidarityOutputMode.VERBOSE) 26 | }) 27 | 28 | test('silent output mode', () => { 29 | const params = { options: { silent: true } } 30 | const settings = {} 31 | const result = setOutputMode(params, settings) 32 | 33 | expect(result).toEqual(SolidarityOutputMode.SILENT) 34 | }) 35 | 36 | test('moderate output mode', () => { 37 | const params = { options: { moderate: true } } 38 | const settings = {} 39 | const result = setOutputMode(params, settings) 40 | 41 | expect(result).toEqual(SolidarityOutputMode.MODERATE) 42 | }) 43 | -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffEnv.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | 3 | export default async (context: SolidarityRunContext): Promise => { 4 | const { print, prompt } = context 5 | const pickSentence = 'Pick from existing environment vars on this machine' 6 | const typeSentence = 'Type the name of the environment variable to enforce' 7 | 8 | const envStyle = await prompt.ask({ 9 | name: 'value', 10 | type: 'select', 11 | message: 'How would you like to pick your environment variable', 12 | choices: [pickSentence, typeSentence], 13 | }) 14 | 15 | let pickEnv 16 | if (envStyle.value === pickSentence) { 17 | pickEnv = await prompt.ask({ 18 | name: 'value', 19 | type: 'select', 20 | message: 'Which environment variable would you like to enforce?', 21 | choices: Object.keys(process.env), 22 | }) 23 | } else { 24 | pickEnv = await prompt.ask({ 25 | name: 'value', 26 | type: 'input', 27 | message: 'Which environment variable would you like to enforce?', 28 | }) 29 | } 30 | 31 | if (pickEnv.value === pickSentence || pickEnv.value === typeSentence) { 32 | throw new Error('No Environment Variable was chosen') 33 | } else { 34 | print.success(`Enforcing ENV for ${pickEnv.value}`) 35 | return { rule: 'env', variable: pickEnv.value } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /__tests__/command_helpers/findPluginInfo.ts: -------------------------------------------------------------------------------- 1 | const findPluginInfo = require('../../src/extensions/functions/findPluginInfo') 2 | const examplePlugin = require('examplePlugin') 3 | const mockContext = examplePlugin(require('mockContext')) 4 | 5 | describe('findPluginInfo Function', () => { 6 | test('can fail to find plugins', () => { 7 | const rule = { 8 | rule: 'custom', 9 | plugin: 'FAKE', 10 | name: 'checkThing', 11 | } 12 | const customPluginRule = findPluginInfo(rule, mockContext) 13 | expect(customPluginRule).toEqual({ message: "Plugin not found 'FAKE'", success: false }) 14 | }) 15 | 16 | test('can fail to find plugins', () => { 17 | const rule = { 18 | rule: 'custom', 19 | plugin: 'Example Plugin', 20 | name: 'FAKE', 21 | } 22 | const customPluginRule = findPluginInfo(rule, mockContext) 23 | expect(customPluginRule.message).toEqual( 24 | "NOT FOUND: Custom rule from 'Example Plugin' plugin with check function 'FAKE'" 25 | ) 26 | expect(customPluginRule.success).toBeFalsy() 27 | }) 28 | 29 | test('can find plugins', () => { 30 | const rule = { 31 | rule: 'custom', 32 | plugin: 'Example Plugin', 33 | name: 'checkThing', 34 | } 35 | const customPluginRule = findPluginInfo(rule, mockContext) 36 | expect(customPluginRule.success).toBeTruthy() 37 | expect(customPluginRule).toMatchSnapshot() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/templates/helpful-plugin.js.ejs: -------------------------------------------------------------------------------- 1 | const addOptionalRules = require('./helpers/addOptionalRules') 2 | 3 | module.exports = (context) => { 4 | // Register this plugin 5 | context.addPlugin({ 6 | name: '<%= props.pluginName %>', 7 | description: '<%= props.description %>', 8 | snapshot: async (context) => { 9 | // start with template 10 | let solidarity = require('../templates/<%= props.pluginName %>-template.json') 11 | // add optional rules 12 | addOptionalRules(context, solidarity.requirements) 13 | // write out .solidarity file 14 | context.solidarity.setSolidaritySettings(solidarity, context) 15 | // update file with local versions 16 | await context.system.run('solidarity snapshot') 17 | }<% if(props.customRules){ %>, 18 | rules: { 19 | ruleName: { 20 | check: async (rule, context) => { 21 | return { 22 | pass: true, 23 | message: 'Check always passes' 24 | } 25 | }, 26 | snapshot: async (rule, context) => [ 27 | { 28 | prop: 'addedProp', 29 | value: '12.0.0' 30 | }, 31 | ], 32 | report: async (rule, context, report) => { 33 | report.addCLI({ 34 | binary: 'imaginaryCLI', 35 | version: '10', 36 | desired: '12' 37 | }) 38 | } 39 | } 40 | }<% } %> 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /__tests__/command_helpers/updateVersions.ts: -------------------------------------------------------------------------------- 1 | import updateVersions from '../../src/extensions/functions/updateVersions' 2 | const solidarity = { 3 | getSolidaritySettings: jest.fn(() => ({ 4 | requirements: { 5 | nachos: [{ rule: 'cli', binary: 'nachos' }], 6 | }, 7 | })), 8 | setSolidaritySettings: jest.fn(), 9 | updateRequirement: jest.fn(async () => [['Setting nachos to 42']]), 10 | } 11 | 12 | const mockContext = require('mockContext') 13 | 14 | const mockContextWithRules = { 15 | ...mockContext, 16 | solidarity, 17 | } 18 | 19 | test('updateVersions exists', () => expect(updateVersions).toMatchSnapshot()) 20 | 21 | test('updateVersions pulls solidarity settings', async () => { 22 | const theVoid = await updateVersions(mockContext) 23 | expect(mockContext.solidarity.getSolidaritySettings).toHaveBeenCalled() 24 | expect(theVoid).toMatchSnapshot() 25 | }) 26 | 27 | test('updateVersions prints success with no changes', async () => { 28 | await updateVersions(mockContext) 29 | expect(mockContext.print.success).toHaveBeenCalled() 30 | expect(mockContext.print.success.mock.calls[0][0]).toBe('\n No Changes') 31 | }) 32 | 33 | test('updateVersions prints success with number of changes', async () => { 34 | await updateVersions(mockContextWithRules) 35 | expect(mockContextWithRules.print.success).toHaveBeenCalled() 36 | expect(mockContextWithRules.print.success.mock.calls[2][0]).toBe('\n 1 Rule updated') 37 | }) 38 | -------------------------------------------------------------------------------- /__tests__/extensions/__snapshots__/extensionCheck.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot of solidarity extension 1`] = ` 4 | Object { 5 | "appendSolidaritySettings": [Function], 6 | "binaryExists": [Function], 7 | "buildSpecificRequirement": [Function], 8 | "checkCLI": [Function], 9 | "checkDir": [Function], 10 | "checkENV": [Function], 11 | "checkFile": [Function], 12 | "checkRequirement": [Function], 13 | "createPlugin": [Function], 14 | "getLineWithVersion": [Function], 15 | "getSolidaritySettings": [Function], 16 | "getVersion": [Function], 17 | "printResults": [Function], 18 | "removeNonVersionCharacters": [Function], 19 | "reviewRule": [Function], 20 | "ruleHandlers": Object { 21 | "cli": Object { 22 | "callback": [Function], 23 | "key": "binary", 24 | }, 25 | "dir": Object { 26 | "callback": [Function], 27 | "key": "location", 28 | }, 29 | "env": Object { 30 | "callback": [Function], 31 | "key": "variable", 32 | }, 33 | "file": Object { 34 | "callback": [Function], 35 | "key": "location", 36 | }, 37 | "shell": Object { 38 | "callback": [Function], 39 | "key": "command", 40 | }, 41 | }, 42 | "setOutputMode": [Function], 43 | "setSolidaritySettings": [Function], 44 | "skipRule": [Function], 45 | "updateRequirement": [Function], 46 | "updateVersions": [Function], 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /__tests__/command_helpers/getLineWithVersion.ts: -------------------------------------------------------------------------------- 1 | import getInLineWithVersion from '../../src/extensions/functions/getLineWithVersion' 2 | 3 | describe('getLineWithVersion', () => { 4 | test('line: number', () => { 5 | const rule = { 6 | rule: 'cli', 7 | binary: 'node', 8 | semver: '>=7.6.0', 9 | error: 'Upgrade to latest node >= 7.6 please.', 10 | number: 'this', 11 | line: 2, 12 | } 13 | 14 | const versionOutput = `Node js \n 7.6.0` 15 | const result = getInLineWithVersion(rule, versionOutput) 16 | 17 | expect(result).toEqual(' 7.6.0') 18 | }) 19 | 20 | test('line: string', () => { 21 | const rule = { 22 | rule: 'cli', 23 | binary: 'node', 24 | semver: '>=7.6.0', 25 | error: 'Upgrade to latest node >= 7.6 please.', 26 | number: 'this', 27 | line: 'version:', 28 | } 29 | 30 | const versionOutput = `Node js \n version: 7.6.0` 31 | const result = getInLineWithVersion(rule, versionOutput) 32 | 33 | expect(result).toEqual(' version: 7.6.0') 34 | }) 35 | 36 | test('no line', () => { 37 | const rule = { 38 | rule: 'cli', 39 | binary: 'node', 40 | semver: '>=7.6.0', 41 | error: 'Upgrade to latest node >= 7.6 please.', 42 | number: 'this', 43 | } 44 | 45 | const versionOutput = `Node js \n 7.6.0` 46 | const result = getInLineWithVersion(rule, versionOutput) 47 | 48 | expect(result).toEqual(versionOutput) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /__tests__/command_helpers/removeNonVersionCharacters.ts: -------------------------------------------------------------------------------- 1 | import removeNonVersionCharacters from '../../src/extensions/functions/removeNonVersionCharacters' 2 | 3 | test('Verify removeNonVersionCharacters function', () => { 4 | expect(typeof removeNonVersionCharacters).toBe('function') 5 | }) 6 | 7 | test('parse the result', () => { 8 | const line = 'Homebrew 1.3.5' 9 | const rule = { binary: 'homebrew' } 10 | expect(removeNonVersionCharacters(rule, line)).toBe('1.3.5') 11 | }) 12 | 13 | test('throws an error when there is no version output', () => { 14 | const line = 'Homebrew without versions' 15 | const rule = { binary: 'ls' } 16 | const errorMessage = `No version was detected from the output of the binary '${rule.binary}'` 17 | expect(() => { 18 | removeNonVersionCharacters(rule, line) 19 | }).toThrowError(errorMessage) 20 | }) 21 | 22 | test('handle multiple versions by return first', () => { 23 | const line = 'Homebrew 1.3.5 and Awaybrew 2.1.0 and Witchesbrew 3.1.4' 24 | const rule = { binary: 'homebrew' } 25 | expect(removeNonVersionCharacters(rule, line)).toBe('1.3.5') 26 | }) 27 | 28 | test('handle matchIndex property', () => { 29 | const line = 'Homebrew 1.3.5 and Awaybrew 2.1.0 and Witchesbrew 3.1.4' 30 | let rule = { binary: 'homebrew', matchIndex: 1 } 31 | expect(removeNonVersionCharacters(rule, line)).toBe('2.1.0') 32 | rule = { binary: 'homebrew', matchIndex: 2 } 33 | expect(removeNonVersionCharacters(rule, line)).toBe('3.1.4') 34 | }) 35 | -------------------------------------------------------------------------------- /__tests__/command_helpers/printResults.ts: -------------------------------------------------------------------------------- 1 | import printResults from '../../src/extensions/functions/printResults' 2 | import { SolidarityRunContext, SolidarityReportResults } from '../../src/types' 3 | import { createReport } from '../../src/extensions/functions/solidarityReport' 4 | import { flatten } from 'ramda' 5 | 6 | let mockContext: SolidarityRunContext 7 | let reportResults: SolidarityReportResults 8 | describe('reviewRule', () => { 9 | beforeEach(async () => { 10 | // fresh mock context 11 | mockContext = require('mockContext') 12 | reportResults = await createReport(mockContext) 13 | }) 14 | 15 | test('printResults uses table', () => { 16 | printResults(reportResults, mockContext) 17 | // print table was called 18 | expect(mockContext.print.table).toHaveBeenCalled() 19 | expect(flatten(mockContext.print.table.mock.calls)).toContain('System Basics') 20 | }) 21 | 22 | test('prints custom tables', () => { 23 | const justHeaderTable = { 24 | title: 'No Values Table', 25 | table: [['PARIS']], 26 | } 27 | const customTable = { 28 | title: 'Custom Value Table', 29 | table: [['Title'], ['NEW ORLEANS']], 30 | } 31 | reportResults.customRules.push(justHeaderTable) 32 | reportResults.customRules.push(customTable) 33 | printResults(reportResults, mockContext) 34 | expect(flatten(mockContext.print.table.mock.calls)).not.toContain('PARIS') 35 | expect(flatten(mockContext.print.table.mock.calls)).toContain('NEW ORLEANS') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/extensions/functions/checkCLI.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, CLIRule } from '../../types' 2 | module.exports = async (rule: CLIRule, context: SolidarityRunContext): Promise => { 3 | const { semver, solidarity } = context 4 | const binaryExists = require('./binaryExists') 5 | 6 | // First check for binary 7 | if (!binaryExists(rule.binary, context)) { 8 | throw new Error(`Binary '${rule.binary}' not found`) 9 | } 10 | 11 | // Is there a semver rule? 12 | if (rule.semver) { 13 | // ensure we have valid rule input 14 | if (!semver.validRange(rule.semver)) throw new Error(`Invalid semver rule ${rule.semver}`) 15 | 16 | let binaryVersion 17 | binaryVersion = await solidarity.getVersion(rule, context) 18 | 19 | // pad zeros for any non-semver version systems (rules still work) 20 | let binarySemantic = binaryVersion 21 | while (binarySemantic.split('.').length < 3) { 22 | binarySemantic += '.0' 23 | } 24 | 25 | const customMessage = (rule.error || '') 26 | .replace(/{{wantedVersion}}/gi, /\^|\~/.test(rule.semver) ? rule.semver.substr(1) : rule.semver) 27 | .replace(/{{installedVersion}}/gi, binaryVersion) 28 | const standardMessage = `${rule.binary}: you have '${binaryVersion}', but the project requires '${rule.semver}'` 29 | const message = customMessage || standardMessage 30 | 31 | // I can't get no satisfaction 32 | if (!semver.satisfies(binarySemantic, rule.semver)) { 33 | throw new Error(message) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /__tests__/command_helpers/quirksNodeModules.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const delineator = process.platform === 'win32' ? ';' : ':' 3 | test('quirks moves node_modules to back', () => { 4 | // key is that it has `node_modules` + path.sep 5 | const injectedStuff = `node_modules${path.sep}testOnly` 6 | // prepend to PATH 7 | process.env.PATH = injectedStuff + delineator + process.env.PATH 8 | const pathAsArray = process.env.PATH.split(delineator) 9 | // Yup it is prepended to front 10 | expect(pathAsArray[0]).toBe(injectedStuff) 11 | // require mutates PATH 12 | require('../../src/extensions/functions/quirksNodeModules') 13 | const newPathAsArray = process.env.PATH.split(delineator) 14 | // Not in the front 15 | expect(newPathAsArray[0]).not.toBe(injectedStuff) 16 | // still there though (moved to back) 17 | expect(process.env.PATH.includes(injectedStuff)).toBeTruthy() 18 | }) 19 | 20 | test('quirks does not move just any injected path to back', () => { 21 | const injectedStuff = `taco${path.sep}testOnly` 22 | // prepend to PATH 23 | process.env.PATH = injectedStuff + delineator + process.env.PATH 24 | const pathAsArray = process.env.PATH.split(delineator) 25 | // Yup it is prepended to front 26 | expect(pathAsArray[0]).toBe(injectedStuff) 27 | // require does not mutate PATH this time 28 | require('../../src/extensions/functions/quirksNodeModules') 29 | const newPathAsArray = process.env.PATH.split(delineator) 30 | // Not in the front 31 | expect(newPathAsArray[0]).toBe(injectedStuff) 32 | }) 33 | -------------------------------------------------------------------------------- /.solidarity.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/solidaritySchema", 3 | "requirements": { 4 | "Android": [ 5 | { "rule": "cli", "binary": "emulator" }, 6 | { "rule": "cli", "binary": "android" }, 7 | { "rule": "env", "variable": "ANDROID_HOME", "error": "The ANDROID_HOME environment variable must be set to your local SDK. Refer to getting started docs for help." } 8 | ], 9 | "NPM": [{ "rule": "cli", "binary": "npm" }], 10 | "Yarn": [{ "rule": "cli", "binary": "yarn", "semver": "^1.0.1", "version": "--version" }], 11 | "Node": [{ "rule": "cli", "binary": "node", "semver": "8.5.0"}], 12 | "Watchman": [ 13 | { 14 | "rule": "cli", 15 | "binary": "watchman", 16 | "error": "install watchman `brew install watchman`", 17 | "platform": "darwin" 18 | } 19 | ], 20 | "Git email": [ 21 | { 22 | "rule": "shell", 23 | "command": "git config user.email", 24 | "match": ".+@.+" 25 | } 26 | ], 27 | "Package.json": [{ "rule": "file", "location": "./package.json"}], 28 | "React Native": [ 29 | { "rule": "cli", "binary": "react-native", "semver": "~2.0.1"}, 30 | { "rule": "cli", "binary": "react-native", "semver": ">0.0.1", "line": 2 } 31 | ], 32 | "Xcode" : [ 33 | { "rule": "cli", "binary": "xcodebuild", "semver": "9.0", "platform": "darwin"}, 34 | { "rule": "cli", "binary": "xcrun", "semver": "35", "platform": "darwin"} 35 | ], 36 | "Cocoapods": [ 37 | { "rule": "cli", "binary": "pod", "semver": "1.2.1" } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkFile.ts: -------------------------------------------------------------------------------- 1 | import checkFile from '../../src/extensions/functions/checkFile' 2 | import * as context from 'gluegun/toolbox' 3 | import { FSRule } from '../../src/types' 4 | 5 | test('checkFile detects an existing file', () => { 6 | // known file 7 | const location = 'package.json' 8 | // Use checkFile to make sure it exists 9 | expect(checkFile({ location } as FSRule, context)).toBe(undefined) 10 | }) 11 | 12 | test('checkFile can fail', () => { 13 | // Use checkFile to make sure it does not exist 14 | expect(() => { 15 | checkFile({ location: 'DOES_NOT_EXIST' } as FSRule, context) 16 | }).toThrow() 17 | }) 18 | 19 | test('checkFile throws false with no location', () => { 20 | expect(() => { 21 | checkFile({} as FSRule, context) 22 | }).toThrow() 23 | }) 24 | 25 | test('checkFile does not throw for a local dir that exists', () => { 26 | // Use checkFile to make sure a known dir returns false since it's not a file 27 | expect(() => { 28 | checkFile({ location: './src' } as FSRule, context) 29 | }).toThrow() 30 | }) 31 | 32 | test('checkFile does not throw for a local dir (in default) that exists', () => { 33 | // Use checkFile to make sure a known dir returns false since it's not a file 34 | expect(() => { 35 | checkFile({ location: 'src' } as FSRule, context) 36 | }).toThrow() 37 | }) 38 | 39 | test('checkFile does not throw for a homedir that exists', () => { 40 | // Use checkFile to make sure a known dir returns false since it's not a file 41 | expect(() => { 42 | checkFile({ location: '~/.ssh/' } as FSRule, context) 43 | }).toThrow() 44 | }) 45 | -------------------------------------------------------------------------------- /__tests__/integration/solidarity-report/report-basic.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import tempy from 'tempy' 3 | 4 | const path = require('path') 5 | const filesystem = require('fs-jetpack') 6 | const SOLIDARITY = `node ${process.cwd()}${path.sep}bin${path.sep}solidarity` 7 | const origCwd = process.cwd() 8 | let originalTimeout 9 | 10 | beforeAll(() => { 11 | // These can be slow on CI 12 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000 14 | const tempDir = tempy.directory() 15 | filesystem.copy( 16 | `__tests__${path.sep}sandbox${path.sep}solidarity_json${path.sep}.solidarity.json`, 17 | `${tempDir}${path.sep}.solidarity` 18 | ) 19 | process.chdir(tempDir) 20 | }) 21 | 22 | afterAll(function() { 23 | // Fix timeout change 24 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 25 | }) 26 | 27 | test('solidarity report works', async done => { 28 | try { 29 | execa 30 | .shell(`${SOLIDARITY} report --compiled`) 31 | .then(result => { 32 | // check a few from the report 33 | expect(result.stdout.includes('OS')).toBeTruthy() 34 | expect(result.stdout.includes('CPU')).toBeTruthy() 35 | expect(result.stdout.includes('Report Info')).toBeTruthy() 36 | expect(result.stdout.includes('node')).toBeTruthy() 37 | expect(result.code).toBe(0) 38 | done() 39 | }) 40 | .catch(err => { 41 | console.error(err) 42 | done.fail() 43 | }) 44 | } catch (err) { 45 | done.fail() 46 | } 47 | }) 48 | 49 | afterEach(() => { 50 | process.chdir(origCwd) 51 | }) 52 | -------------------------------------------------------------------------------- /src/extensions/functions/getVersion.ts: -------------------------------------------------------------------------------- 1 | import { CLIRule, SolidarityRunContext } from '../../types' 2 | // Get the version of a specific CLI 3 | module.exports = async (rule: CLIRule, context: SolidarityRunContext): Promise => { 4 | const { system, solidarity } = context 5 | 6 | let versionOutput 7 | // They specified how to check version 8 | if (rule.version) { 9 | versionOutput = await system.run(`${rule.binary} ${rule.version}`) 10 | if (versionOutput === '') { 11 | // Lets try again 12 | // things like python and java use stderr instead of stdout 13 | const checkSTDERR = require('./checkSTDERR') 14 | versionOutput = await system.run(checkSTDERR(rule, context)) 15 | } 16 | } else { 17 | // We try the following in this order 18 | // -v, --version, -version, version 19 | try { 20 | versionOutput = await system.run(`${rule.binary} -v`) 21 | } catch (_e) { 22 | try { 23 | versionOutput = await system.run(`${rule.binary} --version`) 24 | } catch (_e) { 25 | try { 26 | versionOutput = await system.run(`${rule.binary} -version`) 27 | } catch (_e) { 28 | try { 29 | versionOutput = await system.run(`${rule.binary} version`) 30 | } catch (_e) { 31 | throw 'No version identifier flag for this binary was found' 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | // Now parse 39 | const correctLine = solidarity.getLineWithVersion(rule, versionOutput) 40 | const version = solidarity.removeNonVersionCharacters(rule, correctLine) 41 | return version 42 | } 43 | -------------------------------------------------------------------------------- /__tests__/command_helpers/getVersion.ts: -------------------------------------------------------------------------------- 1 | import getVersion from '../../src/extensions/functions/getVersion' 2 | 3 | import solidarityExtension from '../../src/extensions/solidarity-extension' 4 | 5 | const context = require('gluegun/toolbox') 6 | let originalTimeout 7 | solidarityExtension(context) 8 | 9 | describe('getVersion', () => { 10 | beforeAll(() => { 11 | // These can be slow on CI 12 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 14 | }) 15 | 16 | afterAll(function() { 17 | // Fix timeout change 18 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 19 | }) 20 | 21 | test('returns version for a given binary if not specified', async () => { 22 | const rule = { rule: 'cli', binary: 'yarn' } 23 | const output = await getVersion(rule, context) 24 | 25 | expect(typeof output).toBe('string') 26 | expect(output.length).toBeGreaterThan(0) 27 | }) 28 | 29 | test('returns version for a given binary using specified option', async () => { 30 | const rule = { rule: 'cli', binary: 'yarn', version: '--version' } 31 | const output = await getVersion(rule, context) 32 | 33 | expect(typeof output).toBe('string') 34 | expect(output.length).toBeGreaterThan(0) 35 | }) 36 | 37 | test('throws an error if no version flag works', async () => { 38 | const rule = { rule: 'cli', binary: 'cd' } 39 | let result 40 | 41 | try { 42 | await getVersion(rule, context) 43 | } catch (e) { 44 | result = e 45 | } 46 | expect(result).toEqual('No version identifier flag for this binary was found') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.1 4 | "version": "0.1", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "async", 10 | "ava", 11 | "basic", 12 | "callsite", 13 | "checkmark", 14 | "cli", 15 | "clis", 16 | "codecov", 17 | "color", 18 | "config", 19 | "configs", 20 | "custom", 21 | "delineator", 22 | "docsify", 23 | "e.g.", 24 | "env", 25 | "envinfo", 26 | "execa", 27 | "filesystem", 28 | "gitignore", 29 | "gluegun", 30 | "gluegun's", 31 | "heartpulse", 32 | "infinite.red", 33 | "info", 34 | "listr", 35 | "macos", 36 | "namespacing", 37 | "newclear", 38 | "npmignore", 39 | "onboarding", 40 | "Plugin", 41 | "Plugins", 42 | "PRs", 43 | "roadmap", 44 | "rules", 45 | "ruleset", 46 | "shell", 47 | "shipit", 48 | "snapupdate", 49 | "specs", 50 | "tada", 51 | "tempy", 52 | "typesync", 53 | "updtr", 54 | "v1", 55 | "walkthrough", 56 | "xmark" 57 | ], 58 | // flagWords - list of words to be always considered incorrect 59 | // This is useful for offensive words and common spelling errors. 60 | // For example "hte" should be "the" 61 | "flagWords": [ 62 | "Github", 63 | "Cocoapods" 64 | ] 65 | } -------------------------------------------------------------------------------- /src/extensions/functions/onboard/kickoffs/kickoffAllRules.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidarityRule } from '../../../../types' 2 | 3 | export default async (context: SolidarityRunContext, rule: SolidarityRule): Promise => { 4 | const { prompt } = context 5 | 6 | ////////////////// CUSTOM ERROR MESSAGE? 7 | const customError = await prompt.ask({ 8 | name: 'value', 9 | type: 'confirm', 10 | message: 'Would you like to write a custom error message for if this rule fails to pass?', 11 | }) 12 | 13 | if (customError.value) { 14 | const errorMessage = await prompt.ask({ 15 | name: 'value', 16 | type: 'input', 17 | message: 'Your custom error message', 18 | }) 19 | 20 | rule.error = errorMessage.value 21 | } 22 | 23 | ////////////////// PLATFORM SPECIFIC? 24 | const platformSpecific = await prompt.ask({ 25 | name: 'value', 26 | type: 'confirm', 27 | message: 'Should this rule only apply on certain operating systems?', 28 | }) 29 | 30 | if (platformSpecific.value) { 31 | const platforms = await prompt.ask({ 32 | name: 'value', 33 | type: 'multiselect', 34 | message: 'Which operating systems does this rule run for?', 35 | choices: ['macos', 'freebsd', 'linux', 'sunos', 'windows'], 36 | }) 37 | rule.platform = platforms.value 38 | } 39 | 40 | ////////////////// SKIP CI? 41 | const ciSkip = await prompt.ask({ 42 | name: 'value', 43 | type: 'confirm', 44 | message: 'Should this rule be skipped on a Continuous Integration server?', 45 | }) 46 | 47 | if (ciSkip.value) { 48 | rule.ci = true 49 | } 50 | 51 | return rule 52 | } 53 | -------------------------------------------------------------------------------- /docs/solidarityFromCode.md: -------------------------------------------------------------------------------- 1 | ## Plugins that write Solidarity Files from Code 2 | 3 | Perhaps you want to ask the user questions, read files, or even stamp your own versions. In the [Simple Plugin](/docs/simplePlugin.md) section we copy a template with our plugin. File-copy is cute :heartpulse:, but this section will show you how to do more. All you need to do is provide an async function to the snapshot property, instead of a string like the simple plugin used. 4 | 5 | Whatever async function you provide will be run when your plugin is selected. Now writing the `.solidarity` file is up to you! 6 | 7 | **And you're not alone!** You have full power of Gluegun's context API (the CLI driver of Solidarity). 8 | 9 | Learn more about [Gluegun here](https://infinitered.github.io/gluegun/#/context-api). Any spinner, color, or prompt you see in Solidarity is driven from Gluegun. So open up Solidarity source for examples of things you can do! 10 | 11 | _As an example:_ If we wanted to perform the same exact plugin as the one from [Simple Plugin](/docs/simplePlugin.md), but do everything manually, the plugin would then look like this: 12 | 13 | ```js 14 | module.exports = (context) => { 15 | // Register this plugin 16 | context.addPlugin({ 17 | name: 'Fiesta Time', 18 |    description: 'Make sure your system is ready to party 🎉', 19 | snapshot: async (context) => { 20 | const { filesystem, system } = context 21 | // Copy template 22 | filesystem.copy( 23 | `${__dirname}/../templates/fiesta-template.json`, 24 | '.solidarity' 25 | ) 26 | // Update versions to local 27 | await system.run('solidarity snapshot') 28 | } 29 | }) 30 | } 31 | ``` 32 | 33 | ### [More About Plugins](/docs/plugins.md) 34 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mockContext.ts: -------------------------------------------------------------------------------- 1 | const realThing = require('gluegun/toolbox') 2 | const realSolidarityContext = require('../../src/extensions/solidarity-extension') 3 | realSolidarityContext(realThing) 4 | 5 | const noConfigSolidarity = { 6 | checkRequirement: jest.fn(), 7 | getSolidaritySettings: jest.fn(() => Promise.resolve({})), 8 | printResults: jest.fn(), 9 | setSolidaritySettings: jest.fn(), 10 | updateRequirement: jest.fn(), 11 | updateVersions: jest.fn(() => Promise.resolve()), 12 | getLineWithVersion: jest.fn(), 13 | removeNonVersionCharacters: jest.fn(), 14 | } 15 | 16 | const mockContext = { 17 | ...realThing, 18 | outputMode: undefined, 19 | system: { 20 | startTimer: jest.fn(() => jest.fn()), 21 | run: jest.fn(() => '12'), 22 | which: jest.fn(name => 'usr/local/bin/${name}'), 23 | }, 24 | template: { 25 | generate: jest.fn(), 26 | }, 27 | print: { 28 | error: jest.fn(), 29 | success: jest.fn(), 30 | info: jest.fn(), 31 | spin: jest.fn(() => ({ 32 | stop: jest.fn(), 33 | fail: jest.fn(), 34 | succeed: jest.fn(), 35 | })), 36 | table: jest.fn(), 37 | xmark: jest.fn(), 38 | checkmark: jest.fn(), 39 | color: { 40 | green: jest.fn(), 41 | red: jest.fn(), 42 | blue: jest.fn(), 43 | magenta: jest.fn(), 44 | }, 45 | colors: { 46 | green: jest.fn(), 47 | red: jest.fn(), 48 | blue: jest.fn(), 49 | magenta: jest.fn(), 50 | }, 51 | }, 52 | printSeparator: jest.fn(), 53 | parameters: { 54 | options: {}, 55 | }, 56 | prompt: { 57 | ask: jest.fn(({ name }) => Promise.resolve({ [name]: 'taco', createFile: true })), 58 | confirm: jest.fn(() => true), 59 | }, 60 | solidarity: noConfigSolidarity, 61 | } 62 | 63 | module.exports = mockContext 64 | -------------------------------------------------------------------------------- /src/extensions/functions/getSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidaritySettings } from '../../types' 2 | 3 | module.exports = async (context: SolidarityRunContext): Promise => { 4 | const { filesystem, parameters } = context 5 | const { loadFile, loadModule, loadWebCheck } = require('./getSolidarityHelpers') 6 | const JSON5 = require('json5') 7 | const options = parameters.options || {} // fix possibly undefined from gluegun 8 | const demandedFile = options.solidarityFile || options.f 9 | const demandedModule = options.module || options.d 10 | const demandedCheck = options.stack || options.t 11 | 12 | /* for now only JSON and JSON5 support 13 | * Summary: 14 | * Looks for `.solidarity` or `.solidarity.json` files 15 | * Unless you pass parameter options telling us to look 16 | * in specific paths, node modules, or websites 17 | */ 18 | let solidaritySettings 19 | if (demandedFile) { 20 | solidaritySettings = loadFile(context, demandedFile) 21 | } else if (demandedModule) { 22 | solidaritySettings = loadModule(context, demandedModule) 23 | } else if (demandedCheck) { 24 | solidaritySettings = await loadWebCheck(context, demandedCheck) 25 | } else if (filesystem.exists('.solidarity')) { 26 | solidaritySettings = JSON5.parse(filesystem.read('.solidarity')) 27 | } else if (filesystem.exists('.solidarity.json')) { 28 | solidaritySettings = JSON5.parse(filesystem.read('.solidarity.json')) 29 | } else { 30 | // if we got here there was no solidarity file 31 | throw 'ERROR: No solidarity file was found' 32 | } 33 | 34 | // Check shape 35 | if (solidaritySettings.requirements) { 36 | return solidaritySettings 37 | } else { 38 | throw 'ERROR: Found, but no requirements key. Please validate your solidarity file' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkCLI.ts: -------------------------------------------------------------------------------- 1 | import checkCLI from '../../src/extensions/functions/checkCLI' 2 | import solidarityExtension from '../../src/extensions/solidarity-extension' 3 | 4 | const doesNotExistCLI = { 5 | binary: 'no_way_this_should_be_real', 6 | } 7 | 8 | const alwaysExistCLI = { 9 | binary: 'node', 10 | } 11 | 12 | const badSemver = { 13 | binary: 'node', 14 | semver: 'wtfbbq!!!11', 15 | } 16 | 17 | const outOfDateCLI = { 18 | binary: 'node', 19 | semver: '10.99', 20 | } 21 | 22 | const injectVersion = { 23 | binary: 'node', 24 | semver: '~7.6', 25 | error: "Wanted: '{{wantedVersion}}', Installed: '{{installedVersion}}'. Update with `nvm install {{wantedVersion}}`", 26 | } 27 | 28 | const context = require('gluegun/toolbox') 29 | 30 | test('error on missing binary', async () => { 31 | await expect(checkCLI(doesNotExistCLI, context)).rejects.toThrow() 32 | }) 33 | 34 | test('fine on existing binary', async () => { 35 | expect(await checkCLI(alwaysExistCLI, context)).toBe(undefined) 36 | }) 37 | 38 | test('errors with message when an improper semver is sent', async () => { 39 | await expect(checkCLI(badSemver, context)).rejects.toThrow() 40 | }) 41 | 42 | test('returns message on improper version', async () => { 43 | solidarityExtension(context) 44 | context.solidarity.getVersion = () => '1' 45 | const message = `${outOfDateCLI.binary}: you have '1', but the project requires '${outOfDateCLI.semver}'` 46 | 47 | await expect(checkCLI(outOfDateCLI, context)).rejects.toThrowError(message) 48 | }) 49 | 50 | test('returns message with injected versions', async () => { 51 | const message = `Wanted: '7.6', Installed: '7.5'. Update with \`nvm install 7.6\`` 52 | solidarityExtension(context) 53 | context.solidarity.getVersion = () => '7.5' 54 | 55 | await expect(checkCLI(injectVersion, context)).rejects.toThrowError(message) 56 | }) 57 | -------------------------------------------------------------------------------- /src/commands/report.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import { SolidarityRunContext, SolidarityReportResults } from '../types' 3 | import { map, toPairs } from 'ramda' 4 | import { createReport } from '../extensions/functions/solidarityReport' 5 | 6 | module.exports = { 7 | alias: 'r', 8 | description: 'Report solidarity info about the current machine', 9 | run: async (context: SolidarityRunContext) => { 10 | const { print, solidarity, system } = context 11 | const reportTimer = system.startTimer() 12 | const { getSolidaritySettings, reviewRule, printResults } = solidarity 13 | // Node Modules Quirk 14 | require('../extensions/functions/quirksNodeModules') 15 | 16 | const spinner = print.spin('Building Report') 17 | 18 | // get settings or error 19 | let solidaritySettings 20 | try { 21 | solidaritySettings = await getSolidaritySettings(context) 22 | } catch (e) { 23 | spinner.fail(`No valid ${print.colors.success('solidarity')} file was found to report.`) 24 | process.exit(3) 25 | } 26 | 27 | let results: SolidarityReportResults = await createReport(context) 28 | 29 | // break all rules into requirements 30 | const reportCalls = map( 31 | async requirement => reviewRule(requirement, results, context), 32 | toPairs(solidaritySettings.requirements) 33 | ) 34 | 35 | // run the array of promises you just created 36 | await Promise.all(reportCalls) 37 | .then(reportResults => { 38 | results.basicInfo.push([ 39 | 'Report Info', 40 | `${new Date().toLocaleString()} (in ${(reportTimer() / 1000).toFixed(2)}s)`, 41 | ]) 42 | spinner.stop() 43 | printResults(results, context) 44 | }) 45 | .catch(err => { 46 | print.error(`\n\n${err}`) 47 | process.exit(2) 48 | }) 49 | }, 50 | } as GluegunCommand 51 | -------------------------------------------------------------------------------- /__tests__/command_helpers/skipRule.ts: -------------------------------------------------------------------------------- 1 | import skipRule from '../../src/extensions/functions/skipRule' 2 | 3 | const currentPlatform = process.platform 4 | const makeMockRulePlatform = platform => ({ platform }) 5 | const mockRuleBasic = makeMockRulePlatform(currentPlatform) 6 | const mockRuleUppercase = makeMockRulePlatform(currentPlatform.toUpperCase()) 7 | 8 | test('skipRule takes a string', () => { 9 | expect(skipRule(mockRuleBasic)).toBe(false) 10 | expect(skipRule(mockRuleUppercase)).toBe(false) 11 | if (currentPlatform === 'darwin') { 12 | expect(skipRule(makeMockRulePlatform('macos'))).toBe(false) 13 | } else if (currentPlatform === 'win32') { 14 | expect(skipRule(makeMockRulePlatform('windows'))).toBe(false) 15 | } 16 | }) 17 | 18 | test('skipRule takes an array', () => { 19 | const arrayOfOne = [currentPlatform] 20 | expect(skipRule(makeMockRulePlatform(arrayOfOne))).toBe(false) 21 | const arrayOfMore = [currentPlatform, 'nachos', 'tacos'] 22 | expect(skipRule(makeMockRulePlatform(arrayOfMore))).toBe(false) 23 | if (currentPlatform === 'darwin') { 24 | expect(skipRule(makeMockRulePlatform(['macos', 'nachos', 'tacos']))).toBe(false) 25 | } else if (currentPlatform === 'win32') { 26 | expect(skipRule(makeMockRulePlatform(['windows', 'nachos', 'tacos']))).toBe(false) 27 | } 28 | }) 29 | 30 | test('skipRule false on unknown', () => { 31 | expect(skipRule({})).toBe(false) 32 | }) 33 | 34 | test('skips on platform miss', () => { 35 | expect(skipRule(makeMockRulePlatform('nachos'))).toBe(true) 36 | expect(skipRule(makeMockRulePlatform(['nachos']))).toBe(true) 37 | expect(skipRule(makeMockRulePlatform(['nachos', 'tacos']))).toBe(true) 38 | }) 39 | 40 | test('skips CI when flagged', () => { 41 | const onCI = !!process.env.CI 42 | expect(skipRule({ ci: true })).toBe(false) 43 | expect(skipRule({})).toBe(false) 44 | expect(skipRule({ ci: false })).toBe(onCI) 45 | }) 46 | -------------------------------------------------------------------------------- /src/commands/onboard.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | 3 | import { SolidarityRunContext, SolidarityRule } from '../types' 4 | 5 | namespace Onboard { 6 | export const run = async (context: SolidarityRunContext) => { 7 | const { 8 | onboardAdd, 9 | printWizard, 10 | executeAddRule, 11 | addMore, 12 | reviewAndSave, 13 | } = require('../extensions/functions/onboard') 14 | const { print, filesystem } = context 15 | 16 | // check is there an existing .solidarity file? 17 | // TODO: Delete file for them 18 | // TODO BONUS: Eventually allow live editor to modify .solidarity files 19 | if (filesystem.exists('.solidarity')) { 20 | print.info("There seems to already be a Solidarity file. You're already onboarded") 21 | /* 22 | const userAnswer = await prompt.ask({ 23 | name: 'createFile', 24 | type: 'confirm', 25 | message: 'Existing `.solidarity` file found for this project. Would you like to delete it and start fresh?', 26 | }) 27 | 28 | if (userAnswer.createFile) { 29 | print.info('create wut?') 30 | } else { 31 | print.info('No Solidarity File') 32 | } 33 | */ 34 | } else { 35 | context.bufferSettings = { 36 | requirements: {}, 37 | } 38 | 39 | printWizard(context) 40 | let repeat = true 41 | let rules: Array = [] 42 | while (repeat) { 43 | // Find out what they wanted 44 | let answer = await onboardAdd(context) 45 | // execute their will 46 | const rule = await executeAddRule(context, answer) 47 | rules.push(rule) 48 | // more? 49 | repeat = await addMore(context) 50 | } 51 | 52 | reviewAndSave(context, rules) 53 | } 54 | } 55 | } 56 | 57 | module.exports = { 58 | description: 'Wizard walkthrough to create your Solidarity file', 59 | alias: 'o', 60 | run: Onboard.run, 61 | } as GluegunCommand 62 | -------------------------------------------------------------------------------- /__tests__/command_helpers/createPlugin.ts: -------------------------------------------------------------------------------- 1 | const createPlugin: Function = require('../../src/extensions/functions/createPlugin') 2 | const context = require('mockContext') 3 | 4 | test('check result shape', () => { 5 | expect(createPlugin).toMatchSnapshot() 6 | }) 7 | 8 | test('investigate createPlugin', async () => { 9 | const result = await createPlugin(context) 10 | expect(result).toMatchSnapshot() 11 | expect(context.template.generate).toBeCalled() 12 | expect(context.prompt.ask).toBeCalled() 13 | expect(context.prompt.confirm).toBeCalled() 14 | expect(context.print.success).toBeCalled() 15 | }) 16 | 17 | describe('checking plugin paths', () => { 18 | beforeEach(() => { 19 | const mockedPrompt = jest 20 | .fn() 21 | .mockImplementationOnce(() => Promise.resolve({ plugin: 'Nachos' })) 22 | .mockImplementationOnce(() => Promise.resolve({ pluginDesc: 'das nachos plugin' })) 23 | 24 | context.prompt.ask = mockedPrompt 25 | }) 26 | 27 | test(`Choice 1 - 'I do not want a generated rule file'`, async () => { 28 | context.prompt.ask.mockImplementationOnce(() => 29 | Promise.resolve({ 30 | ruleChoice: 'I do not want a generated rule file', 31 | }) 32 | ) 33 | 34 | const result = await createPlugin(context) 35 | expect(result).toMatchSnapshot() 36 | }) 37 | 38 | test(`Choice 2 - 'Just a simple rule template'`, async () => { 39 | context.prompt.ask.mockImplementationOnce(() => 40 | Promise.resolve({ 41 | ruleChoice: 'Just a simple rule template', 42 | }) 43 | ) 44 | 45 | const result = await createPlugin(context) 46 | expect(result).toMatchSnapshot() 47 | }) 48 | 49 | test(`Choice 3 - 'Template + optional rules'`, async () => { 50 | context.prompt.ask.mockImplementationOnce(() => 51 | Promise.resolve({ 52 | ruleChoice: 'Template + optional rules', 53 | }) 54 | ) 55 | 56 | const result = await createPlugin(context) 57 | expect(result).toMatchSnapshot() 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /__tests__/integration/solidarity-create/create-nada.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import tempy from 'tempy' 3 | 4 | const path = require('path') 5 | const filesystem = require('fs-jetpack') 6 | const SOLIDARITY = `node ${process.cwd()}${path.sep}bin${path.sep}solidarity` 7 | const origCwd = process.cwd() 8 | let originalTimeout 9 | 10 | beforeAll(() => { 11 | // These can be slow on CI 12 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000 14 | const tempDir = tempy.directory() 15 | filesystem.copy( 16 | `__tests__${path.sep}sandbox${path.sep}solidarity_json${path.sep}.solidarity.json`, 17 | `${tempDir}${path.sep}.solidarity` 18 | ) 19 | process.chdir(tempDir) 20 | }) 21 | 22 | afterAll(function() { 23 | // Fix timeout change 24 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 25 | }) 26 | 27 | test('solidarity create works and prompts for what to create', async done => { 28 | try { 29 | execa 30 | .shell(`${SOLIDARITY} create --compiled`) 31 | .then(result => { 32 | // check a few from the report 33 | expect(result.stdout.includes('Missing what to create')).toBeTruthy() 34 | expect(result.code).toBe(0) 35 | done() 36 | }) 37 | .catch(err => { 38 | console.error(err) 39 | done.fail() 40 | }) 41 | } catch (err) { 42 | console.error(err) 43 | done.fail() 44 | } 45 | }) 46 | 47 | test('solidarity create is specific', async done => { 48 | try { 49 | execa 50 | .shell(`${SOLIDARITY} create idonotexist --compiled`) 51 | .then(result => { 52 | // check a few from the report 53 | expect(result.stdout.includes('Missing what to create')).toBeTruthy() 54 | expect(result.code).toBe(0) 55 | done() 56 | }) 57 | .catch(err => { 58 | console.error(err) 59 | done.fail() 60 | }) 61 | } catch (err) { 62 | done.fail() 63 | } 64 | }) 65 | 66 | afterEach(() => { 67 | process.chdir(origCwd) 68 | }) 69 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkCLIForUpdates.ts: -------------------------------------------------------------------------------- 1 | import checkCLIForUpdates from '../../src/extensions/functions/checkCLIForUpdates' 2 | 3 | const context = require('mockContext') 4 | const rule = { 5 | rule: 'cli', 6 | binary: 'bananas', 7 | semver: '1.1.0', 8 | version: '--version', 9 | } 10 | 11 | const ruleTildeSemver = { 12 | rule: 'cli', 13 | binary: 'npm', 14 | semver: '~5.6.0', 15 | version: '--version', 16 | } 17 | 18 | const ruleNoSemver = { 19 | rule: 'cli', 20 | binary: 'yarn', 21 | } 22 | 23 | describe('checkCLIForUpdates', () => { 24 | describe('with a bad binary', () => { 25 | it('should error with binary not found', async () => { 26 | await expect(checkCLIForUpdates(rule, context)).rejects.toThrow() 27 | }) 28 | }) 29 | 30 | describe('with a good binary', () => { 31 | beforeEach(() => { 32 | rule.binary = 'yarn' 33 | context.print = { 34 | colors: { 35 | green: jest.fn(stringy => stringy), 36 | }, 37 | } 38 | }) 39 | 40 | it('pads zeros for non-semver versions', async () => { 41 | context.solidarity = { 42 | getVersion: jest.fn(() => '1.0'), 43 | } 44 | 45 | const result = await checkCLIForUpdates(rule, context) 46 | expect(result).toEqual("Setting yarn to '1.0'") 47 | expect(context.print.colors.green).toHaveBeenCalled() 48 | }) 49 | 50 | it('does nothing if there was no original semver', async () => { 51 | context.solidarity = { 52 | getVersion: jest.fn(() => '1.0'), 53 | } 54 | 55 | const result = await checkCLIForUpdates(ruleNoSemver, context) 56 | expect(result).toEqual(undefined) 57 | expect(ruleNoSemver.semver).toBe(undefined) 58 | }) 59 | 60 | it('copies semver ~ symbol if present', async () => { 61 | context.solidarity = { 62 | getVersion: jest.fn(() => '5.8'), 63 | } 64 | 65 | const result = await checkCLIForUpdates(ruleTildeSemver, context) 66 | expect(result).toEqual("Setting npm to '~5.8'") 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /__tests__/command_helpers/__snapshots__/createPlugin.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`check result shape 1`] = `[Function]`; 4 | 5 | exports[`checking plugin paths Choice 1 - 'I do not want a generated rule file' 1`] = ` 6 | Array [ 7 | Array [ 8 | ".gitignore.ejs", 9 | ".gitignore", 10 | ], 11 | Array [ 12 | "README.md.ejs", 13 | "README.md", 14 | ], 15 | Array [ 16 | "package.json.ejs", 17 | "package.json", 18 | ], 19 | Array [ 20 | "simple-plugin.js.ejs", 21 | "extensions/solidarity-nachos.js", 22 | ], 23 | ] 24 | `; 25 | 26 | exports[`checking plugin paths Choice 2 - 'Just a simple rule template' 1`] = ` 27 | Array [ 28 | Array [ 29 | ".gitignore.ejs", 30 | ".gitignore", 31 | ], 32 | Array [ 33 | "README.md.ejs", 34 | "README.md", 35 | ], 36 | Array [ 37 | "package.json.ejs", 38 | "package.json", 39 | ], 40 | Array [ 41 | "rules-template.json.ejs", 42 | "templates/solidarity-nachos-template.json", 43 | ], 44 | Array [ 45 | "simple-plugin.js.ejs", 46 | "extensions/solidarity-nachos.js", 47 | ], 48 | ] 49 | `; 50 | 51 | exports[`checking plugin paths Choice 3 - 'Template + optional rules' 1`] = ` 52 | Array [ 53 | Array [ 54 | ".gitignore.ejs", 55 | ".gitignore", 56 | ], 57 | Array [ 58 | "README.md.ejs", 59 | "README.md", 60 | ], 61 | Array [ 62 | "package.json.ejs", 63 | "package.json", 64 | ], 65 | Array [ 66 | "rules-template.json.ejs", 67 | "templates/solidarity-nachos-template.json", 68 | ], 69 | Array [ 70 | "helpful-plugin.js.ejs", 71 | "extensions/solidarity-nachos.js", 72 | ], 73 | Array [ 74 | "addOptionalRules.js.ejs", 75 | "extensions/helpers/addOptionalRules.js", 76 | ], 77 | ] 78 | `; 79 | 80 | exports[`investigate createPlugin 1`] = ` 81 | Array [ 82 | Array [ 83 | ".gitignore.ejs", 84 | ".gitignore", 85 | ], 86 | Array [ 87 | "README.md.ejs", 88 | "README.md", 89 | ], 90 | Array [ 91 | "package.json.ejs", 92 | "package.json", 93 | ], 94 | ] 95 | `; 96 | -------------------------------------------------------------------------------- /__tests__/command_helpers/getSolidarityHelpers.ts: -------------------------------------------------------------------------------- 1 | import { isURI, loadFile, loadWebCheck } from '../../src/extensions/functions/getSolidarityHelpers' 2 | 3 | const context = require('mockContext') 4 | 5 | describe('Test helper functions', () => { 6 | describe('isURI', () => { 7 | test('isURI positive case', () => { 8 | expect(isURI('http://www.google.com')).toBeTruthy() 9 | expect(isURI('https://www.google.com')).toBeTruthy() 10 | }) 11 | 12 | test('isURI fail case', () => { 13 | expect(isURI('nachos')).toBeFalsy() 14 | expect(isURI('/nachos')).toBeFalsy() 15 | expect(isURI('./nachos')).toBeFalsy() 16 | }) 17 | }) 18 | 19 | describe('loadFile', () => { 20 | test('loadFile positive cases', () => { 21 | expect(loadFile(context, '__tests__/sandbox/solidarity_json')).toBeTruthy() 22 | expect(loadFile(context, '__tests__/sandbox/solidarity_json/.solidarity.json')).toBeTruthy() 23 | }) 24 | 25 | test('loadFile false cases', () => { 26 | expect(() => { 27 | loadFile(context, '__tests__/sandbox/fake_project') 28 | }).toThrow() 29 | expect(() => { 30 | loadFile(context, '__tests__/sandbox/fake_project/nope.solidarity') 31 | }).toThrow() 32 | }) 33 | }) 34 | 35 | // describe('loadModule', () => { 36 | // }) 37 | 38 | let originalTimeout 39 | describe('loadWebCheck', () => { 40 | beforeAll(() => { 41 | // These can be slow on CI 42 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 43 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 44 | }) 45 | 46 | afterAll(function() { 47 | // Fix timeout change 48 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 49 | }) 50 | 51 | test('loadWebCheck positive cases', async () => { 52 | expect( 53 | await loadWebCheck( 54 | context, 55 | 'https://raw.githubusercontent.com/infinitered/solidarity-stacks/master/stacks/react-native.solidarity' 56 | ) 57 | ).toBeTruthy() 58 | }) 59 | 60 | test('loadWebCheck false cases', async () => { 61 | await expect(loadWebCheck(context, 'https://raw.githubusercontent.com/fail/sauce')).rejects.toThrow() 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /__tests__/commands/solidarity.ts: -------------------------------------------------------------------------------- 1 | import checkCommand from '../../src/commands/solidarity' 2 | import { SolidarityOutputMode } from '../../src/types' 3 | 4 | const noConfigSolidarity = { 5 | checkRequirement: jest.fn(), 6 | getSolidaritySettings: jest.fn(() => ({})), 7 | setOutputMode: require('../../src/extensions/functions/setOutputMode'), // don't mock setOutputMode 8 | } 9 | 10 | const verboseConfigSolidarity = { 11 | ...noConfigSolidarity, 12 | getSolidaritySettings: jest.fn(() => ({ 13 | config: { 14 | output: 'verbose', 15 | }, 16 | })), 17 | } 18 | 19 | const configNoOutput = { 20 | ...noConfigSolidarity, 21 | getSolidaritySettings: jest.fn(() => ({ 22 | config: {}, 23 | })), 24 | } 25 | 26 | const mockContext = { 27 | outputMode: null, 28 | print: { 29 | error: jest.fn(), 30 | success: jest.fn(), 31 | }, 32 | parameters: { 33 | options: {}, 34 | }, 35 | solidarity: noConfigSolidarity, 36 | } 37 | 38 | const verboseMockContext = { 39 | ...mockContext, 40 | solidarity: verboseConfigSolidarity, 41 | } 42 | 43 | const noConfigMockContext = { 44 | ...mockContext, 45 | solidarity: configNoOutput, 46 | } 47 | 48 | test('Snapshot check default command', () => { 49 | expect(checkCommand).toMatchSnapshot() 50 | }) 51 | 52 | test('Enforce required properties', () => { 53 | expect(checkCommand.description).toBeTruthy() 54 | expect(checkCommand.run).toBeTruthy() 55 | expect(typeof checkCommand.run).toBe('function') 56 | }) 57 | 58 | test('check base solidarity run', async () => { 59 | const result = await checkCommand.run(mockContext) 60 | expect(result).toMatchSnapshot() 61 | expect(mockContext.outputMode).toBe(SolidarityOutputMode.MODERATE) 62 | expect(mockContext.solidarity.getSolidaritySettings.mock.calls.length).toBe(1) 63 | expect(mockContext.print.success.mock.calls.length).toBe(2) 64 | }) 65 | 66 | test('check solidarity verbose run', async () => { 67 | await checkCommand.run(verboseMockContext) 68 | expect(verboseMockContext.outputMode).toBe(SolidarityOutputMode.VERBOSE) 69 | }) 70 | 71 | test('check solidarity config no output run', async () => { 72 | await checkCommand.run(noConfigMockContext) 73 | expect(noConfigMockContext.outputMode).toBe(SolidarityOutputMode.MODERATE) 74 | }) 75 | -------------------------------------------------------------------------------- /__tests__/__mocks__/examplePlugin.ts: -------------------------------------------------------------------------------- 1 | module.exports = context => { 2 | // Register this plugin 3 | context.addPlugin({ 4 | name: 'Example Plugin', 5 | description: 'I help test plugins', 6 | snapshot: async context => { 7 | context.addedSnapshot = true 8 | }, 9 | rules: { 10 | checkThing: { 11 | check: async (rule, context) => { 12 | return { 13 | pass: true, 14 | message: 'Yeah good check!', 15 | } 16 | }, 17 | snapshot: async (rule, context) => [ 18 | { 19 | prop: 'semver', 20 | value: '12.0.0', 21 | }, 22 | ], 23 | report: async (rule, context, report) => { 24 | report.addCLI({ 25 | binary: 'node', 26 | version: '10', 27 | desired: '12', 28 | }) 29 | }, 30 | }, 31 | checkSecondThing: { 32 | check: async (rule, context) => { 33 | return { 34 | pass: false, 35 | message: 'Boooo failed check', 36 | } 37 | }, 38 | snapshot: async (rule, context) => [ 39 | { 40 | prop: 'semver', 41 | value: '12.0.0', 42 | }, 43 | { 44 | prop: 'nachos', 45 | value: true, 46 | }, 47 | ], 48 | }, 49 | checkThirdThing: { 50 | check: async (rule, context) => { 51 | return { 52 | pass: true, 53 | message: 'PAZZZZZ', 54 | } 55 | }, 56 | snapshot: async (rule, context) => [], 57 | report: async (rule, context, report) => { 58 | report.customRules.push({ 59 | title: 'Nachos', 60 | table: [ 61 | ['Title 1', 'Title 2', 'Title 3'], 62 | ['Some Val 1', 'Some Val 2', 'Some Val 3'], 63 | ['Dat Val 1', 'Dat Val 2', 'Dat Val 3'], 64 | ], 65 | }) 66 | }, 67 | }, 68 | checkFourthThing: { 69 | check: async (rule, context) => { 70 | return { 71 | pass: false, 72 | message: 'Boooo failed check', 73 | } 74 | }, 75 | }, 76 | }, 77 | }) 78 | 79 | return context 80 | } 81 | -------------------------------------------------------------------------------- /docs/customRuleCheck.md: -------------------------------------------------------------------------------- 1 | ## Custom Rule Check 2 | 3 | If you'd like to create your own rule, this can be done as of Solidarity v2+. This can be useful for any rule that cannot be easily constructed with the existing ruleset. Custom rules reside in you solidarity file among all other rules, and require the following: 4 | 5 | 1. `rule` must be set to 'custom' 6 | 1. `plugin` prop is present and set to the name of your plugin, and defined in your `addPlugin` function config. 7 | 1. `name` prop is present and set to the rule name you'd like to run. 8 | 9 | *All other properties added to your rule can be used by your custom plugin.* 10 | 11 | #### For example: A custom rule that always passes. 12 | We add this rule to any requirement in our Solidarity file: 13 | ```json 14 | { 15 | "rule": "custom", 16 | "plugin": "Always Pass", 17 | "name": "checkThing", 18 | "otherStuff": "nachos" 19 | }, 20 | ``` 21 | 22 | We will now create a plugin (in the location and structure identified in the Simplest Plugin section.) 23 | 24 | We add the property `rules` which identifies custom rules, and give that object a property with the name that will match our rule name. 25 | 26 | ```js 27 | module.exports = (context) => { 28 | // Register this plugin 29 | context.addPlugin({ 30 | name: 'Always Pass', 31 | description: 'Example plugin that always passes', 32 | rules: { 33 | // Here's my rule name 34 | checkThing: { 35 | check: async (rule, context) => { 36 | return { 37 | pass: true, 38 | message: 'Yeah good check!' 39 | } 40 | } 41 | } 42 | } 43 | }) 44 | } 45 | ``` 46 | 47 | As you can see, our `checkThing` prop has a function `check` that is an async function with the `rule`, and the `context` provided. The expected return is an object with two properties that answer "Did your custom check pass?" And "What message should we display?" 48 | 49 | >The message will only show for the appropriate user output setting. 50 | 51 | If `check` were to return an object like: 52 | ```js 53 | { 54 | pass: false, 55 | message: 'OH NO!' 56 | } 57 | ``` 58 | Then the check would forever fail. 59 | 60 | Inside your custom check function, you can perform any logic needed to identify and return a passing or failing state. 61 | 62 | Install your own plugin and your rule will suddenly come to life! 63 | 64 | ### [More About Plugins](/docs/plugins.md) 65 | -------------------------------------------------------------------------------- /src/extensions/functions/appendSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import { keys, propEq, filter, pipe, merge, findIndex, update } from 'ramda' 2 | 3 | const appendNewRequirement = (solidaritySettings, existingRequirementRules, newRequirement, newRequirementKey) => { 4 | return { 5 | ...solidaritySettings, 6 | requirements: { 7 | ...solidaritySettings.requirements, 8 | [newRequirementKey]: [...existingRequirementRules, ...newRequirement[newRequirementKey]], 9 | }, 10 | } 11 | } 12 | 13 | const updateExistingRule = (solidaritySettings, updatedRequirementRules, newRequirement, newRequirementKey) => { 14 | return { 15 | ...solidaritySettings, 16 | requirements: { 17 | ...solidaritySettings.requirements, 18 | [newRequirementKey]: updatedRequirementRules, 19 | }, 20 | } 21 | } 22 | 23 | module.exports = async (context, newRequirement) => { 24 | const { solidarity, parameters } = context 25 | const { getSolidaritySettings, ruleHandlers } = solidarity 26 | const { first } = parameters 27 | 28 | const solidaritySettings = await getSolidaritySettings(context) 29 | 30 | const newRequirementKey = keys(newRequirement)[0] 31 | const existingRequirementRules = solidaritySettings.requirements[newRequirementKey] || [] 32 | 33 | let existingRule 34 | let existingRuleIndex = -1 35 | 36 | if (existingRequirementRules.length) { 37 | const primaryRuleKey = ruleHandlers[first].key 38 | const filterFunction = pipe( 39 | obj => propEq('rule', first), 40 | obj => propEq(primaryRuleKey, newRequirement[newRequirementKey][0][primaryRuleKey]) 41 | ) 42 | // @ts-ignore - filterFunction not playing well with filter. 43 | existingRule = filter(filterFunction, existingRequirementRules) 44 | existingRuleIndex = findIndex( 45 | propEq(primaryRuleKey, newRequirement[newRequirementKey][0][primaryRuleKey]), 46 | existingRequirementRules 47 | ) 48 | } 49 | 50 | if (existingRule && existingRule[0] && existingRuleIndex !== -1) { 51 | const updatedRule = merge(existingRule[existingRuleIndex], newRequirement[newRequirementKey][0]) 52 | const updatedRequirementRules = update(existingRuleIndex, updatedRule)(existingRequirementRules) 53 | 54 | return updateExistingRule(solidaritySettings, updatedRequirementRules, newRequirement, newRequirementKey) 55 | } 56 | 57 | return appendNewRequirement(solidaritySettings, existingRequirementRules, newRequirement, newRequirementKey) 58 | } 59 | -------------------------------------------------------------------------------- /src/extensions/functions/createPlugin.ts: -------------------------------------------------------------------------------- 1 | module.exports = async context => { 2 | let files = [ 3 | ['.gitignore.ejs', '.gitignore'], 4 | ['README.md.ejs', 'README.md'], 5 | ['package.json.ejs', 'package.json'], 6 | ] 7 | const { print, template, prompt } = context 8 | const { colors } = print 9 | 10 | const answerPluginName = await prompt.ask({ 11 | type: 'input', 12 | name: 'plugin', 13 | message: 'Plugin name? (we will add the namespacing for you)', 14 | }) 15 | if (!answerPluginName.plugin) throw Error('A plugin requires a name') 16 | const pluginName = `solidarity-${answerPluginName.plugin.replace('solidarity-', '').toLowerCase()}` 17 | 18 | const description = await prompt.ask({ 19 | type: 'input', 20 | name: 'pluginDesc', 21 | message: 'Short plugin description (used in various places)', 22 | }) 23 | 24 | const ruleChoices = [ 25 | 'I do not want a generated rule file', 26 | 'Just a simple rule template', 27 | 'Template + optional rules', 28 | ] 29 | const answer = await prompt.ask({ 30 | type: 'select', 31 | name: 'ruleChoice', 32 | message: 'Your initial rule file template?', 33 | choices: ruleChoices, 34 | }) 35 | 36 | const noRuleTemplate = answer.ruleChoice === ruleChoices[0] 37 | if (noRuleTemplate) { 38 | files.push(['simple-plugin.js.ejs', `extensions/${pluginName}.js`]) 39 | } else if (answer.ruleChoice === ruleChoices[1]) { 40 | files.push(['rules-template.json.ejs', `templates/${pluginName}-template.json`]) 41 | files.push(['simple-plugin.js.ejs', `extensions/${pluginName}.js`]) 42 | } else if (answer.ruleChoice === ruleChoices[2]) { 43 | files.push(['rules-template.json.ejs', `templates/${pluginName}-template.json`]) 44 | files.push(['helpful-plugin.js.ejs', `extensions/${pluginName}.js`]) 45 | files.push(['addOptionalRules.js.ejs', `extensions/helpers/addOptionalRules.js`]) 46 | } 47 | 48 | const customRules = await prompt.confirm('Custom rules? (e.g. Rules other than basic types)') 49 | 50 | print.info(`Creating plugin ${pluginName}`) 51 | // copy files over 52 | files.map(fileSet => { 53 | template.generate({ 54 | template: fileSet[0], 55 | target: `${pluginName}/${fileSet[1]}`, 56 | props: { pluginName, customRules, description: description.pluginDesc, noRuleTemplate }, 57 | }) 58 | }) 59 | 60 | print.success(` 61 | Done! ${colors.magenta('\n\nPlugin Docs: https://infinitered.github.io/solidarity/#/docs/plugins')} 62 | `) 63 | 64 | // for tests really 65 | return files 66 | } 67 | -------------------------------------------------------------------------------- /docs/customRuleSnapshot.md: -------------------------------------------------------------------------------- 1 | ## Custom Rule Snapshot 2 | 3 | As you know, when a user runs `solidarity snapshot` and existing `.solidarity` file is in place, the versions of rules get updated to that environment. You can provide this same functionality in your custom plugin by implementing a `snapshot` function inside of your `rules` property. 4 | 5 | > This is supported as of Solidarity v2+ 6 | 7 | We'll use the same rule (always pass) that was identified in the [custom rule check](/docs/customRuleCheck.md) section. 8 | 9 | ```json 10 | { 11 | "rule": "custom", 12 | "plugin": "Always Pass", 13 | "name": "checkThing", 14 | "otherStuff": "nachos" 15 | }, 16 | ``` 17 | 18 | We will add an async function named `snapshot` 19 | 20 | ```js 21 | module.exports = (context) => { 22 | // Register this plugin 23 | context.addPlugin({ 24 | name: 'Always Pass', 25 | description: 'Example plugin that always passes', 26 | rules: { 27 | checkThing: { 28 | /* 29 | * commented out to keep focus 30 | * on snapshot for this section 31 | */ 32 | // check: async (rule, context) => { 33 | // return { 34 | // pass: true, 35 | // message: 'Yeah good check!' 36 | // } 37 | // }, 38 | snapshot: async (rule, context) => [ 39 | { 40 | prop: 'semver', 41 | value: '12.0.0' 42 | }, 43 | { 44 | prop: 'otherStuff', 45 | value: 'tacos' 46 | } 47 | ], 48 | } 49 | } 50 | }) 51 | } 52 | ``` 53 | 54 | As you can see, our `checkThing` now has a function named `snapshot` which is an async function with the `rule`, and the `context` provided. The expected return is an array of objects with two properties. The `prop` name, and `value` to set it to. 55 | 56 | Inside your custom snapshot function, you can perform any logic needed to identify and return how to update the rule. 57 | 58 | When our `snapshot` is run with the above, our original rule: 59 | ```json 60 | { 61 | "rule": "custom", 62 | "plugin": "Always Pass", 63 | "name": "checkThing", 64 | "otherStuff": "nachos" 65 | }, 66 | ``` 67 | 68 | Will be updated: adding the prop `semver` and updating the prop `otherStuff` 69 | 70 | ```json 71 | { 72 | "rule": "custom", 73 | "plugin": "Always Pass", 74 | "name": "checkThing", 75 | "otherStuff": "tacos", 76 | "semver": "12.0.0" 77 | }, 78 | ``` 79 | 80 | This helps round out the ability of your custom rule, to check AND update any number of props you choose. 81 | 82 | ### [More About Plugins](/docs/plugins.md) 83 | 84 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkShell.ts: -------------------------------------------------------------------------------- 1 | import { strings } from 'gluegun/toolbox' 2 | const checkShell: any = require('../../src/extensions/functions/checkShell') 3 | 4 | /** 5 | * Creates a mock gluegun environment in which `checkShell` runs. 6 | * 7 | * @param stdout The stdout to mock return 8 | * @param status The exit code to mock return (default: 0) 9 | */ 10 | const createContext = (stdout: string, status: number = 0) => ({ 11 | system: { 12 | spawn: jest.fn().mockReturnValue(Promise.resolve({ stdout, status })), 13 | }, 14 | strings, 15 | }) 16 | 17 | let originalTimeout 18 | 19 | describe('match', () => { 20 | beforeAll(() => { 21 | // These can be slow on CI 22 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 23 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000 24 | }) 25 | 26 | afterAll(function() { 27 | // Fix timeout change 28 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 29 | }) 30 | 31 | describe('seamingly just a string', () => { 32 | const context = createContext('hi') 33 | it('matches exact', async () => { 34 | expect(await checkShell({ match: 'hi' }, context)).toBe(undefined) 35 | }) 36 | 37 | it('detects no matches', async () => { 38 | await expect(checkShell({ match: 'bye' }, context)).rejects.toThrow() 39 | }) 40 | }) 41 | 42 | describe('regexp basics', () => { 43 | const context = createContext("Wow, you don't look a day over 100!") 44 | 45 | it('finds matches', async () => { 46 | expect(await checkShell({ match: '100!$' }, context)).toBe(undefined) 47 | }) 48 | 49 | it('detects no matches', async () => { 50 | await expect(checkShell({ match: '200!$' }, context)).rejects.toThrow() 51 | }) 52 | 53 | it('works with capture groups', async () => { 54 | expect(await checkShell({ match: '.*(look).*(100).*' }, context)).toBe(undefined) 55 | }) 56 | }) 57 | 58 | describe('capture group', () => { 59 | const context = createContext("Wow, you don't look a day over 100!") 60 | it('capture groups with ', async () => { 61 | expect(await checkShell({ match: '.*(look).*' }, context)).toBe(undefined) 62 | }) 63 | }) 64 | 65 | describe('crazy inputs', () => { 66 | const context = createContext('hi') 67 | // const expectBadMatch = (input: any) => async () => { 68 | // expect(await checkShell({ match: input }, context)).toBe(false) 69 | // } 70 | 71 | // test('null', expectBadMatch(null)) 72 | // test('undefined', expectBadMatch(undefined)) 73 | // test('number', expectBadMatch(69)) 74 | // test('boolean', expectBadMatch(true)) 75 | // test('object', expectBadMatch({ omg: 'lol' })) 76 | // test('array', expectBadMatch([1, 2, 3])) 77 | // test('function', expectBadMatch(() => '💩')) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /docs/cliOptions.md: -------------------------------------------------------------------------------- 1 | # Solidarity Command Options 2 | A listing of CLI options can be found by passing the `help` command to the CLI. 3 | 4 | ``` 5 | $ solidarity help 6 | 7 | Solidarity 8 | Commands 9 | 10 | solidarity Check environment against solidarity rules 11 | create (c) Displays this help 12 | help (h) Displays this help 13 | report (r) Report solidarity info about the current machine 14 | snapshot (s) Take a snapshot of the versions and store in solidarity file 15 | 16 | Flags 17 | 18 | --verbose (-a) Prints all detected info during solidarity check 19 | --moderate (-m) Prints failures in check or single success message 20 | --silent (-s) No output, just a return code of success/failure 21 | --solidarityFile (-f) Use given path to solidarity file for settings 22 | --module (-d) Search for a solidarity file in the given npm package 23 | --stack (-t) Use a known technology stack, and not the local file 24 | --fix Apply fixes to failing rules if fix is available on rule 25 | 26 | Solidarity is open source - https://github.com/infinitered/solidarity 27 | If you need additional help, join our Slack at http://community.infinite.red 28 | ``` 29 | 30 | Here we will go into detail on each option flag. 31 | 32 | ## verbose (-a) 33 | Passing `--verbose` or `-a` flags will modify output to be verbose. 34 | 35 | ## moderate (-m) 36 | Passing `--moderate` or `-m` flags will modify output to be moderate, meaning only failures exclusive or a single success will be printed. 37 | 38 | ## silent (-s) 39 | Passing `--silent` or `-s` flags will modify output to be silent, meaning no output will occur. You'll have to see if the command return is non-zero to see if it failed. 40 | 41 | ## solidarityFile (-f) 42 | Passing `--solidarityFile` or `-f` flags will direct the file to use for the solidarity check. 43 | 44 | > For example: `solidarity -solidarityFile ./my/special/file.json` will run the designated file instead of looking for a local folder Solidarity file. 45 | 46 | ## module (-m) 47 | Passing `--module` or `-m` flags will modify the designated solidarity file, to run a file found in the given `node_module` stack. 48 | 49 | > For example: `solidarity --module smoothReporter` will run the solidarity file in the root of the npm package `smoothReporter` instead of our own. 50 | 51 | ## stack (-t) 52 | Passing `--stack` or `-t` flags will make our stack look to GitHub for a well known tech stack. 53 | 54 | > For example: `solidarity --stack react-native` will check our machine if we are ready to run React Native projects, but not a specific React Native project. 55 | 56 | Stacks are community managed and found here: https://github.com/infinitered/solidarity-stacks 57 | 58 | ## fix 59 | Passing `--fix` flag will run fix scripts on any failing rules providing them. -------------------------------------------------------------------------------- /__tests__/command_helpers/appendSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import appendSolidaritySettings from '../../src/extensions/functions/appendSolidaritySettings' 2 | import { keys } from 'ramda' 3 | import solidarityExtension from '../../src/extensions/solidarity-extension' 4 | 5 | const context = require('mockContext') 6 | 7 | describe('appendSolidaritySettings', () => { 8 | beforeAll(() => { 9 | solidarityExtension(context) 10 | 11 | const solidaritySettings = { 12 | $schema: './solidaritySchema.json', 13 | requirements: { 14 | oneTest: [{ rule: 'cli' }, { rule: 'env', variable: 'ANDROID_HOME' }], 15 | twoTest: [{ rule: 'env' }], 16 | }, 17 | } 18 | 19 | context.solidarity = { 20 | ...context.solidarity, 21 | getSolidaritySettings: jest.fn(() => Promise.resolve(solidaritySettings)), 22 | } 23 | }) 24 | 25 | it('appends the given requirement to the existing settings', async () => { 26 | const newRequirement = { 27 | three: [{ rule: 'cli' }], 28 | } 29 | 30 | context.parameters = { 31 | first: 'cli', 32 | } 33 | 34 | const newSettings = await appendSolidaritySettings(context, newRequirement) 35 | 36 | expect(keys(newSettings.requirements).length).toEqual(3) 37 | expect(keys(newSettings.requirements.three).length).toEqual(1) 38 | }) 39 | 40 | it('will append the given requirement to and existing requirement', async () => { 41 | context.parameters = { 42 | first: 'cli', 43 | second: 'ruby', 44 | } 45 | 46 | const newRequirement = { 47 | twoTest: [{ rule: 'cli', binary: 'ruby' }], 48 | } 49 | 50 | let newSettings = await appendSolidaritySettings(context, newRequirement) 51 | 52 | expect(keys(newSettings.requirements).length).toEqual(2) 53 | expect(newSettings.requirements.twoTest.length).toEqual(2) 54 | 55 | expect(Array.isArray(newSettings.requirements.twoTest[0])).toBe(false) 56 | expect(Array.isArray(newSettings.requirements.twoTest[1])).toBe(false) 57 | }) 58 | 59 | describe('given a requirement with a prexisting rule', () => { 60 | it('should just merge the rule w/ the existing rule', async () => { 61 | context.parameters = { 62 | first: 'env', 63 | } 64 | 65 | const newRequirement = { 66 | oneTest: [ 67 | { 68 | rule: 'env', 69 | variable: 'ANDROID_HOME', 70 | error: 71 | 'The ANDROID_HOME environment variable must be set to your local SDK. Refer to getting started docs for help.', 72 | }, 73 | ], 74 | } 75 | 76 | let newSettings = await appendSolidaritySettings(context, newRequirement) 77 | 78 | expect(keys(newSettings.requirements).length).toEqual(2) 79 | expect(newSettings.requirements.oneTest.length).toEqual(2) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/commands/fix.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import { SolidarityOutputMode, SolidarityRequirementChunk, SolidarityRunContext } from '../types' 3 | import Listr from 'listr' 4 | 5 | module.exports = { 6 | alias: 'f', 7 | description: 'Applies all specified fixes for rules', 8 | run: async (context: SolidarityRunContext) => { 9 | // Node Modules Quirk 10 | require('../extensions/functions/quirksNodeModules') 11 | 12 | const { toPairs } = require('ramda') 13 | const { print, solidarity } = context 14 | const { checkRequirement, getSolidaritySettings, setOutputMode } = solidarity 15 | 16 | // get settings or error 17 | let solidaritySettings 18 | try { 19 | solidaritySettings = await getSolidaritySettings(context) 20 | } catch (e) { 21 | print.error(e.message || 'No Solidarity Settings Found') 22 | print.info( 23 | `Make sure you are in the correct folder or run ${print.colors.success( 24 | 'solidarity onboard' 25 | )} to create a .solidarity file for this project.` 26 | ) 27 | process.exit(3) 28 | } 29 | 30 | // Merge flags and configs 31 | context.outputMode = setOutputMode(context.parameters, solidaritySettings) 32 | // Adjust output depending on mode 33 | let listrSettings: Object = { concurrent: true, collapse: false, exitOnError: false } 34 | switch (context.outputMode) { 35 | case SolidarityOutputMode.SILENT: 36 | listrSettings = { ...listrSettings, renderer: 'silent' } 37 | break 38 | case SolidarityOutputMode.MODERATE: 39 | // have input clear itself 40 | listrSettings = { ...listrSettings, clearOutput: true } 41 | } 42 | 43 | // build Listr of checks 44 | const checks = new Listr( 45 | await toPairs(solidaritySettings.requirements).map((requirement: SolidarityRequirementChunk) => ({ 46 | title: requirement[0], 47 | task: async () => checkRequirement(requirement, context, true), 48 | })), 49 | listrSettings 50 | ) 51 | 52 | // run the array of promises in Listr 53 | await checks 54 | .run() 55 | .then(results => { 56 | const silentOutput = context.outputMode === SolidarityOutputMode.SILENT 57 | // Add empty line between final result if printing rule results 58 | if (!silentOutput) print.success('') 59 | if (!silentOutput) print.success(print.checkmark + ' Solidarity checks valid') 60 | }) 61 | .catch(_err => { 62 | const silentOutput = context.outputMode === SolidarityOutputMode.SILENT 63 | // Used to have message in the err, but that goes away with `exitOnError: false` so here's a generic one 64 | if (!silentOutput) print.error('Solidarity checks failed') 65 | process.exit(2) 66 | }) 67 | }, 68 | } as GluegunCommand 69 | -------------------------------------------------------------------------------- /src/extensions/functions/updateRequirement.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRequirement, SolidarityRunContext } from '../../types' 2 | 3 | module.exports = async ( 4 | requirement: SolidarityRequirement, 5 | settings: object, 6 | context: SolidarityRunContext 7 | ): Promise => { 8 | const findPluginInfo = require('./findPluginInfo') 9 | const { head, tail, pipe, flatten, map } = require('ramda') 10 | const checkCLIForUpdates = require('./checkCLIForUpdates') 11 | const skipRule = require('./skipRule') 12 | 13 | const { print } = context 14 | const requirementName = head(requirement) 15 | const rules = pipe(tail, flatten)(requirement) 16 | 17 | let ruleString = '' 18 | const spinner = print.spin(`Updating ${requirementName}`) 19 | 20 | // check each rule for requirement 21 | const ruleChecks = await map(async rule => { 22 | // skip if we can't update 23 | if (skipRule(rule)) return [] 24 | switch (rule.rule) { 25 | // Handle CLI rule update 26 | case 'cli': 27 | if (!rule.semver) return [] 28 | let updateResult 29 | try { 30 | updateResult = await checkCLIForUpdates(rule, context) 31 | } catch (e) { 32 | spinner.fail(e) 33 | return [] 34 | } 35 | const lineMessage = rule.line ? ` line ${rule.line} at` : '' 36 | ruleString = `Keep ${rule.binary}${lineMessage} ${rule.semver}` 37 | if (updateResult) { 38 | spinner.succeed(updateResult) 39 | return updateResult 40 | } else { 41 | spinner.succeed(ruleString) 42 | return [] 43 | } 44 | case 'custom': 45 | const customPluginRule = findPluginInfo(rule, context) 46 | if (customPluginRule.success) { 47 | // if they didn't implement a snapshot, then do nothing 48 | if (!customPluginRule.plugin.snapshot) return [] 49 | const customResult = await customPluginRule.plugin.snapshot(rule, context) 50 | const changes = customResult.map(patch => { 51 | rule[patch.prop] = patch.value 52 | return `'${patch.prop}' to '${patch.value}'` 53 | }) 54 | 55 | // report changes 56 | if (changes.length > 0) { 57 | const message = `Setting ${rule.name} ${changes.join(', ')}` 58 | spinner.succeed(print.colors.green(message)) 59 | return message 60 | } else { 61 | return [] 62 | } 63 | } else { 64 | spinner.fail(customPluginRule.message) 65 | return customPluginRule.message 66 | } 67 | default: 68 | return [] 69 | } 70 | }, rules) 71 | 72 | // Run all the rule checks for a requirement 73 | return Promise.all(ruleChecks) 74 | .then(results => { 75 | spinner.stop() 76 | return results as object[] 77 | }) 78 | .catch(err => print.error(err)) 79 | } 80 | -------------------------------------------------------------------------------- /src/extensions/functions/getSolidarityHelpers.ts: -------------------------------------------------------------------------------- 1 | import * as JSON5 from 'json5' 2 | import * as path from 'path' 3 | 4 | export const isURI = path => !!path.match(/\w+:(\/?\/?)[^\s]+/) 5 | 6 | export const loadFile = (context, filePath) => { 7 | const { filesystem } = context 8 | if (filesystem.exists(filePath) === 'file') { 9 | return JSON5.parse(filesystem.read(filePath)) 10 | } else if (filesystem.exists(filePath + path.sep + '.solidarity')) { 11 | return JSON5.parse(filesystem.read(filePath + path.sep + '.solidarity')) 12 | } else if (filesystem.exists(filePath + path.sep + '.solidarity.json')) { 13 | return JSON5.parse(filesystem.read(filePath + path.sep + '.solidarity.json')) 14 | } else { 15 | throw 'ERROR: There is no solidarity file at the given path' 16 | } 17 | } 18 | 19 | export const loadModule = (context, moduleName) => { 20 | const { filesystem } = context 21 | // We will search that module 22 | const filePath = path.join('node_modules', moduleName, '.solidarity') 23 | 24 | if (filesystem.exists(filePath)) { 25 | return JSON5.parse(filesystem.read(filePath)) 26 | } else if (filesystem.exists(filePath + '.json')) { 27 | return JSON5.parse(filesystem.read(filePath + '.json')) 28 | } else { 29 | throw 'ERROR: There is no solidarity file found with the given module' 30 | } 31 | } 32 | 33 | export const loadWebCheck = async (context, checkOption) => { 34 | const { print, http, parameters } = context 35 | const { options } = parameters 36 | const silentMode = options.silent || options.s 37 | const moderateMode = options.moderate || options.m 38 | const checkSpinner = silentMode || moderateMode ? null : print.spin(`Running check on ${checkOption}`) 39 | // the base URL is throw away, and will go away in next version of apisauce 40 | const api = http.create({ 41 | baseURL: 'https://api.github.com', 42 | timeout: 10000, // 10 seconds 43 | }) 44 | 45 | // Load check from web 46 | const checkURL = isURI(checkOption) 47 | ? checkOption 48 | : `https:\/\/raw.githubusercontent.com/infinitered/solidarity-stacks/master/stacks/${checkOption}.solidarity` 49 | const result = await api.get(checkURL) 50 | // console.log(result) 51 | if (result.ok) { 52 | checkSpinner && checkSpinner.succeed(`Found Stack: ${checkOption}`) 53 | // Convert strings to JSON5 objects 54 | const solidarityData = typeof result.data === 'string' ? JSON5.parse(result.data) : result.data 55 | return solidarityData 56 | } else { 57 | checkSpinner && checkSpinner.fail(`Unable to find a known tech stack for ${checkOption}`) 58 | if (!silentMode) { 59 | print.info(`Check https://github.com/infinitered/solidarity-stacks for options.`) 60 | } 61 | throw `ERROR: Request failed (${result.status} - ${result.problem})` 62 | } 63 | } 64 | 65 | module.exports = { 66 | isURI, 67 | loadFile, 68 | loadModule, 69 | loadWebCheck, 70 | } 71 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: zsh 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | recording: true 14 | title: Solidarity 15 | 16 | # Explicitly set the number of columns 17 | # or use `auto` to take the current 18 | # number of columns of your shell 19 | cols: auto 20 | 21 | # Explicitly set the number of rows 22 | # or use `auto` to take the current 23 | # number of rows of your shell 24 | rows: auto 25 | 26 | # Amount of times to repeat GIF 27 | # If value is -1, play once 28 | # If value is 0, loop indefinitely 29 | # If value is a positive number, loop n times 30 | repeat: 0 31 | 32 | # Quality 33 | # 1 - 100 34 | quality: 100 35 | 36 | # Delay between frames in ms 37 | # If the value is `auto` use the actual recording delays 38 | frameDelay: auto 39 | 40 | # Maximum delay between frames in ms 41 | # Ignored if the `frameDelay` isn't set to `auto` 42 | # Set to `auto` to prevent limiting the max idle time 43 | maxIdleTime: 2000 44 | 45 | # The surrounding frame box 46 | # The `type` can be null, window, floating, or solid` 47 | # To hide the title use the value null 48 | # Don't forget to add a backgroundColor style with a null as type 49 | frameBox: 50 | type: floating 51 | title: Solidarity 52 | style: 53 | border: 0px black solid 54 | # boxShadow: none 55 | # margin: 0px 56 | 57 | # Add a watermark image to the rendered gif 58 | # You need to specify an absolute path for 59 | # the image on your machine or a url, and you can also 60 | # add your own CSS styles 61 | watermark: 62 | imagePath: null 63 | style: 64 | position: absolute 65 | right: 15px 66 | bottom: 15px 67 | width: 100px 68 | opacity: 0.9 69 | 70 | # Cursor style can be one of 71 | # `block`, `underline`, or `bar` 72 | cursorStyle: block 73 | 74 | # Font family 75 | # You can use any font that is installed on your machine 76 | # in CSS-like syntax 77 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 78 | 79 | # The size of the font 80 | fontSize: 16 81 | 82 | # The height of lines 83 | lineHeight: 1 84 | 85 | # The spacing between letters 86 | letterSpacing: 0 87 | 88 | # Theme 89 | theme: 90 | background: "transparent" 91 | foreground: "#afafaf" 92 | cursor: "#c7c7c7" 93 | black: "#232628" 94 | red: "#fc4384" 95 | green: "#b3e33b" 96 | yellow: "#ffa727" 97 | blue: "#75dff2" 98 | magenta: "#ae89fe" 99 | cyan: "#708387" 100 | white: "#d5d5d0" 101 | brightBlack: "#626566" 102 | brightRed: "#ff7fac" 103 | brightGreen: "#c8ed71" 104 | brightYellow: "#ebdf86" 105 | brightBlue: "#75dff2" 106 | brightMagenta: "#ae89fe" 107 | brightCyan: "#b1c6ca" 108 | brightWhite: "#f9f9f4" 109 | -------------------------------------------------------------------------------- /src/extensions/functions/ruleHandlers.ts: -------------------------------------------------------------------------------- 1 | const buildCliRequirement = async (context, requirementName) => { 2 | const { parameters, solidarity, prompt, print } = context 3 | const { getVersion } = solidarity 4 | 5 | const rule = parameters.first 6 | const binary = parameters.second 7 | const requirement = { 8 | [requirementName]: [ 9 | { 10 | rule, 11 | binary, 12 | }, 13 | ], 14 | } 15 | 16 | const userAnswer = await prompt.ask({ 17 | name: 'enforceVersion', 18 | type: 'confirm', 19 | message: 'Would you like to enforce a version requirement?', 20 | }) 21 | 22 | if (userAnswer.enforceVersion) { 23 | return getVersion(requirement[requirementName][0], context) 24 | .then(sysVersion => { 25 | print.info(`Your system currently has version ${sysVersion}`) 26 | print.info(`Semver requirement for '${binary}' binary : ^${sysVersion}`) 27 | requirement[requirementName][0]['semver'] = sysVersion 28 | 29 | return requirement 30 | }) 31 | .catch(() => { 32 | print.error('Seems as though you do not have this binary installed. Please install this binary first') 33 | }) 34 | } 35 | 36 | return requirement 37 | } 38 | 39 | const buildEnvRequirement = (context, requirementName) => { 40 | const { parameters } = context 41 | 42 | const rule = parameters.first 43 | const variable = parameters.second 44 | 45 | return { 46 | [requirementName]: [ 47 | { 48 | rule, 49 | variable, 50 | }, 51 | ], 52 | } 53 | } 54 | 55 | const buildFileRequirement = (context, requirementName) => { 56 | const { parameters } = context 57 | 58 | const rule = parameters.first 59 | const location = parameters.second 60 | 61 | return { 62 | [requirementName]: [ 63 | { 64 | rule, 65 | location, 66 | }, 67 | ], 68 | } 69 | } 70 | 71 | const buildShellRequirement = async (context, requirementName) => { 72 | const { parameters, prompt } = context 73 | 74 | const rule = parameters.first 75 | const shellCommand = parameters.second 76 | 77 | if (rule && shellCommand) { 78 | const response = await prompt.ask({ 79 | name: 'shellMatch', 80 | type: 'input', 81 | message: 'What would you like the shell command to match on?', 82 | }) 83 | 84 | return { 85 | [requirementName]: [ 86 | { 87 | rule, 88 | command: shellCommand, 89 | match: response.shellMatch, 90 | }, 91 | ], 92 | } 93 | } 94 | } 95 | 96 | module.exports = { 97 | cli: { 98 | callback: buildCliRequirement, 99 | key: 'binary', 100 | }, 101 | env: { 102 | callback: buildEnvRequirement, 103 | key: 'variable', 104 | }, 105 | file: { 106 | callback: buildFileRequirement, 107 | key: 'location', 108 | }, 109 | dir: { 110 | callback: buildFileRequirement, 111 | key: 'location', 112 | }, 113 | shell: { 114 | callback: buildShellRequirement, 115 | key: 'command', 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /__tests__/command_helpers/checkRequirementPlugins.ts: -------------------------------------------------------------------------------- 1 | import checkRequirement from '../../src/extensions/functions/checkRequirement' 2 | import { toPairs } from 'ramda' 3 | const examplePlugin = require('examplePlugin') 4 | const mockContext = examplePlugin(require('mockContext')) 5 | 6 | describe('checkRequirement Plugins', () => { 7 | // test('successful CUSTOM rule check', async () => { 8 | // const rule = toPairs({ 9 | // TestRequirement: [{ rule: 'custom', plugin: 'Example Plugin', name: 'checkThing' }], 10 | // })[0] 11 | // const listrTask = await checkRequirement(rule, mockContext) 12 | // const result = await listrTask.storedInit[0].task() 13 | // expect(result).toEqual(true) 14 | // }) 15 | 16 | test('failed CUSTOM rule check', async () => { 17 | const rule = toPairs({ 18 | TestRequirement: [{ rule: 'custom', plugin: 'Example Plugin', name: 'checkSecondThing' }], 19 | })[0] 20 | const listrTask = await checkRequirement(rule, mockContext) 21 | await expect(listrTask.storedInit[0].task()).rejects.toThrow() 22 | // const result = await listrTask.storedInit[0].task() 23 | // expect(result).toEqual(['Boooo failed check']) 24 | }) 25 | 26 | // test('CUSTOM and missing check function OK', async () => { 27 | // mockContext.addPlugin({ 28 | // name: 'Empty Plugin', 29 | // description: 'I help test plugins', 30 | // rules: { 31 | // emptyDude: {}, 32 | // }, 33 | // }) 34 | // const rule = toPairs({ 35 | // TestRequirement: [{ rule: 'custom', plugin: 'Empty Plugin', name: 'emptyDude' }], 36 | // })[0] 37 | // const listrTask = await checkRequirement(rule, mockContext) 38 | // const result = await listrTask.storedInit[0].task() 39 | // expect(result).toEqual([[]]) 40 | // }) 41 | 42 | test('failed to find plugin', async () => { 43 | const rule = toPairs({ 44 | TestRequirement: [{ rule: 'custom', plugin: 'I do not exist', name: 'checkSecondThing' }], 45 | })[0] 46 | await expect(checkRequirement(rule, mockContext)).rejects.toThrow() 47 | // const listrTask = await checkRequirement(rule, mockContext) 48 | // const result = await listrTask.storedInit[0].task() 49 | // expect(result).toEqual(["Error: Plugin not found 'I do not exist'"]) 50 | }) 51 | 52 | test('failed to find check function', async () => { 53 | const rule = toPairs({ 54 | TestRequirement: [{ rule: 'custom', plugin: 'Example Plugin', name: 'notRealName' }], 55 | })[0] 56 | await expect(checkRequirement(rule, mockContext)).rejects.toThrow() 57 | // const listrTask = await checkRequirement(rule, mockContext) 58 | // const result = await listrTask.storedInit[0].task() 59 | // expect(result).toEqual(["Error: NOT FOUND: Custom rule from 'Example Plugin' plugin with check function 'notRealName'"]) 60 | }) 61 | 62 | // test('failed CUSTOM rule with custom message', async () => { 63 | // const error = 'CUSTOM ERROR' 64 | // const rule = toPairs({ 65 | // TestRequirement: [{ rule: 'custom', plugin: 'Example Plugin', name: 'checkSecondThing', error }], 66 | // })[0] 67 | // const listrTask = await checkRequirement(rule, mockContext) 68 | // const result = await listrTask.storedInit[0].task() 69 | // expect(result).toEqual([error]) 70 | // }) 71 | }) 72 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@infinite.red. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/extensions/functions/reviewRule.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SolidarityRequirementChunk, 3 | SolidarityRequirement, 4 | SolidarityRunContext, 5 | SolidarityRule, 6 | SolidarityReportResults, 7 | } from '../../types' 8 | 9 | module.exports = async ( 10 | requirement: SolidarityRequirementChunk, 11 | report: SolidarityReportResults, 12 | context: SolidarityRunContext 13 | ) => { 14 | const { tail, pipe, flatten, map } = require('ramda') 15 | const skipRule = require('./skipRule') 16 | const checkDir = require('./checkDir') 17 | const checkFile = require('./checkFile') 18 | const checkShell = require('./checkShell') 19 | const findPluginInfo = require('./findPluginInfo') 20 | 21 | const { print, solidarity } = context 22 | const { colors, checkmark, xmark } = print 23 | const prettyBool = async checkingFunction => { 24 | try { 25 | await checkingFunction() 26 | return checkmark + colors.green(' YES') 27 | } catch (e) { 28 | return xmark + colors.red(' NO') 29 | } 30 | } 31 | 32 | const rules: SolidarityRequirement = pipe( 33 | tail, 34 | // @ts-ignore - flatten will never get a string bc tail is called first 35 | flatten 36 | )(requirement) 37 | // check each rule for report 38 | const ruleChecks = map(async (rule: SolidarityRule) => { 39 | // Make sure this rule is active 40 | if (skipRule(rule)) return false 41 | 42 | switch (rule.rule) { 43 | // Handle CLI rule report 44 | case 'cli': 45 | let binaryVersion 46 | try { 47 | binaryVersion = await solidarity.getVersion(rule, context) 48 | } catch (_e) { 49 | binaryVersion = colors.red('*UNKNOWN*') 50 | } 51 | 52 | report.addCLI({ 53 | binary: rule.binary, 54 | version: binaryVersion, 55 | desired: rule.semver, 56 | }) 57 | break 58 | // Handle ENV rule report 59 | case 'env': 60 | const envValue = process.env[rule.variable] || colors.red('*UNDEFINED*') 61 | report.envRules.push([`$${rule.variable}`, envValue]) 62 | break 63 | // Handle dir rule report 64 | case 'directory': 65 | case 'dir': 66 | const dirExists = await prettyBool(async () => checkDir(rule, context)) 67 | report.filesystemRules.push([rule.location, 'Dir', dirExists]) 68 | break 69 | // Handle file rule report 70 | case 'file': 71 | const fileExists = await prettyBool(async () => checkFile(rule, context)) 72 | report.filesystemRules.push([rule.location, 'File', fileExists]) 73 | break 74 | case 'shell': 75 | const shellCheckPass = await prettyBool(async () => checkShell(rule, context)) 76 | report.shellRules.push([rule.command, rule.match, shellCheckPass]) 77 | break 78 | case 'custom': 79 | const customPluginRule = findPluginInfo(rule, context) 80 | if (customPluginRule.success) { 81 | // let plugin update the report 82 | if (customPluginRule.plugin.report) await customPluginRule.plugin.report(rule, context, report) 83 | } else { 84 | throw new Error(customPluginRule.message) 85 | } 86 | break 87 | default: 88 | throw new Error('Encountered unknown rule') 89 | } 90 | }, rules) 91 | 92 | // Run all the rule checks for a requirement 93 | return Promise.all(ruleChecks).then(results => { 94 | return results 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /docs/contributorsGuide.md: -------------------------------------------------------------------------------- 1 | # How do I get started contributing? 2 | 3 | ## A moment before you start contributing 4 | Be sure to mention that you're going to take on a task on the designated issue [on GitHub](https://github.com/infinitered/solidarity/issues). If there is no issue on GitHub, please create one first. This will limit the number of people who accidentally create PRs that do not fit the roadmap of the tool 5 | 6 | ## Running Locally 7 | To test this project, you'll need to pull it down and configure your local system to run the development version of Solidarity. We've made this as simple as possible! 8 | 9 | **To get started** 10 | * Pull down project to your local machine 11 | * `cd` into project root 12 | * Run `yarn` to install dependencies 13 | * Run `yarn welcome` to install the Solidarity CLI 14 | 15 | You can now type `solidarity` and it is running from the compiled TypeScript in your local project. 16 | 17 | > If you have problems, `solidarity` has a Solidarity file (SO META!). Just use the last stable version of the CLI from `npm` to check your environment. 18 | 19 | The project is written in TypeScript and the tests are in Jest. [They were originally in Ava](https://shift.infinite.red/switching-from-ava-to-jest-for-typescript-a6dac7d1712f). 20 | 21 | ### Updating Local Code 22 | Whenever you have modified the `/src` folder, you can run `yarn tsc` to compile the typescript into JS, and your global `solidarity` CLI will be updated. 23 | 24 | ### Working with Plugins 25 | If you're building your own plugin, you can begin your project, and then install it to any test-project by the path with `yarn add`. 26 | 27 | _e.g._ 28 | ```sh 29 | $ yarn add ~/playground/solidarity-fiesta 30 | ``` 31 | 32 | **OR** you can modify your local Solidarity to look for a special plugin directory by chaining the `plugin` function onto the `build()` results in index. 33 | 34 | _e.g._ 35 | ```js 36 | module.exports = async () => { 37 | // setup the runtime 38 | build() 39 | .brand('solidarity') 40 | .src(__dirname) 41 | .plugins('./node_modules', { matching: 'solidarity-*', hidden: true }) 42 | // I'm testing here!!!! non-permanent 43 | .plugin('../solidarity-fiesta') 44 | .create() 45 | .run() 46 | } 47 | ``` 48 | 49 | Both of these options allow you to quickly iterate on your plugin. For more information on how to create Solidarity plugins please see [plugins.md](plugins.md) 50 | 51 | ## Submitting a PR 52 | Here's a friendly checklist for submitting your PR 53 | 1. Make sure any extraneous files (_i.e._ build dependencies, IDE configs, etc.) are removed or added to the `.gitignore` 54 | 1. Make sure any files non-critical to the package are added to the `.npmignore` 55 | 1. Update docs with details of changes to the interface. This includes public interfaces, file locations, or changes in parameters. 56 | 1. Make sure you have tests covering your new or changed functionality. 57 | 1. Make sure `yarn test` passes. Otherwise, your PR cannot be merged. 58 | 1. Reference your GitHub issue in your final PR 59 | 60 | ## Changing or Adding Rules 61 | The system is tightly coupled to the existing rules, so making any new rules have a large impact on the existing contract and its enforcement. Note that you have identified the following have changed in accordance. 62 | 1. The TypeScript enumeration of rule types 63 | 1. The JSON Schema of rule types 64 | 1. The Documentation of rule types 65 | 1. Report configuration of rule types 66 | 1. Solidarity core configuration of rule types 67 | -------------------------------------------------------------------------------- /src/commands/solidarity.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import { SolidarityRequirementChunk, SolidarityOutputMode, SolidarityRunContext } from '../types' 3 | // Have to do this for tests rather than import 4 | const Listr = require('listr') 5 | 6 | namespace Solidarity { 7 | const { toPairs } = require('ramda') 8 | 9 | const checkForEscapeHatchFlags = async (context: SolidarityRunContext) => { 10 | const { print, parameters } = context 11 | const { options } = parameters 12 | if (!options) return 13 | if (options.help || options.h) { 14 | // Just looking for help 15 | print.printCommands(context) 16 | process.exit(0) 17 | } else if (options.version || options.v) { 18 | // Just looking for version 19 | print.info(require('../../package.json').version) 20 | process.exit(0) 21 | } 22 | } 23 | 24 | export const run = async (context: SolidarityRunContext) => { 25 | // Node Modules Quirk 26 | require('../extensions/functions/quirksNodeModules') 27 | // drop out fast in these situations 28 | await checkForEscapeHatchFlags(context) 29 | 30 | const { parameters, print, solidarity } = context 31 | const { checkRequirement, getSolidaritySettings, setOutputMode } = solidarity 32 | 33 | // get settings or error 34 | let solidaritySettings 35 | try { 36 | solidaritySettings = await getSolidaritySettings(context) 37 | } catch (e) { 38 | print.error(e.message || 'No Solidarity Settings Found') 39 | print.info( 40 | `Make sure you are in the correct folder or run ${print.colors.success( 41 | 'solidarity onboard' 42 | )} to create a .solidarity file for this project.` 43 | ) 44 | process.exit(3) 45 | } 46 | 47 | // Merge flags and configs 48 | context.outputMode = setOutputMode(context.parameters, solidaritySettings) 49 | // Adjust output depending on mode 50 | let listrSettings: Object = { concurrent: true, collapse: false, exitOnError: false } 51 | switch (context.outputMode) { 52 | case SolidarityOutputMode.SILENT: 53 | listrSettings = { ...listrSettings, renderer: 'silent' } 54 | break 55 | case SolidarityOutputMode.MODERATE: 56 | // have input clear itself 57 | listrSettings = { ...listrSettings, clearOutput: true } 58 | } 59 | 60 | // build Listr of checks 61 | const checks = new Listr( 62 | await toPairs(solidaritySettings.requirements).map((requirement: SolidarityRequirementChunk) => ({ 63 | title: requirement[0], 64 | task: async () => checkRequirement(requirement, context, Boolean(parameters.options.fix)), 65 | })), 66 | listrSettings 67 | ) 68 | 69 | // run the array of promises in Listr 70 | await checks 71 | .run() 72 | .then(results => { 73 | const silentOutput = context.outputMode === SolidarityOutputMode.SILENT 74 | // Add empty line between final result if printing rule results 75 | if (!silentOutput) print.success('') 76 | if (!silentOutput) print.success(print.checkmark + ' Solidarity checks valid') 77 | }) 78 | .catch(_err => { 79 | const silentOutput = context.outputMode === SolidarityOutputMode.SILENT 80 | // Used to have message in the err, but that goes away with `exitOnError: false` so here's a generic one 81 | if (!silentOutput) print.error('Solidarity checks failed') 82 | process.exit(2) 83 | }) 84 | } 85 | } 86 | 87 | // Export command 88 | module.exports = { 89 | description: 'Check environment against solidarity rules', 90 | run: Solidarity.run, 91 | } as GluegunCommand 92 | -------------------------------------------------------------------------------- /docs/simplePlugin.md: -------------------------------------------------------------------------------- 1 | ## The Simplest Plugin 2 | 3 | ### Example Problem 4 | Most of the time the plugin you're going to write will be so _dead simple_ that the art is in the rule set alone. 5 | 6 | Let's pretend we're working with a technology called Fiesta and this is our plugin rule-set: 7 | ```json 8 | "Fiesta": [ 9 | { "rule": "cli", "binary": "nachos" }, 10 | { "rule": "cli", "binary": "tacos" }, 11 | { "rule": "env", "variable": "HEART_SURGEON" } 12 | ] 13 | ``` 14 | _We have 1 requirement that has 3 rules. 2 CLIs to check, and 1 environment variable to verify the Fiesta is on._ 15 | 16 | To learn more about writing rules, take a look at Solidarity [options here](options.md). 17 | 18 | You may notice we haven't specified `semver` for any CLIs because, that's kinda personal to the project. **No problem!** we can place a holder at `0.0.0` and ask for a fresh snapshot of local versions. This will upgrade those numbers to whatever is currently working for that project. 19 | 20 | ```json 21 | "Fiesta": [ 22 | { "rule": "cli", "binary": "nachos", "semver": "0.0.0" }, 23 | { "rule": "cli", "binary": "tacos", "semver": "0.0.0" }, 24 | { "rule": "env", "variable": "HEART_SURGEON" } 25 | ] 26 | ``` 27 | 28 | > So our plugin needs to do 2 things: copy our rules, and then run snapshot... that's it. 29 | 30 | ### Example Plugin Solution 31 | We need 2 folders, and 2 files. 32 | ``` 33 | extensions/ 34 | | 35 | | _ fiesta.js 36 | 37 | templates/ 38 | | 39 | | _ fiesta-template.json 40 | ``` 41 | > **WARNING:** `extensions` folder must only contain extensions. Don't add spurious files here. 42 | 43 | As you may have guessed `fiesta-template.json` is our copy of the rule-set we designed above. `fiesta.js` is going to be how we register our plugin with Solidarity, and copy that file. 44 | 45 | Contents of `fiesta.js` 46 | ```js 47 | module.exports = (context) => { 48 | // Register this plugin 49 | context.addPlugin({ 50 | name: 'Fiesta Time', 51 |    description: 'Make sure your system is ready to party 🎉', 52 | snapshot: `fiesta-template.json` 53 | }) 54 | } 55 | ``` 56 | 57 | #### That's it! 58 | We've written our plugin! It was just 3 simple properties to get our plugin listed. Once our file is copied over, `snapshot` will automatically be called, and our `0.0.0` versions will be stamped with whatever is on the system. 59 | 60 | Let's review the 3 properties: 61 | 62 | | property | purpose | 63 | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | 64 | | name | Name of plugin presented to user | 65 | | description | Description given to user during listing of plugins. | 66 | | snapshot | If type `string`, it is the file to copy as the `.solidarity` file, and must be located in the `templates` folder. Otherwise it's the async function to run when this plugin is selected (_for intermediate plugins_) | 67 | 68 | **Plugin written!** Now you can publish your plugin to `npm`. 69 | 70 | Just make sure that it starts with `solidarity-` so the CLI knows to pick it up. We could publish our above plugin as `solidarity-fiesta` and when installed, our plugin would be listed as a Solidarity snapshot option. 71 | 72 | #### Congratulations! 73 | You can now write solidarity plugins for any tech stack. Be sure to list your plugin [here](pluginsList.md). 74 | 75 | ### [More About Plugins](/docs/plugins.md) 76 | -------------------------------------------------------------------------------- /src/extensions/functions/buildSpecificRequirement.ts: -------------------------------------------------------------------------------- 1 | import { SolidarityRunContext, SolidaritySettings } from '../../types' 2 | import { keys } from 'ramda' 3 | 4 | namespace buildSpecificRequirement { 5 | const requiredInputQuestion = async ({ name, message, prompt }) => { 6 | const result = await prompt.ask({ 7 | name, 8 | type: 'input', 9 | message, 10 | }) 11 | 12 | if (!result[name]) return Promise.reject('An input is required') 13 | return result 14 | } 15 | 16 | const resolveParameters = async ({ parameters, prompt, ruleHandlers }) => { 17 | const { first, second } = parameters 18 | if (second) { 19 | return Promise.resolve({ whatRule: second }) 20 | } 21 | 22 | const message = `What's the ${first} ${ruleHandlers[first].key} you'd like to add a rule for?` 23 | 24 | return requiredInputQuestion({ 25 | name: 'whatRule', 26 | message, 27 | prompt, 28 | }) 29 | } 30 | 31 | const getRequirementNames = (solidaritySettings: SolidaritySettings): Array => 32 | keys(solidaritySettings.requirements) 33 | 34 | const chooseRequirement = async (prompt, solidaritySettings: SolidaritySettings): Promise => { 35 | const shouldMakeNewRequirement = await prompt.ask({ 36 | name: 'makeNewRequirement', 37 | type: 'confirm', 38 | message: 'Would you like to create a new requirement set?', 39 | }) 40 | 41 | let requirementName 42 | 43 | if (shouldMakeNewRequirement.makeNewRequirement) { 44 | const answer = await requiredInputQuestion({ 45 | name: 'newRequirement', 46 | message: 'What would you like to call this new requirement?', 47 | prompt, 48 | }).catch(error => { 49 | return Promise.reject(error) 50 | }) 51 | requirementName = answer.newRequirement 52 | } else { 53 | const requirementOptions = getRequirementNames(solidaritySettings) 54 | const answer = await prompt.ask({ 55 | name: 'selectedRequirement', 56 | message: 'Which of the above technology snapshots will you use for this project?', 57 | type: 'select', 58 | choices: requirementOptions, 59 | }) 60 | requirementName = answer.selectedRequirement 61 | } 62 | 63 | return requirementName 64 | } 65 | 66 | const constructRequirment = async context => { 67 | const { parameters, prompt, solidarity } = context 68 | const { getSolidaritySettings, ruleHandlers } = solidarity 69 | const solidaritySettings = await getSolidaritySettings(context) 70 | 71 | const userAnswer = await prompt.ask({ 72 | name: 'addNewRule', 73 | type: 'confirm', 74 | message: `Would you like to add the ${parameters.first} '${parameters.second}' to your Solidarity file?`, 75 | }) 76 | 77 | if (userAnswer.addNewRule) { 78 | // maybe ask about setting up the new rule w/ a specific version? 79 | const requirementName = await chooseRequirement(prompt, solidaritySettings) 80 | return ruleHandlers[parameters.first].callback(context, requirementName) 81 | } else { 82 | return Promise.reject('Rule not added.') 83 | } 84 | } 85 | 86 | export const run = async (context: SolidarityRunContext) => { 87 | const { parameters, prompt, solidarity } = context 88 | const { first } = parameters 89 | const { ruleHandlers } = solidarity 90 | const resolvedParam = await resolveParameters({ parameters, prompt, ruleHandlers }).catch(() => { 91 | return Promise.reject('Missing required parameters.') 92 | }) 93 | 94 | return constructRequirment({ 95 | ...context, 96 | parameters: { 97 | ...parameters, 98 | first, 99 | second: resolvedParam.whatRule, 100 | }, 101 | }) 102 | } 103 | } 104 | 105 | module.exports = buildSpecificRequirement.run 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidarity", 3 | "version": "3.0.4", 4 | "description": "Make sure all React Native dependencies are uniform across machines", 5 | "homepage": "https://infinitered.github.io/solidarity/", 6 | "repository": "https://github.com/infinitered/solidarity", 7 | "bin": { 8 | "solidarity": "bin/solidarity" 9 | }, 10 | "types": "dist/types/index.d.ts", 11 | "scripts": { 12 | "start": "node bin/solidarity", 13 | "contributors:add": "all-contributors add", 14 | "contributors:generate": "all-contributors generate", 15 | "build": "yarn format && tsc", 16 | "format": "prettier --write \"**/*.ts\" -l \"warn\" && tslint -p . --fix", 17 | "shipit": "tsc && yarn copyTemplates && np", 18 | "test": "jest && yarn test:extras", 19 | "test:ci": "jest --ci --runInBand && yarn test:extras", 20 | "test:extras": "tslint -p . && yarn madge --extensions ts --circular src/", 21 | "lint": "tslint -p .", 22 | "newclear": "rm -rf node_modules && yarn && tsc", 23 | "watch": "jest --watch", 24 | "coverage": "jest --no-cache --ci --coverage", 25 | "snapupdate": "jest -u", 26 | "welcome": "yarn tsc && npm link", 27 | "serve:docs": "docsify serve docs", 28 | "maintenence": "yarn tryupdate && typesync", 29 | "addTypes": "typesync", 30 | "tryupdate": "updtr", 31 | "copyTemplates": "cp -R ./src/templates ./dist/", 32 | "recordDemo": "ZDOTDIR=/Users/gantman/recording terminalizer record pretty -c ./config.yml", 33 | "playDemo": "terminalizer play pretty", 34 | "renderDemo": "terminalizer render pretty", 35 | "cleanDemo": "rm -rf render*.gif" 36 | }, 37 | "author": "Gant Laborde", 38 | "license": "MIT", 39 | "dependencies": { 40 | "callsite": "^1.0.0", 41 | "envinfo": "7.5.0", 42 | "gluegun": "^5.1.6", 43 | "json5": "2.1.1", 44 | "listr": "^0.14.3", 45 | "minimist": "^1.2.0", 46 | "ramda": "0.27.0", 47 | "resolve-dir": "^1.0.1" 48 | }, 49 | "devDependencies": { 50 | "@types/callsite": "^1.0.30", 51 | "@types/execa": "2.0.0", 52 | "@types/jasmine": "3.5.8", 53 | "@types/jest": "25.1.3", 54 | "@types/json5": "0.0.30", 55 | "@types/listr": "^0.14.2", 56 | "@types/minimist": "^1.2.0", 57 | "@types/node": "12.6.6", 58 | "@types/prettier": "1.19.0", 59 | "@types/ramda": "0.26.43", 60 | "@types/tempy": "0.3.0", 61 | "ajv": "6.12.0", 62 | "all-contributors-cli": "6.14.0", 63 | "babel-eslint": "10.1.0", 64 | "codecov.io": "^0.1.6", 65 | "coveralls": "3.0.9", 66 | "danger": "9.2.10", 67 | "danger-plugin-spellcheck": "1.5.0", 68 | "docsify-cli": "4.4.0", 69 | "execa": "1.0.0", 70 | "jest": "22.4.2", 71 | "madge": "3.7.0", 72 | "np": "6.2.0", 73 | "prettier": "1.19.1", 74 | "tempy": "0.4.0", 75 | "ts-jest": "22.4.6", 76 | "ts-node": "8.6.2", 77 | "tslint": "6.0.0", 78 | "tslint-config-standard": "9.0.0", 79 | "typescript": "3.8.3", 80 | "typesync": "0.6.1", 81 | "updtr": "^3.1.0" 82 | }, 83 | "prettier": { 84 | "printWidth": 120, 85 | "semi": false, 86 | "singleQuote": true, 87 | "trailingComma": "es5" 88 | }, 89 | "jest": { 90 | "testURL": "http://localhost/", 91 | "transform": { 92 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 93 | }, 94 | "setupFiles": [ 95 | "./__tests__/setup.ts" 96 | ], 97 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 98 | "testPathIgnorePatterns": [ 99 | "__mocks__", 100 | "setup.ts" 101 | ], 102 | "coverageThreshold": { 103 | "global": { 104 | "statements": 84, 105 | "branches": 77, 106 | "lines": 85, 107 | "functions": 81 108 | } 109 | }, 110 | "moduleFileExtensions": [ 111 | "ts", 112 | "tsx", 113 | "js", 114 | "jsx", 115 | "json" 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /docs/customRuleReport.md: -------------------------------------------------------------------------------- 1 | ## Custom Rule Report 2 | 3 | As you know, when a user runs `solidarity report` as of Solidarity 1.1+, a GitHub friendly report is generated to the screen. You can provide this same functionality in your custom plugin by implementing a `report` function inside of your `rules` property. 4 | 5 | > This is supported as of Solidarity v2+ 6 | 7 | You can add to any report table or create your own. The known report tables are: 8 | * basicInfo - Reports the system and report results 9 | * cliRules - Report any system CLI results 10 | * envRules - Report Environment Variable results 11 | * filesystemRules - Report for existence of filesystem results 12 | * shellRules - Report for any shell commands and their matches 13 | 14 | There is another report called: **customRules** which holds tables created by custom plugins, which will show in the report. 15 | 16 | **Let's do two examples:** 17 | 1. Example 1: Adding to the `cliRules` table 18 | 1. Example 2: Adding to the `customRules` array of tables 19 | 20 | We'll use the same rule (always pass) that was identified in the [custom rule check](/docs/customRuleCheck.md) section. 21 | 22 | We will add an async function named `report` 23 | 24 | ```js 25 | module.exports = (context) => { 26 | // Register this plugin 27 | context.addPlugin({ 28 | name: 'Always Pass', 29 | description: 'Example plugin that always passes', 30 | rules: { 31 | checkThing: { 32 | // report for your custom rule goes here 33 | report: async (rule, context, report) => { 34 | // CODE GOES HERE 35 | } 36 | } 37 | } 38 | }) 39 | } 40 | ``` 41 | 42 | Just like other custom rule sections, the function gets `rule`, and `context`. Additionally the report object is passed into the function for mutation. No return value is expected. 43 | 44 | ### Example 1: Adding to the `cliRules` table 45 | Let's add to the CLI report. The CLI report commonly has `Binary, Location, Version, Desired` as values, so pushing an array of 4 items will add a new row to your report. 46 | 47 | ```js 48 | ... 49 | report: async (rule, context, report) => { 50 | report.cliRules.push([ 51 | 'crazed_monkey', 52 | '/etc/useless/crazed_monkey', 53 | '10', 54 | '12' 55 | ]) 56 | } 57 | ... 58 | ``` 59 | 60 | Now your row will show in the CLI section of the report. As a bonus, there is a simple helper function attached to the report named `addCLI`, which takes an object with the props `binary`, `version`, and optionally `desired`. It will then figure out the location for you, and if no desired is passed, the green `*ANY*` is shown. The above report simplified with `addCLI` looks like so: 61 | 62 | ```js 63 | ... 64 | report: async (rule, context, report) => { 65 | report.addCLI({ 66 | binary: 'crazed_monkey', 67 | version: '10' 68 | }) 69 | } 70 | ... 71 | ``` 72 | 73 | The output inserts the results in the CLI section. 74 | ![crazed_monkey](https://i.imgur.com/iR8uqxm.png) 75 | 76 | ### Example 2: Adding to the `customRules` array of tables 77 | 78 | If any of the existing tables are not fit for your report needs, you can add tables via your custom plugin. Let's add a table by pushing a table object on the `customRules` array. 79 | 80 | The table object structure is simple: `title` for the table title, and then a 2D array of rows. 81 | 82 | ```js 83 | ... 84 | report: async (rule, context, report) => { 85 | // add custom table 86 | report.customRules.push({ 87 | title: 'Menu', 88 | table: [ 89 | ['Food', 'Price', 'Value'], 90 | ['Taco', '1.09', 'Yummy!'], 91 | ['Burrito', '1.89', 'Oh yeahhh'], 92 | ] 93 | }) 94 | } 95 | ... 96 | ``` 97 | 98 | The result when running `solidarity report` will be a whole new table printed in the results! 99 | 100 | ![custom_menu](https://i.imgur.com/MWUiWXI.png) 101 | 102 | #### 🎉 TADAAAA! You're able to add to reports in any way you'd like! 103 | 104 | ### [More About Plugins](/docs/plugins.md) 105 | 106 | -------------------------------------------------------------------------------- /__tests__/command_helpers/reviewRule.ts: -------------------------------------------------------------------------------- 1 | import reviewRule from '../../src/extensions/functions/reviewRule' 2 | import { SolidarityRunContext, SolidarityReportResults } from '../../src/types' 3 | import { createReport } from '../../src/extensions/functions/solidarityReport' 4 | const examplePlugin = require('examplePlugin') 5 | let mockContext: SolidarityRunContext 6 | let reportResults: SolidarityReportResults 7 | describe('reviewRule', () => { 8 | beforeEach(async () => { 9 | // fresh mock context 10 | mockContext = examplePlugin(require('mockContext')) 11 | reportResults = await createReport(mockContext) 12 | }) 13 | 14 | describe('when rule: cli', () => { 15 | test('rule gets added', async () => { 16 | const rule = ['NPM', [{ rule: 'cli', binary: 'npm' }]] 17 | 18 | await reviewRule(rule, reportResults, mockContext) 19 | // CLI rule was added 20 | expect(reportResults.cliRules.length).toBe(2) 21 | }) 22 | }) 23 | 24 | describe('when rule: env', () => { 25 | test('rule gets added', async () => { 26 | const rule = ['ANDROID', [{ rule: 'env', value: 'ANDROID_HOME' }]] 27 | 28 | reviewRule(rule, reportResults, mockContext) 29 | // CLI rule was added 30 | expect(reportResults.envRules.length).toBe(2) 31 | }) 32 | }) 33 | 34 | // TODO: Fix these two tests 35 | // describe('when rule: dir', () => { 36 | // test('rule gets added', async () => { 37 | // const rule = ['DIRECTORY', [{ rule: 'dir', location: './' }]] 38 | 39 | // reviewRule(rule, reportResults, mockContext) 40 | // // dir rule was added 41 | // expect(reportResults.filesystemRules.length).toBe(2) 42 | // }) 43 | // }) 44 | 45 | // describe('when rule: file', () => { 46 | // test('rule gets added', async () => { 47 | // const rule = ['FILE', [{ rule: 'file', location: './package.json' }]] 48 | // const ruleCount = reportResults.filesystemRules.length 49 | // reviewRule(rule, reportResults, mockContext) 50 | // // file rule was added 51 | // expect(reportResults.filesystemRules.length).toBe(ruleCount + 1) 52 | // }) 53 | // }) 54 | 55 | describe('when rule: shell', () => { 56 | test('rule gets added', async () => { 57 | const rule = ['SHELL', [{ rule: 'shell', command: 'ls', match: '.+' }]] 58 | 59 | reviewRule(rule, reportResults, mockContext) 60 | // SHELL rule was added 61 | expect(reportResults.shellRules.length).toBe(1) 62 | }) 63 | }) 64 | 65 | // Custom rule test 66 | describe('when rule: custom', () => { 67 | test('rule gets added', async () => { 68 | const rule = ['CUSTOM', [{ rule: 'custom', plugin: 'Example Plugin', name: 'checkThing' }]] 69 | 70 | expect(reportResults.cliRules.length).toBe(1) 71 | await reviewRule(rule, reportResults, mockContext) 72 | // CUSTOM rule (which adds CLI report) was added 73 | expect(reportResults.cliRules.length).toBe(2) 74 | }) 75 | 76 | test('does nothing when no report exists', async () => { 77 | const rule = ['CUSTOM', [{ rule: 'custom', plugin: 'Example Plugin', name: 'checkSecondThing' }]] 78 | 79 | expect(reportResults.cliRules.length).toBe(1) 80 | await reviewRule(rule, reportResults, mockContext) 81 | // should not change rules 82 | expect(reportResults.cliRules.length).toBe(1) 83 | }) 84 | 85 | test('Errors when plugin doesn not exist', async () => { 86 | const rule = ['CUSTOM', [{ rule: 'custom', plugin: 'FAKE', name: 'checkSecondThing' }]] 87 | 88 | // Async error snapshots (not simple) 89 | try { 90 | await reviewRule(rule, reportResults, mockContext) 91 | fail('Unknown rule should have errored') 92 | } catch (e) { 93 | expect(e).toMatchSnapshot() 94 | } 95 | }) 96 | }) 97 | 98 | describe('when rule: unknown', () => { 99 | test('rule gets added', async () => { 100 | const rule = ['UNKNOWN', [{ rule: 'UNKNOWN', command: 'ls', match: '.+' }]] 101 | 102 | // Async error snapshots (not simple) 103 | try { 104 | await reviewRule(rule, reportResults, mockContext) 105 | fail('Unknown rule should have errored') 106 | } catch (e) { 107 | expect(e).toMatchSnapshot() 108 | } 109 | }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /solidaritySchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Solidarity", 4 | "description": "A rule-set and config for the Solidarity JSON checker", 5 | "type": "object", 6 | "properties": { 7 | "config": { 8 | "output": { 9 | "description": "Identify what kind output should happen when a check is called", 10 | "type": "string", 11 | "enum": ["moderate", "verbose", "silent"] 12 | } 13 | }, 14 | "requirements": { 15 | "description": "List of requirement rules for your particular environment", 16 | "type": "object", 17 | "additionalProperties": { 18 | "type": "array", 19 | "items": { 20 | "type": "object", 21 | "oneOf": [ 22 | { "$ref": "#/definitions/cli" }, 23 | { "$ref": "#/definitions/dir" }, 24 | { "$ref": "#/definitions/file" }, 25 | { "$ref": "#/definitions/env" }, 26 | { "$ref": "#/definitions/shell" }, 27 | { "$ref": "#/definitions/custom" } 28 | ] 29 | }, 30 | "minItems": 1, 31 | "uniqueItems": true 32 | } 33 | } 34 | }, 35 | "required": ["requirements"], 36 | "definitions": { 37 | "cli": { 38 | "description": "CLI Rule", 39 | "type": "object", 40 | "properties": { 41 | "rule": { "enum": [ "cli" ] }, 42 | "binary": { 43 | "type": "string" 44 | }, 45 | "semver": { "type": "string"}, 46 | "version": { "type": "string"}, 47 | "line": { 48 | "type": ["string", "integer"] 49 | }, 50 | "matchIndex": { "type": "integer" }, 51 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 52 | "error": { "type": "string"}, 53 | "ci": { "type": "boolean" }, 54 | "fix": { "type": "string"} 55 | }, 56 | "required": ["rule", "binary"] 57 | }, 58 | "dir": { 59 | "description": "Dir Rule", 60 | "type": "object", 61 | "properties": { 62 | "rule": { "enum": [ "dir", "directory" ] }, 63 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 64 | "error": { "type": "string"}, 65 | "ci": { "type": "boolean" }, 66 | "fix": { "type": "string"} 67 | }, 68 | "required": ["rule", "location"] 69 | }, 70 | "file": { 71 | "description": "File Rule", 72 | "type": "object", 73 | "properties": { 74 | "rule": { "enum": [ "file" ] }, 75 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 76 | "error": { "type": "string"}, 77 | "ci": { "type": "boolean" }, 78 | "fix": { "type": "string"} 79 | }, 80 | "required": ["rule", "location"] 81 | }, 82 | "env": { 83 | "description": "ENV Rule", 84 | "type": "object", 85 | "properties": { 86 | "rule": { "enum": [ "env" ] }, 87 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 88 | "error": { "type": "string"}, 89 | "ci": { "type": "boolean" }, 90 | "fix": { "type": "string"} 91 | }, 92 | "required": ["rule", "variable"] 93 | }, 94 | "shell": { 95 | "description": "Shell Rule", 96 | "type": "object", 97 | "properties": { 98 | "rule": { "enum": [ "shell" ] }, 99 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 100 | "error": { "type": "string" }, 101 | "ci": { "type": "boolean" }, 102 | "match": { "type": "string", "description": "A regexp to search the output." }, 103 | "fix": { "type": "string"} 104 | }, 105 | "required": ["rule", "match"] 106 | }, 107 | "custom": { 108 | "description": "Custom Rule", 109 | "type": "object", 110 | "additionalProperties": true, 111 | "properties": { 112 | "rule": { "enum": [ "custom" ] }, 113 | "plugin": { "type": "string" }, 114 | "name": { "type": "string" }, 115 | "platform": { "enum": ["darwin", "macos", "freebsd", "linux", "sunos", "win32", "windows"] }, 116 | "error": { "type": "string" }, 117 | "ci": { "type": "boolean" }, 118 | "match": { "type": "string", "description": "A regexp to search the output." }, 119 | "fix": { "type": "string"} 120 | }, 121 | "required": ["rule", "plugin", "name"] 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /docs/vendor/search.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function e(e){var n={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return String(e).replace(/[&<>"'\/]/g,function(e){return n[e]})}function n(e){var n=[];return h.dom.findAll("a:not([data-nosearch])").map(function(t){var o=t.href,i=t.getAttribute("href"),r=e.parse(o).path;r&&-1===n.indexOf(r)&&!Docsify.util.isAbsolutePath(i)&&n.push(r)}),n}function t(e){localStorage.setItem("docsify.search.expires",Date.now()+e),localStorage.setItem("docsify.search.index",JSON.stringify(g))}function o(e,n,t,o){void 0===n&&(n="");var i,r=window.marked.lexer(n),a=window.Docsify.slugify,s={};return r.forEach(function(n){if("heading"===n.type&&n.depth<=o)i=t.toURL(e,{id:a(n.text)}),s[i]={slug:i,title:n.text,body:""};else{if(!i)return;s[i]?s[i].body?s[i].body+="\n"+(n.text||""):s[i].body=n.text:s[i]={slug:i,title:"",body:""}}}),a.clear(),s}function i(n){var t=[],o=[];Object.keys(g).forEach(function(e){o=o.concat(Object.keys(g[e]).map(function(n){return g[e][n]}))}),n=[].concat(n,n.trim().split(/[\s\-\,\\\/]+/));for(var i=0;il.length&&(d=l.length);var p="..."+e(l).substring(f,d).replace(o,''+n+"")+"...";s+=p}}),a)){var d={title:e(c),content:s,url:f};t.push(d)}}(i);return t}function r(e,i){h=Docsify;var r="auto"===e.paths,a=localStorage.getItem("docsify.search.expires")
',o=Docsify.dom.create("div",t),i=Docsify.dom.find("aside");Docsify.dom.toggleClass(o,"search"),Docsify.dom.before(i,o)}function c(e){var n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,".results-panel");if(!e)return t.classList.remove("show"),void(t.innerHTML="");var o=i(e),r="";o.forEach(function(e){r+='
\n

'+e.title+"

\n

"+e.content+"

\n
"}),t.classList.add("show"),t.innerHTML=r||'

'+y+"

"}function l(){var e,n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,"input");Docsify.dom.on(n,"click",function(e){return"A"!==e.target.tagName&&e.stopPropagation()}),Docsify.dom.on(t,"input",function(n){clearTimeout(e),e=setTimeout(function(e){return c(n.target.value.trim())},100)})}function f(e,n){var t=Docsify.dom.getNode('.search input[type="search"]');if(t)if("string"==typeof e)t.placeholder=e;else{var o=Object.keys(e).filter(function(e){return n.indexOf(e)>-1})[0];t.placeholder=e[o]}}function d(e,n){if("string"==typeof e)y=e;else{var t=Object.keys(e).filter(function(e){return n.indexOf(e)>-1})[0];y=e[t]}}function p(e,n){var t=n.router.parse().query.s;a(),s(e,t),l(),t&&setTimeout(function(e){return c(t)},500)}function u(e,n){f(e.placeholder,n.route.path),d(e.noData,n.route.path)}var h,g={},y="",m={placeholder:"Type to search",noData:"No Results!",paths:"auto",depth:2,maxAge:864e5},v=function(e,n){var t=Docsify.util,o=n.config.search||m;Array.isArray(o)?m.paths=o:"object"==typeof o&&(m.paths=Array.isArray(o.paths)?o.paths:"auto",m.maxAge=t.isPrimitive(o.maxAge)?o.maxAge:m.maxAge,m.placeholder=o.placeholder||m.placeholder,m.noData=o.noData||m.noData,m.depth=o.depth||m.depth);var i="auto"===m.paths;e.mounted(function(e){p(m,n),!i&&r(m,n)}),e.doneEach(function(e){u(m,n),i&&r(m,n)})};$docsify.plugins=[].concat(v,$docsify.plugins)}(); 2 | -------------------------------------------------------------------------------- /src/extensions/functions/checkRequirement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SolidarityRule, 3 | SolidarityRequirementChunk, 4 | SolidarityRequirement, 5 | SolidarityRunContext, 6 | SolidarityChecker, 7 | } from '../../types' 8 | module.exports = async ( 9 | requirement: SolidarityRequirementChunk, 10 | context: SolidarityRunContext, 11 | shouldRunFix = false 12 | ): Promise => { 13 | const checkCLI = require('./checkCLI') 14 | const checkENV = require('./checkENV') 15 | const checkDir = require('./checkDir') 16 | const checkFile = require('./checkFile') 17 | const checkShell = require('./checkShell') 18 | const skipRule = require('./skipRule') 19 | const findPluginInfo = require('./findPluginInfo') 20 | 21 | // Have to do this for tests rather than import 22 | const Listr = require('listr') 23 | 24 | const { head, tail, pipe, flatten } = require('ramda') 25 | 26 | const requirementName: string = head(requirement) 27 | const rules: SolidarityRequirement = pipe(tail, flatten)(requirement) 28 | 29 | const taskWithFix = (checker: SolidarityChecker, rule: SolidarityRule, context: SolidarityRunContext) => async () => { 30 | if (!shouldRunFix) return checker(rule, context) 31 | try { 32 | const result = await checker(rule, context) 33 | return result 34 | } catch (error) { 35 | if (rule.fix) { 36 | await context.system.run(rule.fix) 37 | return checker(rule, context) 38 | } else { 39 | throw new Error('No fix script provided in .solidarity file') 40 | } 41 | } 42 | } 43 | 44 | const configureSubtask = rule => { 45 | let subTask: Object = {} 46 | switch (rule.rule) { 47 | // Handle CLI rule check 48 | case 'cli': 49 | let ruleString = '' 50 | const semverRequirement = rule.semver || '' 51 | ruleString = `'${rule.binary}' binary ${semverRequirement}` 52 | subTask = { 53 | title: ruleString, 54 | skip: () => skipRule(rule), 55 | task: taskWithFix(checkCLI, rule, context), 56 | } 57 | break 58 | // Handle ENV rule check 59 | case 'env': 60 | subTask = { 61 | title: `${rule.variable} env`, 62 | skip: () => skipRule(rule), 63 | task: taskWithFix(checkENV, rule, context), 64 | } 65 | break 66 | // Handle dir rule check 67 | case 'directory': 68 | case 'dir': 69 | subTask = { 70 | title: `${rule.location} directory exists`, 71 | skip: () => skipRule(rule), 72 | task: taskWithFix(checkDir, rule, context), 73 | } 74 | break 75 | // Handle file rule check 76 | case 'file': 77 | subTask = { 78 | title: `${rule.location} file exists`, 79 | skip: () => skipRule(rule), 80 | task: taskWithFix(checkFile, rule, context), 81 | } 82 | break 83 | // Handle the shell rule 84 | case 'shell': 85 | subTask = { 86 | title: `'${rule.command}' matches '${rule.match}'`, 87 | skip: () => skipRule(rule), 88 | task: taskWithFix(checkShell, rule, context), 89 | } 90 | break 91 | case 'custom': 92 | const customPluginRule = findPluginInfo(rule, context) 93 | if (customPluginRule.success) { 94 | subTask = { 95 | title: `${requirementName} - custom rule '${rule.plugin}' '${rule.name}'`, 96 | // takes into account they didn't provide a check 97 | skip: () => skipRule(rule) || !customPluginRule.plugin.check, 98 | task: async () => { 99 | const customResult = await taskWithFix(customPluginRule.plugin.check, rule, context)() 100 | if (customResult && customResult.pass) { 101 | return true 102 | } else { 103 | const failMessage = 104 | customResult && customResult.message 105 | ? customResult.message 106 | : `${requirementName} - custom rule '${rule.plugin}' '${rule.name}' failed` 107 | throw new Error(rule.error || failMessage) 108 | } 109 | }, 110 | } 111 | } else { 112 | throw new Error(customPluginRule.message) 113 | } 114 | break 115 | default: 116 | throw new Error('Encountered unknown rule') 117 | } 118 | 119 | return subTask 120 | } 121 | 122 | // build Listr of ruleChecks 123 | const ruleChecks = new Listr(rules.map((rule: SolidarityRule) => configureSubtask(rule))) 124 | 125 | // Run all the rule checks for a requirement 126 | return ruleChecks 127 | } 128 | -------------------------------------------------------------------------------- /__tests__/command_helpers/getSolidaritySettings.ts: -------------------------------------------------------------------------------- 1 | import getSolidaritySettings from '../../src/extensions/functions/getSolidaritySettings' 2 | 3 | const context = require('mockContext') 4 | 5 | describe('basic getSolidaritySettings', () => { 6 | describe('w/ success', () => { 7 | test('getSolidaritySettings exists', () => expect(getSolidaritySettings).toMatchSnapshot()) 8 | 9 | test('getSolidaritySettings succeeds', async () => { 10 | const resultSettings = await getSolidaritySettings(context) 11 | // we got an object with requirements defined 12 | expect(resultSettings).toMatchObject({ requirements: {} }) 13 | }) 14 | 15 | test('getSolidaritySettings succeeds', async () => { 16 | process.chdir('__tests__/sandbox/solidarity_json') 17 | const resultSettings = await getSolidaritySettings(context) 18 | // we got an object with requirements defined 19 | expect(resultSettings).toMatchObject({ requirements: {} }) 20 | process.chdir('../../../') 21 | }) 22 | }) 23 | 24 | describe('w/ failure', () => { 25 | test('getSolidaritySettings can fail', async () => { 26 | // Original sync style 27 | // expect(() => { 28 | // process.chdir('__tests__') 29 | // getSolidaritySettings(context) 30 | // }).toThrow() 31 | // process.chdir('../') 32 | 33 | process.chdir('__tests__') 34 | await expect(getSolidaritySettings(context)).rejects.toThrow() 35 | process.chdir('../') 36 | }) 37 | 38 | test('getSolidaritySettings can warn with missing requirements', async () => { 39 | process.chdir('__tests__/sandbox/solidarity_broken') 40 | await expect(getSolidaritySettings(context)).rejects.toThrow() 41 | process.chdir('../../../') 42 | }) 43 | }) 44 | }) 45 | 46 | describe('parameterized getSolidaritySettings', () => { 47 | test('custom path with -f', async () => { 48 | context.parameters.options = { f: '__tests__/sandbox/solidarity_json' } 49 | const resultSettings = await getSolidaritySettings(context) 50 | // we got an object with requirements defined 51 | expect(resultSettings).toMatchObject({ requirements: {} }) 52 | context.parameters.options = {} 53 | }) 54 | 55 | test('custom path with --solidarityFile', async () => { 56 | context.parameters.options = { solidarityFile: '__tests__/sandbox/solidarity_json' } 57 | const resultSettings = await getSolidaritySettings(context) 58 | // we got an object with requirements defined 59 | expect(resultSettings).toMatchObject({ requirements: {} }) 60 | context.parameters.options = {} 61 | }) 62 | 63 | test('failing path message', async () => { 64 | // test longhand 65 | context.parameters.options = { solidarityFile: '__tests__/fake' } 66 | await expect(getSolidaritySettings(context)).rejects.toThrow('ERROR: There is no solidarity file at the given path') 67 | 68 | // test shorthand 69 | context.parameters.options = { f: '__tests__/fake' } 70 | await expect(getSolidaritySettings(context)).rejects.toThrow('ERROR: There is no solidarity file at the given path') 71 | 72 | context.parameters.options = {} 73 | }) 74 | 75 | describe('custom module tests', () => { 76 | beforeAll(() => { 77 | process.chdir('__tests__/sandbox/fake_project') 78 | }) 79 | 80 | test('can find solidarity file in module with flag -d', async () => { 81 | context.parameters.options = { d: 'mock_module' } 82 | const resultSettings = await getSolidaritySettings(context) 83 | // we got an object with requirements defined 84 | expect(resultSettings).toMatchObject({ requirements: {} }) 85 | context.parameters.options = {} 86 | }) 87 | 88 | test('can find solidarity file in module with flag --module', async () => { 89 | context.parameters.options = { module: 'mock_module' } 90 | const resultSettings = await getSolidaritySettings(context) 91 | // we got an object with requirements defined 92 | expect(resultSettings).toMatchObject({ requirements: {} }) 93 | context.parameters.options = {} 94 | }) 95 | 96 | test('can find solidarity JSON file in module with flag --module', async () => { 97 | context.parameters.options = { module: 'mock_second_module' } 98 | const resultSettings = await getSolidaritySettings(context) 99 | // we got an object with requirements defined 100 | expect(resultSettings).toMatchObject({ requirements: {} }) 101 | context.parameters.options = {} 102 | }) 103 | 104 | test('errors if no solidarity file in module', async () => { 105 | context.parameters.options = { module: 'nope' } 106 | await expect(getSolidaritySettings(context)).rejects.toThrow( 107 | 'ERROR: There is no solidarity file found with the given module' 108 | ) 109 | context.parameters.options = {} 110 | }) 111 | 112 | afterAll(() => { 113 | process.chdir('../../../') 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /src/commands/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand, GluegunRunContext } from 'gluegun' 2 | import { filter, propEq, head } from 'ramda' 3 | 4 | import { FriendlyMessages, SolidarityRunContext } from '../types' 5 | 6 | namespace Snapshot { 7 | const runPluginSnapshot = async (runPlugin, context: GluegunRunContext): Promise => { 8 | if (typeof runPlugin.snapshot === 'string') { 9 | // Just a file copy 10 | const { filesystem, system } = context 11 | // @ts-ignore -- strictNullChecks strikes 12 | filesystem.copy(`${runPlugin.templateDirectory}${runPlugin.snapshot}`, '.solidarity') 13 | // force local version update 14 | // @ts-ignore -- strictNullChecks strikes 15 | await system.run('solidarity snapshot') 16 | } else { 17 | // run plugin's snapshot function 18 | await runPlugin.snapshot(context) 19 | } 20 | } 21 | 22 | const createSolidarityFile = async (context: SolidarityRunContext): Promise => { 23 | const { print, printSeparator } = context 24 | const pluginsWithTemplates = filter(plugin => plugin.templateDirectory !== null, context._pluginsList) 25 | // list visible plugins 26 | printSeparator() 27 | print.info('Available technology plugins:\n') 28 | if (pluginsWithTemplates.length > 0) { 29 | const pluginOptions: string[] = [FriendlyMessages.NONE] 30 | pluginsWithTemplates.map(plugin => { 31 | print.info(` ${plugin.name}:\t ${plugin.description}`) 32 | pluginOptions.unshift(plugin.name) 33 | }) 34 | printSeparator() 35 | const answer = await context.prompt.ask({ 36 | name: 'selectedPlugin', 37 | message: 'Which of the above technology snapshots will you use for this project?', 38 | type: 'select', 39 | choices: pluginOptions, 40 | }) 41 | 42 | if (answer.selectedPlugin === FriendlyMessages.NONE) { 43 | print.info(FriendlyMessages.NOTHING) 44 | print.info("If you don't wish to use a plugin, try creating your own rules with `solidarity onboard`") 45 | } else { 46 | const pluginSpinner = print.spin(`Running ${answer.selectedPlugin} Snapshot`) 47 | // Config for selected plugin only 48 | const runPlugin = head(filter(propEq('name', answer.selectedPlugin), pluginsWithTemplates)) 49 | if (runPlugin) { 50 | // run plugin 51 | await runPluginSnapshot(runPlugin, context) 52 | pluginSpinner.succeed('Snapshot complete') 53 | } else { 54 | pluginSpinner.fail("Couldn't find plugin") 55 | } 56 | } 57 | } else { 58 | print.error(`No solidarity plugins found! 59 | Add a plugin for a given technology: 60 | ${print.colors.blue('https://github.com/infinitered/solidarity/blob/master/docs/pluginsList.md')} 61 | OR write your own plugin for generating rules: 62 | ${print.colors.blue('https://github.com/infinitered/solidarity/blob/master/docs/plugins.md')} 63 | OR simply create a .solidarity rule-set by hand for this project: 64 | ${print.colors.blue('https://github.com/infinitered/solidarity/blob/master/docs/options.md')} 65 | `) 66 | printSeparator() 67 | } 68 | } 69 | 70 | export const run = async (context: SolidarityRunContext) => { 71 | const { print, prompt, filesystem, solidarity, parameters } = context 72 | const { first } = parameters 73 | const { setSolidaritySettings, appendSolidaritySettings, buildSpecificRequirement } = solidarity 74 | // Node Modules Quirk 75 | require('../extensions/functions/quirksNodeModules') 76 | 77 | // check is there an existing .solidarity file? 78 | if (filesystem.exists('.solidarity')) { 79 | // load existing file and update rule versions 80 | 81 | if (first) { 82 | await buildSpecificRequirement(context) 83 | .then(async newRequirement => { 84 | const updatedSolidaritySettings = await appendSolidaritySettings(context, newRequirement) 85 | 86 | setSolidaritySettings(updatedSolidaritySettings, context) 87 | }) 88 | .catch(error => { 89 | if (error) print.error(error) 90 | print.error('Your new requirement was not added.') 91 | }) 92 | } else { 93 | print.info('Now loading latest environment') 94 | solidarity.updateVersions(context) 95 | } 96 | } else { 97 | // Find out what they wanted 98 | const userAnswer = await prompt.ask({ 99 | name: 'createFile', 100 | type: 'confirm', 101 | message: 'No `.solidarity` file found for this project. Would you like to create one?', 102 | }) 103 | 104 | if (userAnswer.createFile) { 105 | await createSolidarityFile(context) 106 | } else { 107 | print.info(FriendlyMessages.NOTHING) 108 | } 109 | } 110 | } 111 | } 112 | 113 | module.exports = { 114 | description: 'Take a snapshot of the versions and store in solidarity file', 115 | alias: 's', 116 | run: Snapshot.run, 117 | } as GluegunCommand 118 | --------------------------------------------------------------------------------