├── .editorconfig ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── feature-request.yml │ ├── question.yml │ └── requested-change.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── publish-package.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── bin └── cli.js ├── commands ├── delete.js ├── list.js ├── log.js ├── setup.js └── watch.js ├── docs ├── cmd-delete.md ├── cmd-help.md ├── cmd-list.md ├── cmd-log.md ├── cmd-setup.md ├── cmd-watch.md └── troubleshooting.md ├── icons ├── sfcc-cli.png ├── sfcc-error.png ├── sfcc-reload.png ├── sfcc-success.png └── sfcc-uploading.png ├── lib ├── builds.js ├── config.js ├── find.js ├── logger.js ├── mkdir.js ├── mkdirp.js ├── notify.js ├── read.js ├── search.js ├── slug.js ├── tail.js ├── upload.js └── write.js ├── package-lock.json ├── package.json └── sfcc-cli.code-workspace /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | ecmaVersion: 8, 6 | }, 7 | env: { 8 | es6: true, 9 | node: true, 10 | browser: true, 11 | }, 12 | plugins: ['prettier'], 13 | extends: ['eslint:recommended', 'plugin:import/errors', 'plugin:import/warnings', 'prettier'], 14 | rules: { 15 | 'prettier/prettier': [ 16 | 'error', 17 | { 18 | singleQuote: true, 19 | bracketSpacing: false, 20 | semi: false, 21 | printWidth: 120, 22 | }, 23 | ], 24 | 'no-empty': [ 25 | 'error', 26 | { 27 | allowEmptyCatch: true, 28 | }, 29 | ], 30 | 'no-console': 0, 31 | }, 32 | globals: { 33 | io: true, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing Guide 2 | === 3 | 4 | Issues & Feature Requests 5 | --- 6 | 7 | [![Create Issue](https://img.shields.io/badge/Github-Create_Issue-red.svg?style=for-the-badge&logo=github&logoColor=ffffff&logoWidth=16)](https://github.com/sfccdevops/sfcc-cli/issues/new/choose) 8 | 9 | ### Bug Fix 10 | 11 | > We're sorry things are not working as expected, and want to get things fixed ASAP. In order to help us do that, we need a few things from you. 12 | 13 | 1. Create a [New Issue](https://github.com/sfccdevops/sfcc-cli/issues/new/choose) 14 | 2. Enter a Short but Descriptive Title for the Issue 15 | 3. Use the Template Provided and fill in as much as you can, if something does not apply, enter `N/A` 16 | 4. Look for the `Labels` section, and select `Bug Report` from the drop down menu 17 | 5. Click `Submit new issue` button 18 | 19 | ### Feature Request 20 | 21 | > Got an idea for a new feature? We'd love to hear it! In order to get this knocked out, we will need a few things from you. 22 | 23 | 1. Create a [New Issue](https://github.com/sfccdevops/sfcc-cli/issues/new/choose) 24 | 2. Enter a Short but Descriptive Title for the Feature Request 25 | 3. Use the Template Provided and fill in as much as you can, if something does not apply, enter `N/A` ( you can delete the `Steps to Duplicate:` section as that does not apply ) 26 | 4. Look for the `Labels` section, and select `Feature Request` from the drop down menu 27 | 5. Click `Submit new issue` button 28 | 29 | Pull Requests 30 | --- 31 | 32 | [![Create Pull Request](https://img.shields.io/badge/Github-Create_Pull_Request-blue.svg?style=for-the-badge&logo=github&logoColor=ffffff&logoWidth=16)](https://github.com/sfccdevops/sfcc-cli/compare) 33 | 34 | ### Bug Fix 35 | 36 | > Each Bug Fix reported on GitHub should have its own `fix/*` branch. The branch name should be formatted `fix/###-issue-name` where `###` is the GitHub Issue Number, and `issue-name` is a 1-3 word summary of the issue. 37 | 38 | 1. Checkout latest `develop` branch 39 | 2. Pull down the latest changes via `git pull` 40 | 3. Create a new branch with the structure `fix/*`, e.g. `fix/123-broken-form` 41 | 4. When you are ready to submit your code, submit a new Pull Request that merges your code into `develop` 42 | 5. Tag your new Pull Request with `Ready for Code Review` 43 | 44 | ### Feature Request 45 | 46 | > Each New Feature should reside in its own `feature/` branch. The branch name should be formatted `feature/###-feature-name` where `###` is the GitHub Issue Number, and `feature-name` is a 1-3 word summary of the feature. 47 | 48 | 1. Checkout latest `develop` branch 49 | 2. Pull down the latest changes via `git pull` 50 | 3. Create a new branch with the structure `feature/*`, e.g. `feature/123-mobile-header` 51 | 4. When you are ready to submit your code, submit a new Pull Request that merges your code into `develop` 52 | 5. Tag your new Pull Request with `Ready for Code Review` 53 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sfccdevops 2 | patreon: peter_schmalfeldt 3 | custom: https://www.paypal.me/manifestinteractive 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: I would like to Report a Bug 3 | labels: [Bug Report] 4 | assignees: 5 | - manifestinteractive 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Is there an existing issue for this? 10 | description: Please search to see if an issue already exists for the bug you encountered. 11 | options: 12 | - label: I have searched the existing issues 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Describe the Bug 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Steps To Reproduce 23 | description: Steps to reproduce the behavior. 24 | placeholder: | 25 | 1. Go to ... 26 | 2. Click on ... 27 | 3. Scroll down to ... 28 | 4. See error ... 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Expected Behavior 34 | description: A concise description of what you expected to happen. 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Screenshots 40 | description: If applicable, add screenshots to help explain your problem. 41 | validations: 42 | required: false 43 | - type: textarea 44 | attributes: 45 | label: Environment 46 | description: | 47 | examples: 48 | - **OS**: Ubuntu 20.04 49 | - **Node**: 13.14.0 50 | - **npm**: 7.6.3 51 | value: | 52 | - OS: 53 | - Node: 54 | - npm: 55 | render: markdown 56 | validations: 57 | required: false 58 | - type: textarea 59 | attributes: 60 | label: Additional Context 61 | description: | 62 | Links? References? Anything that will give us more context about the issue you are encountering! 63 | 64 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 65 | validations: 66 | required: false 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: This is a new Feature Request for this project 3 | labels: [Feature Request] 4 | assignees: 5 | - manifestinteractive 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the Problem 10 | description: Is your feature request related to a problem? Please describe. 11 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Describe the Solution 17 | description: Describe the solution you'd like 18 | placeholder: A clear and concise description of what you want to happen. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Alternatives 24 | description: Describe alternatives you've considered 25 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Additional Context 31 | description: | 32 | Add any other context or screenshots about the feature request here. 33 | 34 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: I have a Question about this project 3 | labels: [Question] 4 | assignees: 5 | - manifestinteractive 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Question 10 | description: Please Write your Question Below. 11 | validations: 12 | required: true 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/requested-change.yml: -------------------------------------------------------------------------------- 1 | name: Requested Change 2 | description: This is a Requested Change to the project 3 | labels: [Requested Change] 4 | assignees: 5 | - manifestinteractive 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the Problem 10 | description: Is your requested change related to a problem? Please describe. 11 | placeholder: A clear and concise description of what the request is. 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Describe the Solution 17 | description: Describe the solution you'd like 18 | placeholder: A clear and concise description of what you want to happen. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Alternatives 24 | description: Describe alternatives you've considered 25 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Additional Context 31 | description: | 32 | Add any other context or screenshots about the feature request here. 33 | 34 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Overview 2 | --- 3 | 4 | TEXT 5 | 6 | Reviewer 7 | --- 8 | 9 | > Where should the reviewer start? How to Test? Background Context? etc ( required ) 10 | 11 | TEXT 12 | 13 | Checklist 14 | --- 15 | 16 | > I have tested each of the following, and they work as expected: ( required ) 17 | 18 | - [ ] Meets [Contributing Guide](https://github.com/sfccdevops/sfcc-cli/blob/develop/.github/CONTRIBUTING.md) Requirements 19 | - [ ] Pulled in the Latest Code from the `develop` branch 20 | - [ ] Works on a Desktop / Laptop Device 21 | 22 | Documentation 23 | --- 24 | 25 | > Screenshots, Attachments, Linked GitHub Issues, etc ( optional ) 26 | 27 | 28 | 29 | #### What GIF best describes this PR or how it makes you feel? 30 | 31 | > Drag & Drop Something Fun Here ( optional ) 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 14 13 | - run: npm ci 14 | - run: npm test 15 | publish-gpr: 16 | needs: build 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 14 26 | registry-url: https://npm.pkg.github.com/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Install and Test 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: '14.x' 16 | - run: npm ci 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | .vscode 5 | !sfcc-cli.code-workspace 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @sfccdevops:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.17.3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SFCC DevOps 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project Support 2 | === 3 | 4 | If you or your company enjoy using this project, please consider supporting my work and joining my discord. 💖 5 | 6 | [![Become a GitHub Sponsor](https://img.shields.io/badge/Sponsor-171515.svg?logo=github&logoColor=white&style=for-the-badge "Become a GitHub Sponsor")](https://github.com/sponsors/sfccdevops) 7 | [![Become a Patreon Sponsor](https://img.shields.io/badge/Sponsor-FF424D.svg?logo=patreon&logoColor=white&style=for-the-badge "Become a Patreon Sponsor")](https://patreon.com/peter_schmalfeldt) 8 | [![Donate via PayPal](https://img.shields.io/badge/Donate-169BD7.svg?logo=paypal&logoColor=white&style=for-the-badge "Donate via PayPal")](https://www.paypal.me/manifestinteractive) 9 | [![Join Discord Community](https://img.shields.io/badge/Community-5865F2.svg?logo=discord&logoColor=white&style=for-the-badge "Join Discord Community")](https://discord.gg/M73Maz9B6P) 10 | 11 | ------ 12 | 13 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 14 | 15 | SFCC CLI 16 | --- 17 | 18 | > Command Line Interface for Salesforce Commerce Cloud Sandbox Development 19 | 20 | ![demo](https://sfcc-cli.s3.amazonaws.com/demo.gif?v=1.3.0) 21 | 22 | Introduction 23 | --- 24 | 25 | Make developing for Salesforce Commerce Cloud work with any IDE on MacOS, Windows, and Linux. 26 | 27 | - [X] Easily Manage Multiple Clients & Instances 28 | - [X] Watch for code changes and upload in background ( without being prompted for passwords ) 29 | - [X] Support for SFRA JS & CSS Compilers 30 | - [X] Support for Eclipse Build Processes 31 | - [X] Log Viewing with Advanced Search & Filter Capabilities 32 | 33 | Developer Overview 34 | --- 35 | 36 | #### Commands 37 | 38 | * [`sfcc setup`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-setup.md) - Setup SFCC Development 39 | * [`sfcc list`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-list.md) - List Configured SFCC Clients 40 | * [`sfcc delete`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-delete.md) - Delete Config for Client 41 | * [`sfcc watch`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-watch.md) - Watch for Changes and Push Updates 42 | * [`sfcc log`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-log.md) - View Logs with Advanced Search & Filter Capabilities 43 | * [`sfcc help`](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/cmd-help.md) - Get Help when you need it 44 | 45 | #### Additional Information 46 | 47 | * [Troubleshooting](https://github.com/sfccdevops/sfcc-cli/blob/master/docs/troubleshooting.md) 48 | 49 | Install 50 | --- 51 | 52 | #### Requirements 53 | 54 | - [X] [Node v14+](https://nodejs.org/en/download/) 55 | 56 | #### Install via NPM 57 | 58 | ```bash 59 | npm install -g @sfccdevops/sfcc-cli --no-optional 60 | sfcc setup 61 | ``` 62 | 63 | #### Install via Clone 64 | 65 | ```bash 66 | cd ~ 67 | git clone https://github.com/sfccdevops/sfcc-cli.git 68 | cd sfcc-cli 69 | npm install -g --no-optional 70 | sfcc setup 71 | ``` 72 | 73 | _Inspired by [dw-cli](https://github.com/mzwallace/dw-cli). Custom Built for SFCC Developers._ 74 | 75 | About the Author 76 | --- 77 | 78 | > [Peter Schmalfeldt](https://peterschmalfeldt.com/) is a Certified Senior Salesforce Commerce Cloud Developer with over 20 years of experience building eCommerce websites, providing everything you need to design, develop & deploy eCommerce applications for Web, Mobile & Desktop platforms. 79 | 80 | Disclaimer 81 | --- 82 | 83 | > The trademarks and product names of Salesforce®, including the mark Salesforce®, are the property of Salesforce.com. SFCC DevOps is not affiliated with Salesforce.com, nor does Salesforce.com sponsor or endorse the SFCC DevOps products or website. The use of the Salesforce® trademark on this project does not indicate an endorsement, recommendation, or business relationship between Salesforce.com and SFCC DevOps. 84 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const chalk = require('chalk') 6 | const debug = require('debug')('cli') 7 | const path = require('path') 8 | const yargs = require('yargs') 9 | 10 | const cli = yargs 11 | .scriptName('sfcc') 12 | .usage('Usage: sfcc --switches') 13 | .command('setup', 'Setup SFCC Development', { 14 | client: { 15 | alias: 'c', 16 | describe: 'Client Name', 17 | type: 'string', 18 | }, 19 | hostname: { 20 | alias: 'h', 21 | describe: 'Hostname for Instance', 22 | type: 'string', 23 | }, 24 | version: { 25 | alias: 'v', 26 | describe: 'Code Version', 27 | type: 'string', 28 | }, 29 | directory: { 30 | alias: 'd', 31 | describe: 'Absolute path to Repository', 32 | type: 'string', 33 | }, 34 | username: { 35 | alias: 'u', 36 | describe: 'Your Business Manager Username', 37 | type: 'string', 38 | }, 39 | password: { 40 | alias: 'p', 41 | describe: 'Your Business Manager Password', 42 | type: 'string', 43 | }, 44 | alias: { 45 | alias: 'a', 46 | describe: 'Instance Alias', 47 | default: 'sandbox', 48 | type: 'string', 49 | }, 50 | }) 51 | .command('list', 'List Configured SFCC Clients') 52 | .command('delete [instance]', 'Delete Config for Client') 53 | .command('watch [client] [instance]', 'Watch for Changes and Push Updates', { 54 | log: { 55 | describe: 'Pipe Output to ~/.sffc-cli.log', 56 | type: 'boolean', 57 | default: false, 58 | }, 59 | 'errors-only': { 60 | describe: 'Only Show Notification for Errors', 61 | type: 'boolean', 62 | default: false, 63 | }, 64 | 'compile-only': { 65 | describe: 'No Uploads, just Run Compilers', 66 | type: 'boolean', 67 | default: false, 68 | }, 69 | }) 70 | .command('log [client] [instance]', 'Stream log files from an instance', { 71 | polling: { 72 | alias: 'p', 73 | describe: 'Polling Interval (seconds)', 74 | type: 'number', 75 | default: 2, 76 | }, 77 | lines: { 78 | alias: 'l', 79 | describe: 'Number of Lines to Display', 80 | type: 'number', 81 | default: 100, 82 | }, 83 | include: { 84 | alias: 'i', 85 | describe: 'Log Types to Include', 86 | type: 'array', 87 | default: [], 88 | }, 89 | exclude: { 90 | alias: 'e', 91 | describe: 'Log Types to Exclude', 92 | type: 'array', 93 | default: [], 94 | }, 95 | filter: { 96 | alias: 'f', 97 | describe: 'Filter Log Messages by RegExp', 98 | type: 'string', 99 | default: null, 100 | }, 101 | truncate: { 102 | alias: 't', 103 | describe: 'Length to Truncate Messages', 104 | type: 'number', 105 | default: null, 106 | }, 107 | list: { 108 | describe: 'Output List of Log Types', 109 | type: 'boolean', 110 | default: false, 111 | }, 112 | search: { 113 | describe: 'Search Logs with no Live Updates', 114 | type: 'boolean', 115 | default: false, 116 | }, 117 | latest: { 118 | describe: 'Show Latest Logs Only', 119 | type: 'boolean', 120 | default: false, 121 | }, 122 | }) 123 | .command('sfcc [command] [argument]', 'Execute SFCC CLI Command') 124 | .example('sfcc setup', 'Setup SFCC Development') 125 | .example('sfcc list', 'List Configured SFCC Clients') 126 | .example('sfcc delete my-client sandbox', 'Delete my-client sandbox config') 127 | .example('sfcc watch my-client sandbox', 'Watch for my-client sandbox changes') 128 | .example('sfcc log -i customerror --latest', 'Watch Latest Custom Error Logs') 129 | .example(' ', ' ') 130 | .example('----------------------------------', '------------------------------------------') 131 | .example('NEED MORE HELP ?', 'https://bit.ly/sfcc-cli-help') 132 | .example('----------------------------------', '------------------------------------------') 133 | .demand(1) 134 | .help() 135 | .version(false).argv 136 | 137 | const command = cli._[0] 138 | 139 | try { 140 | debug(`Executing ${command}`) 141 | require(path.join(__dirname, `../commands/${command}.js`))(cli) 142 | } catch (err) { 143 | if (err.code === 'MODULE_NOT_FOUND') { 144 | console.log(chalk.red.bold(`\n✖ Command 'sfcc ${command}' not recognized\n`)) 145 | console.log('Use ' + chalk.cyan('sfcc help') + ' for a list of commands\n') 146 | } else { 147 | throw err 148 | } 149 | } 150 | 151 | module.exports = cli 152 | -------------------------------------------------------------------------------- /commands/delete.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)) 2 | const chalk = require('chalk') 3 | const confirm = require('prompt-confirm') 4 | 5 | const config = require('../lib/config')() 6 | 7 | module.exports = async () => { 8 | const client = argv['_'][1] || null 9 | const instance = argv['_'][2] || null 10 | const currentConfig = config.get() 11 | 12 | let newConfig = {} 13 | 14 | if (currentConfig) { 15 | let found = false 16 | newConfig = Object.assign({}, newConfig, currentConfig) 17 | 18 | if (client && instance) { 19 | if ( 20 | Object.prototype.isPrototypeOf.call(newConfig, client) && 21 | Object.prototype.isPrototypeOf.call(newConfig[client], instance) 22 | ) { 23 | found = true 24 | delete newConfig[client][instance] 25 | } else { 26 | console.log(chalk.red.bold(`\n✖ Config does not contain client '${client}' with instance '${instance}'.\n`)) 27 | } 28 | } else if (client) { 29 | if (Object.prototype.isPrototypeOf.call(newConfig, client)) { 30 | found = true 31 | delete newConfig[client] 32 | } else { 33 | console.log(chalk.red.bold(`\n✖ Config does not contain client '${client}'.\n`)) 34 | } 35 | } 36 | 37 | if (found) { 38 | console.log('') 39 | const prompt = new confirm('Confirm Delete?') 40 | prompt.ask(function (confirmed) { 41 | if (confirmed) { 42 | config.set(newConfig, true) 43 | } 44 | }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /commands/list.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | 3 | const config = require('../lib/config')() 4 | 5 | module.exports = async () => { 6 | const currentConfig = config.get() 7 | 8 | if (Object.keys(currentConfig).length > 0 && currentConfig.constructor === Object) { 9 | console.log('') 10 | for (let client in currentConfig) { 11 | console.log(chalk.green(' client: ') + chalk.green.bold(client)) 12 | 13 | for (let instance in currentConfig[client]) { 14 | console.log(chalk.cyan('instance: ') + chalk.cyan.bold(instance)) 15 | console.log(' path: ' + chalk.bold(currentConfig[client][instance].d)) 16 | console.log(' host: ' + chalk.bold(currentConfig[client][instance].h)) 17 | console.log(' code: ' + chalk.bold(currentConfig[client][instance].v)) 18 | console.log(' user: ' + chalk.bold(currentConfig[client][instance].u)) 19 | console.log(' pass: ' + chalk.bold(currentConfig[client][instance].p.replace(/./g, '*'))) 20 | } 21 | 22 | console.log('') 23 | } 24 | } else { 25 | console.log(chalk.red.bold('\n✖ No Clients')) 26 | console.log('Use ' + chalk.cyan('sfcc setup') + ' to started.\n') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /commands/log.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)) 2 | const chalk = require('chalk') 3 | const forEach = require('lodash/forEach') 4 | const groupBy = require('lodash/groupBy') 5 | const keys = require('lodash/keys') 6 | const pickBy = require('lodash/pickBy') 7 | const sortBy = require('lodash/sortBy') 8 | 9 | const config = require('../lib/config')() 10 | const find = require('../lib/find') 11 | const search = require('../lib/search') 12 | const tail = require('../lib/tail') 13 | 14 | module.exports = async (options) => { 15 | let client = argv['_'][1] || null 16 | let instance = argv['_'][2] || null 17 | let selected = null 18 | 19 | // Get Client & Instance, or check for Default 20 | if (client && instance) { 21 | selected = config.get(client, instance) 22 | } else { 23 | const defaultConfig = config.get(client, instance, true) 24 | 25 | if (defaultConfig) { 26 | client = defaultConfig.client 27 | instance = defaultConfig.instance 28 | selected = defaultConfig.config 29 | } 30 | } 31 | 32 | if (selected) { 33 | let files = await find('Logs', { 34 | baseURL: `https://${selected.h}/on/demandware.servlet/webdav/Sites/`, 35 | auth: { 36 | username: selected.u, 37 | password: selected.p, 38 | }, 39 | }).catch((error) => { 40 | console.log(chalk.red.bold('\n✖') + ` ${error}\n`) 41 | }) 42 | 43 | if (!files) { 44 | return 45 | } 46 | 47 | files = files.filter(({displayname}) => displayname.includes('.log')) 48 | 49 | // Group Logs 50 | let groups = groupBy(files, ({displayname}) => displayname.split('-blade')[0]) 51 | 52 | // pick out logs we want to include 53 | if (options.include.length > 0) { 54 | if (options.include.length === 1 && options.include[0].includes(',')) { 55 | options.include = options.include[0].split(',') 56 | } 57 | groups = pickBy( 58 | groups, 59 | (group, name) => 60 | options.include.filter((level) => { 61 | return new RegExp(level).test(name) 62 | }).length > 0 63 | ) 64 | } 65 | 66 | // pick out logs we want to exclude 67 | if (options.exclude.length > 0) { 68 | if (options.exclude.length === 1 && options.exclude[0].includes(',')) { 69 | options.exclude = options.exclude[0].split(',') 70 | } 71 | groups = pickBy( 72 | groups, 73 | (group, name) => 74 | options.exclude.filter((level) => { 75 | return new RegExp(level).test(name) 76 | }).length === 0 77 | ) 78 | } 79 | 80 | // get list of log types 81 | if (options.list) { 82 | console.log(chalk.green.bold('\nLog Types:\n')) 83 | 84 | forEach(keys(groups).sort(), (group) => { 85 | console.log('· ' + group) 86 | }) 87 | 88 | console.log('') 89 | 90 | process.exit() 91 | } 92 | 93 | // setup logs 94 | const logs = [] 95 | forEach(groups, (files, name) => { 96 | logs[name] = [] 97 | }) 98 | 99 | // sort groups by last modified 100 | forEach(groups, (files, name) => { 101 | var sorted = sortBy(files, (file) => new Date(file.getlastmodified)).reverse() 102 | groups[name] = options.latest ? [sorted[0]] : sorted 103 | }) 104 | 105 | try { 106 | // Start log output 107 | options.search 108 | ? search(selected, client, instance, groups, options) 109 | : tail(selected, client, instance, logs, groups, options) 110 | } catch (err) { 111 | console.log(err) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /commands/setup.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)) 2 | const chalk = require('chalk') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const prompt = require('prompt') 6 | 7 | const builds = require('../lib/builds') 8 | const config = require('../lib/config')() 9 | const slug = require('../lib/slug') 10 | 11 | module.exports = async () => { 12 | const setDefaults = 13 | argv && 14 | typeof argv.c !== 'undefined' && 15 | argv.h !== 'undefined' && 16 | argv.d !== 'undefined' && 17 | argv.u !== 'undefined' && 18 | argv.p !== 'undefined' 19 | 20 | if (setDefaults && typeof argv.a === 'undefined') { 21 | argv.a = 'sandbox' 22 | } 23 | 24 | if (setDefaults && typeof argv.v === 'undefined') { 25 | argv.v = 'develop' 26 | } 27 | 28 | prompt.message = '' 29 | prompt.error = '' 30 | prompt.delimiter = '' 31 | prompt.override = argv 32 | 33 | // Intentional Blank Line 34 | console.log('') 35 | 36 | prompt.start() 37 | prompt.get( 38 | [ 39 | { 40 | description: chalk.cyan('Client Name:'), 41 | name: 'c', 42 | pattern: /^[a-zA-Z-_ ]+$/, 43 | required: true, 44 | }, 45 | { 46 | description: chalk.cyan('Hostname:'), 47 | name: 'h', 48 | pattern: /^[a-z0-9_.-]+$/, 49 | message: 'Invalid Host Name. ( e.g. dev04-web-mysandbox.demandware.net )', 50 | required: true, 51 | conform: function (hostname) { 52 | if (typeof hostname !== 'string') return false 53 | 54 | var parts = hostname.split('.') 55 | if (parts.length <= 1) return false 56 | 57 | var tld = parts.pop() 58 | var tldRegex = /^(?:xn--)?[a-zA-Z0-9]+$/gi 59 | 60 | if (!tldRegex.test(tld)) return false 61 | 62 | var isValid = parts.every(function (host) { 63 | var hostRegex = /^(?!:\/\/)([a-zA-Z0-9]+|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])$/gi 64 | return hostRegex.test(host) 65 | }) 66 | 67 | return isValid 68 | }, 69 | }, 70 | { 71 | description: chalk.cyan('Code Version:'), 72 | name: 'v', 73 | pattern: /^[a-zA-Z0-9]+$/, 74 | message: 'Code Version. ( e.g. develop, sitegenesis, etc )', 75 | required: true, 76 | default: 'develop', 77 | }, 78 | { 79 | description: chalk.cyan('Instance Alias:'), 80 | name: 'a', 81 | pattern: /^[a-zA-Z0-9]+$/, 82 | message: 'Invalid Instance Alias. ( e.g. dev04, sandbox, staging, etc )', 83 | required: true, 84 | default: 'sandbox', 85 | }, 86 | { 87 | description: chalk.cyan('Directory:'), 88 | name: 'd', 89 | required: true, 90 | message: 'Directory does not exist. ( e.g. /Users/Name/Projects/mysandbox )', 91 | conform: function (directory) { 92 | directory = path.normalize(path.resolve(directory.replace(/^\/[a-z]\//, '/'))) 93 | return fs.existsSync(directory) 94 | }, 95 | }, 96 | { 97 | description: chalk.cyan('Username:'), 98 | name: 'u', 99 | pattern: /^[a-zA-Z0-9_@.-]+$/, 100 | message: 'Invalid Username. ( e.g. myusername, my@email.com, etc )', 101 | required: true, 102 | }, 103 | { 104 | description: chalk.cyan('Password:'), 105 | name: 'p', 106 | required: true, 107 | hidden: true, 108 | replace: '*', 109 | }, 110 | ], 111 | function (err, result) { 112 | if (err) { 113 | if (err.message === 'canceled') { 114 | console.log(chalk.red('× Setup Cancelled')) 115 | process.exit(1) 116 | } else { 117 | console.log(chalk.red('× ERROR:', err)) 118 | } 119 | } else { 120 | const client = slug(result.c) 121 | const alias = slug(result.a) 122 | const currentConfig = config.get() 123 | 124 | let newConfig = {} 125 | let isUpdate = false 126 | 127 | // Get current config if it is already there 128 | if (Object.keys(currentConfig).length > 0 && currentConfig.constructor === Object) { 129 | newConfig = Object.assign({}, newConfig, currentConfig) 130 | isUpdate = true 131 | } 132 | 133 | // Check if this is a new client 134 | if (typeof newConfig[client] === 'undefined') { 135 | newConfig[client] = {} 136 | } 137 | 138 | const directory = path.normalize(path.resolve(result.d.replace(/^\/[a-z]\//, '/'))) 139 | 140 | // Fetch Build Scripts from Project Directory 141 | const builders = builds(directory) 142 | 143 | // Create / Overwrite SFCC Instance for Client 144 | newConfig[client][alias] = { 145 | h: result.h, 146 | v: result.v, 147 | a: result.a, 148 | d: directory, 149 | u: result.u, 150 | p: result.p, 151 | b: builders, 152 | } 153 | 154 | // Write Config File 155 | config.set(newConfig, isUpdate) 156 | 157 | process.exit() 158 | } 159 | } 160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /commands/watch.js: -------------------------------------------------------------------------------- 1 | const {exec} = require('child_process') 2 | const argv = require('minimist')(process.argv.slice(2)) 3 | const chalk = require('chalk') 4 | const chokidar = require('chokidar') 5 | const fs = require('fs') 6 | const ora = require('ora') 7 | const path = require('path') 8 | 9 | const config = require('../lib/config')() 10 | const logger = require('../lib/logger')() 11 | const notify = require('../lib/notify')() 12 | const upload = require('../lib/upload') 13 | 14 | module.exports = (options) => { 15 | let client = argv['_'][1] || null 16 | let instance = argv['_'][2] || null 17 | let selected = null 18 | let errorMessage 19 | 20 | const useLog = options.log 21 | const errorsOnly = options.errorsOnly 22 | const compileOnly = options.compileOnly 23 | 24 | // Get Client & Instance, or check for Default 25 | if (client && instance) { 26 | selected = config.get(client, instance) 27 | } else { 28 | const defaultConfig = config.get(client, instance, true) 29 | 30 | if (defaultConfig) { 31 | client = defaultConfig.client 32 | instance = defaultConfig.instance 33 | selected = defaultConfig.config 34 | } 35 | } 36 | 37 | if (selected) { 38 | const text = `${chalk.bold('WATCHING')} ${chalk.cyan.bold(client)} ${chalk.magenta.bold( 39 | instance 40 | )} [Ctrl-C to Cancel]\n` 41 | const spinner = ora(text) 42 | const output = (fn) => { 43 | spinner.stop() 44 | fn() 45 | spinner.start() 46 | } 47 | 48 | // intentional empty 49 | console.log('') 50 | spinner.start() 51 | 52 | const watcher = chokidar.watch(selected.d, { 53 | ignored: [/[/\\]\./, '**/node_modules/**', '**/bundle-analyzer.*'], 54 | ignoreInitial: true, 55 | persistent: true, 56 | awaitWriteFinish: true, 57 | }) 58 | 59 | /** 60 | * Add support for SFRA 61 | * @param {string} ext File Extension 62 | * @param {string} dir Directory 63 | */ 64 | const compile = (ext, dir) => { 65 | const jsCompile = `cd ${dir}; ./node_modules/.bin/sgmf-scripts --compile js` 66 | const cssCompile = `cd ${dir}; ./node_modules/.bin/sgmf-scripts --compile css` 67 | 68 | // Don't Compile if Native Compiler Missing 69 | if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'sgmf-scripts'))) { 70 | return 71 | } 72 | 73 | if (ext === 'js') { 74 | output(() => 75 | console.log( 76 | `${chalk.bgGreen.white.bold(' SFRA ')} ${chalk.cyan.bold('Compiling')} ${chalk.magenta.bold( 77 | 'JavaScript' 78 | )} ...\n` 79 | ) 80 | ) 81 | 82 | if (!errorsOnly) { 83 | notify({ 84 | title: `${client} ${instance}`, 85 | icon: path.join(__dirname, '../icons/', 'sfcc-cli.png'), 86 | subtitle: 'COMPILING JAVASCRIPT ...', 87 | message: `${path.basename(dir)}`, 88 | }) 89 | } 90 | 91 | exec(jsCompile, (err, data, stderr) => { 92 | if (err || stderr) { 93 | output(() => console.log(chalk.red.bold(`✖ Build Error: ${err} ${stderr}`))) 94 | 95 | notify({ 96 | title: `${client} ${instance}`, 97 | icon: path.join(__dirname, '../icons/', 'sfcc-error.png'), 98 | subtitle: 'COMPILING JAVASCRIPT FAILED', 99 | message: `${err} ${stderr}`, 100 | sound: true, 101 | wait: true, 102 | }) 103 | } else { 104 | output(() => console.log(`${chalk.green.bold('COMPLETE')}\n`)) 105 | output(() => console.log(data)) 106 | output(() => console.log('\n')) 107 | 108 | if (!errorsOnly) { 109 | notify({ 110 | title: `${client} ${instance}`, 111 | icon: path.join(__dirname, '../icons/', 'sfcc-success.png'), 112 | subtitle: 'COMPILE COMPLETE', 113 | message: `${path.basename(dir)}`, 114 | }) 115 | } 116 | } 117 | }) 118 | } else if (ext === 'css' || ext === 'scss') { 119 | output(() => 120 | console.log( 121 | `${chalk.bgGreen.white.bold(' SFRA ')} ${chalk.cyan.bold('Compiling')} ${chalk.magenta.bold('CSS')} ...\n` 122 | ) 123 | ) 124 | 125 | if (!errorsOnly) { 126 | notify({ 127 | title: `${client} ${instance}`, 128 | icon: path.join(__dirname, '../icons/', 'sfcc-cli.png'), 129 | subtitle: 'COMPILING CSS ...', 130 | message: `${path.basename(dir)}`, 131 | }) 132 | } 133 | 134 | exec(cssCompile, (err, data, stderr) => { 135 | if (err || stderr) { 136 | output(() => console.log(chalk.red.bold(`✖ SFRA Compile Error: ${err} ${stderr}`))) 137 | 138 | notify({ 139 | title: `${client} ${instance}`, 140 | icon: path.join(__dirname, '../icons/', 'sfcc-error.png'), 141 | subtitle: 'COMPILING CSS FAILED', 142 | message: `${err} ${stderr}`, 143 | sound: true, 144 | wait: true, 145 | }) 146 | } else { 147 | output(() => console.log(`${chalk.green.bold('COMPLETE')}\n`)) 148 | output(() => console.log(data)) 149 | output(() => console.log('\n')) 150 | 151 | if (!errorsOnly) { 152 | notify({ 153 | title: `${client} ${instance}`, 154 | icon: path.join(__dirname, '../icons/', 'sfcc-success.png'), 155 | subtitle: 'COMPILE COMPLETE', 156 | message: `${path.basename(dir)}`, 157 | }) 158 | } 159 | } 160 | }) 161 | } 162 | } 163 | 164 | const buildCheck = (file) => { 165 | if (Object.keys(selected.b).length > 0) { 166 | const checkPath = path.dirname(file).replace(path.normalize(selected.d), '') 167 | Object.keys(selected.b).map((build) => { 168 | const builder = selected.b[build] 169 | if ( 170 | builder.enabled && 171 | new RegExp(builder.watch.join('|')).test(checkPath) && 172 | typeof builder.cmd.exec !== 'undefined' && 173 | builder.cmd.exec.length > 0 174 | ) { 175 | const cmd = builder.cmd.exec 176 | const building = build.split('_') 177 | 178 | output(() => 179 | console.log( 180 | `\n${chalk.bgGreen.white.bold(' BUILDING ')} ${chalk.cyan.bold( 181 | building[1] 182 | )} for cartridge ${chalk.magenta.bold(building[0])} ...\n\n` 183 | ) 184 | ) 185 | exec(cmd, (err, data, stderr) => { 186 | if (err || stderr) { 187 | output(() => console.log(chalk.red.bold(`✖ Build Error: ${err} ${stderr}`))) 188 | } 189 | }) 190 | } 191 | }) 192 | } else { 193 | const filePath = path.dirname(file) 194 | const ext = file.split('.').pop() 195 | const dirs = filePath.split('/') 196 | const length = path.normalize(selected.d).split('/').length 197 | 198 | // Ignore file changes that are likely results from builds 199 | const ignoredPath = ['/css/', '/static/'] 200 | 201 | // Check current directory for WebPack 202 | if (fs.existsSync(path.join(filePath, 'webpack.config.js')) && !new RegExp(ignoredPath.join('|')).test(file)) { 203 | compile(ext, filePath) 204 | } else { 205 | // Work our way backwards to look for WebPack until we get to project root 206 | for (var i = dirs.length; i >= length; i--) { 207 | dirs.pop() 208 | let curPath = dirs.join('/') 209 | 210 | if ( 211 | fs.existsSync(path.join(curPath, 'webpack.config.js')) && 212 | !new RegExp(ignoredPath.join('|')).test(file) 213 | ) { 214 | compile(ext, curPath) 215 | break 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | // Custom Callback ( not currently in use, might extend this in the future ) 223 | const callback = () => {} 224 | 225 | // Watch for File Changes 226 | watcher.on('change', (file) => { 227 | if (!compileOnly) { 228 | upload({file, spinner, selected, client, instance, options, callback}) 229 | } 230 | 231 | buildCheck(file) 232 | }) 233 | 234 | watcher.on('add', (file) => { 235 | if (!compileOnly) { 236 | upload({file, spinner, selected, client, instance, options, callback}) 237 | } 238 | 239 | buildCheck(file) 240 | }) 241 | 242 | // @TODO: Watch for Removing Files 243 | watcher.on('unlink', (file) => { 244 | if (!compileOnly) { 245 | output(() => console.log(`${chalk.red('✖ REMOVING')} ${file.replace(selected.d, '.')}`)) 246 | } 247 | }) 248 | 249 | // Watch for Errors 250 | watcher.on('error', (error) => { 251 | notify({ 252 | title: `${client} ${instance}`, 253 | icon: path.join(__dirname, '../icons/', 'sfcc-error.png'), 254 | subtitle: 'WATCH FAILED', 255 | message: error, 256 | sound: true, 257 | wait: true, 258 | }) 259 | 260 | errorMessage = `✖ Watch Error for '${client}' '${instance}': ${error}.` 261 | if (useLog) { 262 | logger.log(errorMessage) 263 | } else { 264 | output(() => console.log(chalk.red.bold(`\n${errorMessage}`))) 265 | } 266 | }) 267 | 268 | watcher.on('ready', () => { 269 | if (!errorsOnly) { 270 | notify({ 271 | title: `${client} ${instance}`, 272 | icon: path.join(__dirname, '../icons/', 'sfcc-cli.png'), 273 | subtitle: 'STARTING WATCHER', 274 | message: 'Waiting for Changes ...', 275 | }) 276 | } 277 | 278 | if (useLog) { 279 | logger.log(`Watching ${client} ${instance}`, true) 280 | } 281 | }) 282 | } else if (client && instance) { 283 | errorMessage = `✖ Config does not contain client '${client}' with instance '${instance}'.` 284 | if (useLog) { 285 | logger.log(errorMessage) 286 | } else { 287 | console.log(chalk.red.bold(`\n${errorMessage}`)) 288 | console.log('Use ' + chalk.cyan('sfcc setup') + ' to get started.\n') 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /docs/cmd-delete.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc delete` 6 | --- 7 | 8 | > Delete Config for Client 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/delete.gif?v=1.3.0) 11 | 12 | To delete a configuration option, you can pass in a client and instance you want to delete. Or just delete everything for that client: 13 | 14 | ```bash 15 | sfcc delete 16 | sfcc delete 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/cmd-help.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc help` 6 | --- 7 | 8 | > Get Help when you need it 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/help.gif?v=1.3.0) 11 | 12 | You can get help at any time by running: 13 | 14 | ```bash 15 | sfcc help 16 | ``` 17 | 18 | You can get help on a specific command by just adding `help` to the end of the command: 19 | 20 | ```bash 21 | sfcc log help 22 | sfcc setup help 23 | sfcc watch help 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/cmd-list.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc list` 6 | --- 7 | 8 | > List Configured SFCC Clients 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/list.gif?v=1.3.0) 11 | 12 | To get a list of your current clients & instances, you can run: 13 | 14 | ```bash 15 | sfcc list 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/cmd-log.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc log` 6 | --- 7 | 8 | > Stream/Search log files from an instance 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/log.gif?v=1.3.0) 11 | 12 | If you only have a single project, you can run: 13 | 14 | ```bash 15 | sfcc log 16 | ``` 17 | 18 | If needed to setup multiple clients, or multiple instances for the same client, you will need to specify what to use: 19 | 20 | ```bash 21 | sfcc log 22 | ``` 23 | 24 | **FLAGS:** 25 | 26 | Name | Param | Default | Definition 27 | ---------|------------|---------|---------------------------------------------- 28 | Polling | `-p` | `2` | Polling Interval ( in Seconds ) 29 | Lines | `-l` | `100` | Number of Existing Lines to Display 30 | Include | `-i` | | Log Types to Include ( use `sfcc log --list` for list ) 31 | Exclude | `-e` | | Log Types to Exclude ( use `sfcc log --list` for list ) 32 | Filter | `-f` | | Filter Log Messages that contain this string or RegExp 33 | Truncate | `-t` | | Length to Truncate Messages ( if they are too long ) 34 | List | `--list` | | Output List of Log Types for `-i` & `-e` 35 | Search | `--search` | `false` | Search Logs with no Live Updates 36 | Latest | `--latest` | `false` | Show Latest Logs Only ( default is to use ALL logs ) 37 | 38 | #### ADVANCED USE: 39 | 40 | Get current list of log types: 41 | 42 | ```bash 43 | sfcc log --list 44 | ``` 45 | 46 | Watch latest `customerror` logs that contain the text `PipelineCallServlet`: 47 | 48 | ```bash 49 | sfcc log -i customerror -l 5 -f PipelineCallServlet --latest 50 | ``` 51 | 52 | View any `warn,error` logs that contain a matching `[0-9]{15}` RegExp pattern, and watch for new entries: 53 | 54 | ```bash 55 | sfcc log -i warn,error -f '[0-9]{15}' 56 | ``` 57 | 58 | Search all latest existing logs except `staging,syslog` that contain either `WARN|ERROR` RegExp pattern: 59 | 60 | ```bash 61 | sfcc log -e staging,syslog -f 'WARN|ERROR' --search --latest 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/cmd-setup.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc setup` 6 | --- 7 | 8 | > Get your SFCC Repo added to the SFCC CLI Config 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/setup.gif?v=1.3.0) 11 | 12 | #### Enter Params via Prompt 13 | 14 | If installed globally, you can run: 15 | 16 | ```bash 17 | sfcc setup 18 | ``` 19 | 20 | otherwise: 21 | 22 | ```bash 23 | ./bin/cli.js setup 24 | ``` 25 | 26 | #### Pass Params via CLI 27 | 28 | ```bash 29 | sfcc setup -c "My Client" -h dev04-web-mysandbox.demandware.net -d /Users/Name/Projects/mysandbox -u my@email.com -p 'my^pass' 30 | ``` 31 | 32 | NOTE: When using the password flag, make sure to wrap the text with SINGLE QUOTES. Using Double Quotes will cause issues with passwords that contain dollar signs. 33 | 34 | **FLAGS:** 35 | 36 | Name | Param | Required | Default | Definition 37 | ---------------|-------|----------|-----------|---------------------------------------------- 38 | Client Name | `-c` | Yes | | Used to group config instances 39 | Hostname | `-h` | Yes | | The root domain name for environment 40 | Directory | `-d` | Yes | | Absolute path to the projects SFCC repository 41 | Username | `-u` | Yes | | Instances SFCC Business Manager Username 42 | Password | `-p` | Yes | | Instances SFCC Business Manager Password 43 | Instance Alias | `-a` | No | `sandbox` | Custom Name to give this Instance 44 | Code Version | `-v` | No | `develop` | SFCC Code Version to use 45 | 46 | **SAVED TO: ~/.sfcc-cli** 47 | 48 | ```json 49 | { 50 | "my-client": { 51 | "sandbox": { 52 | "h": "dev04-web-mysandbox.demandware.net", 53 | "d": "/Users/Name/Projects/mysandbox", 54 | "u": "my@email.com", 55 | "p": "my^pass", 56 | "a": "sandbox", 57 | "v": "develop" 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | If you have Eclipse Build scripts in your cartridges ( `*.launch` files contained inside `.externalToolBuilders` ), these will also get added to your config file. An exammple of that project might look like this: 64 | 65 | ```json 66 | { 67 | "my-client": { 68 | "sandbox": { 69 | "h": "dev04-web-mysandbox.demandware.net", 70 | "d": "/Users/Name/Projects/mysandbox", 71 | "u": "my@email.com", 72 | "p": "my^pass", 73 | "a": "sandbox", 74 | "v": "develop", 75 | "b": { 76 | "gulp-builder-javascript-builder": { 77 | "enabled": true, 78 | "watch": [ 79 | "/app_storefront_core/cartridge/js" 80 | ], 81 | "cmd": { 82 | "basedir": "/path/to/gulp_builder", 83 | "exec": "cd /path/to/gulp_builder; gulp js --basedir=/path/to/gulp_builder" 84 | } 85 | }, 86 | "gulp-builder-styles-builder": { 87 | "enabled": true, 88 | "watch": [ 89 | "/app_storefront_core/cartridge/scss" 90 | ], 91 | "cmd": { 92 | "basedir": "/path/to/gulp_builder", 93 | "exec": "cd /path/to/gulp_builder; gulp styles --basedir=/path/to/gulp_builder" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /docs/cmd-watch.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md#developer-overview)** 4 | 5 | `sfcc watch` 6 | --- 7 | 8 | > Watch for Code Changes and Push Updates 9 | 10 | ![demo](https://sfcc-cli.s3.amazonaws.com/watch.gif?v=1.3.0) 11 | 12 | Watch for any changes to your projects source code, and automagically upload in the background, while you use whatever IDE you want. 13 | 14 | If you have already run `sfcc setup`, and you only have a single project, you can run: 15 | 16 | ```bash 17 | sfcc watch 18 | ``` 19 | 20 | If needed to setup multiple clients, or multiple instances for the same client, you will need to specify what to watch: 21 | 22 | ```bash 23 | sfcc watch 24 | ``` 25 | 26 | If you don't want to get notified of every upload, but would rather only get notified about errors, you can pass `--errors-only`: 27 | 28 | ```bash 29 | sfcc watch --errors-only 30 | ``` 31 | 32 | If you just want to trigger the compiler on file changes, and let your IDE handle uploads, you can pass `--compile-only`: 33 | 34 | ```bash 35 | sfcc watch --compile-only 36 | ``` 37 | 38 | If you would like to run the watcher as a background process, but capture the log output, you can pass `--log`, and a log will be created at `~/.sfcc-cli.log` ( log is truncated to last 500 lines each time you start a new `watch` ): 39 | 40 | ```bash 41 | sfcc watch --log & 42 | ``` 43 | 44 | NOTE: To run the background process, you'll need to make sure to add a `&` to the end of the command. You should see output from that command that looks like `[2] 6768`. You can bring the process to the foreground by running using the number in the brackets, and running a command like `fg %2`. You can get a list of all jobs running in the background by running `jobs`. 45 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://sfccdevops.s3.amazonaws.com/logo-128.png "Logo") 2 | 3 | **[↤ Developer Overview](../README.md)** 4 | 5 | Troubleshooting 6 | === 7 | 8 | > This document contains a list of known issues, and how to solve them. 9 | 10 | 11 | 12 | `Error [ERR_TLS_CERT_ALTNAME_INVALID]` 13 | --- 14 | 15 | For `sfcc-cli` commands that initiate network requests, you may get an error similar to the following: 16 | 17 | `Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: Host: dev09.commerce.myclient.demandware.net. is not in the cert's altnames: DNS:*.demandware.net, DNS:demandware.net` 18 | 19 | This is because node cannot verify that the server is valid according to the TLS certificate. The following command executed in your terminal will temporarily disable this check while you have your terminal session open. 20 | 21 | #### Mac and Linux 22 | 23 | ```bash 24 | export NODE_TLS_REJECT_UNAUTHORIZED=0 25 | ``` 26 | 27 | #### Windows 28 | 29 | ```bash 30 | set NODE_TLS_REJECT_UNAUTHORIZED=0 31 | ``` 32 | 33 | `SyntaxError: Unexpected Token (` 34 | --- 35 | 36 | You are getting this since the CLI tool uses ES6 Javascript, and you are likely using an older version of node that does not support it. Make sure you are using the latest version of node. We suggest v10+. 37 | 38 | Once you have a newer version of Node installed, you will need to uninstall / reinstall this package. Your setup will be save to `~/.sfcc-cli` so it is safe to do a clean install without losing anything. 39 | -------------------------------------------------------------------------------- /icons/sfcc-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfccdevops/sfcc-cli/f142ec4bf340e6cfcd7d4e60d8e4fdaa2f00a666/icons/sfcc-cli.png -------------------------------------------------------------------------------- /icons/sfcc-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfccdevops/sfcc-cli/f142ec4bf340e6cfcd7d4e60d8e4fdaa2f00a666/icons/sfcc-error.png -------------------------------------------------------------------------------- /icons/sfcc-reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfccdevops/sfcc-cli/f142ec4bf340e6cfcd7d4e60d8e4fdaa2f00a666/icons/sfcc-reload.png -------------------------------------------------------------------------------- /icons/sfcc-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfccdevops/sfcc-cli/f142ec4bf340e6cfcd7d4e60d8e4fdaa2f00a666/icons/sfcc-success.png -------------------------------------------------------------------------------- /icons/sfcc-uploading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfccdevops/sfcc-cli/f142ec4bf340e6cfcd7d4e60d8e4fdaa2f00a666/icons/sfcc-uploading.png -------------------------------------------------------------------------------- /lib/builds.js: -------------------------------------------------------------------------------- 1 | const convert = require('xml-js') 2 | const fs = require('fs') 3 | const os = require('os').type() 4 | const path = require('path') 5 | 6 | const slug = require('./slug') 7 | 8 | module.exports = (project) => { 9 | let externalToolBuilders = [] 10 | let build = {} 11 | 12 | // Look for *.launch files in .externalToolBuilders folder 13 | fs.readdirSync(project).map((fileName) => { 14 | const filePath = path.join(project, fileName) 15 | if (fs.statSync(filePath).isDirectory()) { 16 | const buildPath = path.join(filePath, '.externalToolBuilders') 17 | if (fs.existsSync(buildPath) && fs.statSync(buildPath).isDirectory()) { 18 | fs.readdirSync(buildPath).map((buildFile) => { 19 | if (path.extname(buildFile) === '.launch') { 20 | externalToolBuilders.push(path.join(buildPath, buildFile)) 21 | } 22 | }) 23 | } 24 | } 25 | }) 26 | 27 | // Loop through found .externalToolBuilders files and parse build instructions 28 | if (externalToolBuilders.length > 0) { 29 | externalToolBuilders.map((launch) => { 30 | const xml = fs.readFileSync(launch) 31 | const json = convert.xml2json(xml) 32 | const builder = json ? JSON.parse(json) : null 33 | const name = slug( 34 | path.basename(path.dirname(path.dirname(launch))).replace('_', '-') + 35 | '_' + 36 | path.basename(launch).replace('.launch', '') 37 | ) 38 | 39 | // setup placeholder for this config file 40 | build[name] = { 41 | enabled: false, 42 | watch: [], 43 | cmd: { 44 | basedir: null, 45 | exec: null, 46 | }, 47 | } 48 | 49 | if ( 50 | builder && 51 | typeof builder.elements !== 'undefined' && 52 | builder.elements.length === 1 && 53 | builder.elements[0].name === 'launchConfiguration' && 54 | builder.elements[0].elements 55 | ) { 56 | builder.elements[0].elements.map((elm) => { 57 | // Get Watch Directories 58 | if (elm.attributes.key === 'org.eclipse.ui.externaltools.ATTR_BUILD_SCOPE') { 59 | const buildScopeJson = convert.xml2json( 60 | elm.attributes.value.replace('${working_set:', '').replace(/}$/, '') 61 | ) 62 | const buildScope = buildScopeJson ? JSON.parse(buildScopeJson) : null 63 | 64 | if ( 65 | buildScope && 66 | typeof buildScope.elements !== 'undefined' && 67 | buildScope.elements.length === 1 && 68 | buildScope.elements[0].name === 'resources' && 69 | builder.elements[0].elements 70 | ) { 71 | buildScope.elements[0].elements.map((buildSrc) => { 72 | build[name].watch.push(buildSrc.attributes.path) 73 | }) 74 | } 75 | } 76 | 77 | // Check if we should enable this build 78 | if (elm.attributes.key === 'org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS') { 79 | build[name].enabled = 80 | elm.attributes.value.includes('full') || 81 | elm.attributes.value.includes('incremental') || 82 | elm.attributes.value.includes('auto') 83 | } 84 | 85 | // Get Build Instructuctions 86 | if (elm.attributes.key === 'org.eclipse.ui.externaltools.ATTR_LOCATION') { 87 | const buildFilePath = elm.attributes.value.replace('${workspace_loc:', '').replace(/}$/, '') 88 | const buildFileXml = fs.readFileSync(path.join(project, buildFilePath)) 89 | const buildFileJson = convert.xml2json(buildFileXml) 90 | const buildInstructions = buildFileJson ? JSON.parse(buildFileJson) : null 91 | 92 | if ( 93 | buildInstructions && 94 | typeof buildInstructions.elements !== 'undefined' && 95 | buildInstructions.elements.length === 1 && 96 | buildInstructions.elements[0].name === 'project' && 97 | buildInstructions.elements[0].elements 98 | ) { 99 | buildInstructions.elements[0].elements.map((buildInstruction) => { 100 | build[name].cmd.basedir = 101 | typeof buildInstructions.elements[0].attributes.basedir !== 'undefined' 102 | ? path.join( 103 | project, 104 | path.basename(path.dirname(path.dirname(launch))), 105 | buildInstructions.elements[0].attributes.basedir 106 | ) 107 | : null 108 | buildInstruction.elements.map((instruction) => { 109 | if (instruction.name === 'exec') { 110 | let exec = instruction.attributes.executable 111 | if ( 112 | (exec === 'cmd' && os === 'Windows_NT') || 113 | (exec !== 'cmd' && (os === 'Darwin' || os === 'Linux')) 114 | ) { 115 | instruction.elements.map((arg) => { 116 | if (arg.name === 'arg') { 117 | exec = exec.concat(' ' + arg.attributes.value) 118 | } 119 | }) 120 | 121 | // Replace commands that are not needed outside Eclipse 122 | exec = exec.replace('/bin/bash -l -c ', '') 123 | exec = exec.replace(/\${basedir}/g, build[name].cmd.basedir) 124 | 125 | build[name].cmd.exec = exec 126 | } 127 | } 128 | }) 129 | }) 130 | } 131 | } 132 | }) 133 | } 134 | }) 135 | } 136 | 137 | return build 138 | } 139 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const fs = require('fs') 3 | const homedir = require('os').homedir() 4 | const path = require('path') 5 | 6 | module.exports = () => { 7 | const configFile = path.join(homedir, '.sfcc-cli') 8 | let config = {} 9 | let requireParams = false 10 | 11 | if (fs.existsSync(configFile)) { 12 | const currentConfig = fs.readFileSync(configFile, 'utf8') 13 | if (currentConfig) { 14 | config = Object.assign({}, config, JSON.parse(currentConfig)) 15 | } 16 | } 17 | 18 | const get = (client, instance, hasSingle) => { 19 | if (client && instance) { 20 | if ( 21 | Object.prototype.isPrototypeOf.call(config, client) && 22 | Object.prototype.isPrototypeOf.call(config[client], instance) 23 | ) { 24 | return config[client][instance] 25 | } 26 | } else if (client) { 27 | if (Object.prototype.isPrototypeOf.call(config, client)) { 28 | return config[client] 29 | } 30 | } 31 | 32 | if (hasSingle && Object.keys(config).length === 1 && config.constructor === Object) { 33 | var singleClient = Object.keys(config)[0] 34 | 35 | if (Object.keys(config[singleClient]).length === 1 && config[singleClient].constructor === Object) { 36 | var singleInstance = Object.keys(config[singleClient])[0] 37 | return { 38 | client: singleClient, 39 | instance: singleInstance, 40 | config: config[singleClient][singleInstance], 41 | } 42 | } else if (Object.keys(config[singleClient]).length > 1 && config[singleClient].constructor === Object) { 43 | requireParams = true 44 | } 45 | } else if (hasSingle && Object.keys(config).length > 1 && config.constructor === Object) { 46 | requireParams = true 47 | } 48 | 49 | if (requireParams) { 50 | console.log(chalk.red.bold(`\n✖ Multiple clients / instance detected.`)) 51 | console.log('Use ' + chalk.cyan('sfcc ') + ' to specify which client instance.') 52 | console.log('Use ' + chalk.cyan('sfcc list') + ' to view complete list.\n') 53 | return null 54 | } 55 | 56 | return config 57 | } 58 | 59 | const getAll = () => { 60 | return config 61 | } 62 | 63 | const set = (config, isUpdate) => { 64 | fs.writeFileSync(configFile, JSON.stringify(config), 'utf8') 65 | 66 | if (isUpdate) { 67 | console.log(chalk.green(`\n【ツ】Updated: ${configFile} \n`)) 68 | } else { 69 | console.log(chalk.green(`\n【ツ】Created: ${configFile} \n`)) 70 | } 71 | } 72 | 73 | return { 74 | get, 75 | getAll, 76 | set, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/find.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const path = require('path') 3 | const {parseString} = require('xml2js') 4 | const get = require('lodash/get') 5 | const forEach = require('lodash/forEach') 6 | 7 | module.exports = async (file, options) => { 8 | try { 9 | const {data} = await axios( 10 | Object.assign({}, options, { 11 | headers: { 12 | Depth: 1, 13 | }, 14 | url: path.isAbsolute(file) ? file : `/${file}`, 15 | method: 'PROPFIND', 16 | }) 17 | ) 18 | return await new Promise((resolve, reject) => { 19 | parseString(data, (err, res) => { 20 | if (err) { 21 | return reject(err) 22 | } 23 | 24 | resolve( 25 | res.multistatus.response.map((file) => { 26 | const info = get(file, 'propstat.0.prop.0') 27 | forEach(info, (value, name) => { 28 | info[name] = get(value, '0') 29 | }) 30 | return info 31 | }) 32 | ) 33 | }) 34 | }) 35 | } catch (err) { 36 | return await new Promise((resolve, reject) => { 37 | let errorMessage = 'Unable to connect to WebDAV. Check credentials in ~/.sfcc-cli' 38 | if (err && err.message === 'Request failed with status code 401') { 39 | errorMessage = 'Invalid Username or Password. Run `sfcc setup` or edit ~/.sfcc-cli' 40 | } 41 | 42 | reject(errorMessage) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const homedir = require('os').homedir() 3 | const path = require('path') 4 | const truncate = require('truncate-logs') 5 | 6 | module.exports = () => { 7 | const logFile = path.join(homedir, '.sfcc-cli.log') 8 | 9 | fs.ensureFileSync(logFile) 10 | 11 | const createMessage = (message) => { 12 | return new Date().toLocaleString('en-US', {hour12: false}).replace(', ', '-') + ': ' + message + '\n' 13 | } 14 | 15 | const log = (message, start) => { 16 | // Starting a new Watch, let's clean out some old junk to keep log file down 17 | if (start) { 18 | truncate([logFile], {lines: 500}) 19 | } 20 | 21 | fs.appendFile(logFile, createMessage(message), function (err) { 22 | if (err) throw err 23 | }) 24 | } 25 | 26 | return { 27 | log, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/mkdir.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const axios = require('axios') 3 | 4 | module.exports = async (dir, options) => { 5 | try { 6 | const url = path.join('/', dir) 7 | const response = await axios( 8 | Object.assign({}, options, { 9 | url, 10 | method: 'MKCOL', 11 | }) 12 | ) 13 | return response 14 | } catch (err) { 15 | return new Promise((resolve, reject) => { 16 | if (!err.response) { 17 | reject(new Error(err)) 18 | } else if (err.response.status === 405) { 19 | resolve() 20 | } else { 21 | reject(new Error(err.response.statusText)) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/mkdirp.js: -------------------------------------------------------------------------------- 1 | const {normalize} = require('path') 2 | const Bluebird = require('bluebird') 3 | const mkdir = require('./mkdir') 4 | 5 | module.exports = (dir, options) => { 6 | const folders = normalize(dir) 7 | .split('/') 8 | .filter((folder) => folder.length) 9 | return Bluebird.each(folders, (folder, i) => { 10 | if (i > 0) { 11 | folder = folders.slice(0, i + 1).join('/') 12 | } 13 | return mkdir(folder, options) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /lib/notify.js: -------------------------------------------------------------------------------- 1 | const debounce = require('lodash/debounce') 2 | const notifier = require('node-notifier') 3 | 4 | module.exports = () => { 5 | return debounce((args) => notifier.notify(args), 150) 6 | } 7 | -------------------------------------------------------------------------------- /lib/read.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const axios = require('axios') 3 | 4 | module.exports = async (file, options) => { 5 | try { 6 | const {data} = await axios( 7 | Object.assign({}, options, { 8 | url: path.isAbsolute(file) ? file : `/${file}`, 9 | method: 'GET', 10 | }) 11 | ) 12 | 13 | return data 14 | } catch (err) {} 15 | } 16 | -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const compact = require('lodash/compact') 3 | const map = require('lodash/map') 4 | const ora = require('ora') 5 | const truncate = require('lodash/truncate') 6 | 7 | const read = require('./read') 8 | 9 | module.exports = async (selected, client, instance, groups, options) => { 10 | const including = options.include && options.include.length > 0 ? ` '${options.include.join(', ')}'` : '' 11 | const excluding = options.exclude && options.exclude.length > 0 ? ` '${options.exclude.join(', ')}'` : '' 12 | const filters = options.filter && options.filter.length > 0 ? ` containing '${options.filter}'` : '' 13 | 14 | const text = chalk.bold(`Searching${including}${excluding} Logs${filters}`).concat(' [Ctrl-C to Cancel]\n') 15 | const spinner = ora(text) 16 | const output = (fn) => { 17 | spinner.stop() 18 | fn() 19 | spinner.start() 20 | } 21 | 22 | const promiseGroups = map(groups, (files, name) => { 23 | return map(files, async (file) => { 24 | const displayname = file.displayname 25 | try { 26 | const response = await read(`Logs/${displayname}`, { 27 | baseURL: `https://${selected.h}/on/demandware.servlet/webdav/Sites/`, 28 | auth: { 29 | username: selected.u, 30 | password: selected.p, 31 | }, 32 | }) 33 | return { 34 | response, 35 | name, 36 | } 37 | } catch (err) { 38 | output(() => console.log(err)) 39 | } 40 | }) 41 | }) 42 | 43 | for (const promises of promiseGroups) { 44 | const results = await Promise.all(promises) 45 | 46 | for (const {response, name} of compact(results)) { 47 | const lines = response.split('\n').slice(-options.lines) 48 | 49 | for (let line of lines) { 50 | if (line) { 51 | if (!options.noTimestamp) { 52 | line = line.replace(/\[(.+)\sGMT\]/g, (exp, match) => { 53 | const date = new Date(Date.parse(match + 'Z')) 54 | return chalk.magenta(`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`) 55 | }) 56 | } 57 | // if there's a filter and it doesn't pass .,., the ignore line 58 | if (options.filter && !new RegExp(options.filter, 'ig').test(line)) { 59 | continue 60 | } 61 | // highlight the matching parts of the line 62 | if (options.filter) { 63 | line = line.replace(new RegExp(options.filter, 'ig'), (exp) => { 64 | return chalk.white.bgGreen.bold(exp) 65 | }) 66 | } 67 | 68 | if (options.truncate > 0) { 69 | line = truncate(line.trim(), { 70 | length: options.truncate, 71 | omission: '…', 72 | }) 73 | } 74 | 75 | output(() => console.log(`${chalk.cyan.bold(name)} ${line}`)) 76 | } 77 | } 78 | } 79 | } 80 | 81 | // spinner.stop(); 82 | spinner.text = `Search ${including}${excluding} Logs${filters} Complete` 83 | spinner.succeed() 84 | process.exit() 85 | } 86 | -------------------------------------------------------------------------------- /lib/slug.js: -------------------------------------------------------------------------------- 1 | module.exports = (text) => { 2 | text = text.toString().toLowerCase().trim() 3 | 4 | const sets = [ 5 | { 6 | to: 'a', 7 | from: '[ÀÁÂÃÄÅÆĀĂĄẠẢẤẦẨẪẬẮẰẲẴẶ]', 8 | }, 9 | { 10 | to: 'c', 11 | from: '[ÇĆĈČ]', 12 | }, 13 | { 14 | to: 'd', 15 | from: '[ÐĎĐÞ]', 16 | }, 17 | { 18 | to: 'e', 19 | from: '[ÈÉÊËĒĔĖĘĚẸẺẼẾỀỂỄỆ]', 20 | }, 21 | { 22 | to: 'g', 23 | from: '[ĜĞĢǴ]', 24 | }, 25 | { 26 | to: 'h', 27 | from: '[ĤḦ]', 28 | }, 29 | { 30 | to: 'i', 31 | from: '[ÌÍÎÏĨĪĮİỈỊ]', 32 | }, 33 | { 34 | to: 'j', 35 | from: '[Ĵ]', 36 | }, 37 | { 38 | to: 'ij', 39 | from: '[IJ]', 40 | }, 41 | { 42 | to: 'k', 43 | from: '[Ķ]', 44 | }, 45 | { 46 | to: 'l', 47 | from: '[ĹĻĽŁ]', 48 | }, 49 | { 50 | to: 'm', 51 | from: '[Ḿ]', 52 | }, 53 | { 54 | to: 'n', 55 | from: '[ÑŃŅŇ]', 56 | }, 57 | { 58 | to: 'o', 59 | from: '[ÒÓÔÕÖØŌŎŐỌỎỐỒỔỖỘỚỜỞỠỢǪǬƠ]', 60 | }, 61 | { 62 | to: 'oe', 63 | from: '[Œ]', 64 | }, 65 | { 66 | to: 'p', 67 | from: '[ṕ]', 68 | }, 69 | { 70 | to: 'r', 71 | from: '[ŔŖŘ]', 72 | }, 73 | { 74 | to: 's', 75 | from: '[ߌŜŞŠ]', 76 | }, 77 | { 78 | to: 't', 79 | from: '[ŢŤ]', 80 | }, 81 | { 82 | to: 'u', 83 | from: '[ÙÚÛÜŨŪŬŮŰŲỤỦỨỪỬỮỰƯ]', 84 | }, 85 | { 86 | to: 'w', 87 | from: '[ẂŴẀẄ]', 88 | }, 89 | { 90 | to: 'x', 91 | from: '[ẍ]', 92 | }, 93 | { 94 | to: 'y', 95 | from: '[ÝŶŸỲỴỶỸ]', 96 | }, 97 | { 98 | to: 'z', 99 | from: '[ŹŻŽ]', 100 | }, 101 | { 102 | to: '-', 103 | from: "[·/_,:;']", 104 | }, 105 | ] 106 | 107 | sets.forEach((set) => { 108 | text = text.replace(new RegExp(set.from, 'gi'), set.to) 109 | }) 110 | 111 | text = text 112 | .toString() 113 | .toLowerCase() 114 | .replace(/\s+/g, '-') 115 | .replace(/&/g, '-and-') 116 | .replace(/[^\w-]+/g, '') 117 | .replace(/--+/g, '-') 118 | .replace(/^-+/, '') 119 | .replace(/-+$/, '') 120 | 121 | return text 122 | } 123 | -------------------------------------------------------------------------------- /lib/tail.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const compact = require('lodash/compact') 3 | const map = require('lodash/map') 4 | const ora = require('ora') 5 | const truncate = require('lodash/truncate') 6 | 7 | const read = require('./read') 8 | 9 | module.exports = async (selected, client, instance, logs, groups, options) => { 10 | const including = options.include && options.include.length > 0 ? ` '${options.include.join(', ')}'` : '' 11 | const excluding = options.exclude && options.exclude.length > 0 ? ` excluding '${options.exclude.join(', ')}'` : '' 12 | const filters = options.filter && options.filter.length > 0 ? ` containing '${options.filter}'` : '' 13 | const text = chalk.bold(`Streaming${including} Logs${excluding}${filters}`).concat(' [Ctrl-C to Cancel]\n') 14 | const spinner = ora(text) 15 | const output = (fn) => { 16 | spinner.stop() 17 | fn() 18 | spinner.start() 19 | } 20 | 21 | const tail = async () => { 22 | const promises = map(groups, async (files, name) => { 23 | const displayname = files[0].displayname 24 | try { 25 | const response = await read(`Logs/${displayname}`, { 26 | baseURL: `https://${selected.h}/on/demandware.servlet/webdav/Sites/`, 27 | auth: { 28 | username: selected.u, 29 | password: selected.p, 30 | }, 31 | }) 32 | return { 33 | response, 34 | name, 35 | } 36 | } catch (err) { 37 | output(() => console.log(err)) 38 | } 39 | }) 40 | 41 | const results = await Promise.all(promises) 42 | 43 | for (const {response, name} of compact(results)) { 44 | const lines = response.split('\n').slice(-options.lines) 45 | 46 | for (let line of lines) { 47 | if (line && !logs[name].includes(line)) { 48 | logs[name].push(line) 49 | 50 | if (options.filter && !new RegExp(options.filter).test(line)) { 51 | continue 52 | } 53 | 54 | if (options.truncate > 0) { 55 | line = truncate(line.trim(), { 56 | length: options.truncate, 57 | omission: '…', 58 | }) 59 | } 60 | 61 | if (options.filter) { 62 | line = line.replace(new RegExp(options.filter, 'g'), (exp) => { 63 | return chalk.white.bgGreen.bold(exp) 64 | }) 65 | } 66 | 67 | line = line.replace(/\[(.+)\sGMT\]/g, (exp, match) => { 68 | const date = new Date(Date.parse(match + 'Z')) 69 | return chalk.magenta(`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`) 70 | }) 71 | 72 | output(() => console.log(`${chalk.cyan.bold(name)} ${line}`)) 73 | } 74 | } 75 | } 76 | 77 | setTimeout(tail, options.polling * 1000) 78 | } 79 | 80 | tail() 81 | } 82 | -------------------------------------------------------------------------------- /lib/upload.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const path = require('path') 3 | const pRetry = require('p-retry') 4 | 5 | const logger = require('../lib/logger')() 6 | const notify = require('../lib/notify')() 7 | const write = require('./write') 8 | const mkdirp = require('./mkdirp') 9 | 10 | module.exports = async ({file, spinner, selected, client, instance, options, callback}) => { 11 | const src = path.relative(process.cwd(), file) 12 | const uploading = new Set() 13 | const useLog = options.log 14 | const errorsOnly = options.errorsOnly 15 | 16 | if (!uploading.has(src)) { 17 | const dir = path.dirname(file).replace(path.normalize(selected.d), '') 18 | const dest = path.join('/', 'Cartridges', selected.v, dir) 19 | const text = chalk.bold(`Watching ${client} ${instance}`).concat(' [Ctrl-C to Cancel]\n') 20 | 21 | uploading.add(src) 22 | 23 | if (!errorsOnly) { 24 | notify({ 25 | title: `${client} ${instance}`, 26 | icon: path.join(__dirname, '../icons/', 'sfcc-uploading.png'), 27 | subtitle: 'UPLOADING ...', 28 | message: `${path.basename(src)}`, 29 | }) 30 | } 31 | 32 | let logMessage = `${chalk.cyan('▲ UPLOADING')} ${file.replace(selected.d, '.')}...` 33 | 34 | if (useLog) { 35 | logger.log(logMessage) 36 | } else { 37 | spinner.stop() 38 | console.log(logMessage) 39 | } 40 | 41 | try { 42 | const request = { 43 | baseURL: `https://${selected.h}/on/demandware.servlet/webdav/Sites/`, 44 | auth: { 45 | username: selected.u, 46 | password: selected.p, 47 | }, 48 | } 49 | 50 | const tryToMkdir = () => mkdirp(dest, request) 51 | const tryToWrite = () => write(file, dest, request) 52 | 53 | await pRetry(tryToMkdir, {retries: 5}) 54 | await pRetry(tryToWrite, {retries: 5}) 55 | 56 | if (!errorsOnly) { 57 | notify({ 58 | title: `${client} ${instance}`, 59 | icon: path.join(__dirname, '../icons/', 'sfcc-success.png'), 60 | subtitle: 'UPLOAD COMPLETE', 61 | message: `${path.basename(src)}`, 62 | }) 63 | } 64 | 65 | if (typeof callback === 'function') { 66 | callback({ 67 | type: 'upload', 68 | client: client, 69 | instance: instance, 70 | message: file.replace(selected.d, '.'), 71 | timestamp: new Date().toString(), 72 | }) 73 | } 74 | 75 | logMessage = `${chalk.green('COMPLETE')} ${file.replace(selected.d, '.')}` 76 | 77 | if (useLog) { 78 | logger.log(logMessage) 79 | } else { 80 | spinner.text = logMessage 81 | spinner.succeed() 82 | 83 | setTimeout(() => { 84 | spinner.text = text 85 | spinner.start() 86 | }, 3000) 87 | } 88 | } catch (err) { 89 | let errorMessage = `Unable to Upload ${path.basename(src)}` 90 | if (typeof err.retriesLeft !== 'undefined' && err.retriesLeft === 0) { 91 | errorMessage = 'Invalid Username or Password. Run `sfcc setup` or edit ~/.sfcc-cli' 92 | } 93 | 94 | if (useLog) { 95 | logger.log('✖ ' + errorMessage) 96 | } else { 97 | spinner.text = errorMessage 98 | spinner.fail() 99 | } 100 | 101 | notify({ 102 | title: `${client} ${instance}`, 103 | icon: path.join(__dirname, '../icons/', 'sfcc-error.png'), 104 | subtitle: 'UPLOAD FAILED', 105 | message: errorMessage, 106 | sound: true, 107 | wait: true, 108 | }) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/write.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const axios = require('axios') 4 | const followRedirects = require('follow-redirects') 5 | 6 | followRedirects.maxBodyLength = 100 * 1024 * 1024 7 | 8 | module.exports = (src, dest, options) => { 9 | try { 10 | const url = path.join('/', dest, path.basename(src)) 11 | const stream = fs.createReadStream(src) 12 | const config = Object.assign( 13 | { 14 | url, 15 | method: 'put', 16 | validateStatus: (status) => status < 400, 17 | maxRedirects: 0, 18 | }, 19 | {data: stream}, 20 | options 21 | ) 22 | 23 | const request = axios(config) 24 | .then(() => url) 25 | .catch((error) => { 26 | console.log('error', error) 27 | }) 28 | 29 | return request 30 | } catch (err) {} 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sfccdevops/sfcc-cli", 3 | "version": "1.3.1", 4 | "description": "Command Line Interface for Salesforce Commerce Cloud Sandbox Development", 5 | "homepage": "https://github.com/sfccdevops/sfcc-cli#readme", 6 | "license": "MIT", 7 | "publishConfig": { 8 | "registry": "https://npm.pkg.github.com" 9 | }, 10 | "main": "./bin/cli.js", 11 | "bin": { 12 | "sfcc": "./bin/cli.js" 13 | }, 14 | "engines": { 15 | "node": ">=14.17.3", 16 | "npm": ">= 6.14.13" 17 | }, 18 | "keywords": [ 19 | "cli", 20 | "salesforce", 21 | "sfcc", 22 | "commerce-cloud", 23 | "demandware", 24 | "sandbox", 25 | "sfra", 26 | "watch", 27 | "build", 28 | "upload", 29 | "log", 30 | "search", 31 | "macos", 32 | "windows", 33 | "linux" 34 | ], 35 | "contributors": [ 36 | { 37 | "name": "Peter Schmalfeldt", 38 | "email": "me@peterschmalfeldt.com", 39 | "url": "https://peterschmalfeldt.com" 40 | } 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/sfccdevops/sfcc-cli.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/sfccdevops/sfcc-cli/issues" 48 | }, 49 | "scripts": { 50 | "test": "eslint --ext .js ./ --fix && echo '\n【ツ】CODE PERFECTION !!!\n'" 51 | }, 52 | "lint-staged": { 53 | "*.js": [ 54 | "eslint . --fix", 55 | "git add" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "babel-eslint": "^10.0.1", 60 | "eslint": "^8.19.0", 61 | "eslint-config-prettier": "^8.5.0", 62 | "eslint-plugin-import": "^2.26.0", 63 | "eslint-plugin-prettier": "^4.2.1", 64 | "husky": "^8.0.1", 65 | "lint-staged": "^13.0.3", 66 | "minimist": "^1.2.6", 67 | "prettier": "^2.7.1" 68 | }, 69 | "dependencies": { 70 | "axios": "^0.27.2", 71 | "bluebird": "^3.7.2", 72 | "chalk": "^4.1.2", 73 | "chokidar": "^3.5.3", 74 | "debug": "^4.3.4", 75 | "express": "^4.18.1", 76 | "follow-redirects": "^1.15.1", 77 | "fs-extra": "^10.1.0", 78 | "https": "^1.0.0", 79 | "lodash": "^4.17.21", 80 | "node-notifier": "^10.0.1", 81 | "ora": "^5.4.1", 82 | "os": "^0.1.2", 83 | "p-retry": "^4.6.2", 84 | "prompt": "^1.3.0", 85 | "prompt-confirm": "^2.0.4", 86 | "truncate-logs": "^1.0.4", 87 | "xml-js": "^1.6.11", 88 | "xml2js": "^0.4.23", 89 | "yargs": "^17.5.1" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /sfcc-cli.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "cSpell.words": [ 9 | "sfccdevops", 10 | "sfra" 11 | ] 12 | } 13 | } 14 | --------------------------------------------------------------------------------