├── .bookignore
├── .circleci
├── config.yml
└── update-github-page.sh
├── .commitlintrc.yml
├── .editorconfig
├── .eslintrc.yml
├── .flowconfig
├── .gitattributes
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-push
├── .ncmrc.yml
├── .npmignore
├── .nvmrc
├── .scrutinizer.yml
├── LICENSE
├── Makefile
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── renovate.json
├── src
├── actions
│ ├── common-steps.js
│ ├── config
│ │ ├── index.js
│ │ └── steps.js
│ ├── init
│ │ ├── index.js
│ │ └── steps.js
│ ├── secret
│ │ └── index.js
│ └── setup
│ │ ├── index.js
│ │ └── steps.js
├── constants.js
├── index.js
└── lib
│ ├── __tests__
│ ├── __snapshots__
│ │ └── format.js.snap
│ └── format.js
│ ├── child-process.js
│ ├── format.js
│ ├── fs.js
│ ├── github.js
│ ├── package-json.js
│ ├── util.js
│ └── vault.js
├── tsconfig.json
└── yarn.lock
/.bookignore:
--------------------------------------------------------------------------------
1 | # gitbook would first read .gitignore
2 |
3 | # ignore everything
4 | .*
5 | *
6 |
7 | # only include
8 | !.circleci/
9 | !docs/
10 | !book.json
11 | !README.md
12 | !SUMMARY.md
13 | !API.md
14 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 |
4 | references:
5 | container-config-node8: &container-config-node8
6 | docker:
7 | - image: circleci/node:14
8 | restore-deps-cache: &restore-deps-cache
9 | restore_cache:
10 | keys:
11 | - yarn-packages-{{ checksum "yarn.lock" }}
12 | save-deps-cache: &save-deps-cache
13 | save_cache:
14 | key: yarn-packages-{{ checksum "yarn.lock" }}
15 | paths:
16 | - ~/.cache/yarn
17 | - node_modules
18 | jobs:
19 | install:
20 | <<: *container-config-node8
21 | steps:
22 | - checkout
23 | - *restore-deps-cache
24 | - run:
25 | name: Install Dependencies
26 | command: make install
27 | - *save-deps-cache
28 | test:
29 | <<: *container-config-node8
30 | steps:
31 | - checkout
32 | - *restore-deps-cache
33 | - run:
34 | name: Install Dependencies and Run Test Coverage
35 | command: make install test-coverage
36 | - *save-deps-cache
37 | - run: npx coveralls < coverage/lcov.info
38 | release:
39 | <<: *container-config-node8
40 | steps:
41 | - checkout
42 | - *restore-deps-cache
43 | - run:
44 | name: Build Package and Semantic-Release
45 | command: make install build release
46 | - run:
47 | name: Update Doc GitHub Page
48 | command: sh .circleci/update-github-page.sh
49 |
50 | workflows:
51 | version: 2
52 | test-and-release:
53 | jobs:
54 | - test:
55 | filters:
56 | branches:
57 | ignore: gh-pages
58 | - release:
59 | requires:
60 | - test
61 | filters:
62 | branches:
63 | only: master
64 |
--------------------------------------------------------------------------------
/.circleci/update-github-page.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | git config --global user.name 'CircleCI';
6 | git config --global user.email 'circleci@users.noreply.github.com';
7 | npx documentation build src/** -f html -o docs
8 | npx gh-pages -d docs -m 'Automated Github Page Update [skip ci]';
9 |
--------------------------------------------------------------------------------
/.commitlintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: ['@commitlint/config-conventional']
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [Makefile]
15 | indent_style = tab
16 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | parser: babel-eslint
3 | parserOptions:
4 | ecmaVersion: 6
5 | sourceType: module
6 | env:
7 | browser: true
8 | es6: true
9 | node: true
10 | jest/globals: true
11 | extends:
12 | - airbnb-base
13 | - plugin:jest/recommended
14 | - plugin:jsdoc/recommended
15 | - plugin:prettier/recommended
16 | plugins:
17 | - import
18 | - jest
19 | - jsdoc
20 | - prettier
21 | rules:
22 | valid-jsdoc: 'off'
23 | jsdoc/check-types: 'off'
24 | jsdoc/require-param: 'off'
25 | jsdoc/require-returns: 'off'
26 | import/extensions: [warn, {js: never}]
27 | import/no-extraneous-dependencies: 'off'
28 | import/no-named-as-default: 'off'
29 | jest/no-disabled-tests: warn
30 | jest/no-focused-tests: error
31 | jest/no-identical-title: error
32 | jest/no-try-expect: 'off'
33 | jest/valid-expect: error
34 | jest/no-conditional-expect: 'off'
35 | prettier/prettier:
36 | - error
37 | - {
38 | singleQuote: true,
39 | trailingComma: all
40 | }
41 | settings:
42 | import/resolver:
43 | node:
44 | moduleDirectory:
45 | - ./src
46 | - ./node_modules
47 | jsdoc:
48 | mode: typescript
49 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules/*.
3 |
4 | [include]
5 | src/
6 |
7 | [libs]
8 | ./flow-typed
9 |
10 | [lints]
11 |
12 | [options]
13 |
14 | [strict]
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | yarn.lock binary
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules/
3 |
4 | # test output
5 | coverage/
6 | reports/
7 |
8 | # dist
9 | dist/
10 | types/
11 |
12 | # docs output
13 | docs/
14 |
15 | # build cache
16 | .cache
17 | .serverless
18 | .webpack
19 | .build
20 | *_book
21 |
22 | # dot files
23 | .DS_Store
24 | *.env*
25 | package-lock.json
26 |
27 | # log files
28 | *.log
29 |
30 | # data file
31 | output/
32 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | make lint type-check test-coverage
--------------------------------------------------------------------------------
/.ncmrc.yml:
--------------------------------------------------------------------------------
1 | ## created by @opbi/ncm
2 | ---
3 | component:
4 | type: package
5 | name: ncm
6 | description: node config manager - create and update dotfiles made easy
7 | keywords: automation, dotfiles, dotenv, node, cli
8 | private: false
9 | package:
10 | environment: cli
11 | npmScope: opbi
12 | owner:
13 | type: organisation
14 | github: opbi
15 | team: product
16 | name: OPBI
17 | email: engineering@opbi.org
18 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # ignore everything
2 | *
3 |
4 | # exclusion list
5 | !package.json
6 | !dist/**/*
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.17.6
2 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | ---
2 | build:
3 | nodes:
4 | analysis:
5 | # disable inferred commands [dependencies]
6 | dependencies:
7 | override:
8 | - true
9 | # as we're only use the code quality analyser
10 | tests:
11 | override:
12 | - js-scrutinizer-run
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 OPBI
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ## CONFIG
2 | export PATH := ./node_modules/.bin:$(PATH)
3 | SHELL := /bin/bash -v
4 |
5 | ## VARIABLES
6 |
7 | ## COMMANDS
8 | install:
9 | @yarn
10 | @husky install
11 |
12 | upgrade:
13 | @yarn upgrade-interactive --latest
14 |
15 | nvm-update:
16 | @bash -l -c 'nvm install --latest-npm --reinstall-packages-from=$(shell node -v)'
17 |
18 | cleanup:
19 | @rm -rf node_modules coverage dist types docs *.log
20 |
21 | build:
22 | @rm -rf dist
23 | @babel src -d dist --ignore '**/__tests__/*.js'
24 |
25 | build-watch:
26 | @babel src -d dist --ignore '**/__tests__/*.js' --watch
27 |
28 | type-check:
29 | @tsc
30 |
31 | lint:
32 | @eslint_d src
33 |
34 | lint-fix:
35 | @eslint_d src --fix
36 |
37 | lint-reset:
38 | @eslint_d restart
39 |
40 | test:
41 | @jest
42 |
43 | test-watch:
44 | @jest --watch --coverage
45 |
46 | test-coverage:
47 | @jest --coverage
48 |
49 | docs:
50 | @documentation build src/** -f html -o docs
51 |
52 | docs-watch:
53 | @documentation serve --watch src
54 |
55 | commit:
56 | @commit
57 |
58 | release:
59 | @semantic-release
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ncm
6 | node config manager - cross-project config consistency made easy
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ---
45 |
46 | ### Motivation
47 |
48 | #### Make Micro-Services Consistent
49 |
50 | With the growing amount of toolings and ci services we need to use to create a production-grade codebase, the difficulty of managing configs across multiple repos is increasing. Tools like [lerna](https://github.com/lerna/lerna) is easy to result in a heavy monorepo and doesn't help to manage apps in different teams, plus a lot of Github integrations are optmised on a repo level. `ncm` is created to manage packages (components or apps) in separate repos, keeping the repos light-weight for swift development experience, enjoying the benefits of micro-services while not sacarificing build layer consistency.
51 |
52 | #### Best-Practices In Code
53 |
54 | `ncm` is built to look after the whole lifecycle of a package (component or app) systematically from creation to development, maintainance and deprecation. We want to make it very easy to create and maintain a production-grade packages. It is built and released following [@opbi/principles](https://github.com/opbi/opbi#principles).
55 |
56 |
57 | ### How to Use
58 |
59 | #### Install
60 | ```shell
61 | yarn add @opbi/ncm -D
62 | ```
63 |
64 | #### Before We Start
65 | It assumes that you've `git` installed and setup with SSH keys to clone public and private repo in your scope.
66 |
67 | Also to setup Github repo it requires you to provide a Github Access Token. You can create it on Github Settings page, and it requires repo:private access if you want to create private repo. When you have got the access token, you can store it in MacOS Keychain, and export it to `NCM_GITHUB_ACCESS_TOKEN` in your shell profile.
68 |
69 | #### Setup New Component
70 |
71 | ```shell
72 | ncm init # you will be asked a list of questions like `npm init` or `yarn init`
73 | # check .ncmrc.yml, add more details if necessary
74 | ncm setup # it will create dotfiles based on preset and custom config as well as create GitHub repo
75 | make install
76 | # all set, start development 🎉
77 | ```
78 | Currently it needs manual clicks to setup project in CI and code quality reports, this can be automated by running a worker triggered by GitHub hook.
79 |
80 | #### Use Custom Template Repo
81 | By default, `ncm` uses ncm-preset-[component.type] as the config file template, which is a repo that is constantly being updated and tested to maintain fresh high-quality build config standards. In case you prefer use your own preset, you just need to add your Github repo to `.ncmrc.yml` under `component.template`.
82 |
83 | ```yml
84 | ---
85 | component:
86 | type: package # [package, service, app, job]
87 | template: opbi/ncm-preset-package # default to this repo for [package]
88 | ```
89 |
90 | #### Update Config Files
91 | ```shell
92 | ncm config # it will replace the dotfiles with latest ones from the template
93 | ```
94 |
95 | #### Fetch Dotenv Secrets
96 | ```shell
97 | ncm .env -s # it will write secrets in vault to .env file
98 | ```
99 |
100 | #### Upgrade Node Version [TODO]
101 | ```shell
102 | ncm upgrade node # this will upgrade node version via nvm, update .ncmrc.yml, .nvmrc, babel.config.js, package.json, .circleci/config.yml
103 | ```
104 |
105 | #### Develop A Component [TODO]
106 | As `ncm` is created to manage the whole lifecycle of a component in microservice system, workflow automation is included in the scope. Workflow logics over lifecycles of different types of components defined in [@opbi/practices](https://github.com/opbi/opbi#practices) would be materialised as automation in `ncm`. For example, when the following command in a package component, it would move the Trello card to development stage, create a new branch with preset naming convention, open a PR, link the PR to the Trello Card.
107 |
108 | ```shell
109 | ncm start #TrelloCard
110 | ```
111 |
112 | #### Repo Specific Config [TODO]
113 | Add repo specific configuration to `.ncmrc.yml` to overwrite certain rules in the common dotfiles from the template. `ncm` will pick up the settings and inject them into the config files based on dotfiles from template.
114 |
115 | For example, if you want to use a specific babel-plugin that is not included in the template, then you can add the devDependency, and set `.ncmrc.yml` like the following, `ncm` will then pick up the `config.babel` and merge it into `babel.config.js`, the structure of `config.babel` would be exactly the same as `.babelrc`, except that it more yamlful.
116 |
117 | ```yml
118 | ---
119 | component:
120 | type: package
121 | config:
122 | babel:
123 | plugins:
124 | - @babel/plugin-of-your-choice
125 | gitbook:
126 | disabled: true
127 | ```
128 |
129 | #### Rename A Component [TODO]
130 | ```
131 | ncm rename # this would rename the dir, GitHub Repo, package.json, deployment service, etc.
132 | ```
133 |
134 | #### Archive A Component [TODO]
135 | ```shell
136 | ncm archive # this will archive the repo, teardown all services, deprecate component/service/app
137 | ```
138 |
139 | ### Glossary
140 |
141 | #### Component
142 |
143 | We use `component` to refer to a complete codebase unit that serves a specific function in a micro-service system, which could be in the type of [app, service, job, package, etc.]. For more details, please refer to [@opbi/glossary](https://github.com/opbi/opbi#glossary).
144 |
145 |
146 | ### Inspiration
147 | * [helm](https://github.com/helm/helm)
148 | * [mrm](https://github.com/sapegin/mrm)
149 | * [kyt](https://github.com/NYTimes/kyt/)
150 | * [yarn](https://github.com/yarnpkg/yarn)
151 |
152 | ### License
153 | [MIT](License)
154 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: '14.17',
8 | },
9 | },
10 | ],
11 | '@babel/typescript',
12 | ],
13 | plugins: [
14 | '@babel/plugin-proposal-object-rest-spread',
15 | ['module-resolver', { root: ['./src'] }],
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | modulePaths: ['node_modules', './src'],
3 | testPathIgnorePatterns: [
4 | 'node_modules',
5 | 'dist',
6 | 'types',
7 | '/__fixtures__/',
8 | '__tests__/helpers',
9 | ],
10 | testEnvironment: 'node',
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opbi/ncm",
3 | "description": "node config manager - create and update dotfiles made easy",
4 | "repository": "git@github.com:opbi/ncm.git",
5 | "author": "OPBI ",
6 | "license": "MIT",
7 | "publishConfig": {
8 | "access": "public"
9 | },
10 | "dependencies": {
11 | "@octokit/rest": "^18.11.1",
12 | "commander": "^8.2.0",
13 | "cosmiconfig": "^7.0.1",
14 | "cpy": "^8.1.2",
15 | "inquirer": "^8.1.5",
16 | "jsonfile": "^6.1.0",
17 | "node-vault": "^0.9.22",
18 | "replace-in-file": "^6.2.0",
19 | "write-yaml": "^1.0.0"
20 | },
21 | "devDependencies": {
22 | "@babel/cli": "^7.15.7",
23 | "@babel/core": "^7.15.5",
24 | "@babel/node": "^7.10.1",
25 | "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
26 | "@babel/preset-env": "^7.15.6",
27 | "@babel/preset-typescript": "^7.15.0",
28 | "@commitlint/cli": "^13.1.0",
29 | "@commitlint/config-conventional": "^13.1.0",
30 | "@commitlint/prompt-cli": "^13.1.0",
31 | "babel-core": "^7.0.0-bridge.0",
32 | "babel-eslint": "^10.1.0",
33 | "babel-jest": "^27.2.1",
34 | "babel-plugin-module-resolver": "^4.1.0",
35 | "coveralls": "^3.1.1",
36 | "documentation": "^13.2.5",
37 | "eslint": "^7.32.0",
38 | "eslint-config-airbnb-base": "^14.2.1",
39 | "eslint-config-prettier": "^8.3.0",
40 | "eslint-config-standard-jsdoc": "^9.3.0",
41 | "eslint-import-resolver-node": "^0.3.6",
42 | "eslint-plugin-import": "^2.24.2",
43 | "eslint-plugin-jest": "^24.4.2",
44 | "eslint-plugin-jsdoc": "^36.1.0",
45 | "eslint-plugin-prettier": "^4.0.0",
46 | "eslint_d": "^11.0.0",
47 | "gh-pages": "^3.2.3",
48 | "husky": "^7.0.0",
49 | "jest": "^27.2.1",
50 | "prettier": "^2.4.1",
51 | "semantic-release": "^18.0.0",
52 | "typescript": "^4.4.3"
53 | },
54 | "optionalDependencies": {},
55 | "engines": {
56 | "node": ">=12"
57 | },
58 | "bin": {
59 | "ncm": "dist/index.js"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":preserveSemverRanges",
5 | "docker:disable",
6 | "schedule:earlyMondays"
7 | ],
8 | "node": false
9 | }
10 |
--------------------------------------------------------------------------------
/src/actions/common-steps.js:
--------------------------------------------------------------------------------
1 | import { cosmiconfig } from 'cosmiconfig';
2 | import { exec } from 'lib/child-process';
3 |
4 | // TODO: setup schema and sanitise config file
5 | export const readConfig = async () => {
6 | const MODULE_NAME = 'ncm';
7 | const { config } = await cosmiconfig(MODULE_NAME).search();
8 | return config;
9 | };
10 |
11 | export const cloneTemplateRepo = async (config) => {
12 | const DEFAULT_TEMPLATE = `opbi/ncm-preset-${config.component.type}`;
13 | const template = config.component.template || DEFAULT_TEMPLATE;
14 | await exec(`echo template: ${template}`);
15 | await exec(`rm -rf .template`);
16 | await exec(`git clone git@github.com:${template}.git .template`);
17 | };
18 |
19 | export const removeTemplateDir = async () => exec('rm -rf .template');
20 |
--------------------------------------------------------------------------------
/src/actions/config/index.js:
--------------------------------------------------------------------------------
1 | import * as commonSteps from '../common-steps';
2 | import * as steps from './steps';
3 |
4 | export default async () => {
5 | const config = await commonSteps.readConfig();
6 |
7 | await commonSteps.cloneTemplateRepo(config);
8 |
9 | await steps.copyConfigFiles();
10 |
11 | await steps.updatePackageJson();
12 |
13 | await commonSteps.removeTemplateDir();
14 | };
15 |
--------------------------------------------------------------------------------
/src/actions/config/steps.js:
--------------------------------------------------------------------------------
1 | import cpy from 'cpy';
2 | import jsonfile from 'jsonfile';
3 | import { exec } from 'lib/child-process';
4 | import { sortObjectByKeys } from 'lib/util';
5 |
6 | /**
7 | * Copy only config files.
8 | */
9 | export const copyConfigFiles = async () => {
10 | await cpy(
11 | [
12 | '.template/*',
13 | '.template/.*',
14 | '!.template/.ncmrc.yml', // there can be .ncmrc.yml in template
15 | '!.template/*.md',
16 | '!.template/package.json',
17 | '!.template/yarn.lock',
18 | ],
19 | '.',
20 | );
21 | await exec('cp -r .template/.circleci .');
22 | await exec('cp -r .template/.husky .');
23 | };
24 |
25 | /**
26 | * Merge devDependencies and optionalDependencies from template package.json.
27 | */
28 | export const updatePackageJson = async () => {
29 | const template = await jsonfile.readFile('.template/package.json');
30 | const target = await jsonfile.readFile('./package.json');
31 | const updated = { ...target };
32 | updated.devDependencies = sortObjectByKeys({
33 | ...target.devDependencies,
34 | ...template.devDependencies,
35 | });
36 | updated.optionalDependencies = sortObjectByKeys({
37 | ...target.optionalDependencies,
38 | ...template.optionalDependencies,
39 | });
40 | await jsonfile.writeFile('./package.json', updated, { spaces: 2 });
41 | };
42 |
--------------------------------------------------------------------------------
/src/actions/init/index.js:
--------------------------------------------------------------------------------
1 | import inquirer from 'inquirer';
2 |
3 | import { COMPONENT_TYPES, PACKAGE_ENVIRONMENTS, OWNER_TYPES } from 'constants';
4 | import * as steps from './steps';
5 |
6 | const componentQuestions = [
7 | {
8 | type: 'list',
9 | name: 'componentType',
10 | message: 'Which type of component do you want to create?',
11 | choices: COMPONENT_TYPES,
12 | },
13 | {
14 | type: 'list',
15 | name: 'packageEnvironment',
16 | message: 'Where will the package be used?',
17 | choices: PACKAGE_ENVIRONMENTS,
18 | when: ({ componentType }) => componentType === 'package',
19 | },
20 | {
21 | type: 'confirm',
22 | name: 'componentPublic',
23 | message: 'Is the package to be public?',
24 | default: ({ componentType }) => componentType === 'package',
25 | when: ({ componentType }) => componentType === 'package',
26 | },
27 | {
28 | type: 'input',
29 | name: 'componentName',
30 | message: ({ componentType }) => `Enter the name of the ${componentType}:`,
31 | // TODO: offer hooks to add naming convention validation as plugin
32 | },
33 | ];
34 |
35 | const ownerQuestions = [
36 | {
37 | type: 'list',
38 | name: 'ownerType',
39 | message: 'Which is the type of the owner?',
40 | choices: OWNER_TYPES,
41 | },
42 | {
43 | type: 'input',
44 | name: 'ownerGithub',
45 | message: ({ ownerType }) => `Enter the owner [${ownerType}] GitHub id:`,
46 | },
47 | {
48 | type: 'input',
49 | name: 'packageNpmScope',
50 | message: 'What is the npm scope name for the package?',
51 | default: ({ ownerType, ownerGithub }) =>
52 | ownerType === 'organisation' ? ownerGithub : undefined,
53 | when: ({ componentType, componentPublic }) =>
54 | componentType === 'package' && componentPublic,
55 | },
56 | {
57 | type: 'input',
58 | name: 'ownerEmail',
59 | message: 'Please enter the email of the primary maintainer:',
60 | // TODO: add validation function
61 | },
62 | ];
63 |
64 | const questions = [...componentQuestions, ...ownerQuestions];
65 |
66 | export default () =>
67 | inquirer.prompt(questions).then(async (answers) => {
68 | await steps.createNcmrc(answers);
69 | const { componentType, componentName } = answers;
70 | await steps.addCommentsToNcmrc({ componentName });
71 | await steps.initGit({ componentName });
72 | await steps.cdToComponentDir({ componentName });
73 | console.log(
74 | `created new [${componentType}]: ${componentName}\ncheck .ncmrc.yml and run 'ncm setup'`,
75 | );
76 | });
77 |
--------------------------------------------------------------------------------
/src/actions/init/steps.js:
--------------------------------------------------------------------------------
1 | import yaml from 'write-yaml';
2 |
3 | import { readFile, writeFile } from 'lib/fs';
4 | import { exec } from 'lib/child-process';
5 |
6 | export const createNcmrc = async (answers) => {
7 | // TODO: parse answers to an object and validate against the schema
8 | const template = {
9 | component: {
10 | type: answers.componentType,
11 | name: answers.componentName,
12 | description: '',
13 | keywords: '',
14 | private: !answers.componentPublic,
15 | },
16 | ...(answers.componentType === 'package'
17 | ? {
18 | package: {
19 | environment: answers.packageEnvironment,
20 | npmScope: answers.packageNpmScope,
21 | },
22 | }
23 | : {}),
24 | owner: {
25 | type: answers.ownerType,
26 | github: answers.ownerGithub,
27 | team: '',
28 | name: '',
29 | email: answers.ownerEmail,
30 | },
31 | };
32 | const NCMRC_PATH = `./${answers.componentName}/.ncmrc.yml`;
33 | yaml.sync(NCMRC_PATH, template, {
34 | safe: true,
35 | });
36 | };
37 |
38 | export const addCommentsToNcmrc = async ({ componentName }) => {
39 | const NCMRC_PATH = `./${componentName}/.ncmrc.yml`;
40 | const template = await readFile(NCMRC_PATH);
41 | const updated = `## created by @opbi/ncm\n---\n${template}`;
42 | await writeFile(NCMRC_PATH, updated);
43 | };
44 |
45 | export const initGit = async ({ componentName }) =>
46 | exec(`git init -q ./${componentName}`);
47 |
48 | export const cdToComponentDir = async ({ componentName }) =>
49 | exec(`cd ./${componentName}`);
50 |
--------------------------------------------------------------------------------
/src/actions/secret/index.js:
--------------------------------------------------------------------------------
1 | import getSecretsFromVault from 'lib/vault';
2 | import { objToDotenv } from 'lib/format';
3 | import { writeFile } from 'lib/fs';
4 |
5 | export default async ({ scope, endpoint, token, auth }) => {
6 | try {
7 | const secrets = await getSecretsFromVault({
8 | scope,
9 | endpoint,
10 | token,
11 | auth,
12 | });
13 | const content = objToDotenv(secrets);
14 | await writeFile('./.env', content);
15 | console.log(`secrets have been written to .env:\n${content}`);
16 | } catch (e) {
17 | console.log(e);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/actions/setup/index.js:
--------------------------------------------------------------------------------
1 | import * as commonSteps from '../common-steps';
2 | import * as steps from './steps';
3 |
4 | export default async () => {
5 | const config = await commonSteps.readConfig();
6 |
7 | await commonSteps.cloneTemplateRepo(config);
8 |
9 | await steps.copyTemplateFiles();
10 |
11 | await commonSteps.removeTemplateDir();
12 |
13 | await steps.updatePackageJson(config);
14 |
15 | await steps.generateReadme(config);
16 |
17 | await steps.createGithubRepo(config);
18 | console.log(`GitHub repo created`);
19 |
20 | await steps.addGitRemoteOrigin(config);
21 |
22 | await steps.commitAndPushToGitHub();
23 | };
24 |
--------------------------------------------------------------------------------
/src/actions/setup/steps.js:
--------------------------------------------------------------------------------
1 | import cpy from 'cpy';
2 | import jsonfile from 'jsonfile';
3 | import replace from 'replace-in-file';
4 |
5 | import { setupGithubClient } from 'lib/github';
6 | import { exec } from 'lib/child-process';
7 |
8 | import configPackageJsonFromTemplate from 'lib/package-json';
9 |
10 | export const copyTemplateFiles = async () => {
11 | await cpy(
12 | [
13 | '.template/*',
14 | '.template/.*',
15 | '!.template/.ncmrc.yml', // there can be .ncmrc.yml in template
16 | '!.template/README.md',
17 | ],
18 | '.',
19 | {
20 | rename: (fileName) =>
21 | fileName === 'TEMPLATE.md' ? 'README.md' : fileName,
22 | },
23 | );
24 | await exec('cp -r .template/src .');
25 | await exec('cp -r .template/.circleci .');
26 | };
27 |
28 | export const updatePackageJson = async (config) => {
29 | const PACKAGE_JSON_PATH = './package.json';
30 | const template = await jsonfile.readFile(PACKAGE_JSON_PATH);
31 | const packageJson = configPackageJsonFromTemplate(config, template);
32 | await jsonfile.writeFile(PACKAGE_JSON_PATH, packageJson, {
33 | spaces: 2,
34 | });
35 | };
36 |
37 | export const generateReadme = async (config) => {
38 | const PACKAGE_JSON_PATH = './package.json';
39 | const { name: packageJsonName, license: packageJsonLicense } =
40 | await jsonfile.readFile(PACKAGE_JSON_PATH);
41 | await replace({
42 | files: `./README.md`,
43 | from: [
44 | /{{componentName}}/g,
45 | /{{componentDescription}}/g,
46 | /{{ownerGithub}}/g,
47 | /{{packageJsonName}}/g,
48 | /{{packageJsonLicense}}/g,
49 | ],
50 | to: [
51 | config.component.name,
52 | config.component.description,
53 | config.owner.github,
54 | packageJsonName,
55 | packageJsonLicense,
56 | ],
57 | });
58 | };
59 |
60 | export const createGithubRepo = async (config) => {
61 | const authRequired =
62 | config.owner.type === 'organisation' || config.component.private;
63 | const github = await setupGithubClient({ authRequired });
64 | await github.repos.createInOrg({
65 | org: config.owner.github,
66 | name: config.component.name,
67 | description: config.component.description,
68 | private: config.component.private,
69 | });
70 | };
71 |
72 | export const addGitRemoteOrigin = async (config) =>
73 | exec(
74 | `git remote add origin git@github.com:${config.owner.github}/${config.component.name}.git`,
75 | );
76 |
77 | export const commitAndPushToGitHub = async () => {
78 | await exec('git add .');
79 | await exec(`git commit -m 'chore: init'`);
80 | await exec('git push -u origin master');
81 | };
82 |
83 | // export const installDeps = async () => {};
84 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | import os from 'os';
2 |
3 | export const COMPONENT_TYPES = [
4 | 'package',
5 | 'service',
6 | 'worker(TODO)',
7 | 'app(TODO)',
8 | ];
9 | export const PACKAGE_ENVIRONMENTS = [
10 | 'node',
11 | 'cli',
12 | 'browser(TODO)',
13 | 'universal(TODO)',
14 | ];
15 | export const OWNER_TYPES = ['organisation', 'personal'];
16 |
17 | export const CWD = process.cwd(); // current working directory: location where node command is invoked
18 | export const HOME = os.homedir();
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import cli from 'commander';
4 |
5 | import initAction from 'actions/init';
6 | import setupAction from 'actions/setup';
7 | import configAction from 'actions/config';
8 | import secretAction from 'actions/secret';
9 |
10 | import packageJson from '../package';
11 |
12 | cli.version(packageJson.version, '-v, --version');
13 |
14 | cli
15 | .command('init')
16 | .description('create the ncm specs of a new component')
17 | .action(initAction);
18 |
19 | cli
20 | .command('setup')
21 | .description('setup the new component according to .ncmrc.yml')
22 | .action(setupAction);
23 |
24 | cli
25 | .command('config')
26 | .option('-e, --environment [envID]', 'dev, ci')
27 | .option('-t, --target [targetTemplate]', 'repo of config files template')
28 | .description(
29 | 'generate config files based on template and specs in .ncmrc.yml',
30 | )
31 | .action(configAction);
32 |
33 | cli
34 | .command('secret')
35 | .alias('.env')
36 | .description('write vault dev secrets to .env')
37 | .option('-s, --scope ', 'path of the secret in vault')
38 | .option('-e --endpoint [vaultEndpoint]', 'endpoint of the vault server')
39 | .option('-t, --token [token]', 'vault auth token')
40 | .option('-a, --auth [method]', 'vault auth method: [token], github')
41 | .action(secretAction);
42 |
43 | cli.parse(process.argv);
44 |
--------------------------------------------------------------------------------
/src/lib/__tests__/__snapshots__/format.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`objToDotenv transform json into dotenv style key-value content 1`] = `
4 | "id=1
5 | foo=bar
6 | "
7 | `;
8 |
--------------------------------------------------------------------------------
/src/lib/__tests__/format.js:
--------------------------------------------------------------------------------
1 | import { objToDotenv } from '../format';
2 |
3 | describe('objToDotenv', () => {
4 | it('transform json into dotenv style key-value content', () => {
5 | const obj = {
6 | id: 1,
7 | foo: 'bar',
8 | };
9 | const result = objToDotenv(obj);
10 | expect(result).toMatchSnapshot();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/lib/child-process.js:
--------------------------------------------------------------------------------
1 | import childProcess from 'child_process';
2 | import { promisify } from 'util';
3 |
4 | export const exec = promisify(childProcess.exec);
5 |
6 | export default {};
7 |
--------------------------------------------------------------------------------
/src/lib/format.js:
--------------------------------------------------------------------------------
1 | export const objToDotenv = (obj) =>
2 | Object.keys(obj).reduce((output, key) => `${output}${key}=${obj[key]}\n`, '');
3 |
4 | export default {
5 | objToDotenv,
6 | };
7 |
--------------------------------------------------------------------------------
/src/lib/fs.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { promisify } from 'util';
3 |
4 | export const readFile = (filePath, option = { encoding: 'utf-8' }) =>
5 | promisify(fs.readFile)(filePath, option);
6 | export const writeFile = promisify(fs.writeFile);
7 | export const mkdir = promisify(fs.mkdir);
8 |
--------------------------------------------------------------------------------
/src/lib/github.js:
--------------------------------------------------------------------------------
1 | import octokit from '@octokit/rest';
2 |
3 | export const setupGithubClient = async ({ authRequired = false }) => {
4 | const github = octokit();
5 | if (authRequired) {
6 | if (!process.env.NCM_AUTH_GITHUB_TOKEN) {
7 | throw Error('NCM_AUTH_GITHUB_TOKEN not found in environment');
8 | }
9 | await github.authenticate({
10 | type: 'oauth',
11 | token: process.env.NCM_AUTH_GITHUB_TOKEN,
12 | });
13 | }
14 | return github;
15 | };
16 |
17 | export default {};
18 |
--------------------------------------------------------------------------------
/src/lib/package-json.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Logics of generate package.json as per component type.
3 | *
4 | * @type {Object} The generated package.json content.
5 | */
6 | const configPackageJsonFromTemplate = (config, template) => {
7 | const output = {};
8 |
9 | // header
10 | if (config.component.type === 'package') {
11 | output.name = `@${config.package.npmScope}/${config.component.name}`;
12 | output.description = config.component.description;
13 | output.keywords = config.component.keywords;
14 | output.repository = `git@github.com:${config.owner.github}/${config.component.name}.git`; // ASSUME: using GitHub
15 | output.author = `${config.owner.name} <${config.owner.email}>`;
16 |
17 | if (!config.component.private) {
18 | output.license = config.package.license || template.license;
19 | output.publishConfig = {
20 | access: 'public',
21 | }; // make it compatible with semantic-release
22 | } else {
23 | output.private = true;
24 | }
25 | } else {
26 | output.name = config.component.name; // only attached to make it easy to distinguish files in development
27 | output.private = config.component.private;
28 | }
29 |
30 | // dependencies
31 | output.dependencies = template.dependencies;
32 | output.devDependencies = template.devDependencies;
33 | output.optionalDependencies = template.optionalDependencies;
34 |
35 | // footer - package only
36 | if (config.component.type === 'package') {
37 | output.engines = template.engines;
38 |
39 | if (config.package.environment === 'cli') {
40 | output.bin = {
41 | [config.component.name]: 'dist/index.js', // ASSUME: cli built use Babel
42 | };
43 | } else {
44 | output.main = 'dist/index.js';
45 | }
46 | }
47 |
48 | return output;
49 | };
50 |
51 | export default configPackageJsonFromTemplate;
52 |
--------------------------------------------------------------------------------
/src/lib/util.js:
--------------------------------------------------------------------------------
1 | export const sortObjectByKeys = (obj) =>
2 | Object.keys(obj)
3 | .sort()
4 | .reduce(
5 | (output, key) => ({
6 | ...output,
7 | [key]: obj[key],
8 | }),
9 | {},
10 | );
11 |
12 | export default {};
13 |
--------------------------------------------------------------------------------
/src/lib/vault.js:
--------------------------------------------------------------------------------
1 | import vault from 'node-vault';
2 |
3 | import { readFile } from 'lib/fs';
4 | import { HOME } from 'constants';
5 |
6 | const getVaultTokenFromFile = async () => {
7 | const VAULT_TOKEN_FILE = '.vault-token';
8 | const token = await readFile(`${HOME}/${VAULT_TOKEN_FILE}`);
9 | return token;
10 | };
11 |
12 | const setupVaultWithGithubToken = async ({ endpoint }) => {
13 | const { VAULT_AUTH_GITHUB_TOKEN } = process.env;
14 | if (!VAULT_AUTH_GITHUB_TOKEN)
15 | throw Error('VAULT_AUTH_GITHUB_TOKEN not found');
16 |
17 | const vaultClient = vault({ endpoint });
18 |
19 | const res = await vaultClient.githubLogin({
20 | token: VAULT_AUTH_GITHUB_TOKEN,
21 | });
22 | vaultClient.token = res.auth.client_token;
23 |
24 | return vaultClient;
25 | };
26 |
27 | const setupVaultClient = async ({
28 | endpoint = process.env.VAULT_ADDR,
29 | token = process.env.VAULT_TOKEN,
30 | auth = 'token',
31 | }) => {
32 | if (auth.toLowerCase() === 'github') {
33 | const vaultClient = await setupVaultWithGithubToken({ endpoint });
34 | return vaultClient;
35 | }
36 | if (token) {
37 | return vault({ endpoint, token });
38 | }
39 | try {
40 | const fileToken = await getVaultTokenFromFile();
41 | return vault({ endpoint, token: fileToken });
42 | } catch (e) {
43 | if (e.code === 'ENOENT') {
44 | throw Error(
45 | 'vault access token is not provided and local vault not authenticated',
46 | );
47 | }
48 | throw e;
49 | }
50 | };
51 |
52 | const getVaultSecrets = async ({ endpoint, token, auth, scope }) => {
53 | const vaultClient = await setupVaultClient({ endpoint, token, auth });
54 | const { data } = await vaultClient.read(scope);
55 | return data;
56 | };
57 |
58 | export default getVaultSecrets;
59 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "allowJs": true,
5 | "checkJs": false,
6 | "removeComments": false,
7 | "declaration": true,
8 | "alwaysStrict": true,
9 | "strictNullChecks": false,
10 | "target": "es6",
11 | "moduleResolution": "node",
12 | "module": "commonjs",
13 | "outDir": "types",
14 | "baseUrl": "./src"
15 | },
16 | "include": [
17 | "src/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "types",
22 | "dist",
23 | "coverage",
24 | "docs",
25 | "**/__tests__/**"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------