├── .gitignore ├── .eslintrc ├── config.js ├── scripts └── remove-conf.js ├── lib ├── commands │ ├── add-types │ │ ├── module.js │ │ ├── widget.js │ │ └── piece.js │ ├── add.js │ └── create.js ├── conf-utils.js ├── boilerplate.js └── util.js ├── package.json ├── bin └── apostrophe ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "apostrophe" 3 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const config = {}; 2 | 3 | module.exports = config; 4 | 5 | config.SHELL_DEPENDS = [ 'git' ]; 6 | 7 | const REPO_ROOT = 'https://github.com/apostrophecms'; 8 | config.BOILERPLATE = `${REPO_ROOT}/starter-kit-essentials.git`; 9 | config.A2_BOILERPLATE = `${REPO_ROOT}/apostrophe-boilerplate.git`; 10 | -------------------------------------------------------------------------------- /scripts/remove-conf.js: -------------------------------------------------------------------------------- 1 | const confUtils = require('../lib/conf-utils'); 2 | const fs = require('fs'); 3 | 4 | confUtils.clearConf(); 5 | 6 | const filePath = confUtils.getPath(); 7 | 8 | try { 9 | // file removed 10 | fs.unlinkSync(filePath); 11 | } catch (err) { 12 | /* eslint-disable no-console */ 13 | console.error('There was an error while attempting to delete the CLI configuration file.', err); 14 | console.error(`You can find the file at ${filePath}`); 15 | /* eslint-enable no-console */ 16 | } 17 | -------------------------------------------------------------------------------- /lib/commands/add-types/module.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | // Utilities from shelljs 3 | /* globals mkdir */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const util = require('../../util'); 7 | 8 | module.exports = function (moduleName, majorVersion, isEsm) { 9 | const modulesDir = 'modules'; 10 | const stringSet = 'current'; 11 | 12 | const strings = util.getStrings(stringSet, 'add-module', moduleName); 13 | 14 | util.log('add module', `Adding ${moduleName} folder to /${modulesDir}.`); 15 | 16 | const modulePath = path.join(modulesDir, moduleName); 17 | 18 | mkdir('-p', modulePath); 19 | 20 | const moduleConfig = util.esmify(strings.moduleConfig, isEsm); 21 | 22 | util.log('add module', `Setting up index.js for the ${moduleName} module.`); 23 | 24 | fs.writeFileSync(path.join(modulePath, 'index.js'), moduleConfig); 25 | 26 | return true; 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apostrophecms/cli", 3 | "version": "3.5.0", 4 | "description": "Commandline generator and configurator for Apostrophe CMS", 5 | "main": "bin/apostrophe", 6 | "scripts": { 7 | "test": "npx eslint .", 8 | "preuninstall": "node ./scripts/remove-conf" 9 | }, 10 | "author": "Apostrophe Technologies, Inc.", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/apostrophecms/cli.git" 15 | }, 16 | "dependencies": { 17 | "colors": "1.4.0", 18 | "commander": "^5.1.0", 19 | "common-tags": "^1.8.0", 20 | "conf": "^6.2.4", 21 | "lodash": "^4.17.20", 22 | "ora": "^5.4.1", 23 | "package-json": "^6.5.0", 24 | "pkginfo": "^0.4.1", 25 | "prompts": "^2.3.2", 26 | "semver": "^7.3.2", 27 | "shell-quote": "^1.8.1", 28 | "shelljs": "~0.3.x", 29 | "sync-request": "^6.0.0", 30 | "uuid": "^8.0.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^7.31.0", 34 | "eslint-config-apostrophe": "^3.4.1", 35 | "eslint-config-standard": "^14.1.0", 36 | "eslint-plugin-import": "^2.22.1", 37 | "eslint-plugin-node": "^11.0.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.1.0" 40 | }, 41 | "preferGlobal": true, 42 | "bin": { 43 | "apostrophe": "bin/apostrophe", 44 | "apos": "bin/apostrophe" 45 | } 46 | } -------------------------------------------------------------------------------- /lib/conf-utils.js: -------------------------------------------------------------------------------- 1 | const confUtils = {}; 2 | const Conf = require('conf'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | 5 | const conf = new Conf({ 6 | configName: 'cli_config', 7 | projectName: '@apostrophecms/cli', 8 | projectSuffix: '', 9 | schema: { 10 | uid: { 11 | type: 'string' 12 | }, 13 | versionNotified: { 14 | type: 'string' 15 | } 16 | } 17 | }); 18 | 19 | module.exports = confUtils; 20 | 21 | confUtils.getConf = function (propertyName) { 22 | return conf.get(propertyName); 23 | }; 24 | 25 | confUtils.setConf = function (propertyName, value) { 26 | return conf.set(propertyName, value); 27 | }; 28 | 29 | confUtils.checkConf = async function () { 30 | let details = conf.store; 31 | 32 | if (!details.uid) { 33 | details = await setupConf(); 34 | } 35 | 36 | return details; 37 | }; 38 | 39 | confUtils.clearConf = function () { 40 | // Just in case there are permissions issues with deleting the file, let's 41 | // clear it out first. 42 | conf.clear(); 43 | }; 44 | 45 | confUtils.getPath = function () { 46 | return conf.path; 47 | }; 48 | 49 | async function setupConf () { 50 | // eslint-disable-next-line no-console 51 | console.info('\n👋 It looks like this might be your first time using the Apostrophe CLI on this computer. Run `apos --help` for the available commands.\n'); 52 | 53 | const uid = uuidv4(); 54 | conf.set('uid', uid); 55 | 56 | return conf.store; 57 | } 58 | -------------------------------------------------------------------------------- /bin/apostrophe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('shelljs/global'); 4 | require('colors'); 5 | const { program } = require('commander'); 6 | const util = require('../lib/util'); 7 | const confUtils = require('../lib/conf-utils'); 8 | const fs = require('fs'); 9 | 10 | async function execute () { 11 | const versionInfo = getVersionInfo(); 12 | program.version(versionInfo.message); 13 | 14 | // Check for required shell dependencies. 15 | util.checkDependencies(); 16 | 17 | require('../lib/commands/create')(program); 18 | require('../lib/commands/add')(program, versionInfo.core); 19 | 20 | await confUtils.checkConf(); 21 | 22 | program.parseAsync(process.argv); 23 | 24 | if (process.argv.length <= 2) { 25 | // This means user passed no args, so display help information. 26 | // Needs to come after parse, or command name won't register in help text. 27 | program.help(); 28 | } 29 | } 30 | 31 | execute(); 32 | 33 | // Get version of Apostrophe CLI and installed Apostrophe (if in a project) 34 | function getVersionInfo() { 35 | require('pkginfo')(module, 'version'); 36 | const cwd = process.cwd(); 37 | const response = {}; 38 | response.cli = module.exports.version; 39 | response.message = `Apostrophe CLI: v${response.cli}\n`; 40 | 41 | const aposPath = `${cwd}/node_modules/apostrophe`; 42 | 43 | // Append the installed Apostrophe version, if in an active project. 44 | if (fs.existsSync(aposPath)) { 45 | const aposPkg = require(`${aposPath}/package.json`); 46 | 47 | response.core = aposPkg.version; 48 | response.message += `Apostrophe v${response.core} is installed in this project.`; 49 | } 50 | 51 | return response; 52 | } 53 | -------------------------------------------------------------------------------- /lib/commands/add-types/widget.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | // Utilities from shelljs 3 | /* globals mkdir */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const util = require('../../util'); 7 | 8 | module.exports = function(moduleName, majorVersion, isEsm, options) { 9 | const modulesDir = 'modules'; 10 | const stringSet = 'current'; 11 | const fullWidgetName = `${moduleName}-widget`; 12 | 13 | util.log('add widget', `Adding ${fullWidgetName} folder to /${modulesDir}.`); 14 | 15 | const strings = util.getStrings(stringSet, 'add-widget', moduleName, options); 16 | 17 | const modulePath = path.join(modulesDir, fullWidgetName); 18 | 19 | mkdir('-p', modulePath); 20 | 21 | util.log('add widget', `Creating a views folder and widget.html for ${fullWidgetName}.`); 22 | 23 | mkdir('-p', path.join(modulePath, 'views')); 24 | 25 | const widgetView = strings.widgetsView || ''; 26 | 27 | fs.writeFileSync(path.join(modulePath, 'views/widget.html'), widgetView); 28 | 29 | const widgetsConfig = util.esmify(strings.widgetsConfig, isEsm); 30 | 31 | util.log('add widget', `Setting up index.js for ${fullWidgetName}.`); 32 | 33 | fs.writeFileSync(path.join(modulePath, 'index.js'), widgetsConfig); 34 | 35 | if (options.player) { 36 | const playerFilename = 'index.js'; 37 | util.log('add widget', `Setting up widget player ${playerFilename} for ${fullWidgetName}.`); 38 | 39 | const jsDir = 'ui/src'; 40 | 41 | mkdir('-p', path.join(modulePath, jsDir)); 42 | 43 | const jsConfig = strings.jsConfig; 44 | 45 | fs.writeFileSync(path.join(modulePath, jsDir, playerFilename), jsConfig); 46 | } 47 | 48 | util.log('add piece', `YOUR NEXT STEP: add the ${moduleName} module to "modules" in app.js.`); 49 | 50 | return true; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/commands/add-types/piece.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | // Utilities from shelljs 3 | /* globals mkdir */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const util = require('../../util'); 7 | 8 | const apostropheCurrent = { 9 | modulesDir: 'modules', 10 | version: 'current', 11 | pageSuffix: '-page', 12 | widgetSuffix: '-widget', 13 | pageModuleDir: '@apostrophecms/page/index.js' 14 | }; 15 | 16 | module.exports = function(moduleName, majorVersion, isEsm, options) { 17 | const { 18 | modulesDir, 19 | version, 20 | pageSuffix, 21 | widgetSuffix, 22 | pageModuleDir 23 | } = apostropheCurrent; 24 | const strings = util.getStrings(version, 'add-piece', moduleName); 25 | 26 | util.log('add piece', `Adding ${moduleName} folder to /${modulesDir}.`); 27 | 28 | const modulePath = path.join(modulesDir, moduleName); 29 | 30 | mkdir('-p', modulePath); 31 | 32 | util.log('add piece', `Setting up index.js for ${moduleName} module`); 33 | 34 | const pieceConfig = util.esmify(strings.pieceConfig, isEsm); 35 | fs.writeFileSync(path.join(modulePath, 'index.js'), pieceConfig); 36 | 37 | util.log('add piece', `YOUR NEXT STEP: add the ${moduleName} module to "modules" in app.js.`); 38 | 39 | // Piece page setup 40 | // **************** 41 | if (options.page) { 42 | const pageDir = `${modulePath}${pageSuffix}`; 43 | util.log('add piece', `Creating a ${pageDir} folder with index.js and appropriate views`); 44 | 45 | const pagesConfig = util.esmify(strings.pagesConfig, isEsm); 46 | 47 | mkdir('-p', path.join(pageDir)); 48 | fs.writeFileSync(path.join(pageDir, 'index.js'), pagesConfig); 49 | 50 | mkdir('-p', path.join(pageDir, 'views')); 51 | fs.writeFileSync(path.join(pageDir, 'views/show.html'), ''); 52 | fs.writeFileSync(path.join(pageDir, 'views/index.html'), ''); 53 | 54 | util.log('add piece', `YOUR NEXT STEP: add the ${pageDir} module to "modules" in app.js.`); 55 | util.log('add piece', `YOUR NEXT STEP: add the ${pageDir} page type to the "types" array in ${modulesDir}/${pageModuleDir}`); 56 | } 57 | 58 | return true; 59 | }; 60 | -------------------------------------------------------------------------------- /lib/commands/add.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | // Utilities from shelljs 3 | // /* globals mkdir */ 4 | // const fs = require('fs'); 5 | // const path = require('path'); 6 | const util = require('../util'); 7 | const { stripIndent } = require('common-tags'); 8 | 9 | module.exports = function (program, version) { 10 | program 11 | .command('add ') 12 | .description(stripIndent` 13 | Add an Apostrophe module with boilerplate configuration to get you started. 14 | - Add "module" for to add a generic module. 15 | - Add "piece" for to add a piece-type module. 16 | - Add "widget" for to add a widget-type module. 17 | Example: \`apos add piece article\` 18 | `) 19 | .option('--page', 'Used with the "piece" module type. Also add a corresponding piece page module.') 20 | .option('--widget', 'Used with the "piece" module type. Also add a corresponding piece widget module (A2 only).') 21 | .option('--player', 'Used with the "widget" module type. Also add a Javascript player file for browser-side code.') 22 | .action(async function(type, moduleName, options) { 23 | const allowedTypes = [ 'module', 'piece', 'widget' ]; 24 | if (!allowedTypes.includes(type)) { 25 | await util.error('add module', `Module type ${type} was not recognized. Options include "module", "piece", and "widget".`); 26 | return false; 27 | } 28 | const appPath = await util.getAppPath(`add ${type}`); 29 | 30 | if (!appPath) { 31 | return false; 32 | } 33 | 34 | const majorVersion = await util.getMajorVersion('add', version); 35 | 36 | if (!majorVersion) { 37 | await util.error('add module', 'Error finding the version of Apostrophe installed in this project'); 38 | return false; 39 | } 40 | 41 | const isEsm = await util.isEsm(`add ${type}`); 42 | 43 | const success = require(`./add-types/${type}`)(moduleName, majorVersion, isEsm, options); 44 | 45 | if (success) { 46 | await util.success(`add ${type}`); 47 | } 48 | 49 | return true; 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /lib/boilerplate.js: -------------------------------------------------------------------------------- 1 | const { stripIndent } = require('common-tags'); 2 | const util = require('./util'); 3 | 4 | module.exports = function (moduleName, options = {}) { 5 | const label = util.titleCase(moduleName.replace(/-/g, ' ')); 6 | 7 | return { 8 | current: { 9 | 'add-module': { 10 | moduleConfig: stripIndent` 11 | module.exports = { 12 | extend: '@apostrophecms/module', 13 | init(self) { 14 | 15 | } 16 | }; 17 | ` 18 | }, 19 | 'add-piece': { 20 | pieceConfig: stripIndent` 21 | module.exports = { 22 | extend: '@apostrophecms/piece-type', 23 | options: { 24 | label: '${label}', 25 | // Additionally add a \`pluralLabel\` option if needed. 26 | }, 27 | fields: { 28 | add: {}, 29 | group: {} 30 | } 31 | }; 32 | `, 33 | pagesConfig: stripIndent` 34 | module.exports = { 35 | extend: '@apostrophecms/piece-page-type', 36 | options: { 37 | label: '${label} Page', 38 | pluralLabel: '${label} Pages', 39 | }, 40 | fields: { 41 | add: {}, 42 | group: {} 43 | } 44 | }; 45 | ` 46 | }, 47 | 'add-widget': { 48 | widgetsConfig: stripIndent` 49 | module.exports = { 50 | extend: '@apostrophecms/widget-type', 51 | options: { 52 | label: '${label} Widget', 53 | }, 54 | fields: { 55 | add: {} 56 | } 57 | }; 58 | `, 59 | widgetsView: stripIndent` 60 |
61 |
62 | `, 63 | jsConfig: stripIndent` 64 | export default () => { 65 | apos.util.widgetPlayers['${moduleName}'] = { 66 | selector: '[data-${moduleName}-widget]', 67 | player: function(el) { 68 | // Add player code 69 | } 70 | }; 71 | }; 72 | ` 73 | } 74 | } 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.5.0 (2025-01-07) 4 | 5 | ### Adds 6 | 7 | - `add widget`, `add piece` and `add module` are compatible with our new ESM-based starter kits. commonjs starter kits can still be used. 8 | 9 | ## 3.4.0 (2024-03-12) 10 | 11 | ### Adds 12 | 13 | - Fully compatible with the new major version of Apostrophe (`4.0.0`). 14 | - Slight improvements to messaging and documentation. 15 | 16 | ### Removes 17 | 18 | - Removed vestigial support for Apostrophe 2.x, which has passed its end of life date and should not be used, 19 | therefore its removal is not considered a major version change in the CLI. Of course, those who 20 | need to create new 2.x projects can fork existing projects without the use of the CLI. 21 | 22 | ## 3.3.0 (2024-01-25) 23 | 24 | ### Adds 25 | 26 | - Adds the `--mongodb-uri` flag to pass a MongoDB server connection string allowing for initial user addition during project creation when a host server is being used. 27 | 28 | ## 3.2.0 (2023-08-03) 29 | 30 | ### Adds 31 | 32 | - Adds additional options to the `--starter` flag to make use of the starter kits easier. Also adds fallbacks for obtaining templates from other repositories. 33 | - Changes the `config.js` file to reflect the new name for the old `a3-boilerplate` template repo, `starter-kit-essentials` 34 | 35 | ## 3.1.2 (2022-09-15) 36 | 37 | ### Fixes 38 | 39 | - Fixes apostrophe 3 paths in console output. 40 | - Fixed typo in CLI help to clarify install options. 41 | 42 | ## 3.1.1 (2022-01-10) 43 | 44 | ### Fixes 45 | 46 | - Pinned `package.json` to version `1.4.0` of the `colors` module to ensure the [liberty bug](https://github.com/Marak/colors.js/issues/285) does not corrupt the display. This should not be possible when installing normally with `-g` since we were already shipping a `package-lock.json` that contains 1.4.0, however the bug did occur if a user cloned the repo and ran `npm update`, so in an abundance of caution we are making sure it is not possible even when doing so. 47 | 48 | ## 3.1.0 (2021-10-14) 49 | 50 | ### Adds 51 | 52 | - Adds a spinner indicator during package install to avoid the impression that the process is failing. 53 | 54 | ## 3.0.1 (2021-08-03) 55 | 56 | - Updates ESLint to v7 to meet the eslint-config-apostrophe peer dependency requirement. 57 | 58 | ## 3.0.0 (2021-06-16) 59 | 60 | - The initial build of the overhauled ApostropheCMS CLI. Uses the `3.0.0` major version number as this is very much an advanced version of the `apostrophe-cli` package (currently at 2.x.x), but moved to a new package name for logistical reasons. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apostrophe CLI 2 | 3 | The Apostrophe CLI is a cross-platform starting point for creating and configuring [ApostropheCMS](https://github.com/apostrophecms/apostrophe) projects, providing a simple boilerplate generator and wrapping other useful functions into an easy to use command line tool. 4 | 5 | **Requires Node.js 8+** 6 | 7 | First, install `@apostrophecms/cli` as a global NPM module: 8 | 9 | ```bash 10 | npm install -g @apostrophecms/cli 11 | ``` 12 | 13 | To view the available commands in a given context, execute the `apos` command with no arguments: 14 | 15 | ```bash 16 | apos 17 | ``` 18 | 19 | **Note:** All Apostrophe CLI commands can also be run with `apostrophe`, the legacy command, in addition to `apos`. 20 | 21 | ## Create a project 22 | 23 | To create a new project with the tool: 24 | ```bash 25 | apos create 26 | ``` 27 | 28 | This will create a local copy of the [apostrophe essentials starter kit](https://github.com/apostrophecms/starter-kit-essentials). 29 | 30 | ### options 31 | 32 | #### `--starter` 33 | 34 | Run `create` with a `--starter` flag to start from a Github repository other than the standard starters. For example, `apos create --starter=https://github.com/apostrophecms/apostrophe-open-museum.git` would create a project using the [Open Museum](https://github.com/apostrophecms/apostrophe-open-museum) demo. The `--starter` flag also accepts shortened names for any of the [existing starter kits](https://github.com/orgs/apostrophecms/repositories?q=starter-kit&type=all) that consists of the name of the repo with the `starter-kit-` prefix removed. For example, `apos create --starter=ecommerce` for the `starter-kit-ecommerce` repo. Finally, if you are using a personal or organizational repo, you can prefix your repo with it's location followed by the name to automatically add `https://github.com/`. For example, `apos create --starter=mycoolcompany/my-starter`. 35 | 36 | #### `--mongodb-uri` 37 | If you are not using a locally hosted MongoDB server, you can provide a connection string with the `--mongodb-uri` flag. For the standard Atlas connection string, you will need to add quotes around the connection string due to the query parameters. This allows for the creation of an admin user during project creation. **Note**: this will not add your connection string to the project. It needs to be included through the `APOS_MONGODB_URI` environment variable, potentially through a `.env` file. 38 | 39 | 40 | ## Create a widget 41 | To bootstrap the necessary files and basic configuration for a new Apostrophe widget, run the following command from within your Apostrophe project's root directory: 42 | ```bash 43 | # "-widgets" will automatically be appended to the end of your module name 44 | apos add widget fancy-button 45 | ``` 46 | 47 | **Note:** You will then need to register this widget module in `app.js` so it is available in your project code. The same is true for the commands below when you create a piece module or generic module. 48 | 49 | ```javascript 50 | // app.js 51 | module.exports = { 52 | // ... 53 | 'fancy-button-widgets': {}, 54 | // ... 55 | } 56 | ``` 57 | 58 | Add a `--player` option to the command to include the client-side Javascript "player" boilerplate to the new widget module as well. 59 | 60 | ```bash 61 | apos add widget tabs --player 62 | ``` 63 | 64 | ## Create a piece 65 | To bootstrap the necessary files and basic configuration for a new Apostrophe piece type, run the following command from within your Apostrophe project's root directory: 66 | 67 | ```bash 68 | apos add piece vegetable 69 | ``` 70 | 71 | Then remember to register `'vegetable': {}` in `app.js` above. 72 | 73 | If you run the `add piece` command with the `--page` flag, the command will also set up a corresponding piece-pages module with your new piece type. Similarly, you can run the `add piece` command with the `--widget` flag, which will also set up a corresponding piece-widgets module along with your new piece type. These flags can be used together or separately. 74 | 75 | ```bash 76 | apos add piece vegetable --page --widget 77 | ``` 78 | 79 | ## Create an empty Apostrophe module 80 | To bootstrap the necessary files and basic configuration for a brand-new Apostrophe module that doesn't extend one of the usual suspects like pieces or widgets: 81 | ```bash 82 | apos add module 83 | ``` 84 | 85 | Remember to register the module in `app.js` with the other module types. 86 | 87 | --------------- 88 | 89 | For more documentation for ApostropheCMS, visit the [documentation site](https://docs.apostrophecms.org). 90 | -------------------------------------------------------------------------------- /lib/commands/create.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | // Utilities from shelljs 3 | /* globals exec cd rm */ 4 | const prompts = require('prompts'); 5 | const util = require('../util'); 6 | const config = require('../../config'); 7 | const fs = require('fs'); 8 | const { stripIndent } = require('common-tags'); 9 | const quote = require('shell-quote').quote; 10 | 11 | module.exports = function (program) { 12 | program 13 | .command('create ') 14 | .description( 15 | stripIndent` 16 | Create an Apostrophe project (using the apostrophe essentials starter kit by default). 17 | Example: \`apos create my-website\` 18 | ` 19 | ) 20 | .option( 21 | '--starter ', 22 | 'Use a specific git repository to use as the project starter.\n You can also use the short name of any Apostrophe starter kit, e.g. "ecommerce"' 23 | ) 24 | .option( 25 | '--mongodb-uri ', 26 | 'Add a connection string to connect to a hosted MongoDB instance.' 27 | ) 28 | .action(async function (shortName, options) { 29 | // If options.starter is undefined, use config.BOILERPLATE 30 | const input = options.starter ? options.starter : config.BOILERPLATE; 31 | 32 | // Check for complete repo URL, starterkit name, or incomplete URL fallback 33 | let boilerplateUrl; 34 | if (/^\w+:/.test(input)) { 35 | boilerplateUrl = input; 36 | } else if (input.includes('/')) { 37 | boilerplateUrl = `https://github.com/${ 38 | input.startsWith('/') ? input.slice(1) : input 39 | }`; 40 | } else { 41 | boilerplateUrl = `https://github.com/apostrophecms/starter-kit-${input}.git`; 42 | } 43 | util.log( 44 | 'create', 45 | `Grabbing the ${boilerplateUrl} starter from Github [1/4]` 46 | ); 47 | 48 | // Clone the boilerplate project 49 | if (exec(`git clone ${boilerplateUrl} ${shortName}`).code !== 0) { 50 | await util.error('create', 'Error cloning starter code.'); 51 | return false; 52 | } 53 | 54 | cd(shortName); 55 | 56 | // Remove the initial .git directory. 57 | rm('-rf', '.git/'); 58 | 59 | util.log('create', `Adding your project shortname (${shortName}) [2/4]`); 60 | 61 | // Do some token replaces to rename the project 62 | // replaces the shortname in app.js 63 | replaceInConfig(/(shortName:).*?,/gi, `$1 '${shortName}',`); 64 | // replaces the shortname in package.json 65 | replaceInConfig(/("name":).*?,/g, `$1 "${shortName}",`); 66 | 67 | // Generate session secret 68 | let secret = util.secret(); 69 | 70 | if (fs.existsSync('./lib/modules/apostrophe-express/index.js')) { 71 | util.replaceInFiles( 72 | ['./lib/modules/apostrophe-express/index.js'], 73 | /secret: undefined/, 74 | `secret: '${secret}'` 75 | ); 76 | } 77 | 78 | // Set disabledFileKey for uploadfs 79 | secret = util.secret(); 80 | 81 | util.replaceInFiles( 82 | ['./app.js'], 83 | /disabledFileKey: undefined/, 84 | `disabledFileKey: '${secret}'` 85 | ); 86 | 87 | // Remove lock file and install packages. 88 | util.log('create', 'Installing packages [3/4]'); 89 | 90 | if (fs.existsSync('package-lock.json')) { 91 | rm('package-lock.json'); 92 | } 93 | if (fs.existsSync('yarn.lock')) { 94 | rm('yarn.lock'); 95 | } 96 | 97 | try { 98 | await util.spawnWithSpinner('npm install', { 99 | spinnerMessage: 100 | 'Installing packages. This will take a little while...' 101 | }); 102 | } catch (error) { 103 | await util.error('create', 'Error installing packages'); 104 | /* eslint-disable-next-line no-console */ 105 | console.error(error); 106 | } 107 | 108 | const cwd = process.cwd(); 109 | const aposPath = `${cwd}/node_modules/apostrophe`; 110 | 111 | // Append the installed Apostrophe version, if in an active project. 112 | if (!fs.existsSync(aposPath)) { 113 | await util.error('create', 'Error installing new project packages.'); 114 | return false; 115 | } 116 | const version = require(`${aposPath}/package.json`).version; 117 | const majorVersion = await util.getMajorVersion('create', version); 118 | 119 | // Create an admin user (note this will prompt for password) 120 | util.log('create', 'Creating an admin user [4/4]'); 121 | util.log('create', 'Choose a password for the admin user'); 122 | 123 | const response = await prompts({ 124 | type: 'password', 125 | name: 'pw', 126 | message: '🔏 Please enter a password:' 127 | }); 128 | 129 | const userTask ='@apostrophecms/user:add'; 130 | let createUserCommand = `node app.js ${userTask} admin admin`; 131 | 132 | // Prepend MongoDB URI if provided 133 | if (options.mongodbUri) { 134 | // Ensure the URI is properly quoted to handle special characters 135 | createUserCommand = `APOS_MONGODB_URI=${quote([options.mongodbUri])} ` + createUserCommand; 136 | } 137 | util.log('create', `Creating admin user with command: ${createUserCommand}`); 138 | exec(`echo "${response.pw}" | ${createUserCommand}`); 139 | util.log('create', 'All done! 🎉 Login as "admin" at the /login URL.'); 140 | 141 | await util.success('create'); 142 | return true; 143 | }); 144 | }; 145 | 146 | function replaceInConfig(regex, replacement) { 147 | util.replaceInFiles(['./app.js', './package.json'], regex, replacement); 148 | } 149 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | require('shelljs/global'); 3 | // Utilities from shelljs 4 | /* globals exit which */ 5 | const fs = require('fs'); 6 | const util = {}; 7 | const _ = require('lodash'); 8 | const cliVersion = require('../package.json').version; 9 | const confUtils = require('./conf-utils'); 10 | const packageJson = require('package-json'); 11 | const semver = require('semver'); 12 | const ora = require('ora'); 13 | const { spawn } = require('child_process'); 14 | 15 | module.exports = util; 16 | 17 | const prefix = ' Apostrophe '.black.bgWhite.bold; 18 | 19 | util.styleCommand = function(commandName, style) { 20 | const bgStyle = style === 'danger' ? 'bgRed' 21 | : style === 'success' ? 'bgGreen' : 'bgBlue'; 22 | return ' '[bgStyle] + commandName[bgStyle].white + ' '[bgStyle]; 23 | }; 24 | 25 | util.log = function(commandName, message) { 26 | console.log(' '); 27 | console.log(prefix + util.styleCommand(commandName) + ' ' + message); 28 | }; 29 | 30 | util.success = async function(commandName) { 31 | console.log(' '); 32 | console.log(prefix + util.styleCommand(commandName, 'success') + ' Finished successfully.'.green); 33 | 34 | try { 35 | await checkIfUpdated(); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | }; 40 | 41 | util.error = async function(commandName, msg) { 42 | console.error(' '); 43 | console.error(prefix + util.styleCommand(commandName, 'danger') + ' Failed'.red); 44 | if (msg) { 45 | console.error('\n' + msg.red + '\n'); 46 | } 47 | 48 | try { 49 | await checkIfUpdated(); 50 | } catch (error) { 51 | console.error(error); 52 | } 53 | }; 54 | 55 | util.notValid = function(commandName) { 56 | console.log(' '); 57 | console.log(prefix + ' Not a valid command'.red); 58 | }; 59 | 60 | util.isWindows = (require('os').platform() === 'win32'); 61 | 62 | util.missingDependency = function(dependencyName) { 63 | console.log(' '); 64 | console.log(dependencyName + ' not found'.red); 65 | console.log('Please install missing dependency'.red); 66 | }; 67 | 68 | util.checkDependencies = function() { 69 | const config = require('../config'); 70 | 71 | for (const i in config.SHELL_DEPENDS) { 72 | const dep = config.SHELL_DEPENDS[i]; 73 | if (!which(dep)) { 74 | util.missingDependency(dep); 75 | exit(1); 76 | } 77 | } 78 | }; 79 | 80 | util.getAppPath = async function(command, path) { 81 | path = path || './'; 82 | 83 | if (fs.existsSync(path + '/app.js')) { 84 | return path; 85 | } else { 86 | let rootPath = /\/$/; 87 | // In case of Windows, top level directory is some variation on C:\ 88 | if (util.isWindows) { 89 | rootPath = /([A-Z]):\\$/; 90 | } 91 | 92 | if (fs.realpathSync(path).match(rootPath)) { 93 | // we've reached top level folder, no app.js 94 | await util.error(command, 'Unable to locate an app.js in this directory. You need to be in the root directory of an Apostrophe project to run this command.'); 95 | return null; 96 | } 97 | 98 | return util.getAppPath(command, path + '../'); 99 | } 100 | }; 101 | 102 | util.isEsm = async function(command, path) { 103 | const appPath = await util.getAppPath(command, path); 104 | let info; 105 | try { 106 | info = JSON.parse(fs.readFileSync(`${appPath}/package.json`)); 107 | } catch (e) { 108 | await util.error('package.json is missing or invalid.'); 109 | return null; 110 | } 111 | console.log(`isEsm: ${info.type === 'module'}`); 112 | return info.type === 'module'; 113 | } 114 | 115 | util.esmify = function(code, isEsm) { 116 | if (!isEsm) { 117 | return code; 118 | } 119 | return code.replace('module.exports =', 'export default'); 120 | } 121 | 122 | util.getMajorVersion = async (command, v) => { 123 | if (!v) { 124 | await util.error(command, 'Unable to identify the installed version of Apostrophe. Please install packages before creating modules with the CLI tool.'); 125 | 126 | return null; 127 | } 128 | 129 | return v.split('.')[0]; 130 | }; 131 | 132 | util.getStrings = (version, command, moduleName, options) => { 133 | const allStrings = require('./boilerplate')(moduleName, options); 134 | return allStrings[version][command]; 135 | }; 136 | 137 | util.titleCase = function(string) { 138 | return string.replace(/\w\S*/g, function(txt) { 139 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 140 | }); 141 | }; 142 | 143 | // Replace the given regexp with the given replacement in all of the files in 144 | // the given array. 145 | // As always, if you want global replace within the file, use `/g` 146 | 147 | util.replaceInFiles = function(files, regex, replacement) { 148 | _.each(files, function(file) { 149 | let content = fs.readFileSync(file, 'utf8'); 150 | content = content.replace(regex, replacement); 151 | fs.writeFileSync(file, content); 152 | }); 153 | }; 154 | 155 | util.secret = function() { 156 | const bytes = require('crypto').randomBytes(8); 157 | let string = ''; 158 | let i; 159 | for (i = 0; (i < bytes.length); i++) { 160 | let s = bytes[i].toString(16); 161 | if (s.length < 2) { 162 | s = '0' + s; 163 | } 164 | string += s; 165 | } 166 | return string; 167 | }; 168 | 169 | util.spawnWithSpinner = async function (command, options = { 170 | spinnerMessage: 'Processing...', 171 | codeMessage: 'Spawn process error code:' 172 | }) { 173 | if (!command) { 174 | throw new Error('No command provided to spawnWithSpinner'); 175 | } 176 | 177 | const spinner = ora({ 178 | text: options.spinnerMessage, 179 | interval: 100, 180 | isEnabled: true 181 | }); 182 | 183 | return new Promise((resolve, reject) => { 184 | try { 185 | const spawned = spawn(command, { 186 | shell: true 187 | }); 188 | 189 | spinner.start(); 190 | 191 | let output = ''; 192 | spawned.stdout.on('data', (data) => { 193 | output += data.toString(); 194 | }); 195 | spawned.stderr.on('data', (data) => { 196 | output += data.toString(); 197 | }); 198 | 199 | spawned.on('exit', (code) => { 200 | spinner.stop(); 201 | 202 | if (code !== null && code !== 0) { 203 | return reject(new Error(output)); 204 | } 205 | // Log the install process output. Not using the CLI logging utility 206 | // since this isn't CLI messaging. 207 | console.log(output); 208 | 209 | return resolve(); 210 | }); 211 | } catch (error) { 212 | return reject(error); 213 | } 214 | }); 215 | }; 216 | 217 | async function checkIfUpdated () { 218 | try { 219 | // Get the latest published version number. 220 | const { version: latest } = await packageJson('@apostrophecms/cli'); 221 | 222 | // Check if they've been notified for this version already. If so, bail out. 223 | const latestChecked = await confUtils.getConf('versionNotified'); 224 | if (latestChecked && semver.gte(latestChecked, latest)) { 225 | return; 226 | } 227 | 228 | // If the local is behind the published version, suggest updating it. 229 | if (semver.gt(latest, cliVersion)) { 230 | console.log(`\n🆕 There is an updated version of the @apostrophecms/cli module. The latest is ${latest}. You are on ${cliVersion}.\nUse \`npm i -g @apostrophecms/cli\` to get the latest version.`); 231 | } 232 | // Stash the last notified version in user conf. 233 | confUtils.setConf('versionNotified', latest); 234 | 235 | } catch (error) { 236 | // Unable to check the latest version of the CLI package for some reason. 237 | // No need to interrupt the user. 238 | } 239 | } 240 | --------------------------------------------------------------------------------