├── .bowerrc
├── .eslintignore
├── .eslintrc.json
├── .gitattributes
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── issue_template.md
├── pull_request_template.md
└── workflows
│ ├── addtomainproject.yml
│ └── releases.yml
├── .gitignore
├── .travis.yml
├── README.md
├── bin
└── adapt.js
├── json
├── help-create.json
├── help-create
│ ├── component.json
│ ├── course.json
│ └── question.json
├── help-devinstall.json
├── help-install.json
├── help-ls.json
├── help-register.json
├── help-rename.json
├── help-search.json
├── help-uninstall.json
├── help-unregister.json
├── help-update.json
├── help-version.json
└── help.json
├── lib
├── api.js
├── cli.js
├── commands
│ ├── authenticate.js
│ ├── create.js
│ ├── create
│ │ ├── component.js
│ │ ├── course.js
│ │ ├── extension.js
│ │ └── question.js
│ ├── devinstall.js
│ ├── help.js
│ ├── install.js
│ ├── ls.js
│ ├── register.js
│ ├── rename.js
│ ├── search.js
│ ├── uninstall.js
│ ├── unregister.js
│ ├── update.js
│ └── version.js
├── econnreset.js
├── integration
│ ├── AdaptFramework.js
│ ├── AdaptFramework
│ │ ├── build.js
│ │ ├── clone.js
│ │ ├── deleteSrcCore.js
│ │ ├── deleteSrcCourse.js
│ │ ├── download.js
│ │ ├── erase.js
│ │ ├── getLatestVersion.js
│ │ └── npmInstall.js
│ ├── Plugin.js
│ ├── PluginManagement.js
│ ├── PluginManagement
│ │ ├── autenticate.js
│ │ ├── install.js
│ │ ├── print.js
│ │ ├── register.js
│ │ ├── rename.js
│ │ ├── schemas.js
│ │ ├── search.js
│ │ ├── uninstall.js
│ │ ├── unregister.js
│ │ └── update.js
│ ├── Project.js
│ ├── Target.js
│ └── getBowerRegistryConfig.js
├── logger.js
└── util
│ ├── JSONReadValidate.js
│ ├── constants.js
│ ├── createPromptTask.js
│ ├── download.js
│ ├── errors.js
│ ├── extract.js
│ ├── getDirNameFromImportMeta.js
│ └── promises.js
├── package-lock.json
└── package.json
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "registry": "http://adapt-bower-repository.herokuapp.com/"
3 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": false,
4 | "node": true,
5 | "commonjs": false,
6 | "es2020": true
7 | },
8 | "extends": [
9 | "standard"
10 | ],
11 | "parserOptions": {
12 | "ecmaVersion": 2020
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | bin/adapt text eol=lf
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We heartily welcome contributions to the Adapt project source code and community.
2 | Here is a list of resources you may find useful:
3 |
4 | * [Contributing to the Adapt project documentation](https://github.com/adaptlearning/adapt_framework/wiki/Contributing-to-the-Adapt-Project)
5 | * [The Adapt framework wiki](https://github.com/adaptlearning/adapt_framework/wiki)
6 | * [Gitter chat room](https://gitter.im/adaptlearning/adapt_framework)
7 | * [General GitHub documentation](http://help.github.com/)
8 | * [GitHub pull request documentation](http://help.github.com/send-pull-requests/)
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Subject of the issue/enhancement/features
2 | Describe your issue here.
3 |
4 | ### Your environment
5 | * version of the cli
6 | * operating system(s)
7 |
8 | ### Steps to reproduce
9 | Tell us how to reproduce this issue.
10 |
11 | ### Expected behaviour
12 | Tell us what should happen
13 |
14 | ### Actual behaviour
15 | Tell us what happens instead
16 |
17 | ### Screenshots (if you can)
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue template
3 | about: Create an issue to help us improve
4 | ---
5 |
6 | ### Subject of the issue/enhancement/features
7 | Describe your issue here.
8 |
9 | ### Your environment
10 | * version (AT/Framework)
11 | * which browser and its version
12 | * device(s) + operating system(s)
13 |
14 | ### Steps to reproduce
15 | Tell us how to reproduce this issue.
16 |
17 | ### Expected behaviour
18 | Tell us what should happen
19 |
20 | ### Actual behaviour
21 | Tell us what happens instead
22 |
23 | ### Screenshots (if you can)
24 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | [//]: # (Please title your PR according to eslint commit conventions)
2 | [//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
3 |
4 | [//]: # (Link the PR to the original issue)
5 |
6 | [//]: # (Delete Fix, Update, New and/or Breaking sections as appropriate)
7 | ### Fix
8 | * A sentence describing each fix
9 |
10 | ### Update
11 | * A sentence describing each update
12 |
13 | ### New
14 | * A sentence describing each new feature
15 |
16 | ### Breaking
17 | * A sentence describing each breaking change
18 |
19 | [//]: # (List appropriate steps for testing if needed)
20 | ### Testing
21 | 1. Steps for testing
22 |
23 | [//]: # (Mention any other dependencies)
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/addtomainproject.yml:
--------------------------------------------------------------------------------
1 | name: Add to main project
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 | pull_request:
8 | types:
9 | - opened
10 |
11 | jobs:
12 | add-to-project:
13 | name: Add to main project
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/add-to-project@v0.1.0
17 | with:
18 | project-url: https://github.com/orgs/adaptlearning/projects/2
19 | github-token: ${{ secrets.ADDTOPROJECT_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.github/workflows/releases.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | with:
14 | fetch-depth: 0
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 'lts/*'
19 | - name: Install dependencies
20 | run: npm ci
21 | - name: Release
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
25 | run: npx semantic-release
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # we don't want to include the src file that may be installed by the cli
3 | src
4 | bower.json
5 | adapt.json
6 | .idea
7 | .idea/workspace.xml
8 | my-adapt-course
9 | test-plugin
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "lts/*"
5 |
6 | sudo: false
7 |
8 | env:
9 | - CXX=g++-4.8
10 |
11 | addons:
12 | apt:
13 | sources:
14 | - ubuntu-toolchain-r-test
15 | packages:
16 | - g++-4.8
17 |
18 | branches:
19 | only:
20 | - master
21 | - develop
22 |
23 | git:
24 | depth: 10
25 |
26 | cache:
27 | directories:
28 | - node_modules
29 |
30 | before_script:
31 | - npm install -g grunt-cli
32 |
33 | install:
34 | - npm config set spin false
35 | - npm install
36 |
37 | matrix:
38 | fast_finish: true
39 |
40 | notifications:
41 | webhooks:
42 | urls:
43 | - https://webhooks.gitter.im/e/2b42e20cf5f5550c1dc0
44 | on_success: change # options: [always|never|change] default: always
45 | on_failure: always # options: [always|never|change] default: always
46 | on_start: never # options: [always|never|change] default: always
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Adapt Command Line Interface (CLI)
2 | ============================
3 |
4 | [](https://travis-ci.org/adaptlearning/adapt-cli) [](https://gitter.im/adaptlearning/adapt-cli?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5 |
6 | The **Adapt CLI** is a command line interface for use with the [Adapt framework](https://github.com/adaptlearning/adapt_framework). Its primary usefulness is to install, update, and uninstall Adapt plug-ins. In doing so, it references the [Adapt Plugin Browser](https://www.adaptlearning.org/index.php/plugin-browser/). Consequently, the CLI includes several commands for working with this plug-in registry.
7 |
8 | >IMPORTANT: The **Adapt CLI** is not intended to be used with courses in the [Adapt authoring tool](https://github.com/adaptlearning/adapt_authoring). The authoring tool tracks versions of plug-ins in its database. Using the CLI bypasses this tracking.
9 |
10 | ## Installation
11 |
12 | Before installing the Adapt CLI, you must install [NodeJS](http://nodejs.org) and [git](http://git-scm.com/downloads).
13 | To install the Adapt CLI globally, run the following command:
14 |
15 | `npm install -g adapt-cli`
16 |
17 | Some systems may require elevated permissions in order to install a script globally.
18 |
19 | ## Commands
20 |
21 | ### adapt version
22 |
23 | ##### Model:
24 | `adapt version`
25 |
26 | This command reports both the version of the CLI and the version of the Adapt framework. The version of the framework is reported as "0.0.0" unless the command is run from the root directory of an installed framework.
27 |
28 |
29 | ### adapt help
30 |
31 | ##### Model:
32 | `adapt help []`
33 |
34 | **command**: A CLI command.
35 |
36 | ##### Examples:
37 |
38 | 1. To list all the CLI commands with a brief description of each:
39 | `adapt help`
40 |
41 | 1. To report a brief description of a specific command:
42 | `adapt help create`
43 |
44 |
45 |
46 | ### adapt create
47 |
48 |
49 | ##### Model:
50 | `adapt create [ ]`
51 |
52 | **type**: What to create. Acceptable values are `course` and `component`.
53 | **path**: The directory of the new course. Enclose in quotes if the path/name includes spaces.
54 | **branch|tag**: The branch or tag name of the framework to be downloaded. This is optional. If not specified, the master branch will be used.
55 |
56 | ##### Examples:
57 |
58 | 1. To create an Adapt course *(do not use in conjunction with the Adapt authoring tool)*:
59 | `adapt create course "My Course"`
60 | This will create a new directory named "My Course" in your current working directory. It will download the Adapt framework from its master branch, including the default course, into the new directory. Before using the course, run:
61 | `grunt build`
62 |
63 | 1. To create an Adapt course from a specific branch:
64 | `adapt create course "My Course" legacy`
65 | This is the same as example 1 except that the framework will be downloaded from the 'legacy' branch, not from the default master branch.
66 |
67 | 1. To create an Adapt course from a specific tag:
68 | `adapt create course "My Course" v3.5.1`
69 | This is the same as example 1 except that v3.5.1 of the framework will be downloaded, rather than the master branch.
70 |
71 | 1. To create an empty component:
72 | `adapt create component "test-component"`
73 | This command is useful for developing new components. It will create a new directory named "test-component" in the current working directory. It will populate the directory with files required for an Adapt component and insert "test-component" into the code where required.
74 |
75 |
76 | ### adapt search
77 |
78 | ##### Model:
79 | `adapt search []`
80 |
81 | **plug-in**: The optional name of the plug-in that you want to search for. Any part of the name may be used; multiple results may be returned if the partial name is not unique.
82 |
83 | The **search** command searches within [Adapt Plugin Browser](https://www.adaptlearning.org/index.php/plugin-browser/). It will return the plug-in's complete name and its source repository only if the plug-in is registered.
84 |
85 | ##### Examples:
86 | 1. To view the name and repository of all plug-ins registered with the Adapt Plugin Browser:
87 | `adapt search`
88 |
89 | 1. To locate the repository of a known plug-in that is registered:
90 | `adapt search adapt-contrib-pageLevelProgress` OR
91 | `adapt search contrib-pageLevelProgress` OR
92 | `adapt search pageLevel`
93 |
94 |
95 | ### adapt install
96 |
97 | ##### Models:
98 | `adapt install [#|@]`
99 | `adapt install [--dry-run|--compatible]`
100 |
101 | **plug-in**: The name of the plug-in to be installed. The name may be fully qualified with "adapt-" or the prefix may be omitted. Parts of names are not acceptable.
102 | **version**: A specific version number of the plug-in. This is optional.
103 |
104 | ##### Examples:
105 | 1. To install all plug-ins listed in *adapt.json*:
106 | `adapt install`
107 | This command must be run in the root directory of an Adapt framework installation.
108 |
109 | 1. To report the plug-ins that will be installed if `adapt install` is run:
110 | `adapt install --dry-run`
111 | This command must be run in the root directory of an Adapt framework installation.
112 |
113 | 1. To install versions of all plug-ins listed in *adapt.json* that are compatible with the installed framework version. This overrides any incompatible settings provided on the command line or in *adapt.json*.
114 | `adapt install --compatible`
115 | This command must be run in the root directory of an Adapt framework installation.
116 |
117 | 1. To install a plug-in that has been registered with the [Adapt Plug-in Browser](https://www.adaptlearning.org/index.php/plugin-browser/) registry:
118 | `adapt install adapt-contrib-pageLevelProgress` OR
119 | `adapt install contrib-pageLevelProgress`
120 |
121 | 1. To install a specific version of a registered plug-in:
122 | `adapt install adapt-contrib-pageLevelProgress#1.1.0` OR
123 | `adapt install adapt-contrib-pageLevelProgress@1.1.0` OR
124 | `adapt install contrib-pageLevelProgress#1.1.0` OR
125 | `adapt install contrib-pageLevelProgress@1.1.0`
126 |
127 | 1. To use the CLI to install a plug-in that is not registered with [Adapt Plug-in Browser](https://www.adaptlearning.org/index.php/plugin-browser/) registry:
128 | 1. Copy the uncompressed folder to the proper location for the type of plug-in. Components: *src/components*. Extensions: *src/extensions*. Menu: *src/menu*. Theme: *src/theme*. Note: The Adapt framework allows only one theme. Uninstall the current before replacing it with an alternative. More than one menu is allowed.
129 | 1. Open *adapt.json* in an editor and add the full name of the new component to the list.
130 | 1. Run the following command from the course root:
131 | `adapt install`
132 | After installation, most CLI commands will operate on the plug-in with the exception of `adapt update`. Plug-ins must be registered with the [Adapt Plugin Browser](https://www.adaptlearning.org/index.php/plugin-browser/) for `adapt update` to succeed.
133 |
134 | 1. To update all registered plug-ins to their most recent public release:
135 | `adapt update`
136 | Since no plug-in name is specified in this command, all plug-ins listed in *adapt.json* are reinstalled. Whether the plug-in is updated will be determined by the compatible framework versions specified in *adapt.json*. If it includes a plug-in that is not registered, it will not be updated.
137 | **Note to developers:** The CLI determines newest version by comparing release tags in the GitHub repo. Be sure to use a tag when you release a new version.
138 |
139 |
140 | ### adapt ls
141 |
142 | ##### Model:
143 | `adapt ls`
144 |
145 | This command lists the name and version number of all plug-ins listed in the *adapt.json* file of the current directory.
146 |
147 |
148 | ### adapt uninstall
149 |
150 | ##### Model:
151 | `adapt uninstall `
152 |
153 | **plug-in**: The name of the plug-in to be installed. The name may be fully qualified with "adapt-" or the prefix may be omitted. Parts of names are not acceptable.
154 |
155 | ##### Examples:
156 | 1. To uninstall a plug-in:
157 | `adapt uninstall adapt-contrib-pageLevelProgress` OR
158 | `adapt uninstall contrib-pageLevelProgress`
159 | Because the plug-in registry is not referenced during the uninstall process, this command will work whether or not the plug-in is registered with the Adapt Plugin Browser..
160 |
161 |
162 | ### adapt devinstall
163 |
164 | ##### Model:
165 | `adapt devinstall [[#]]`
166 |
167 | **plug-in**: Name of the plug-in to be cloned.
168 | **version**: Version of the plug-in to be installed.
169 |
170 | ##### Examples:
171 |
172 | 1. To clone as git repositories the Adapt framework and all the plug-ins listed in *adapt.json* of the current directory:
173 | `adapt devinstall`
174 |
175 | 1. To clone a specific plug-in listed in the *adapt.json*:
176 | `adapt devinstall adapt-contrib-matching`
177 |
178 | 1. To clone a specific version of a plug-in listed in the *adapt.json*:
179 | `adapt devinstall adapt-contrib-matching#v2.2.0`
180 |
181 |
182 | ### adapt update
183 |
184 | ##### Models:
185 | `adapt update [[#|@]][--check]`
186 | `adapt update [components|extensions|menu|theme|all][--check]`
187 |
188 | **plug-in**: Name of the plug-in to be cloned.
189 | **version**: Version of the plug-in to be installed.
190 | Before running the update command, ensure that there is no *bower.json* file in your project directory.
191 |
192 | ##### Examples:
193 |
194 | 1. To report the latest compatible version for each plug-in in the current directory (plug-ins are not updated):
195 | `adapt update --check`
196 | Note: The `--check` option may be used to report on a specific plug-in or on a complete plug-in group (components, extensions, theme, menu):
197 | `adapt update adapt-contrib-matching --check`
198 | `adapt update extensions --check`
199 |
200 | 1. To update a registered plug-in:
201 | `adapt update adapt-contrib-pageLevelProgress` OR
202 | `adapt update contrib-pageLevelProgress`
203 |
204 | 1. To update a specific version of a registered plug-in:
205 | `adapt update adapt-contrib-pageLevelProgress#1.1.0` OR
206 | `adapt update adapt-contrib-pageLevelProgress@1.1.0` OR
207 | `adapt update contrib-pageLevelProgress#1.1.0` OR
208 | `adapt update contrib-pageLevelProgress@1.1.0`
209 |
210 |
211 | ### adapt register
212 |
213 | ##### Command:
214 | `adapt register`
215 |
216 | This command must be run from within the root directory of the plug-in you want to register. "name" and "repository" will be read from *bower.json* in the current directory. The plug-in name must be prefixed with "adapt-" and each word separated with a hyphen (-). Plug-in names are checked against those already registered to avoid duplicates.
217 |
218 |
219 | URL format must be of the form `https://github.com//.git`
220 |
221 | ### adapt rename
222 |
223 | ##### Command:
224 | `adapt rename `
225 |
226 | **current-name**: Name of the plug-in currently used in the plug-in registry.
227 | **new-name**: Name proposed to replace the current plug-in name.
228 |
229 | Please note that you must authenticate with GitHub to use **rename**. You must be a collaborator on the endpoint registered with the plug-in or a collaborator on the Adapt framework. Access to GitHub is for authentication only.
230 |
231 | ##### Example:
232 |
233 | 1. To rename a plug-in:
234 | `adapt rename adapt-incorrectName adapt-betterName`
235 |
236 |
237 | ### adapt unregister
238 |
239 | ##### Command:
240 | `adapt unregister []`
241 |
242 | **plug-in**: Name of the plug-in currently used in the plug-in registry.
243 |
244 | Please note that you must authenticate with GitHub to use **unregister**. You must be a collaborator on the endpoint registered with the plug-in or a collaborator on the Adapt framework. Access to GitHub is for authentication only.
245 |
246 | ##### Examples:
247 |
248 | 1. To unregister a plug-in while in the root directory of the plug-in:
249 | `adapt unregister`
250 |
251 | 1. To unregister a plug-in by name:
252 | `adapt unregister adapt-myMistake`
253 |
254 |
255 |
256 | The Plug-in Registry
257 | -------------------
258 |
259 | The Adapt community maintains the [Adapt Plugin Browser](https://www.adaptlearning.org/index.php/plugin-browser/) as a convenient registry of components, extensions, themes, and menus. The plug-in system is powered by [Bower](http://bower.io/): http://adapt-bower-repository.herokuapp.com/packages/. To register, a plug-in must be a valid bower package with *bower.json*, and have a unique name that is prefixed with "adapt-".
260 |
261 | See [Developing plug-ins](https://github.com/adaptlearning/adapt_framework/wiki/Developing-plugins) for more information on defining your plug-in's package and on [registering your plug-in](https://github.com/adaptlearning/adapt_framework/wiki/Registering-a-plugin).
262 |
263 |
264 | ----------------------------
265 |
266 | **Author / maintainer:** Adapt Core Team with [contributors](https://github.com/adaptlearning/adapt-contrib-hotgraphic/graphs/contributors)
267 |
--------------------------------------------------------------------------------
/bin/adapt.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import cli from '../lib/cli.js'
3 | cli.withOptions().execute()
4 |
--------------------------------------------------------------------------------
/json/help-create.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "create",
3 | "description": "Create a new adapt course",
4 | "usage": [
5 | "create "
6 | ],
7 | "commands": {
8 | "course": "Create new adapt course",
9 | "component": "Create new component using template adapt-component",
10 | "question": "Create new question component using template adapt-questionComponent"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/json/help-create/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "create component",
3 | "description": "create new component",
4 | "usage": [
5 | "create component",
6 | "create component ",
7 | "create component "
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/json/help-create/course.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "create course",
3 | "description": "to create new course",
4 | "usage": [
5 | "create course",
6 | "create course ",
7 | "create course "
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/json/help-create/question.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "create question",
3 | "description": "create new question component",
4 | "usage": [
5 | "create question",
6 | "create question ",
7 | "create question "
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/json/help-devinstall.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "devinstall",
3 | "description": "This command will clone the adapt_framework and all the plugins defined in adapt.json as git repos. For more info visit: https://github.com/adaptlearning/adapt-cli/pull/46",
4 | "usage": [
5 | "devinstall",
6 | "devinstall ",
7 | "devinstall #"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/json/help-install.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "install",
3 | "description": "Install plugins according to the Adapt manifest (adapt.json). You can also specify which plugins to install as arguments. When specifying arguments you can provide an optional semantic version (semver). If a semver is not given the command will attempt to install the latest compatible version. Add the --dry-run option to simulate an installation without making any changes. Add the --compatible option to override any given semvers: the command will attempt to install the latest compatible version for all requested plugins.",
4 | "usage": [
5 | "install [--dry-run, --compatible]",
6 | "install ",
7 | "install #",
8 | "install @"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/json/help-ls.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "ls",
3 | "description": "List the plugin(s) name with version number mentioned in adapt.json file.",
4 | "usage": [
5 | "ls"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/json/help-register.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "register",
3 | "description": "Register a plugin to adapt's remote registry by reading bower.json file. Check folowing URL for more detail - https://github.com/adaptlearning/adapt_framework/wiki/Registering-a-plugin",
4 | "usage": [
5 | "register"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/json/help-rename.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "rename",
3 | "description": "Unregister a plugin at Adapt's remote registry. Specify as two arguments the current name of the plugin to be renamed and a new name. Please note that you must authenticate with GitHub to unregister and must be a collaborator on the endpoint registered with the plugin or a collaborator on the Adapt framework. Access to GitHub is for authentication only.",
4 | "usage": [
5 | "rename "
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/json/help-search.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "search",
3 | "description": "List out all the plugin(s) in adapt's remote registry or could also search a plugin with supplied partial name.",
4 | "usage": [
5 | "search",
6 | "search "
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/json/help-uninstall.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "uninstall",
3 | "description": "Remove a local plugin by its name.",
4 | "usage": [
5 | "uninstall "
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/json/help-unregister.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "unregister",
3 | "description": "Unregister a plugin at Adapt's remote registry. This command can be run without arguments in a directory which contains the bower.json file for the plugin to be unregistered. Alternatively, specify the name of the plugin as a single argument. Please note that you must authenticate with GitHub to unregister and must be a collaborator on the endpoint registered with the plugin or a collaborator on the Adapt framework. Access to GitHub is for authentication only.",
4 | "usage": [
5 | "unregister",
6 | "unregister "
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/json/help-update.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "update",
3 | "description": "Update plugins to their latest compatible version. You can also specify which plugins to update as arguments. When specifying arguments you can provide an optional semantic version (semver). Add the --check option for a summary of available updates for the given plugin(s). When using this option no changes are made. Ensure that there is no bower.json file in your project directory.",
4 | "usage": [
5 | "update [--check]",
6 | "update [ ..] where is one of [components|extensions|menu|theme|all]",
7 | "update ",
8 | "update #",
9 | "update @",
10 | "update [ ..] where target is the name of a plugin or group"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/json/help-version.json:
--------------------------------------------------------------------------------
1 | {
2 | "command": "version",
3 | "description": "Display version of adapt-cli.",
4 | "usage": [
5 | "version"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/json/help.json:
--------------------------------------------------------------------------------
1 | {
2 | "usage": [
3 | ""
4 | ],
5 | "commands": {
6 | "create": "Create a new Adapt course or plugin",
7 | "devinstall": "Get the framework and plugins as Git repository",
8 | "help": "List out the commands available with adapt-cli",
9 | "install": "Install plugin(s) within the Adapt course directory",
10 | "ls": "List all the plugin names mentioned in adapt.json",
11 | "rename": "Rename a plugin at the Adapt remote registry",
12 | "register": "Register a plugin with the Adapt remote registry",
13 | "search": "List/Search for plugin(s) at the Adapt remote registry",
14 | "uninstall": "Remove a local plugin",
15 | "unregister": "Unregister a plugin from the Adapt remote registry",
16 | "update": "Update one or more plugins",
17 | "version": "Display version of adapt-cli"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | import http from 'http' // api http import, to hold open process for testing
2 | import { ADAPT_FRAMEWORK } from './util/constants.js'
3 | import {
4 | build,
5 | download,
6 | erase,
7 | getLatestVersion,
8 | npmInstall,
9 | deleteSrcCourse,
10 | deleteSrcCore
11 | } from './integration/AdaptFramework.js'
12 | import {
13 | install,
14 | uninstall,
15 | update,
16 | schemas
17 | } from './integration/PluginManagement.js'
18 | import Plugin from './integration/Plugin.js'
19 | import Project from './integration/Project.js'
20 | import fs from 'fs-extra'
21 | import async from 'async'
22 | import './econnreset.js'
23 |
24 | // TODO: api, check no interactivity, should be sorted, will fail silently if it absolutely cannot do what you've asked
25 | // TODO: api, console and error output, error on fail when isInteractive: false? or something else? return Targets?
26 | // TODO: api, figure out error translations, should probably error with codes?
27 |
28 | class API {
29 | /**
30 | * Installs a clean copy of the framework
31 | * @param {Object} options
32 | * @param {string} [options.version=null] Specific version of the framework to install
33 | * @param {string} [options.repository] URL to github repo
34 | * @param {string} [options.cwd=process.cwd()] Directory to install into
35 | * @return {Promise}
36 | */
37 | async installFramework ({
38 | version = null,
39 | repository = ADAPT_FRAMEWORK,
40 | cwd = process.cwd(),
41 | logger
42 | } = {}) {
43 | if (!version) version = await getLatestVersion({ repository })
44 | await erase({
45 | isInteractive: false,
46 | cwd,
47 | logger
48 | })
49 | await download({
50 | repository,
51 | branch: version,
52 | cwd,
53 | logger
54 | })
55 | await deleteSrcCourse({
56 | cwd
57 | })
58 | await deleteSrcCore({
59 | cwd
60 | })
61 | await npmInstall({
62 | cwd,
63 | logger
64 | })
65 | }
66 |
67 | /**
68 | * Updates an existing installation of the framework
69 | * @param {Object} options
70 | * @param {string} [options.version=null] Specific version of the framework to install
71 | * @param {string} [options.repository] URL to github repo
72 | * @param {string} [options.cwd=process.cwd()] Directory to install into
73 | * @return {Promise}
74 | */
75 | async updateFramework ({
76 | version = null,
77 | repository = ADAPT_FRAMEWORK,
78 | cwd = process.cwd(),
79 | logger
80 | } = {}) {
81 | // cache state of plugins, as these will be wiped
82 | const plugins = (await new Project({ cwd }).getInstallTargets())
83 | .map(p => p.isLocalSource ? p.sourcePath : `${p.name}@${p.requestedVersion}`)
84 |
85 | await this.installFramework({ version, repository, cwd, logger })
86 | // restore plugins
87 | await this.installPlugins({ plugins, cwd, logger })
88 | }
89 |
90 | /**
91 | * @param {Object} options
92 | * @param {string} [options.cwd=process.cwd()] Directory to install into
93 | * @returns {string}
94 | */
95 | getCurrentFrameworkVersion ({
96 | cwd = process.cwd(),
97 | } = {}) {
98 | return new Project({ cwd }).version
99 | }
100 |
101 | /**
102 | * @param {Object} options
103 | * @param {Object} [options.repository=ADAPT_FRAMEWORK] The github repository url
104 | * @returns {string}
105 | */
106 | async getLatestFrameworkVersion ({
107 | repository = ADAPT_FRAMEWORK
108 | } = {}) {
109 | return getLatestVersion({ repository })
110 | }
111 |
112 | /**
113 | * Runs build for a current course
114 | * @param {Object} options
115 | * @param {boolean} [options.sourceMaps=false] Whether to run the build with sourcemaps
116 | * @param {boolean} [options.checkJSON=false] Whether to run without checking the json
117 | * @param {boolean} [options.cache=true] Whether to clear build caches
118 | * @param {string} [options.outputDir="build/"] Root path of the framework installation
119 | * @param {string} [options.cachePath="build/.cache"] Path of compilation cache file
120 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
121 | * @return {Promise}
122 | */
123 | async buildCourse ({
124 | sourceMaps = false,
125 | checkJSON = false,
126 | cache = true,
127 | outputDir = null,
128 | cachePath = './build/.cache',
129 | cwd = process.cwd(),
130 | logger
131 | } = {}) {
132 | await build({
133 | sourceMaps,
134 | checkJSON,
135 | cache,
136 | cwd,
137 | outputDir,
138 | cachePath,
139 | logger
140 | })
141 | }
142 |
143 | /**
144 | * Installs multiple plugins
145 | * Can install from source folder or bower registry
146 | * @param {Object} options
147 | * @param {[string]} [options.plugins=null] An array with name@version or name@path strings
148 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
149 | * @return {Promise<[Target]>}
150 | */
151 | async installPlugins ({
152 | plugins = null,
153 | cwd = process.cwd(),
154 | logger
155 | } = {}) {
156 | return await install({
157 | plugins,
158 | isInteractive: false,
159 | cwd,
160 | logger
161 | })
162 | }
163 |
164 | /**
165 | * Uninstalls multiple plugins
166 | * @param {Object} options
167 | * @param {[string]} [options.plugins=null] An array with name@version or name@path strings
168 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
169 | * @return {Promise<[Target]>}
170 | */
171 | async uninstallPlugins ({
172 | plugins = null,
173 | cwd = process.cwd(),
174 | logger
175 | } = {}) {
176 | return await uninstall({
177 | plugins,
178 | isInteractive: false,
179 | cwd,
180 | logger
181 | })
182 | }
183 |
184 | /**
185 | * Updates multiple plugins
186 | * Can install from source folder or bower registry
187 | * @param {Object} options
188 | * @param {[string]} [options.plugins=null] An array with name@version or name@path strings
189 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
190 | * @return {Promise<[Target]>}
191 | */
192 | async updatePlugins ({
193 | plugins = null,
194 | cwd = process.cwd(),
195 | logger
196 | } = {}) {
197 | return await update({
198 | plugins,
199 | isInteractive: false,
200 | cwd,
201 | logger
202 | })
203 | }
204 |
205 | /**
206 | * Retrieves all schemas defined in the project
207 | * Clears schema cache
208 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
209 | * @return {Promise<[string]>} Resolves with array of JSON schema filepaths
210 | */
211 | async getSchemaPaths ({
212 | cwd = process.cwd()
213 | } = {}) {
214 | this._schemaPaths = await schemas({ cwd })
215 | this._schemas = {}
216 | return this._schemaPaths
217 | }
218 |
219 | /**
220 | * Retrieves named schema
221 | * Caches schemas for subsequent use
222 | * Call getSchemaPaths to reset cache
223 | * @param {string} options.name Schema filepath as returned from getSchemaPaths
224 | * @return {Promise} Resolves with the JSON schema contents
225 | */
226 | async getSchema ({
227 | name
228 | } = {}) {
229 | if (!this._schemaPaths) return new Error('Please run getSchemaPaths first')
230 | if (!fs.existsSync(name) || !this._schemaPaths.includes(name)) throw new Error(`Schema does not exist: ${name}`)
231 | return (this._schemas[name] = this._schemas[name] ?? await fs.readJSON(name))
232 | }
233 |
234 | /**
235 | * Returns all installed plugins
236 | * @return {Promise<[Plugin]>}
237 | */
238 | async getInstalledPlugins ({
239 | cwd = process.cwd()
240 | } = {}) {
241 | const project = new Project({ cwd })
242 | if (!project.isAdaptDirectory) throw new Error(`Not in an adapt folder at: ${cwd}`)
243 | const installedPlugins = await project.getInstalledPlugins()
244 | for (const plugin of installedPlugins) {
245 | await plugin.fetchProjectInfo()
246 | }
247 | return installedPlugins
248 | }
249 |
250 | /**
251 | * Gets the update information for installed and named plugins
252 | * @param {Object} options
253 | * @param {[string]} [options.plugins=null] An y of plugin names (if not specified, all plugins are checked)
254 | * @param {string} [options.cwd=process.cwd()] Root path of the framework installation
255 | * @return {Promise<[Plugin]>}
256 | */
257 | async getPluginUpdateInfos ({
258 | plugins = null,
259 | cwd = process.cwd()
260 | } = {}) {
261 | /** @type {Project} */
262 | const project = new Project({ cwd })
263 | if (!project.isAdaptDirectory) throw new Error(`Not in an adapt folder at: ${cwd}`)
264 | const frameworkVersion = project.version
265 | /** @type {[Plugin]} */
266 | const installedPlugins = await project.getInstalledPlugins()
267 | const filteredPlugins = !plugins?.length
268 | ? installedPlugins
269 | : plugins
270 | .map(name => installedPlugins.find(plugin => plugin.packageName === name))
271 | .filter(Boolean)
272 | await async.eachOfLimit(filteredPlugins, 8, async plugin => {
273 | await plugin.fetchProjectInfo()
274 | await plugin.fetchBowerInfo()
275 | await plugin.findCompatibleVersion(frameworkVersion)
276 | })
277 | return filteredPlugins
278 | }
279 |
280 | /**
281 | * Returns an object representing the plugin at the path specified
282 | * @returns {Plugin}
283 | */
284 | async getPluginFromPath ({
285 | pluginPath,
286 | cwd = null
287 | } = {}) {
288 | const project = cwd ? new Project({ cwd }) : null
289 | return Plugin.fromPath({ pluginPath, project })
290 | }
291 | }
292 |
293 | const api = new API()
294 | export default api
295 |
296 | // debugging
297 | if (process.argv.includes('--debug-wait')) {
298 | // http server to hold open process for testing
299 | const a = http.createServer((request, response) => {
300 | response.writeHead(200, { 'Content-Type': 'text/plain' })
301 | response.end('test', 'utf-8')
302 | })
303 | a.listen(999)
304 | // make api global to debug more easily
305 | global.api = api
306 | console.log('API Ready')
307 | }
308 |
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | import authenticate from './commands/authenticate.js'
2 | import create from './commands/create.js'
3 | import devinstall from './commands/devinstall.js'
4 | import help from './commands/help.js'
5 | import install from './commands/install.js'
6 | import ls from './commands/ls.js'
7 | import register from './commands/register.js'
8 | import rename from './commands/rename.js'
9 | import search from './commands/search.js'
10 | import uninstall from './commands/uninstall.js'
11 | import unregister from './commands/unregister.js'
12 | import update from './commands/update.js'
13 | import version from './commands/version.js'
14 | import logger from './logger.js'
15 | import './econnreset.js'
16 |
17 | const commands = {
18 | authenticate,
19 | create,
20 | devinstall,
21 | help,
22 | install,
23 | ls,
24 | register,
25 | rename,
26 | search,
27 | uninstall,
28 | unregister,
29 | update,
30 | version
31 | }
32 |
33 | const translationTable = [
34 | { pattern: /^-v$|^--version$/i, replacement: 'version' },
35 | { pattern: /^upgrade$/i, replacement: 'update' }
36 | ]
37 |
38 | class CLI {
39 | withOptions (argv = process.argv || ['node', 'path']) {
40 | const parameters = argv.slice(2).map(param => {
41 | const translation = translationTable.find(item => item.pattern.test(param))
42 | return translation ? translation.replacement : param
43 | })
44 | const name = parameters.length
45 | ? String.prototype.toLowerCase.call(parameters.shift())
46 | : ''
47 | this.command = {
48 | name,
49 | parameters
50 | }
51 | return this
52 | }
53 |
54 | async execute () {
55 | try {
56 | if (!commands[this.command.name]) {
57 | const e = new Error(`Unknown command "${this.command.name}", please check the documentation.`)
58 | logger?.log(e.message)
59 | throw e
60 | }
61 | const commandArguments = [logger].concat(this.command.parameters)
62 | await commands[this.command.name](...commandArguments)
63 | } catch (err) {
64 | console.error(err)
65 | process.exit(err ? 1 : 0)
66 | }
67 | }
68 | }
69 |
70 | export default new CLI()
71 |
--------------------------------------------------------------------------------
/lib/commands/authenticate.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { authenticate as pluginAuthenticate } from '../integration/PluginManagement.js'
3 |
4 | export default async function authenticate (logger, ...args) {
5 | // strip flags
6 | args = args.filter(arg => !String(arg).startsWith('--'))
7 | try {
8 | const confirmation = await pluginAuthenticate({
9 | logger,
10 | cwd: process.cwd(),
11 | pluginName: args[0]
12 | })
13 | const { username, type, pluginName } = confirmation
14 | logger?.log(chalk.green(`${username} authenticated as ${type} for ${pluginName}`))
15 | } catch (err) {
16 | logger?.error('Authentication failed')
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/commands/create.js:
--------------------------------------------------------------------------------
1 | import inquirer from 'inquirer'
2 | import component from './create/component.js'
3 | import question from './create/question.js'
4 | import extension from './create/extension.js'
5 | import course from './create/course.js'
6 | import { ADAPT_FRAMEWORK } from '../util/constants.js'
7 | import { getLatestVersion as getAdaptLatestVersion } from '../integration/AdaptFramework.js'
8 |
9 | const subTasks = {
10 | component,
11 | question,
12 | extension,
13 | course
14 | }
15 |
16 | /**
17 | * TODO: Change component name to camel case
18 | */
19 | export const DEFAULT_TYPE_NAME = {
20 | course: 'my-adapt-course',
21 | component: 'myAdaptComponent',
22 | question: 'myAdaptQuestion',
23 | extension: 'myAdaptExtension'
24 | }
25 |
26 | export default async function create (logger, type = 'course', name, branch, bypassPrompts) {
27 | let options = {
28 | type,
29 | name,
30 | branch
31 | }
32 |
33 | if (!bypassPrompts) {
34 | options = await confirmOptions({
35 | type,
36 | name,
37 | branch,
38 | logger
39 | })
40 | }
41 |
42 | const action = subTasks[options.type]
43 | if (!action) throw new Error('' + options.type + ' is not a supported type')
44 | try {
45 | await action({
46 | name: options.name,
47 | branch: options.branch,
48 | cwd: process.cwd(),
49 | logger
50 | })
51 | } catch (err) {
52 | logger?.error("Oh dear, something went wrong. I'm terribly sorry.", err.message)
53 | throw err
54 | }
55 | }
56 |
57 | async function confirmOptions ({ logger, type, name, branch }) {
58 | const typeSchema = [
59 | {
60 | name: 'type',
61 | choices: ['course', 'component', 'question', 'extension'],
62 | type: 'list',
63 | default: type
64 | }
65 | ]
66 | const typeSchemaResults = await inquirer.prompt(typeSchema)
67 | branch = branch || (typeSchemaResults.type === 'course')
68 | ? await getAdaptLatestVersion({ repository: ADAPT_FRAMEWORK })
69 | : 'master'
70 | const propertySchema = [
71 | {
72 | name: 'branch',
73 | message: 'branch/tag',
74 | type: 'input',
75 | default: branch || 'not specified'
76 | },
77 | {
78 | name: 'name',
79 | message: 'name',
80 | type: 'input',
81 | default: name || DEFAULT_TYPE_NAME[typeSchemaResults.type]
82 | },
83 | {
84 | name: 'ready',
85 | message: 'create now?',
86 | type: 'confirm',
87 | default: true
88 | }
89 | ]
90 | const propertySchemaResults = await inquirer.prompt(propertySchema)
91 | if (!propertySchemaResults.ready) throw new Error('Aborted. Nothing has been created.')
92 | const finalProperties = {
93 | ...typeSchemaResults,
94 | ...propertySchemaResults
95 | }
96 | return finalProperties
97 | }
98 |
--------------------------------------------------------------------------------
/lib/commands/create/component.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import globs from 'globs'
3 | import path from 'path'
4 | import chalk from 'chalk'
5 | import slug from 'speakingurl'
6 | import Project from '../../integration/Project.js'
7 | import downloader from '../../util/download.js'
8 | import { ADAPT_COMPONENT } from '../../util/constants.js'
9 |
10 | export default async function component ({
11 | name,
12 | repository = ADAPT_COMPONENT,
13 | cwd,
14 | branch,
15 | logger
16 | }) {
17 | name = slug(name, { maintainCase: true })
18 |
19 | const project = new Project({ cwd, logger })
20 | let pluginDir
21 | if (project.containsManifestFile) {
22 | const componentsDirectory = 'src/components'
23 | pluginDir = path.join(cwd, componentsDirectory, 'adapt-' + name)
24 | if (!fs.existsSync(componentsDirectory)) fs.mkdirSync(componentsDirectory)
25 | } else {
26 | pluginDir = path.join(cwd, name)
27 | }
28 |
29 | await downloader({
30 | branch,
31 | repository,
32 | cwd: pluginDir,
33 | logger
34 | })
35 |
36 | const files = await new Promise((resolve, reject) => {
37 | globs('**', { cwd: pluginDir }, (err, matches) => {
38 | if (err) return reject(err)
39 | resolve(matches.map(match => path.join(pluginDir, match)))
40 | })
41 | })
42 |
43 | const filesRenamed = files.map(from => {
44 | const to = from.replace(/((contrib-)?componentName)/g, name)
45 | fs.renameSync(from, to)
46 | return to
47 | })
48 |
49 | await Promise.all(filesRenamed.map(async function (file) {
50 | if (fs.statSync(file).isDirectory()) return
51 | const lowerCaseName = name.toLowerCase()
52 | const content = (await fs.readFile(file)).toString()
53 | const modifiedContent = content
54 | .replace(/((contrib-)?componentName)/g, name)
55 | .replace(/((contrib-)?componentname)/g, lowerCaseName)
56 | return fs.writeFile(file, modifiedContent)
57 | }))
58 |
59 | logger?.log('\n' + chalk.green(pluginDir), 'has been created.\n')
60 |
61 | if (fs.existsSync('./adapt.json')) {
62 | logger?.log(chalk.grey('To use this component in your course, use the registered name:') + chalk.yellow(name))
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/commands/create/course.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import {
3 | erase,
4 | download,
5 | npmInstall
6 | } from '../../integration/AdaptFramework.js'
7 | import path from 'path'
8 | import { install as pluginsInstall } from '../../integration/PluginManagement.js'
9 |
10 | export default async function course ({ name, branch, cwd, logger }) {
11 | cwd = path.join(cwd, name)
12 | await erase({ logger, cwd })
13 | await download({ logger, cwd, branch })
14 | await npmInstall({ logger, cwd })
15 | await pluginsInstall({ logger, cwd })
16 | logger?.log(`
17 | ${chalk.green(name)} has been created.
18 |
19 | ${chalk.grey('To build the course, run:')}
20 | cd ${name}
21 | grunt build
22 |
23 | ${chalk.grey('Then to view the course, run:')}
24 | grunt server
25 | `)
26 | }
27 |
--------------------------------------------------------------------------------
/lib/commands/create/extension.js:
--------------------------------------------------------------------------------
1 | import component from 'adapt-cli/lib/commands/create/component.js'
2 | import { ADAPT_EXTENSION } from 'adapt-cli/lib/util/constants.js'
3 |
4 | export default async function extension ({
5 | name,
6 | repository = ADAPT_EXTENSION,
7 | cwd,
8 | branch,
9 | logger
10 | }) {
11 | return component({
12 | name,
13 | repository,
14 | cwd,
15 | branch,
16 | logger
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/lib/commands/create/question.js:
--------------------------------------------------------------------------------
1 | import component from './component.js'
2 | import { ADAPT_QUESTION } from '../../util/constants.js'
3 |
4 | export default async function question ({
5 | name,
6 | repository = ADAPT_QUESTION,
7 | cwd,
8 | branch,
9 | logger
10 | }) {
11 | return component({
12 | name,
13 | repository,
14 | cwd,
15 | branch,
16 | logger
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/lib/commands/devinstall.js:
--------------------------------------------------------------------------------
1 | import { clone as adaptClone } from '../integration/AdaptFramework.js'
2 | import { install as pluginsInstall } from '../integration/PluginManagement.js'
3 | import { ADAPT_FRAMEWORK } from '../util/constants.js'
4 | import path from 'path'
5 | import Project from '../integration/Project.js'
6 | import gh from 'parse-github-url'
7 |
8 | export default async function devinstall (logger, ...args) {
9 | const NAME = gh(ADAPT_FRAMEWORK).repo
10 | const isInAdapt = new Project().isAdaptDirectory
11 | // In adapt folder or download adapt into adapt_framework folder
12 | const cwd = isInAdapt
13 | ? process.cwd()
14 | : path.resolve(NAME)
15 | // strip flags
16 | const isClean = args.includes('--clean')
17 | const isDryRun = args.includes('--dry-run') || args.includes('--check')
18 | const isCompatibleEnabled = args.includes('--compatible')
19 | args = args.filter(arg => !String(arg).startsWith('--'))
20 | // always perform a clone on the adapt directory
21 | if (!isInAdapt || args.includes(NAME)) {
22 | await adaptClone({ logger, cwd })
23 | args = args.filter(arg => arg !== NAME)
24 | }
25 | const plugins = args
26 | return await pluginsInstall({
27 | logger,
28 | cwd,
29 | isClean,
30 | isDryRun,
31 | isCompatibleEnabled,
32 | dev: true,
33 | plugins
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/lib/commands/help.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import path from 'path'
3 | import chalk from 'chalk'
4 | import getDirNameFromImportMeta from '../util/getDirNameFromImportMeta.js'
5 | const __dirname = getDirNameFromImportMeta(import.meta)
6 |
7 | export default function help (logger, ...args) {
8 | const name = args.join(' ')
9 | const json = name
10 | ? path.resolve(__dirname, `../../json/help-${name.replace(/\s+/g, '/')}.json`)
11 | : path.resolve(__dirname, '../../json/help.json')
12 | if (!fs.existsSync(json)) {
13 | logger?.log(`adapt ${chalk.red(name)} Unknown command: ${name}`)
14 | return
15 | }
16 | const jsonData = fs.readJSONSync(json)
17 | logger?.log('\nUsage: \n')
18 | jsonData.usage.forEach(usage => logger?.log(` ${chalk.cyan('adapt')} ${usage}`))
19 | if (jsonData.commands && Object.entries(jsonData.commands).length) {
20 | logger?.log('\n\nwhere is one of:\n')
21 | Object.entries(jsonData.commands).forEach(([command, description]) => {
22 | logger?.log(` ${command}${new Array(23 - command.length).join(' ')}${description}`)
23 | })
24 | }
25 | if (jsonData.description) {
26 | logger?.log(`\nDescription:\n\n ${jsonData.description}`)
27 | }
28 | if (!name) {
29 | logger?.log('\nSee \'adapt help \' for more information on a specific command.\n')
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/commands/install.js:
--------------------------------------------------------------------------------
1 | import { install as pluginsInstall } from '../integration/PluginManagement.js'
2 |
3 | export default async function install (logger, ...args) {
4 | /** strip flags */
5 | const isClean = args.includes('--clean')
6 | const isDryRun = args.includes('--dry-run') || args.includes('--check')
7 | const isCompatibleEnabled = args.includes('--compatible')
8 | const plugins = args.filter(arg => !String(arg).startsWith('--'))
9 | await pluginsInstall({
10 | logger,
11 | isClean,
12 | isDryRun,
13 | isCompatibleEnabled,
14 | plugins
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/lib/commands/ls.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import _ from 'lodash'
3 | import Project from '../integration/Project.js'
4 | import { eachOfLimitProgress } from '../util/promises.js'
5 |
6 | export default async function ls (logger) {
7 | const project = new Project({ logger })
8 | const frameworkVersion = project.version
9 | project.tryThrowInvalidPath()
10 | const installTargets = (await project.getInstallTargets())
11 | const installTargetsIndex = installTargets.reduce((hash, p) => {
12 | const name = (p.name || p.sourcePath)
13 | hash[name] = p
14 | return hash
15 | }, {})
16 | const installedPlugins = await project.getInstalledPlugins();
17 | const notInstalled = _.difference(installTargets.map(p => (p.name || p.sourcePath)), installedPlugins.map(p => (p.name || p.sourcePath)))
18 | const plugins = installedPlugins.concat(notInstalled.map(name => installTargetsIndex[name]))
19 | await eachOfLimitProgress(
20 | plugins,
21 | async (target) => {
22 | await target.fetchProjectInfo()
23 | await target.fetchSourceInfo()
24 | await target.findCompatibleVersion(frameworkVersion)
25 | },
26 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Getting plugin info ${percentage}% complete`)
27 | )
28 | logger?.log(`${chalk.bold.cyan('')} Getting plugin info 100% complete`)
29 | plugins
30 | .sort((a, b) => a.name.localeCompare(b.name))
31 | .forEach(p => {
32 | const name = (p.name || p.sourcePath)
33 | logger?.log(`${chalk.cyan(p.name || p.sourcePath)} ${chalk.green('adapt.json')}: ${installTargetsIndex[name]?.requestedVersion || 'n/a'} ${chalk.green('installed')}: ${p.sourcePath || p.projectVersion || 'n/a'} ${chalk.green('latest')}: ${p.sourcePath || p.latestCompatibleSourceVersion || 'n/a'}`)
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/lib/commands/register.js:
--------------------------------------------------------------------------------
1 | import { register as pluginRegister } from '../integration/PluginManagement.js'
2 |
3 | export default async function register (logger, ...args) {
4 | // strip flags
5 | args = args.filter(arg => !String(arg).startsWith('--'))
6 | return await pluginRegister({
7 | logger,
8 | cwd: process.cwd(),
9 | args
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/lib/commands/rename.js:
--------------------------------------------------------------------------------
1 | import { rename as pluginRename } from '../integration/PluginManagement.js'
2 |
3 | export default async function rename (logger, ...args) {
4 | /** strip flags */
5 | args = args.filter(arg => !String(arg).startsWith('--'))
6 | const oldName = args[0]
7 | const newName = args[1]
8 | await pluginRename({
9 | logger,
10 | cwd: process.cwd(),
11 | oldName,
12 | newName
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/lib/commands/search.js:
--------------------------------------------------------------------------------
1 | import { search as pluginsSearch } from '../integration/PluginManagement.js'
2 |
3 | export default async function search (logger, ...args) {
4 | /** strip flags */
5 | args = args.filter(arg => !String(arg).startsWith('--'))
6 | const searchTerm = (args[0] || '')
7 | await pluginsSearch({
8 | logger,
9 | searchTerm
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/lib/commands/uninstall.js:
--------------------------------------------------------------------------------
1 | import { uninstall as pluginsUninstall } from '../integration/PluginManagement.js'
2 |
3 | export default async function uninstall (logger, ...args) {
4 | const plugins = args.filter(arg => !String(arg).startsWith('--'))
5 | await pluginsUninstall({
6 | logger,
7 | plugins
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/lib/commands/unregister.js:
--------------------------------------------------------------------------------
1 | import { unregister as pluginUnregister } from '../integration/PluginManagement.js'
2 |
3 | export default async function register (logger, ...args) {
4 | // strip flags
5 | args = args.filter(arg => !String(arg).startsWith('--'))
6 | const pluginName = args[0]
7 | return await pluginUnregister({
8 | logger,
9 | cwd: process.cwd(),
10 | pluginName
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/lib/commands/update.js:
--------------------------------------------------------------------------------
1 | import { update as pluginsUpdate } from '../integration/PluginManagement.js'
2 |
3 | export default async function update (logger, ...args) {
4 | /** strip flags */
5 | const isDryRun = args.includes('--dry-run') || args.includes('--check')
6 | const plugins = args.filter(arg => !String(arg).startsWith('--'))
7 | await pluginsUpdate({
8 | logger,
9 | plugins,
10 | isDryRun
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/lib/commands/version.js:
--------------------------------------------------------------------------------
1 | import Project from '../integration/Project.js'
2 | import path from 'path'
3 | import { readValidateJSONSync } from '../util/JSONReadValidate.js'
4 | import getDirNameFromImportMeta from '../util/getDirNameFromImportMeta.js'
5 | const __dirname = getDirNameFromImportMeta(import.meta)
6 |
7 | export default function version (logger) {
8 | const cliVersionPath = path.join(__dirname, '../../package.json')
9 | const cliVersion = readValidateJSONSync(cliVersionPath).version
10 | const project = new Project()
11 | logger?.log('CLI: ' + cliVersion)
12 | logger?.log('Framework: ' + project.version)
13 | }
14 |
--------------------------------------------------------------------------------
/lib/econnreset.js:
--------------------------------------------------------------------------------
1 | // guard against ECONNRESET issues https://github.com/adaptlearning/adapt-cli/issues/169
2 | process.on('uncaughtException', (error, origin) => {
3 | if (error?.code === 'ECONNRESET') return
4 | console.error('UNCAUGHT EXCEPTION')
5 | console.error(error)
6 | console.error(origin)
7 | })
8 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework.js:
--------------------------------------------------------------------------------
1 | import build from './AdaptFramework/build.js'
2 | import getLatestVersion from './AdaptFramework/getLatestVersion.js'
3 | import clone from './AdaptFramework/clone.js'
4 | import erase from './AdaptFramework/erase.js'
5 | import download from './AdaptFramework/download.js'
6 | import npmInstall from './AdaptFramework/npmInstall.js'
7 | import deleteSrcCourse from './AdaptFramework/deleteSrcCourse.js'
8 | import deleteSrcCore from './AdaptFramework/deleteSrcCore.js'
9 |
10 | export {
11 | build,
12 | getLatestVersion,
13 | clone,
14 | erase,
15 | download,
16 | npmInstall,
17 | deleteSrcCourse,
18 | deleteSrcCore
19 | }
20 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/build.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { exec } from 'child_process'
3 | import Project from '../Project.js'
4 | import path from 'path'
5 |
6 | export default async function adaptBuild ({
7 | sourceMaps = false,
8 | checkJSON = false,
9 | cache = true,
10 | outputDir = null,
11 | cachePath = null,
12 | cwd = process.cwd(),
13 | logger
14 | } = {}) {
15 | cwd = path.resolve(process.cwd(), cwd)
16 | const project = new Project({ cwd, logger })
17 | project.tryThrowInvalidPath()
18 | logger?.log(chalk.cyan('running build'))
19 | await new Promise((resolve, reject) => {
20 | const cmd = [
21 | 'npx grunt',
22 | !checkJSON
23 | ? `server-build:${sourceMaps ? 'dev' : 'prod'}` // AAT
24 | : `${sourceMaps ? 'diff' : 'build'}`, // Handbuilt
25 | !cache && '--disable-cache',
26 | outputDir && `--outputdir=${outputDir}`,
27 | cachePath && `--cachepath=${cachePath}`
28 | ].filter(Boolean).join(' ');
29 | exec(cmd, { cwd }, (error, stdout, stderr) => {
30 | if(error || stderr) {
31 | const matches = stdout.match(/>> Error:\s(.+)\s/);
32 | const e = new Error('grunt tasks failed')
33 | e.cmd = cmd;
34 | e.raw = matches?.[1] ?? stdout;
35 | e.source = error;
36 | e.stderr = stderr;
37 | return reject(e)
38 | }
39 | resolve()
40 | })
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/clone.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { exec } from 'child_process'
3 | import { ADAPT_FRAMEWORK } from '../../util/constants.js'
4 | import path from 'path'
5 |
6 | export default async function clone ({
7 | repository = ADAPT_FRAMEWORK,
8 | branch = 'master',
9 | cwd = process.cwd(),
10 | logger
11 | } = {}) {
12 | repository = repository.replace(/\.git/g, '')
13 | cwd = path.resolve(process.cwd(), cwd)
14 | if (!branch && !repository) throw new Error('Repository details are required.')
15 | logger?.write(chalk.cyan('cloning framework to', cwd, '\t'))
16 | await new Promise(function (resolve, reject) {
17 | const child = exec(`git clone ${repository} "${cwd}"`)
18 | child.addListener('error', reject)
19 | child.addListener('exit', resolve)
20 | })
21 | await new Promise(function (resolve, reject) {
22 | const child = exec(`git checkout ${branch}`)
23 | child.addListener('error', reject)
24 | child.addListener('exit', resolve)
25 | })
26 | logger?.log(' ', 'done!')
27 | }
28 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/deleteSrcCore.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import path from 'path'
3 |
4 | export default function deleteSrcCore ({
5 | cwd = process.cwd()
6 | } = {}) {
7 | cwd = path.resolve(process.cwd(), cwd)
8 | return fs.rm(path.resolve(cwd, 'src/core'), { recursive: true, force: true })
9 | }
10 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/deleteSrcCourse.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import path from 'path'
3 |
4 | export default function deleteSrcCourse ({
5 | cwd = process.cwd()
6 | } = {}) {
7 | cwd = path.resolve(process.cwd(), cwd)
8 | return fs.rm(path.resolve(cwd, 'src/course'), { recursive: true, force: true })
9 | }
10 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/download.js:
--------------------------------------------------------------------------------
1 | import downloader from '../../util/download.js'
2 | import { ADAPT_FRAMEWORK } from '../../util/constants.js'
3 | import path from 'path'
4 |
5 | export default async function download ({
6 | repository = ADAPT_FRAMEWORK,
7 | branch,
8 | tmp,
9 | cwd,
10 | logger
11 | } = {}) {
12 | repository = repository.replace(/\.git/g, '')
13 | cwd = path.resolve(process.cwd(), cwd)
14 | return downloader({
15 | repository,
16 | branch,
17 | tmp,
18 | cwd,
19 | logger
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/erase.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import fs from 'fs-extra'
3 | import inquirer from 'inquirer'
4 | import path from 'path'
5 |
6 | export default async function erase ({
7 | isInteractive = true,
8 | cwd,
9 | logger
10 | } = {}) {
11 | cwd = path.resolve(process.cwd(), cwd)
12 | if (!fs.existsSync(cwd)) return
13 | if (isInteractive) {
14 | const results = await inquirer.prompt([{
15 | name: 'overwrite existing course?',
16 | type: 'confirm',
17 | default: false
18 | }])
19 | if (!results['overwrite existing course?']) {
20 | throw new Error('Course already exists and cannot overwrite.')
21 | }
22 | }
23 | logger?.log(chalk.cyan('deleting existing course'))
24 | await fs.rm(cwd, { recursive: true })
25 | }
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/getLatestVersion.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 | import semver from 'semver'
3 | import gh from 'parse-github-url'
4 | import { ADAPT_DEFAULT_USER_AGENT, ADAPT_FRAMEWORK, ADAPT_ALLOW_PRERELEASE } from '../../util/constants.js'
5 | const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
6 |
7 | export default async function getLatestVersion ({ versionLimit, repository = ADAPT_FRAMEWORK }) {
8 | repository = repository.replace(/\.git/g, '')
9 | const OWNER = gh(repository).owner
10 | const NAME = gh(repository).name
11 | // used in pagination
12 | let nextPage = `https://api.github.com/repos/${OWNER}/${NAME}/releases`
13 | // taken from https://gist.github.com/niallo/3109252
14 | const parseLinkHeader = header => {
15 | if (!header || header.length === 0) {
16 | return []
17 | }
18 | const links = {}
19 | // Parse each part into a named link
20 | header.split(',').forEach(p => {
21 | const section = p.split(';')
22 | if (section.length !== 2) {
23 | throw new Error("section could not be split on ';'")
24 | }
25 | const url = section[0].replace(/<(.*)>/, '$1').trim()
26 | const name = section[1].replace(/rel="(.*)"/, '$1').trim()
27 | links[name] = url
28 | })
29 | return links
30 | }
31 | const processPage = async () => {
32 | const response = await fetch(nextPage, {
33 | headers: {
34 | 'User-Agent': ADAPT_DEFAULT_USER_AGENT
35 | },
36 | method: 'GET'
37 | })
38 | const body = await response.text()
39 | if (response?.status === 403 && response?.headers['x-ratelimit-remaining'] === '0') {
40 | // we've exceeded the API limit
41 | const reqsReset = new Date(response.headers['x-ratelimit-reset'] * 1000)
42 | throw new Error(`Couldn't check latest version of ${NAME}. You have exceeded GitHub's request limit of ${response.headers['x-ratelimit-limit']} requests per hour. Please wait until at least ${reqsReset.toTimeString()} before trying again.`)
43 | }
44 | if (response?.status !== 200) {
45 | throw new Error(`Couldn't check latest version of ${NAME}. GitubAPI did not respond with a 200 status code.`)
46 | }
47 | nextPage = parseLinkHeader(response.headers.link).next
48 | let releases
49 | try {
50 | // parse and sort releases (newest first)
51 | releases = JSON.parse(body).sort((a, b) => {
52 | if (semver.lt(a.tag_name, b.tag_name, semverOptions)) return 1
53 | if (semver.gt(a.tag_name, b.tag_name, semverOptions)) return -1
54 | return 0
55 | })
56 | } catch (e) {
57 | throw new Error(`Failed to parse GitHub release data\n${e}`)
58 | }
59 | const compatibleRelease = releases.find(release => {
60 | const isFullRelease = !release.draft && !release.prerelease
61 | const satisfiesVersion = !versionLimit || semver.satisfies(release.tag_name, versionLimit, semverOptions)
62 | if (!isFullRelease || !satisfiesVersion) return false
63 | return true
64 | })
65 | if (!compatibleRelease && nextPage) {
66 | return await processPage()
67 | }
68 | if (!compatibleRelease) {
69 | throw new Error(`Couldn't find any releases compatible with specified framework version (${versionLimit}), please check that it is a valid version.`)
70 | }
71 | return compatibleRelease.tag_name
72 | }
73 | return await processPage()
74 | }
75 |
--------------------------------------------------------------------------------
/lib/integration/AdaptFramework/npmInstall.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { spawn } from 'child_process'
3 | import path from 'path'
4 |
5 | export default async function npmInstall ({
6 | logger,
7 | cwd
8 | } = {}) {
9 | cwd = path.resolve(process.cwd(), cwd)
10 | await new Promise((resolve, reject) => {
11 | logger?.log(chalk.cyan('installing node dependencies'))
12 | const npm = spawn((process.platform === 'win32' ? 'npm.cmd' : 'npm'), ['--unsafe-perm', 'install'], {
13 | stdio: 'inherit',
14 | cwd,
15 | shell: true
16 | })
17 | npm.on('close', code => {
18 | if (code) return reject(new Error('npm install failed'))
19 | resolve()
20 | })
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/lib/integration/Plugin.js:
--------------------------------------------------------------------------------
1 | import slug from 'speakingurl'
2 | import globs from 'globs'
3 | import bower from 'bower'
4 | import endpointParser from 'bower-endpoint-parser'
5 | import semver from 'semver'
6 | import fs from 'fs-extra'
7 | import path from 'path'
8 | import getBowerRegistryConfig from './getBowerRegistryConfig.js'
9 | import { ADAPT_ALLOW_PRERELEASE, PLUGIN_TYPES, PLUGIN_TYPE_FOLDERS, PLUGIN_DEFAULT_TYPE } from '../util/constants.js'
10 | /** @typedef {import("./Project.js").default} Project */
11 | const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
12 |
13 | // when a bower command errors this is the maximum number of attempts the command will be repeated
14 | const BOWER_MAX_TRY = 5
15 |
16 | export default class Plugin {
17 | /**
18 | * @param {Object} options
19 | * @param {string} options.name
20 | * @param {string} options.requestedVersion
21 | * @param {boolean} options.isContrib
22 | * @param {boolean} options.isCompatibleEnabled whether to target the latest compatible version for all plugin installations (overrides requestedVersion)
23 | * @param {Project} options.project
24 | * @param {string} options.cwd
25 | * @param {Object} options.logger
26 | */
27 | constructor ({
28 | name,
29 | requestedVersion = '*',
30 | isContrib = false,
31 | isCompatibleEnabled = false,
32 | project,
33 | cwd = (project?.cwd ?? process.cwd()),
34 | logger
35 | } = {}) {
36 | this.logger = logger
37 | /** @type {Project} */
38 | this.project = project
39 | this.cwd = cwd
40 | this.BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd: this.cwd })
41 | const endpoint = name + '#' + (isCompatibleEnabled ? '*' : requestedVersion)
42 | const ep = endpointParser.decompose(endpoint)
43 | this.sourcePath = null
44 | this.name = ep.name || ep.source
45 | this.packageName = (/^adapt-/i.test(this.name) ? '' : 'adapt-') + (!isContrib ? '' : 'contrib-') + slug(this.name, { maintainCase: true })
46 | // the constraint given by the user
47 | this.requestedVersion = requestedVersion
48 | // the most recent version of the plugin compatible with the given framework
49 | this.latestCompatibleSourceVersion = null
50 | // a non-wildcard constraint resolved to the highest version of the plugin that satisfies the requestedVersion and is compatible with the framework
51 | this.matchedVersion = null
52 | // a flag describing if the plugin can be updated
53 | this.canBeUpdated = null
54 |
55 | const isNameAPath = /\\|\//g.test(this.name)
56 | const isVersionAPath = /\\|\//g.test(this.requestedVersion)
57 | const isLocalPath = (isNameAPath || isVersionAPath)
58 | if (isLocalPath) {
59 | // wait to name the plugin until the local config file is loaded
60 | this.sourcePath = isNameAPath ? this.name : this.requestedVersion
61 | this.name = isVersionAPath ? this.packageName : ''
62 | this.packageName = isNameAPath ? '' : this.packageName
63 | this.requestedVersion = '*'
64 | }
65 | // the path of the source files
66 | this.projectPath = null
67 | // the project plugin .bower.json or bower.json
68 | this._projectInfo = null
69 | // the result of a query to the server or disk for source files
70 | this._sourceInfo = null
71 | // server given versions
72 | this._versionsInfo = null
73 |
74 | Plugin.instances.push(this)
75 | }
76 |
77 | /**
78 | * the installed version is the latest version
79 | * @returns {boolean|null}
80 | */
81 | get isUpToDate () {
82 | if (!this.hasFrameworkCompatibleVersion) return true
83 | const canCheckSourceAgainstProject = (this.latestSourceVersion && this.projectVersion)
84 | if (!canCheckSourceAgainstProject) return null
85 | const isLatestVersion = (this.projectVersion === this.latestSourceVersion)
86 | const isLatestMatchedVersion = (this.projectVersion === this.matchedVersion)
87 | const isProjectVersionGreater = semver.gt(this.projectVersion, this.matchedVersion)
88 | return (isLatestVersion || isLatestMatchedVersion || isProjectVersionGreater)
89 | }
90 |
91 | /**
92 | * the most recent version of the plugin
93 | * @returns {string|null}
94 | */
95 | get latestSourceVersion () {
96 | return (this._sourceInfo?.version || null)
97 | }
98 |
99 | /**
100 | * the installed version of the plugin
101 | * @returns {string|null}
102 | */
103 | get projectVersion () {
104 | return (this._projectInfo?.version || null)
105 | }
106 |
107 | /**
108 | * the required framework version from the source package json
109 | * @returns {string|null}
110 | */
111 | get frameworkVersion () {
112 | return (this._sourceInfo?.framework || null)
113 | }
114 |
115 | /**
116 | * a list of tags denoting the source versions of the plugin
117 | * @returns {[string]}
118 | */
119 | get sourceVersions () {
120 | return this._versionsInfo
121 | }
122 |
123 | /**
124 | * plugin will be or was installed from a local source
125 | * @returns {boolean}
126 | */
127 | get isLocalSource () {
128 | return Boolean(this.sourcePath || this?._projectInfo?._wasInstalledFromPath)
129 | }
130 |
131 | /**
132 | * check if source path is a zip
133 | * @returns {boolean}
134 | */
135 | get isLocalSourceZip () {
136 | return Boolean(this.isLocalSource && (this.sourcePath?.includes('.zip') || this._projectInfo?._source?.includes('.zip')))
137 | }
138 |
139 | /** @returns {boolean} */
140 | get isVersioned () {
141 | return Boolean(this.sourceVersions?.length)
142 | }
143 |
144 | /**
145 | * is a contrib plugin
146 | * @returns {boolean}
147 | */
148 | get isContrib () {
149 | return /^adapt-contrib/.test(this.packageName)
150 | }
151 |
152 | /**
153 | * whether querying the server or disk for plugin information worked
154 | * @returns {boolean}
155 | */
156 | get isPresent () {
157 | return Boolean(this._projectInfo || this._sourceInfo)
158 | }
159 |
160 | /**
161 | * has user requested version
162 | * @returns {boolean}
163 | */
164 | get hasUserRequestVersion () {
165 | return (this.requestedVersion !== '*')
166 | }
167 |
168 | /**
169 | * the supplied a constraint is valid and supported by the plugin
170 | * @returns {boolean|null}
171 | */
172 | get hasValidRequestVersion () {
173 | return (this.latestSourceVersion)
174 | ? semver.validRange(this.requestedVersion, semverOptions) &&
175 | (this.isVersioned
176 | ? semver.maxSatisfying(this.sourceVersions, this.requestedVersion, semverOptions) !== null
177 | : semver.satisfies(this.latestSourceVersion, this.requestedVersion, semverOptions)
178 | )
179 | : null
180 | }
181 |
182 | /** @returns {boolean} */
183 | get hasFrameworkCompatibleVersion () {
184 | return (this.latestCompatibleSourceVersion !== null)
185 | }
186 |
187 | async fetchSourceInfo () {
188 | if (this.isLocalSource) return await this.fetchLocalSourceInfo()
189 | await this.fetchBowerInfo()
190 | }
191 |
192 | async fetchLocalSourceInfo () {
193 | if (this._sourceInfo) return this._sourceInfo
194 | this._sourceInfo = null
195 | if (!this.isLocalSource) throw new Error('Plugin name or version must be a path to the source')
196 | if (this.isLocalSourceZip) throw new Error('Cannot install from zip files')
197 | this._sourceInfo = await new Promise((resolve, reject) => {
198 | // get bower.json data
199 | const paths = [
200 | path.resolve(this.cwd, `${this.sourcePath}/bower.json`)
201 | ]
202 | const bowerJSON = paths.reduce((bowerJSON, bowerJSONPath) => {
203 | if (bowerJSON) return bowerJSON
204 | if (!fs.existsSync(bowerJSONPath)) return null
205 | return fs.readJSONSync(bowerJSONPath)
206 | }, null)
207 | resolve(bowerJSON)
208 | })
209 | if (!this._sourceInfo) return
210 | this.name = this._sourceInfo.name
211 | this.matchedVersion = this.latestSourceVersion
212 | this.packageName = this.name
213 | }
214 |
215 | async fetchBowerInfo () {
216 | if (this._sourceInfo) return this._sourceInfo
217 | this._sourceInfo = null
218 | if (this.isLocalSource) return
219 | const perform = async (attemptCount = 0) => {
220 | try {
221 | return await new Promise((resolve, reject) => {
222 | bower.commands.info(`${this.packageName}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
223 | .on('end', resolve)
224 | .on('error', reject)
225 | })
226 | } catch (err) {
227 | const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
228 | if (isFinished) return null
229 | return await perform(attemptCount + 1)
230 | }
231 | }
232 | const info = await perform()
233 | if (!info) return
234 | this._sourceInfo = info.latest
235 | this._versionsInfo = info.versions.filter(version => semverOptions.includePrerelease ? true : !semver.prerelease(version))
236 | }
237 |
238 | async fetchProjectInfo () {
239 | if (this._projectInfo) return this._projectInfo
240 | this._projectInfo = null
241 | this._projectInfo = await new Promise((resolve, reject) => {
242 | // get bower.json data
243 | globs([
244 | `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/.bower.json`,
245 | `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/bower.json`
246 | ], (err, matches) => {
247 | if (err) return resolve(null)
248 | const tester = new RegExp(`/${this.packageName}/`, 'i')
249 | const match = matches.find(match => tester.test(match))
250 | if (!match) {
251 | // widen the search
252 | globs([
253 | `${this.cwd.replace(/\\/g, '/')}/src/**/.bower.json`,
254 | `${this.cwd.replace(/\\/g, '/')}/src/**/bower.json`
255 | ], (err, matches) => {
256 | if (err) return resolve(null)
257 | const tester = new RegExp(`/${this.packageName}/`, 'i')
258 | const match = matches.find(match => tester.test(match))
259 | if (!match) return resolve(null)
260 | this.projectPath = path.resolve(match, '../')
261 | resolve(fs.readJSONSync(match))
262 | })
263 | return
264 | }
265 | this.projectPath = path.resolve(match, '../')
266 | resolve(fs.readJSONSync(match))
267 | })
268 | })
269 | if (!this._projectInfo) return
270 | this.name = this._projectInfo.name
271 | this.packageName = this.name
272 | }
273 |
274 | async findCompatibleVersion (framework) {
275 | const getBowerVersionInfo = async (version) => {
276 | const perform = async (attemptCount = 0) => {
277 | try {
278 | return await new Promise((resolve, reject) => {
279 | bower.commands.info(`${this.packageName}@${version}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
280 | .on('end', resolve)
281 | .on('error', reject)
282 | })
283 | } catch (err) {
284 | const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
285 | if (isFinished) return null
286 | return await perform(attemptCount++)
287 | }
288 | }
289 | return await perform()
290 | }
291 | const getMatchingVersion = async () => {
292 | if (this.isLocalSource) {
293 | const info = this.projectVersion ? this._projectInfo : this._sourceInfo
294 | const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(info.version, this.requestedVersion, semverOptions)
295 | const satisfiesFramework = semver.satisfies(framework, info.framework)
296 | if (satisfiesFramework && satisfiesConstraint) this.latestCompatibleSourceVersion = info.version
297 | return info.version
298 | }
299 |
300 | if (!this.isPresent) return null
301 |
302 | // check if the latest version is compatible
303 | const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(this._sourceInfo.version, this.requestedVersion, semverOptions)
304 | const satisfiesFramework = semver.satisfies(framework, this.frameworkVersion, semverOptions)
305 | if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = this.latestSourceVersion
306 | if (satisfiesConstraint && satisfiesFramework) {
307 | return this.latestSourceVersion
308 | }
309 |
310 | if (!this.isVersioned) return null
311 |
312 | // find the highest version that is compatible with the framework and constraint
313 | const searchVersionInfo = async (framework, versionIndex = 0) => {
314 | const versioninfo = await getBowerVersionInfo(this.sourceVersions[versionIndex])
315 | // give up if there is any failure to obtain version info
316 | if (!this.isPresent) return null
317 | // check that the proposed plugin is compatible with the contraint and installed framework
318 | const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(versioninfo.version, this.requestedVersion, semverOptions)
319 | const satisfiesFramework = semver.satisfies(framework, versioninfo.framework, semverOptions)
320 | if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = versioninfo.version
321 | const checkNext = (!satisfiesFramework || !satisfiesConstraint)
322 | const hasNoMoreVersions = (versionIndex + 1 >= this.sourceVersions.length)
323 | if (checkNext && hasNoMoreVersions) return null
324 | if (checkNext) return await searchVersionInfo(framework, versionIndex + 1)
325 | return versioninfo.version
326 | }
327 | return await searchVersionInfo(framework)
328 | }
329 | this.matchedVersion = await getMatchingVersion()
330 | this.canBeUpdated = (this.projectVersion && this.matchedVersion) && (this.projectVersion !== this.matchedVersion)
331 | }
332 |
333 | /**
334 | * @returns {string}
335 | */
336 | async getType () {
337 | if (this._type) return this._type
338 | const info = await this.getInfo()
339 | const foundAttributeType = PLUGIN_TYPES.find(type => info[type])
340 | const foundKeywordType = info.keywords
341 | .map(keyword => {
342 | const typematches = PLUGIN_TYPES.filter(type => keyword?.toLowerCase()?.includes(type))
343 | return typematches.length ? typematches[0] : null
344 | })
345 | .filter(Boolean)[0]
346 | return (this._type = foundAttributeType || foundKeywordType || PLUGIN_DEFAULT_TYPE)
347 | }
348 |
349 | async getTypeFolder () {
350 | const type = await this.getType()
351 | return PLUGIN_TYPE_FOLDERS[type]
352 | }
353 |
354 | async getInfo () {
355 | if (this._projectInfo) return this._projectInfo
356 | if (!this._sourceInfo) await this.fetchSourceInfo()
357 | return this._sourceInfo
358 | }
359 |
360 | async getRepositoryUrl () {
361 | if (this._repositoryUrl) return this._repositoryUrl
362 | if (this.isLocalSource) return
363 | const url = await new Promise((resolve, reject) => {
364 | bower.commands.lookup(this.packageName, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
365 | .on('end', resolve)
366 | .on('error', reject)
367 | })
368 | return (this._repositoryUrl = url)
369 | }
370 |
371 | /** @returns {string} */
372 | toString () {
373 | const isAny = (this.projectVersion === '*' || this.projectVersion === null)
374 | return `${this.packageName}${isAny ? '' : `@${this.projectVersion}`}`
375 | }
376 |
377 | async getSchemaPaths () {
378 | if (this.isLocalSource) await this.fetchLocalSourceInfo()
379 | else if (this.project) await this.fetchProjectInfo()
380 | else throw new Error(`Cannot fetch schemas from remote plugin: ${this.name}`)
381 | const pluginPath = this.projectPath ?? this.sourcePath
382 | return new Promise((resolve, reject) => {
383 | return globs(path.resolve(this.cwd, pluginPath, '**/*.schema.json'), (err, matches) => {
384 | if (err) return reject(err)
385 | resolve(matches)
386 | })
387 | })
388 | }
389 |
390 | /**
391 | * Read plugin data from pluginPath
392 | * @param {Object} options
393 | * @param {string} options.pluginPath Path to source directory
394 | * @param {string} [options.projectPath=process.cwd()] Optional path to potential installation project
395 | * @returns {Plugin}
396 | */
397 | static async fromPath ({
398 | pluginPath,
399 | projectPath = process.cwd()
400 | }) {
401 | const plugin = new Plugin({
402 | name: pluginPath,
403 | cwd: projectPath
404 | })
405 | await plugin.fetchLocalSourceInfo()
406 | return plugin
407 | }
408 |
409 | static get instances () {
410 | return (Plugin._instances = Plugin._instances || [])
411 | }
412 | }
413 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement.js:
--------------------------------------------------------------------------------
1 | import authenticate from './PluginManagement/autenticate.js'
2 | import install from './PluginManagement/install.js'
3 | import register from './PluginManagement/register.js'
4 | import rename from './PluginManagement/rename.js'
5 | import schemas from './PluginManagement/schemas.js'
6 | import search from './PluginManagement/search.js'
7 | import uninstall from './PluginManagement/uninstall.js'
8 | import unregister from './PluginManagement/unregister.js'
9 | import update from './PluginManagement/update.js'
10 |
11 | export {
12 | authenticate,
13 | install,
14 | register,
15 | rename,
16 | schemas,
17 | search,
18 | uninstall,
19 | unregister,
20 | update
21 | }
22 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/autenticate.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import inquirer from 'inquirer'
3 | import fetch from 'node-fetch'
4 | import getBowerRegistryConfig from '../getBowerRegistryConfig.js'
5 | import path from 'path'
6 |
7 | export default async function authenticate ({
8 | pluginName,
9 | cwd = process.cwd()
10 | } = {}) {
11 | cwd = path.resolve(process.cwd(), cwd)
12 | const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd })
13 | // check if github, do github device oauth workflow
14 | // if not github, send request to repo anyway
15 | const questions = [
16 | {
17 | name: 'username',
18 | message: chalk.cyan('GitHub username')
19 | },
20 | {
21 | name: 'token',
22 | message: chalk.cyan('GitHub personal access token (with public_repo access)'),
23 | type: 'password',
24 | mask: '*'
25 | }
26 | ]
27 | if (!pluginName) {
28 | questions.unshift({
29 | name: 'pluginName',
30 | message: chalk.cyan('Plugin name'),
31 | default: pluginName
32 | })
33 | }
34 | const confirmation = await inquirer.prompt(questions)
35 | if (!pluginName) {
36 | ({ pluginName } = confirmation)
37 | }
38 | const { username, token } = confirmation
39 | const response = await fetch(`${BOWER_REGISTRY_CONFIG.register}authenticate/${username}/${pluginName}?access_token=${token}`, {
40 | headers: { 'User-Agent': 'adapt-cli' },
41 | followRedirect: false,
42 | method: 'GET'
43 | })
44 | if (response.status !== 200) throw new Error(`The server responded with ${response.status}`)
45 | const body = await response.json()
46 | return { username, token, pluginName, ...body }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/install.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { eachOfSeries } from 'async'
3 | import { createPromptTask } from '../../util/createPromptTask.js'
4 | import { errorPrinter, packageNamePrinter, versionPrinter } from './print.js'
5 | import { eachOfLimitProgress, eachOfSeriesProgress } from '../../util/promises.js'
6 | import Project from '../Project.js'
7 | import Target from '../Target.js'
8 | import bower from 'bower'
9 | import { difference } from 'lodash-es'
10 | import path from 'path'
11 |
12 | export default async function install ({
13 | plugins,
14 | dev = false,
15 | isInteractive = true,
16 | isDryRun = false, // whether to summarise installation without modifying anything
17 | isCompatibleEnabled = false,
18 | isClean = false,
19 | cwd = process.cwd(),
20 | logger = null
21 | }) {
22 | cwd = path.resolve(process.cwd(), cwd)
23 | isClean && await new Promise(resolve => bower.commands.cache.clean().on('end', resolve))
24 | const project = new Project({ cwd, logger })
25 | project.tryThrowInvalidPath()
26 |
27 | logger?.log(chalk.cyan(`${dev ? 'cloning' : 'installing'} adapt dependencies...`))
28 |
29 | const targets = await getInstallTargets({ logger, project, plugins, isCompatibleEnabled })
30 | if (!targets?.length) return targets
31 |
32 | await loadPluginData({ logger, project, targets })
33 | await conflictResolution({ logger, targets, isInteractive, dev })
34 | if (isDryRun) {
35 | await summariseDryRun({ logger, targets })
36 | return targets
37 | }
38 | const installTargetsToBeInstalled = targets.filter(target => target.isToBeInstalled)
39 | if (installTargetsToBeInstalled.length) {
40 | await eachOfSeriesProgress(
41 | installTargetsToBeInstalled,
42 | target => target.install({ clone: dev }),
43 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Installing plugins ${percentage}% complete`)
44 | )
45 | logger?.log(`${chalk.bold.cyan('')} Installing plugins 100% complete`)
46 | const manifestDependencies = await project.getManifestDependencies()
47 | await updateManifest({ logger, project, targets, manifestDependencies, isInteractive })
48 | }
49 | await summariseInstallation({ logger, targets, dev })
50 | return targets
51 | }
52 |
53 | /**
54 | * @param {Object} options
55 | * @param {Project} options.project
56 | * @param {[string]} options.plugins
57 | */
58 | async function getInstallTargets ({ logger, project, plugins, isCompatibleEnabled }) {
59 | if (typeof plugins === 'string') plugins = [plugins]
60 | /** whether adapt.json is being used to compile the list of plugins to install */
61 | const isEmpty = !plugins?.length
62 | /** a list of plugin name/version pairs */
63 | const itinerary = isEmpty
64 | ? await project.getManifestDependencies()
65 | : plugins.reduce((itinerary, arg) => {
66 | const [name, version = '*'] = arg.split(/[#@]/)
67 | // Duplicates are removed by assigning to object properties
68 | itinerary[name] = version
69 | return itinerary
70 | }, {})
71 | const pluginNames = Object.entries(itinerary).map(([name, version]) => `${name}#${version}`)
72 |
73 | /**
74 | * @type {[Target]}
75 | */
76 | const targets = pluginNames.length
77 | ? pluginNames.map(nameVersion => {
78 | const [name, requestedVersion] = nameVersion.split(/[#@]/)
79 | return new Target({ name, requestedVersion, isCompatibleEnabled, project, logger })
80 | })
81 | : await project.getInstallTargets()
82 | return targets
83 | }
84 |
85 | /**
86 | * @param {Object} options
87 | * @param {Project} options.project
88 | * @param {[Target]} options.targets
89 | */
90 | async function loadPluginData ({ logger, project, targets }) {
91 | const frameworkVersion = project.version
92 | await eachOfLimitProgress(
93 | targets,
94 | target => target.fetchSourceInfo(),
95 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Getting plugin info ${percentage}% complete`)
96 | )
97 | logger?.log(`${chalk.bold.cyan('')} Getting plugin info 100% complete`)
98 | await eachOfLimitProgress(
99 | targets,
100 | target => target.findCompatibleVersion(frameworkVersion),
101 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Finding compatible source versions ${percentage}% complete`)
102 | )
103 | logger?.log(`${chalk.bold.cyan('')} Finding compatible source versions 100% complete`)
104 | await eachOfLimitProgress(
105 | targets,
106 | target => target.markInstallable(),
107 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Marking installable ${percentage}% complete`)
108 | )
109 | logger?.log(`${chalk.bold.cyan('')} Marking installable 100% complete`)
110 | }
111 |
112 | /**
113 | * @param {Object} options
114 | * @param {[Target]} options.targets
115 | */
116 | async function conflictResolution ({ logger, targets, isInteractive, dev }) {
117 | /** @param {Target} target */
118 | async function checkVersion (target) {
119 | const canApplyRequested = target.hasValidRequestVersion &&
120 | (target.hasFrameworkCompatibleVersion
121 | ? (target.latestCompatibleSourceVersion !== target.matchedVersion)
122 | : (target.latestSourceVersion !== target.matchedVersion))
123 | if (!isInteractive) {
124 | if (canApplyRequested) return target.markRequestedForInstallation()
125 | return target.markSkipped()
126 | }
127 | const choices = [
128 | dev && { name: 'master [master]', value: 'm' },
129 | canApplyRequested && { name: `requested version [${target.matchedVersion}]`, value: 'r' },
130 | target.hasFrameworkCompatibleVersion
131 | ? { name: `latest compatible version [${target.latestCompatibleSourceVersion}]`, value: 'l' }
132 | : target.latestSourceVersion
133 | ? { name: `latest version [${target.latestSourceVersion}]`, value: 'l' }
134 | : { name: 'master [master]', value: 'm' },
135 | { name: 'skip', value: 's' }
136 | ].filter(Boolean)
137 | const result = await createPromptTask({ message: chalk.reset(target.packageName), choices, type: 'list', default: 's' })
138 | const installMasterBranch = (result === 'm')
139 | const installRequested = (result === 'r')
140 | const installLatest = result === 'l'
141 | const skipped = result === 's'
142 | if (installMasterBranch) target.markMasterForInstallation()
143 | if (installRequested) target.markRequestedForInstallation()
144 | if (installLatest && target.hasFrameworkCompatibleVersion) target.markLatestCompatibleForInstallation()
145 | if (installLatest && !target.hasFrameworkCompatibleVersion) target.markLatestForInstallation()
146 | if (skipped) target.markSkipped()
147 | }
148 | function add (list, header, prompt) {
149 | if (!list.length) return
150 | return {
151 | header: chalk.cyan(' ') + header,
152 | list,
153 | prompt
154 | }
155 | }
156 | const allQuestions = [
157 | add(targets.filter(target => !target.hasFrameworkCompatibleVersion), 'There is no compatible version of the following plugins:', checkVersion),
158 | add(targets.filter(target => target.hasFrameworkCompatibleVersion && !target.hasValidRequestVersion), 'The version requested is invalid, there are newer compatible versions of the following plugins:', checkVersion),
159 | add(targets.filter(target => target.hasFrameworkCompatibleVersion && target.hasValidRequestVersion && !target.isApplyLatestCompatibleVersion), 'There are newer compatible versions of the following plugins:', checkVersion)
160 | ].filter(Boolean)
161 | if (allQuestions.length === 0) return
162 | for (const question of allQuestions) {
163 | logger?.log(question.header)
164 | await eachOfSeries(question.list, question.prompt)
165 | }
166 | }
167 |
168 | /**
169 | * @param {Object} options
170 | * @param {Project} options.project
171 | * @param {[Target]} options.targets
172 | */
173 | async function updateManifest ({ project, targets, manifestDependencies, isInteractive }) {
174 | if (targets.filter(target => target.isInstallSuccessful).length === 0) return
175 | if (difference(targets.filter(target => target.isInstallSuccessful).map(target => target.packageName), Object.keys(manifestDependencies)).length === 0) return
176 | if (isInteractive) {
177 | const shouldUpdate = await createPromptTask({
178 | message: chalk.white('Update the manifest (adapt.json)?'),
179 | type: 'confirm',
180 | default: true
181 | })
182 | if (!shouldUpdate) return
183 | }
184 | targets.forEach(target => target.isInstallSuccessful && project.add(target))
185 | }
186 |
187 | /**
188 | * @param {Object} options
189 | * @param {[Target]} options.targets
190 | */
191 | function summariseDryRun ({ logger, targets }) {
192 | const toBeInstalled = targets.filter(target => target.isToBeInstalled)
193 | const toBeSkipped = targets.filter(target => !target.isToBeInstalled || target.isSkipped)
194 | const missing = targets.filter(target => target.isMissing)
195 | summarise(logger, toBeSkipped, packageNamePrinter, 'The following plugins will be skipped:')
196 | summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
197 | summarise(logger, toBeInstalled, versionPrinter, 'The following plugins will be installed:')
198 | }
199 |
200 | /**
201 | * @param {Object} options
202 | * @param {[Target]} options.targets
203 | */
204 | function summariseInstallation ({ logger, targets, dev }) {
205 | const installSucceeded = targets.filter(target => target.isInstallSuccessful)
206 | const installSkipped = targets.filter(target => !target.isToBeInstalled || target.isSkipped)
207 | const installErrored = targets.filter(target => target.isInstallFailure)
208 | const missing = targets.filter(target => target.isMissing)
209 | const noneInstalled = (installSucceeded.length === 0)
210 | const allInstalledSuccessfully = (installErrored.length === 0 && missing.length === 0)
211 | const someInstalledSuccessfully = (!noneInstalled && !allInstalledSuccessfully)
212 | summarise(logger, installSkipped, packageNamePrinter, 'The following plugins were skipped:')
213 | summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
214 | summarise(logger, installErrored, errorPrinter, 'The following plugins could not be installed:')
215 | if (noneInstalled) logger?.log(chalk.cyanBright('None of the requested plugins could be installed'))
216 | else if (allInstalledSuccessfully) summarise(logger, installSucceeded, dev ? packageNamePrinter : versionPrinter, 'All requested plugins were successfully installed. Summary of installation:')
217 | else if (someInstalledSuccessfully) summarise(logger, installSucceeded, dev ? packageNamePrinter : versionPrinter, 'The following plugins were successfully installed:')
218 | }
219 |
220 | function summarise (logger, list, iterator, header) {
221 | if (!list || !iterator || list.length === 0) return
222 | logger?.log(chalk.cyanBright(header))
223 | list.forEach(item => iterator(item, logger))
224 | }
225 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/print.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import semver from 'semver'
3 | import { ADAPT_ALLOW_PRERELEASE } from '../../util/constants.js'
4 | const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
5 |
6 | function highlight (pluginname) {
7 | return ['adapt-contrib', 'adapt-'].reduce((output, prefix) => {
8 | if (output || !pluginname.startsWith(prefix)) return output
9 | return chalk.reset(prefix) + chalk.yellowBright(pluginname.substring(prefix.length))
10 | }, null) || pluginname
11 | }
12 |
13 | function greenIfEqual (v1, v2) {
14 | if (v1 === '*') return chalk.greenBright(v2)
15 | return semver.satisfies(v1, v2, semverOptions)
16 | ? chalk.greenBright(v2)
17 | : chalk.magentaBright(v2)
18 | }
19 |
20 | export function versionPrinter (plugin, logger) {
21 | const {
22 | versionToApply,
23 | latestCompatibleSourceVersion
24 | } = plugin
25 | logger?.log(highlight(plugin.packageName), latestCompatibleSourceVersion === null
26 | ? '(no version information)'
27 | : `${chalk.greenBright(versionToApply)}${plugin.isLocalSource ? ' (local)' : ` (latest compatible version is ${greenIfEqual(versionToApply, latestCompatibleSourceVersion)})`}`
28 | )
29 | }
30 |
31 | export function existingVersionPrinter (plugin, logger) {
32 | const {
33 | preUpdateProjectVersion,
34 | projectVersion,
35 | latestCompatibleSourceVersion
36 | } = plugin
37 | const fromTo = preUpdateProjectVersion !== null
38 | ? `from ${chalk.greenBright(preUpdateProjectVersion)} to ${chalk.greenBright(projectVersion)}`
39 | : `${chalk.greenBright(projectVersion)}`
40 | logger?.log(highlight(plugin.packageName), latestCompatibleSourceVersion === null
41 | ? fromTo
42 | : `${fromTo}${plugin.isLocalSource ? ' (local)' : ` (latest compatible version is ${greenIfEqual(projectVersion, latestCompatibleSourceVersion)})`}`
43 | )
44 | }
45 |
46 | export function errorPrinter (plugin, logger) {
47 | logger?.log(highlight(plugin.packageName), plugin.installError ? '(error: ' + plugin.installError + ')' : '(unknown error)')
48 | }
49 |
50 | export function packageNamePrinter (plugin, logger) {
51 | logger?.log(highlight(plugin.packageName))
52 | }
53 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/register.js:
--------------------------------------------------------------------------------
1 |
2 | import getBowerRegistryConfig from '../getBowerRegistryConfig.js'
3 | import fs from 'fs-extra'
4 | import path from 'path'
5 | import bower from 'bower'
6 | import fetch from 'node-fetch'
7 | import chalk from 'chalk'
8 | import inquirer from 'inquirer'
9 | import { readValidateJSON } from '../../util/JSONReadValidate.js'
10 | import Plugin from '../Plugin.js'
11 | import semver from 'semver'
12 | import { ADAPT_ALLOW_PRERELEASE } from '../../util/constants.js'
13 | const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
14 |
15 | export default async function register ({
16 | logger,
17 | cwd = process.cwd()
18 | } = {}) {
19 | cwd = path.resolve(process.cwd(), cwd)
20 | const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd })
21 | logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register)
22 | try {
23 | const bowerJSONPath = path.join(cwd, 'bower.json')
24 | const hasBowerJSON = fs.existsSync(bowerJSONPath)
25 |
26 | const bowerJSON = {
27 | name: undefined,
28 | repository: undefined,
29 | framework: undefined,
30 | ...(hasBowerJSON ? await readValidateJSON(bowerJSONPath) : {})
31 | }
32 | const properties = await confirm(bowerJSON)
33 | hasBowerJSON && await fs.writeJSON(bowerJSONPath, properties, { spaces: 2, replacer: null })
34 |
35 | // given a package name, create two Plugin representations
36 | // if supplied name is adapt-contrib-myPackageName do a check against this name only
37 | // if suppled name is adapt-myPackageName check against this name and adapt-contrib-myPackageName
38 | // becase we don't want to allow adapt-myPackageName if adapt-contrib-myPackageName exists
39 | const plugin = new Plugin({ name: properties.name, logger })
40 | const contribPlugin = new Plugin({ name: properties.name, isContrib: true, logger })
41 | const contribExists = await exists(BOWER_REGISTRY_CONFIG, contribPlugin)
42 | const pluginExists = await exists(BOWER_REGISTRY_CONFIG, plugin)
43 |
44 | if (contribExists || pluginExists) {
45 | logger?.warn(plugin.toString(), 'has been previously registered. Plugin names must be unique. Try again with a different name.')
46 | return
47 | }
48 |
49 | const result = await registerWithBowerRepo(BOWER_REGISTRY_CONFIG, plugin, properties.repository)
50 | if (!result) throw new Error('The plugin was unable to register.')
51 | logger?.log(chalk.green(plugin.packageName), 'has been registered successfully.')
52 | } catch (err) {
53 | logger?.error(err)
54 | }
55 | }
56 |
57 | async function confirm (properties) {
58 | const plugin = new Plugin({ name: properties.name })
59 | const schema = [
60 | {
61 | name: 'name',
62 | message: chalk.cyan('name'),
63 | validate: v => {
64 | return /^adapt-[\w|-]+?$/.test(v) ||
65 | 'Name must prefixed with \'adapt\' and each word separated with a hyphen(-)'
66 | },
67 | type: 'input',
68 | default: plugin.toString() || 'not specified'
69 | },
70 | {
71 | name: 'repositoryUrl',
72 | message: chalk.cyan('repository URL'),
73 | validate: v => {
74 | return /https:\/\/([\w.@:/\-~]+)(\.git)(\/)?/.test(v) ||
75 | 'Please provide a repository URL of the form https://.git'
76 | },
77 | type: 'input',
78 | default: properties.repository ? properties.repository.url : undefined
79 | },
80 | {
81 | name: 'framework',
82 | message: chalk.cyan('framework'),
83 | validate: v => {
84 | return semver.validRange(v, semverOptions) !== null ||
85 | 'Please provide a valid semver (see https://semver.org/)'
86 | },
87 | type: 'input',
88 | default: properties.framework || '>=5.15'
89 | },
90 | {
91 | name: 'ready',
92 | message: chalk.cyan('Register now?'),
93 | type: 'confirm',
94 | default: true
95 | }
96 | ]
97 | const confirmation = await inquirer.prompt(schema)
98 | if (!confirmation.ready) throw new Error('Aborted. Nothing has been registered.')
99 | properties.name = confirmation.name
100 | properties.repository = { type: 'git', url: confirmation.repositoryUrl }
101 | properties.framework = confirmation.framework
102 | return properties
103 | }
104 |
105 | /**
106 | * @param {Plugin} plugin
107 | * @returns {boolean}
108 | */
109 | async function exists (BOWER_REGISTRY_CONFIG, plugin) {
110 | const pluginName = plugin.toString().toLowerCase()
111 | return new Promise((resolve, reject) => {
112 | bower.commands.search(pluginName, {
113 | registry: BOWER_REGISTRY_CONFIG.register
114 | })
115 | .on('end', result => {
116 | const matches = result.filter(({ name }) => name.toLowerCase() === pluginName)
117 | resolve(Boolean(matches.length))
118 | })
119 | .on('error', reject)
120 | })
121 | }
122 |
123 | async function registerWithBowerRepo (BOWER_REGISTRY_CONFIG, plugin, repository) {
124 | const data = {
125 | name: plugin.toString(),
126 | url: repository.url || repository
127 | }
128 | const response = await fetch(`${BOWER_REGISTRY_CONFIG.register}packages`, {
129 | headers: {
130 | 'User-Agent': 'adapt-cli',
131 | 'Content-Type': 'application/json'
132 | },
133 | followRedirect: false,
134 | method: 'POST',
135 | body: JSON.stringify(data)
136 | })
137 | return (response?.status === 201)
138 | }
139 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/rename.js:
--------------------------------------------------------------------------------
1 | import getBowerRegistryConfig from '../getBowerRegistryConfig.js'
2 | import authenticate from './autenticate.js'
3 | import bower from 'bower'
4 | import chalk from 'chalk'
5 | import inquirer from 'inquirer'
6 | import fetch from 'node-fetch'
7 | import path from 'path'
8 | import Plugin from '../Plugin.js'
9 |
10 | export default async function rename ({
11 | logger,
12 | oldName,
13 | newName,
14 | cwd = process.cwd()
15 | } = {}) {
16 | cwd = path.resolve(process.cwd(), cwd)
17 | const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd })
18 | if (!oldName || !newName) {
19 | logger?.error('You must call rename with the following arguments: ')
20 | return
21 | }
22 | // use Plugin to standardise name
23 | newName = new Plugin({ name: newName, logger }).packageName
24 | oldName = new Plugin({ name: oldName, logger }).packageName
25 | logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register)
26 | logger?.warn(`Plugin will be renamed from ${oldName} to ${newName}`)
27 | try {
28 | const oldExists = await exists(BOWER_REGISTRY_CONFIG, oldName)
29 | if (!oldExists) throw new Error(`Plugin "${oldName}" does not exist`)
30 | const newExists = await exists(BOWER_REGISTRY_CONFIG, newName)
31 | if (newExists) throw new Error(`Name "${newName}" already exists`)
32 | const { username, token, type } = await authenticate({ pluginName: oldName })
33 | logger?.log(`${username} authenticated as ${type}`)
34 | await confirm()
35 | await renameInBowerRepo({
36 | username,
37 | token,
38 | oldName,
39 | newName,
40 | BOWER_REGISTRY_CONFIG
41 | })
42 | logger?.log(chalk.green('The plugin was successfully renamed.'))
43 | } catch (err) {
44 | logger?.error(err)
45 | logger?.error('The plugin was not renamed.')
46 | }
47 | }
48 |
49 | async function confirm () {
50 | const schema = [
51 | {
52 | name: 'ready',
53 | message: chalk.cyan('Confirm rename now?'),
54 | type: 'confirm',
55 | default: true
56 | }
57 | ]
58 | const confirmation = await inquirer.prompt(schema)
59 | if (!confirmation.ready) throw new Error('Aborted. Nothing has been renamed.')
60 | }
61 |
62 | async function renameInBowerRepo ({
63 | username,
64 | token,
65 | oldName,
66 | newName,
67 | BOWER_REGISTRY_CONFIG
68 | }) {
69 | const path = 'packages/rename/' + username + '/' + oldName + '/' + newName
70 | const query = '?access_token=' + token
71 | const response = await fetch(BOWER_REGISTRY_CONFIG.register + path + query, {
72 | method: 'GET',
73 | headers: { 'User-Agent': 'adapt-cli' },
74 | followRedirect: false
75 | })
76 | if (response.status !== 201) throw new Error(`The server responded with ${response.status}`)
77 | }
78 |
79 | /**
80 | * @param {Plugin} plugin
81 | * @returns {boolean}
82 | */
83 | async function exists (BOWER_REGISTRY_CONFIG, plugin) {
84 | const pluginName = plugin.toString().toLowerCase()
85 | return new Promise((resolve, reject) => {
86 | bower.commands.search(pluginName, {
87 | registry: BOWER_REGISTRY_CONFIG.register
88 | })
89 | .on('end', result => {
90 | const matches = result.filter(({ name }) => name.toLowerCase() === pluginName)
91 | resolve(Boolean(matches.length))
92 | })
93 | .on('error', reject)
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/schemas.js:
--------------------------------------------------------------------------------
1 | import Project from '../Project.js'
2 | import path from 'path'
3 |
4 | export default async function schemas ({ cwd = process.cwd() } = {}) {
5 | cwd = path.resolve(process.cwd(), cwd)
6 | const project = new Project({ cwd })
7 | return await project.getSchemaPaths({ cwd })
8 | }
9 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/search.js:
--------------------------------------------------------------------------------
1 | import getBowerRegistryConfig from '../getBowerRegistryConfig.js'
2 | import chalk from 'chalk'
3 | import fetch from 'node-fetch'
4 | import path from 'path'
5 |
6 | export default async function search ({
7 | logger,
8 | searchTerm,
9 | cwd = process.cwd()
10 | } = {}) {
11 | cwd = path.resolve(process.cwd(), cwd)
12 | const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd })
13 | try {
14 | const uniqueResults = {}
15 | for (const serverURI of BOWER_REGISTRY_CONFIG.search) {
16 | try {
17 | const response = await fetch(`${serverURI}packages/search/${searchTerm}`, {
18 | method: 'GET',
19 | headers: { 'User-Agent': 'adapt-cli' },
20 | followRedirect: false
21 | })
22 | if (response.status !== 200) throw new Error(`The server responded with ${response.status}`)
23 | const immediateResults = await response.json()
24 | immediateResults?.forEach(result => (uniqueResults[result.name] = uniqueResults[result.name] ?? result))
25 | } catch (err) {}
26 | }
27 | const results = Object.values(uniqueResults)
28 | if (!results.length) {
29 | logger?.warn(`no plugins found containing: ${searchTerm}`)
30 | }
31 | results.forEach(function (result) {
32 | logger?.log(chalk.cyan(result.name) + ' ' + result.url)
33 | })
34 | } catch (err) {
35 | logger?.error("Oh dear, something went wrong. I'm terribly sorry.", err)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/uninstall.js:
--------------------------------------------------------------------------------
1 |
2 | import chalk from 'chalk'
3 | import Project from '../Project.js'
4 | import Target from '../Target.js'
5 | import { eachOfLimitProgress } from '../../util/promises.js'
6 | import { createPromptTask } from '../../util/createPromptTask.js'
7 | import { errorPrinter, packageNamePrinter } from './print.js'
8 | import { intersection } from 'lodash-es'
9 | import path from 'path'
10 |
11 | export default async function uninstall ({
12 | plugins,
13 | isInteractive = true,
14 | cwd = process.cwd(),
15 | logger = null
16 | }) {
17 | cwd = path.resolve(process.cwd(), cwd)
18 | const project = new Project({ cwd, logger })
19 | project.tryThrowInvalidPath()
20 |
21 | logger?.log(chalk.cyan('uninstalling adapt dependencies...'))
22 |
23 | const targets = await getUninstallTargets({ logger, project, plugins, isInteractive })
24 | if (!targets?.length) return targets
25 |
26 | await loadPluginData({ logger, targets })
27 | await eachOfLimitProgress(
28 | targets.filter(target => target.isToBeUninstalled),
29 | target => target.uninstall(),
30 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Uninstalling plugins ${percentage}% complete`)
31 | )
32 | logger?.log(`${chalk.bold.cyan('')} Uninstalling plugins 100% complete`)
33 | const installedDependencies = await project.getInstalledDependencies()
34 | await updateManifest({ project, targets, installedDependencies, isInteractive })
35 | await summariseUninstallation({ logger, targets })
36 | return targets
37 | }
38 |
39 | /**
40 | * @param {Object} options
41 | * @param {Project} options.project
42 | * @param {[Target]} options.targets
43 | */
44 | async function getUninstallTargets ({ logger, project, plugins, isInteractive }) {
45 | if (typeof plugins === 'string') plugins = [plugins]
46 | /** whether adapt.json is being used to compile the list of targets to install */
47 | const isEmpty = !plugins?.length
48 | if (isEmpty && isInteractive) {
49 | const shouldContinue = await createPromptTask({
50 | message: chalk.reset('This command will attempt to uninstall all installed plugins. Do you wish to continue?'),
51 | type: 'confirm'
52 | })
53 | if (!shouldContinue) return
54 | }
55 |
56 | /** a list of plugin name/version pairs */
57 | const itinerary = isEmpty
58 | ? await project.getInstalledDependencies()
59 | : plugins.reduce((itinerary, arg) => {
60 | const [name, version = '*'] = arg.split(/[#@]/)
61 | // Duplicates are removed by assigning to object properties
62 | itinerary[name] = version
63 | return itinerary
64 | }, {})
65 | const pluginNames = Object.entries(itinerary).map(([name, version]) => `${name}@${version}`)
66 |
67 | /** @type {[Target]} */
68 | const targets = pluginNames
69 | ? pluginNames.map(nameVersion => {
70 | const [name] = nameVersion.split(/[#@]/)
71 | return new Target({ name, project, logger })
72 | })
73 | : await project.getUninstallTargets()
74 | return targets
75 | }
76 |
77 | /**
78 | * @param {Object} options
79 | * @param {Project} options.project
80 | * @param {[Target]} options.targets
81 | */
82 | async function loadPluginData ({ logger, targets }) {
83 | await eachOfLimitProgress(
84 | targets,
85 | target => target.fetchProjectInfo(),
86 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Getting plugin info ${percentage}% complete`)
87 | )
88 | logger?.log(`${chalk.bold.cyan('')} Getting plugin info 100% complete`)
89 | await eachOfLimitProgress(
90 | targets,
91 | target => target.markUninstallable(),
92 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Marking uninstallable ${percentage}% complete`)
93 | )
94 | logger?.log(`${chalk.bold.cyan('')} Marking uninstallable 100% complete`)
95 | }
96 |
97 | /**
98 | * @param {Object} options
99 | * @param {Project} options.project
100 | * @param {[Target]} options.targets
101 | * @returns
102 | */
103 | async function updateManifest ({ project, targets, installedDependencies, isInteractive }) {
104 | if (targets.filter(target => target.isToBeUninstalled).length === 0) return
105 | if (intersection(Object.keys(installedDependencies), targets.map(target => target.packageName)).length) return
106 | if (isInteractive) {
107 | const shouldUpdate = await createPromptTask({
108 | message: chalk.white('Update the manifest (adapt.json)?'),
109 | type: 'confirm',
110 | default: true
111 | })
112 | if (!shouldUpdate) return
113 | }
114 | targets.forEach(target => target.isToBeUninstalled && project.remove(target))
115 | }
116 |
117 | /**
118 | * @param {Object} options
119 | * @param {[Target]} options.targets
120 | */
121 | function summariseUninstallation ({ logger, targets }) {
122 | const uninstallSucceeded = targets.filter(target => target.isUninstallSuccessful)
123 | const uninstallSkipped = targets.filter(target => !target.isToBeUninstalled || target.isSkipped)
124 | const uninstallErrored = targets.filter(target => target.isUninstallFailure)
125 | const missing = targets.filter(target => target.isMissing)
126 | const noneUninstalled = (uninstallSucceeded.length === 0)
127 | const allUninstalledSuccessfully = (uninstallErrored.length === 0 && missing.length === 0)
128 | const someUninstalledSuccessfully = (!noneUninstalled && !allUninstalledSuccessfully)
129 | summarise(logger, uninstallSkipped, packageNamePrinter, 'The following plugins were skipped:')
130 | summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
131 | summarise(logger, uninstallErrored, errorPrinter, 'The following plugins could not be uninstalled:')
132 | if (noneUninstalled) logger?.log(chalk.cyanBright('None of the requested plugins could be uninstalled'))
133 | else if (allUninstalledSuccessfully) summarise(logger, uninstallSucceeded, packageNamePrinter, 'All requested plugins were successfully uninstalled. Summary of uninstallation:')
134 | else if (someUninstalledSuccessfully) summarise(logger, uninstallSucceeded, packageNamePrinter, 'The following plugins were successfully uninstalled:')
135 | }
136 |
137 | function summarise (logger, list, iterator, header) {
138 | if (!list || !iterator || list.length === 0) return
139 | logger?.log(chalk.cyanBright(header))
140 | list.forEach(item => iterator(item, logger))
141 | }
142 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/unregister.js:
--------------------------------------------------------------------------------
1 | import getBowerRegistryConfig from '../getBowerRegistryConfig.js'
2 | import authenticate from './autenticate.js'
3 | import fs from 'fs-extra'
4 | import path from 'path'
5 | import chalk from 'chalk'
6 | import inquirer from 'inquirer'
7 | import { readValidateJSON } from '../../util/JSONReadValidate.js'
8 | import Plugin from '../Plugin.js'
9 | import fetch from 'node-fetch'
10 |
11 | export default async function unregister ({
12 | logger,
13 | cwd = process.cwd(),
14 | pluginName
15 | } = {}) {
16 | cwd = path.resolve(process.cwd(), cwd)
17 | const BOWER_REGISTRY_CONFIG = getBowerRegistryConfig({ cwd })
18 | logger?.warn('Using registry at', BOWER_REGISTRY_CONFIG.register)
19 | try {
20 | const bowerJSONPath = path.join(cwd, 'bower.json')
21 | const hasBowerJSON = fs.existsSync(bowerJSONPath)
22 | const bowerJSON = hasBowerJSON ? await readValidateJSON(bowerJSONPath) : {}
23 | if (pluginName) bowerJSON.name = pluginName
24 | const props = await confirm(bowerJSON)
25 | pluginName = props.pluginName
26 | const repository = props.repository
27 | const { username, token, type } = await authenticate({ repository, pluginName })
28 | logger?.log(`${username} authenticated as ${type}`)
29 | await finalConfirm()
30 | await unregisterInBowerRepo({ pluginName, username, token, BOWER_REGISTRY_CONFIG })
31 | logger?.log(chalk.green('The plugin was successfully unregistered.'))
32 | } catch (err) {
33 | logger?.error(err)
34 | logger?.log('The plugin was not unregistered.')
35 | }
36 | }
37 |
38 | async function confirm (properties) {
39 | const plugin = new Plugin({ name: properties.name })
40 | const schema = [
41 | {
42 | name: 'pluginName',
43 | message: chalk.cyan('name'),
44 | validate: v => {
45 | return /^adapt-[\w|-]+?$/.test(v) ||
46 | 'Name must prefixed with \'adapt\' and each word separated with a hyphen(-)'
47 | },
48 | type: 'input',
49 | default: plugin.toString() || 'not specified'
50 | },
51 | {
52 | name: 'repository',
53 | message: chalk.cyan('repository URL'),
54 | validate: v => {
55 | return /https:\/\/([\w.@:/\-~]+)(\.git)(\/)?/.test(v) ||
56 | 'Please provide a repository URL of the form https://.git'
57 | },
58 | type: 'input',
59 | default: properties.repository ? properties.repository.url : undefined
60 | }
61 | ]
62 | return await inquirer.prompt(schema)
63 | }
64 |
65 | async function finalConfirm () {
66 | const schema = [
67 | {
68 | name: 'ready',
69 | message: chalk.cyan('Confirm Unregister now?'),
70 | type: 'confirm',
71 | default: true
72 | }
73 | ]
74 | const confirmation = await inquirer.prompt(schema)
75 | if (!confirmation.ready) throw new Error('Aborted. Nothing has been unregistered.')
76 | }
77 |
78 | async function unregisterInBowerRepo ({
79 | pluginName,
80 | username,
81 | token,
82 | BOWER_REGISTRY_CONFIG
83 | }) {
84 | const uri = `${BOWER_REGISTRY_CONFIG.register}packages/${username}/${pluginName}?access_token=${token}`
85 | const response = await fetch(uri, {
86 | method: 'DELETE',
87 | headers: { 'User-Agent': 'adapt-cli' },
88 | followRedirect: false
89 | })
90 | if (response.status !== 204) throw new Error(`The server responded with ${response.status}`)
91 | }
92 |
--------------------------------------------------------------------------------
/lib/integration/PluginManagement/update.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { eachOfSeries } from 'async'
3 | import path from 'path'
4 | import Project from '../Project.js'
5 | import { createPromptTask } from '../../util/createPromptTask.js'
6 | import { errorPrinter, packageNamePrinter, existingVersionPrinter } from './print.js'
7 | import { eachOfLimitProgress, eachOfSeriesProgress } from '../../util/promises.js'
8 | /** @typedef {import("../Target.js").default} Target */
9 |
10 | export default async function update ({
11 | plugins,
12 | // whether to summarise installed plugins without modifying anything
13 | isDryRun = false,
14 | isInteractive = true,
15 | cwd = process.cwd(),
16 | logger = null
17 | }) {
18 | cwd = path.resolve(process.cwd(), cwd)
19 | const project = new Project({ cwd, logger })
20 | project.tryThrowInvalidPath()
21 |
22 | logger?.log(chalk.cyan('update adapt dependencies...'))
23 |
24 | const targets = await getUpdateTargets({ logger, project, plugins, isDryRun, isInteractive })
25 | if (!targets?.length) return targets
26 |
27 | await loadPluginData({ logger, project, targets })
28 | await conflictResolution({ logger, targets, isInteractive })
29 | if (isDryRun) {
30 | await summariseDryRun({ logger, targets })
31 | return targets
32 | }
33 | const updateTargetsToBeUpdated = targets.filter(target => target.isToBeInstalled)
34 | if (updateTargetsToBeUpdated.length) {
35 | await eachOfSeriesProgress(
36 | updateTargetsToBeUpdated,
37 | target => target.update(),
38 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Updating plugins ${percentage}% complete`)
39 | )
40 | logger?.log(`${chalk.bold.cyan('')} Updating plugins 100% complete`)
41 | }
42 | await summariseUpdates({ logger, targets })
43 | return targets
44 | }
45 |
46 | /**
47 | * @param {Object} options
48 | * @param {Project} options.project
49 | * @param {[string]} options.plugins
50 | */
51 | async function getUpdateTargets ({ project, plugins, isDryRun, isInteractive }) {
52 | if (typeof plugins === 'string') plugins = [plugins]
53 | if (!plugins) plugins = []
54 | const allowedTypes = ['all', 'components', 'extensions', 'menu', 'theme']
55 | const selectedTypes = [...new Set(plugins.filter(type => allowedTypes.includes(type)))]
56 | const isEmpty = (!plugins.length)
57 | const isAll = (isDryRun || isEmpty || selectedTypes.includes('all'))
58 | const pluginNames = plugins
59 | // remove types
60 | .filter(arg => !allowedTypes.includes(arg))
61 | // split name/version
62 | .map(arg => {
63 | const [name, version = '*'] = arg.split(/[#@]/)
64 | return [name, version]
65 | })
66 | // make sure last applies
67 | .reverse()
68 |
69 | /** @type {[Target]} */
70 | let targets = await project.getUpdateTargets()
71 | for (const target of targets) {
72 | await target.fetchProjectInfo()
73 | }
74 | if (!isDryRun && isEmpty && isInteractive) {
75 | const shouldContinue = await createPromptTask({
76 | message: chalk.reset('This command will attempt to update all installed plugins. Do you wish to continue?'),
77 | type: 'confirm'
78 | })
79 | if (!shouldContinue) return
80 | }
81 | if (!isAll) {
82 | const filtered = {}
83 | for (const target of targets) {
84 | const typeFolder = await target.getTypeFolder()
85 | if (!typeFolder) continue
86 | const lastSpecifiedPluginName = pluginNames.find(([name]) => target.isNameMatch(name))
87 | const isPluginNameIncluded = Boolean(lastSpecifiedPluginName)
88 | const isTypeIncluded = selectedTypes.includes(typeFolder)
89 | if (!isPluginNameIncluded && !isTypeIncluded) continue
90 | // Resolve duplicates
91 | filtered[target.packageName] = target
92 | // Set requested version from name
93 | target.requestedVersion = lastSpecifiedPluginName[1] || '*'
94 | }
95 | targets = Object.values(filtered)
96 | }
97 | return targets
98 | }
99 |
100 | /**
101 | * @param {Object} options
102 | * @param {Project} options.project
103 | * @param {[Target]} options.targets
104 | */
105 | async function loadPluginData ({ logger, project, targets }) {
106 | const frameworkVersion = project.version
107 | await eachOfLimitProgress(
108 | targets,
109 | target => target.fetchSourceInfo(),
110 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Getting plugin info ${percentage}% complete`)
111 | )
112 | logger?.log(`${chalk.bold.cyan('')} Getting plugin info 100% complete`)
113 | await eachOfLimitProgress(
114 | targets,
115 | target => target.findCompatibleVersion(frameworkVersion),
116 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Finding compatible source versions ${percentage}% complete`)
117 | )
118 | logger?.log(`${chalk.bold.cyan('')} Finding compatible source versions 100% complete`)
119 | await eachOfLimitProgress(
120 | targets,
121 | target => target.markUpdateable(),
122 | percentage => logger?.logProgress?.(`${chalk.bold.cyan('')} Marking updateable ${percentage}% complete`)
123 | )
124 | logger?.log(`${chalk.bold.cyan('')} Marking updateable 100% complete`)
125 | }
126 |
127 | /**
128 | * @param {Object} options
129 | * @param {[Target]} options.targets
130 | */
131 | async function conflictResolution ({ logger, targets, isInteractive }) {
132 | /** @param {Target} target */
133 | async function checkVersion (target) {
134 | const canApplyRequested = target.hasValidRequestVersion &&
135 | (target.hasFrameworkCompatibleVersion
136 | ? (target.latestCompatibleSourceVersion !== target.matchedVersion)
137 | : (target.latestSourceVersion !== target.matchedVersion))
138 | if (!isInteractive) {
139 | if (target.canApplyRequested) return target.markRequestedForInstallation()
140 | return target.markSkipped()
141 | }
142 | const choices = [
143 | canApplyRequested && { name: `requested version [${target.matchedVersion}]`, value: 'r' },
144 | target.hasFrameworkCompatibleVersion
145 | ? { name: `latest compatible version [${target.latestCompatibleSourceVersion}]`, value: 'l' }
146 | : { name: `latest version [${target.latestSourceVersion}]`, value: 'l' },
147 | { name: 'skip', value: 's' }
148 | ].filter(Boolean)
149 | const result = await createPromptTask({ message: chalk.reset(target.packageName), choices, type: 'list', default: 's' })
150 | const installRequested = (result === 'r')
151 | const installLatest = result === 'l'
152 | const skipped = result === 's'
153 | if (installRequested) target.markRequestedForInstallation()
154 | if (installLatest && target.hasFrameworkCompatibleVersion) target.markLatestCompatibleForInstallation()
155 | if (installLatest && !target.hasFrameworkCompatibleVersion) target.markLatestForInstallation()
156 | if (skipped) target.markSkipped()
157 | }
158 | function add (list, header, prompt) {
159 | if (!list.length) return
160 | return {
161 | header: chalk.bold.cyan(' ') + header,
162 | list,
163 | prompt
164 | }
165 | }
166 | const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
167 | const allQuestions = [
168 | add(preFilteredPlugins.filter(target => !target.hasFrameworkCompatibleVersion && target.latestSourceVersion), 'There is no compatible version of the following plugins:', checkVersion),
169 | add(preFilteredPlugins.filter(target => target.hasFrameworkCompatibleVersion && !target.hasValidRequestVersion), 'The version requested is invalid, there are newer compatible versions of the following plugins:', checkVersion),
170 | add(preFilteredPlugins.filter(target => target.hasFrameworkCompatibleVersion && target.hasValidRequestVersion && !target.isApplyLatestCompatibleVersion), 'There are newer compatible versions of the following plugins:', checkVersion)
171 | ].filter(Boolean)
172 | if (allQuestions.length === 0) return
173 | for (const question of allQuestions) {
174 | logger?.log(question.header)
175 | await eachOfSeries(question.list, question.prompt)
176 | }
177 | }
178 |
179 | /**
180 | * @param {Object} options
181 | * @param {[Target]} options.targets
182 | */
183 | function summariseDryRun ({ logger, targets }) {
184 | const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
185 | const localSources = targets.filter(target => target.isLocalSource)
186 | const toBeInstalled = preFilteredPlugins.filter(target => target.isToBeUpdated)
187 | const toBeSkipped = preFilteredPlugins.filter(target => !target.isToBeUpdated || target.isSkipped)
188 | const missing = preFilteredPlugins.filter(target => target.isMissing)
189 | summarise(logger, localSources, packageNamePrinter, 'The following plugins were installed from a local source and cannot be updated:')
190 | summarise(logger, toBeSkipped, packageNamePrinter, 'The following plugins will be skipped:')
191 | summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
192 | summarise(logger, toBeInstalled, existingVersionPrinter, 'The following plugins will be updated:')
193 | }
194 |
195 | /**
196 | * @param {Object} options
197 | * @param {[Target]} options.targets
198 | */
199 | function summariseUpdates ({ logger, targets }) {
200 | const preFilteredPlugins = targets.filter(target => !target.isLocalSource)
201 | const localSources = targets.filter(target => target.isLocalSource)
202 | const installSucceeded = preFilteredPlugins.filter(target => target.isUpdateSuccessful)
203 | const installSkipped = preFilteredPlugins.filter(target => target.isSkipped)
204 | const noUpdateAvailable = preFilteredPlugins.filter(target => !target.isToBeUpdated && !target.isSkipped)
205 | const installErrored = preFilteredPlugins.filter(target => target.isUpdateFailure)
206 | const missing = preFilteredPlugins.filter(target => target.isMissing)
207 | const noneInstalled = (installSucceeded.length === 0)
208 | const allInstalledSuccessfully = (installErrored.length === 0 && missing.length === 0)
209 | const someInstalledSuccessfully = (!noneInstalled && !allInstalledSuccessfully)
210 | summarise(logger, localSources, existingVersionPrinter, 'The following plugins were installed from a local source and cannot be updated:')
211 | summarise(logger, installSkipped, existingVersionPrinter, 'The following plugins were skipped:')
212 | summarise(logger, noUpdateAvailable, existingVersionPrinter, 'The following plugins had no update available:')
213 | summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
214 | summarise(logger, installErrored, errorPrinter, 'The following plugins could not be updated:')
215 | if (noneInstalled) logger?.log(chalk.cyanBright('None of the requested plugins could be updated'))
216 | else if (allInstalledSuccessfully) summarise(logger, installSucceeded, existingVersionPrinter, 'All requested plugins were successfully updated. Summary of installation:')
217 | else if (someInstalledSuccessfully) summarise(logger, installSucceeded, existingVersionPrinter, 'The following plugins were successfully updated:')
218 | }
219 |
220 | function summarise (logger, list, iterator, header) {
221 | if (!list || !iterator || list.length === 0) return
222 | logger?.log(chalk.cyanBright(header))
223 | list.forEach(item => iterator(item, logger))
224 | }
225 |
--------------------------------------------------------------------------------
/lib/integration/Project.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import path from 'path'
3 | import globs from 'globs'
4 | import { readValidateJSON, readValidateJSONSync } from '../util/JSONReadValidate.js'
5 | import Plugin from './Plugin.js'
6 | import Target from './Target.js'
7 | export const MANIFEST_FILENAME = 'adapt.json'
8 | export const FRAMEWORK_FILENAME = 'package.json'
9 |
10 | /**
11 | * A representation of the target Adapt Framework project
12 | */
13 | export default class Project {
14 | constructor ({
15 | cwd = process.cwd(),
16 | logger
17 | } = {}) {
18 | this.logger = logger
19 | this.cwd = cwd
20 | this.manifestFilePath = path.resolve(this.cwd, MANIFEST_FILENAME)
21 | this.frameworkPackagePath = path.resolve(this.cwd, FRAMEWORK_FILENAME)
22 | }
23 |
24 | /** @returns {boolean} */
25 | get isAdaptDirectory () {
26 | try {
27 | // are we inside an existing adapt_framework project.
28 | const packageJSON = fs.readJSONSync(this.cwd + '/package.json')
29 | return (packageJSON.name === 'adapt_framework')
30 | } catch (err) {
31 | // don't worry, we're not inside a framework directory.
32 | }
33 | return false
34 | }
35 |
36 | /** @returns {boolean} */
37 | get containsManifestFile () {
38 | if (!this.isAdaptDirectory) return false
39 | return fs.existsSync(this.manifestFilePath)
40 | }
41 |
42 | /** @returns {string} */
43 | get version () {
44 | try {
45 | return readValidateJSONSync(this.frameworkPackagePath).version
46 | } catch (ex) {
47 | return null
48 | }
49 | }
50 |
51 | tryThrowInvalidPath () {
52 | if (this.containsManifestFile) return
53 | this.logger?.error('Fatal error: please run above commands in adapt course directory.')
54 | throw new Error('Fatal error: please run above commands in adapt course directory.')
55 | }
56 |
57 | /** @returns {[Target]} */
58 | async getInstallTargets () {
59 | return Object.entries(await this.getManifestDependencies()).map(([name, requestedVersion]) => new Target({ name, requestedVersion, project: this, logger: this.logger }))
60 | }
61 |
62 | /** @returns {[string]} */
63 | async getManifestDependencies () {
64 | const manifest = await readValidateJSON(this.manifestFilePath)
65 | return manifest.dependencies
66 | }
67 |
68 | /** @returns {[Plugin]} */
69 | async getInstalledPlugins () {
70 | return Object.entries(await this.getInstalledDependencies()).map(([name]) => new Plugin({ name, project: this, logger: this.logger }))
71 | }
72 |
73 | /** @returns {[Target]} */
74 | async getUninstallTargets () {
75 | return Object.entries(await this.getInstalledDependencies()).map(([name]) => new Target({ name, project: this, logger: this.logger }))
76 | }
77 |
78 | /** @returns {[Target]} */
79 | async getUpdateTargets () {
80 | return Object.entries(await this.getInstalledDependencies()).map(([name]) => new Target({ name, project: this, logger: this.logger }))
81 | }
82 |
83 | async getInstalledDependencies () {
84 | const getDependencyBowerJSONs = async () => {
85 | const glob = `${this.cwd.replace(/\\/g, '/')}/src/**/bower.json`
86 | const bowerJSONPaths = await new Promise((resolve, reject) => {
87 | globs(glob, (err, matches) => {
88 | if (err) return reject(err)
89 | resolve(matches)
90 | })
91 | })
92 | const bowerJSONs = []
93 | for (const bowerJSONPath of bowerJSONPaths) {
94 | try {
95 | bowerJSONs.push(await fs.readJSON(bowerJSONPath))
96 | } catch (err) {}
97 | }
98 | return bowerJSONs
99 | }
100 | const dependencies = (await getDependencyBowerJSONs())
101 | .filter(bowerJSON => bowerJSON?.name && bowerJSON?.version)
102 | .reduce((dependencies, bowerJSON) => {
103 | dependencies[bowerJSON.name] = bowerJSON.version
104 | return dependencies
105 | }, {})
106 | return dependencies
107 | }
108 |
109 | async getSchemaPaths () {
110 | const glob = `${this.cwd.replace(/\\/g, '/')}/src/**/*.schema.json`
111 | const bowerJSONPaths = await new Promise((resolve, reject) => {
112 | globs(glob, (err, matches) => {
113 | if (err) return reject(err)
114 | resolve(matches)
115 | })
116 | })
117 | return bowerJSONPaths
118 | }
119 |
120 | /**
121 | * @param {Plugin} plugin
122 | */
123 | add (plugin) {
124 | if (typeof Plugin !== 'object' && !(plugin instanceof Plugin)) {
125 | plugin = new Plugin({ name: plugin })
126 | }
127 | let manifest = { version: '0.0.0', dependencies: {} }
128 | if (this.containsManifestFile) {
129 | manifest = readValidateJSONSync(this.manifestFilePath)
130 | }
131 | manifest.dependencies[plugin.packageName] = plugin.sourcePath || plugin.requestedVersion || plugin.version
132 | fs.writeJSONSync(this.manifestFilePath, manifest, { spaces: 2, replacer: null })
133 | }
134 |
135 | /**
136 | * @param {Plugin} plugin
137 | */
138 | remove (plugin) {
139 | if (typeof Plugin !== 'object' && !(plugin instanceof Plugin)) {
140 | plugin = new Plugin({ name: plugin })
141 | }
142 | const manifest = readValidateJSONSync(this.manifestFilePath)
143 | delete manifest.dependencies[plugin.packageName]
144 | fs.writeJSONSync(this.manifestFilePath, manifest, { spaces: 2, replacer: null })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/lib/integration/Target.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import bower from 'bower'
3 | import { exec } from 'child_process'
4 | import semver from 'semver'
5 | import fs from 'fs-extra'
6 | import path from 'path'
7 | import { ADAPT_ALLOW_PRERELEASE } from '../util/constants.js'
8 | import Plugin from './Plugin.js'
9 | /** @typedef {import("./Project.js").default} Project */
10 | const semverOptions = { includePrerelease: ADAPT_ALLOW_PRERELEASE }
11 |
12 | export default class Target extends Plugin {
13 | /**
14 | * @param {Object} options
15 | * @param {string} options.name
16 | * @param {string} options.requestedVersion
17 | * @param {boolean} options.isContrib
18 | * @param {boolean} options.isCompatibleEnabled whether to target the latest compatible version for all plugin installations (overrides requestedVersion)
19 | * @param {Project} options.project
20 | * @param {string} options.cwd
21 | * @param {Object} options.logger
22 | */
23 | constructor ({
24 | name,
25 | requestedVersion = '*',
26 | isContrib = false,
27 | isCompatibleEnabled = false,
28 | project,
29 | cwd = (project?.cwd ?? process.cwd()),
30 | logger
31 | } = {}) {
32 | super({
33 | name,
34 | requestedVersion,
35 | isContrib,
36 | isCompatibleEnabled,
37 | project,
38 | cwd,
39 | logger
40 | })
41 | // The version to be installed
42 | this.versionToApply = null
43 | // Keep the project version preupdate
44 | this.preUpdateProjectVersion = null
45 | // Was explicitly skipped by the user
46 | this._isSkipped = null
47 | // Marks that this target was uninstalled, true, false and null
48 | this._wasUninstalled = null
49 | }
50 |
51 | /**
52 | * Was explicitly skipped by the user
53 | * @returns {boolean}
54 | */
55 | get isSkipped () {
56 | return Boolean(this._isSkipped)
57 | }
58 |
59 | get isNoApply () {
60 | return (this.isPresent && this.versionToApply === null)
61 | }
62 |
63 | /** @returns {boolean} */
64 | get hasProposedVersion () {
65 | return (this.matchedVersion !== null)
66 | }
67 |
68 | /** @returns {boolean} */
69 | get isToBeInstalled () {
70 | return (this.versionToApply !== null && !this._isSkipped)
71 | }
72 |
73 | /** @returns {boolean} */
74 | get isInstallSuccessful () {
75 | return (this.isToBeInstalled && this.isUpToDate)
76 | }
77 |
78 | /** @returns {boolean} */
79 | get isInstallFailure () {
80 | return (this.isToBeInstalled && !this.isUpToDate)
81 | }
82 |
83 | /** @returns {boolean} */
84 | get isToBeUninstalled () {
85 | return (this.versionToApply !== null && !this._isSkipped)
86 | }
87 |
88 | /** @returns {boolean} */
89 | get isUninstallSuccessful () {
90 | return (this.isToBeUninstalled && this._wasUninstalled)
91 | }
92 |
93 | /** @returns {boolean} */
94 | get isUninstallFailure () {
95 | return (this.isToBeUninstalled && !this._wasUninstalled)
96 | }
97 |
98 | /** @returns {boolean} */
99 | get isToBeUpdated () {
100 | return (this.versionToApply !== null && !this._isSkipped)
101 | }
102 |
103 | /** @returns {boolean} */
104 | get isUpdateSuccessful () {
105 | return (this.isToBeUpdated && this.isUpToDate)
106 | }
107 |
108 | /** @returns {boolean} */
109 | get isUpdateFailure () {
110 | return (this.isToBeUpdated && !this.isUpToDate)
111 | }
112 |
113 | /** @returns {boolean} */
114 | get isApplyLatestCompatibleVersion () {
115 | return (this.hasFrameworkCompatibleVersion &&
116 | semver.satisfies(this.latestCompatibleSourceVersion, this.matchedVersion, semverOptions))
117 | }
118 |
119 | markSkipped () {
120 | this._isSkipped = true
121 | }
122 |
123 | markInstallable () {
124 | if (!this.isApplyLatestCompatibleVersion && !(this.isLocalSource && this.latestSourceVersion)) return
125 | this.versionToApply = this.matchedVersion
126 | }
127 |
128 | markUpdateable () {
129 | if (!this.isPresent || this.isSkipped || !this.canBeUpdated) return
130 | if (this.projectVersion === this.matchedVersion) return
131 | this.versionToApply = this.matchedVersion
132 | }
133 |
134 | markMasterForInstallation () {
135 | this.versionToApply = 'master'
136 | }
137 |
138 | markRequestedForInstallation () {
139 | this.matchedVersion = this.matchedVersion ?? semver.maxSatisfying(this.sourceVersions, this.requestedVersion, semverOptions)
140 | if (this.projectVersion === this.matchedVersion) return
141 | this.versionToApply = this.matchedVersion
142 | }
143 |
144 | markLatestCompatibleForInstallation () {
145 | if (this.projectVersion === this.latestCompatibleSourceVersion) return
146 | this.versionToApply = this.latestCompatibleSourceVersion
147 | }
148 |
149 | markLatestForInstallation () {
150 | if (this.projectVersion === this.latestSourceVersion) return
151 | this.versionToApply = this.latestSourceVersion
152 | }
153 |
154 | markUninstallable () {
155 | if (!this.isPresent) return
156 | this.versionToApply = this.projectVersion
157 | }
158 |
159 | async install ({ clone = false } = {}) {
160 | const logger = this.logger
161 | const pluginTypeFolder = await this.getTypeFolder()
162 | if (this.isLocalSource) {
163 | await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder))
164 | const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.packageName)
165 | await fs.rm(pluginPath, { recursive: true, force: true })
166 | await fs.copy(this.sourcePath, pluginPath, { recursive: true })
167 | const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json'))
168 | bowerJSON._source = this.sourcePath
169 | bowerJSON._wasInstalledFromPath = true
170 | await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null })
171 | this._projectInfo = null
172 | await this.fetchProjectInfo()
173 | return
174 | }
175 | if (clone) {
176 | // clone install
177 | const repoDetails = await this.getRepositoryUrl()
178 | if (!repoDetails) throw new Error('Error: Plugin repository url could not be found.')
179 | await fs.ensureDir(path.resolve(this.cwd, 'src', pluginTypeFolder))
180 | const pluginPath = path.resolve(this.cwd, 'src', pluginTypeFolder, this.packageName)
181 | await fs.rm(pluginPath, { recursive: true, force: true })
182 | const url = repoDetails.url.replace(/^git:\/\//, 'https://')
183 | try {
184 | const exitCode = await new Promise((resolve, reject) => {
185 | try {
186 | exec(`git clone ${url} "${pluginPath}"`, resolve)
187 | } catch (err) {
188 | reject(err)
189 | }
190 | })
191 | if (exitCode) throw new Error(`The plugin was found but failed to download and install. Exit code ${exitCode}`)
192 | } catch (error) {
193 | throw new Error(`The plugin was found but failed to download and install. Error ${error}`)
194 | }
195 | if (this.versionToApply !== '*') {
196 | try {
197 | await new Promise(resolve => exec(`git -C "${pluginPath}" checkout v${this.versionToApply}`, resolve))
198 | logger?.log(chalk.green(this.packageName), `is on branch "${this.versionToApply}".`)
199 | } catch (err) {
200 | throw new Error(chalk.yellow(this.packageName), `could not checkout branch "${this.versionToApply}".`)
201 | }
202 | }
203 | this._projectInfo = null
204 | await this.fetchProjectInfo()
205 | return
206 | }
207 | // bower install
208 | const outputPath = path.join(this.cwd, 'src', pluginTypeFolder)
209 | const pluginPath = path.join(outputPath, this.packageName)
210 | try {
211 | await fs.rm(pluginPath, { recursive: true, force: true })
212 | } catch (err) {
213 | throw new Error(`There was a problem writing to the target directory ${pluginPath}`)
214 | }
215 | await new Promise((resolve, reject) => {
216 | const pluginNameVersion = `${this.packageName}@${this.versionToApply}`
217 | bower.commands.install([pluginNameVersion], null, {
218 | directory: outputPath,
219 | cwd: this.cwd,
220 | registry: this.BOWER_REGISTRY_CONFIG
221 | })
222 | .on('end', resolve)
223 | .on('error', err => {
224 | err = new Error(`Bower reported ${err}`)
225 | this._error = err
226 | reject(err)
227 | })
228 | })
229 | const bowerJSON = await fs.readJSON(path.join(pluginPath, 'bower.json'))
230 | bowerJSON.version = bowerJSON.version ?? this.versionToApply;
231 | await fs.writeJSON(path.join(pluginPath, '.bower.json'), bowerJSON, { spaces: 2, replacer: null })
232 | this._projectInfo = null
233 | await this.fetchProjectInfo()
234 | }
235 |
236 | async update () {
237 | if (!this.isToBeUpdated) throw new Error()
238 | const typeFolder = await this.getTypeFolder()
239 | const outputPath = path.join(this.cwd, 'src', typeFolder)
240 | const pluginPath = path.join(outputPath, this.name)
241 | try {
242 | await fs.rm(pluginPath, { recursive: true, force: true })
243 | } catch (err) {
244 | throw new Error(`There was a problem writing to the target directory ${pluginPath}`)
245 | }
246 | await new Promise((resolve, reject) => {
247 | const pluginNameVersion = `${this.packageName}@${this.matchedVersion}`
248 | bower.commands.install([pluginNameVersion], null, {
249 | directory: outputPath,
250 | cwd: this.cwd,
251 | registry: this.BOWER_REGISTRY_CONFIG
252 | })
253 | .on('end', resolve)
254 | .on('error', err => {
255 | err = new Error(`Bower reported ${err}`)
256 | this._error = err
257 | reject(err)
258 | })
259 | })
260 | this.preUpdateProjectVersion = this.projectVersion
261 | this._projectInfo = null
262 | await this.fetchProjectInfo()
263 | }
264 |
265 | async uninstall () {
266 | try {
267 | if (!this.isToBeUninstalled) throw new Error()
268 | await fs.rm(this.projectPath, { recursive: true, force: true })
269 | this._wasUninstalled = true
270 | } catch (err) {
271 | this._wasUninstalled = false
272 | throw new Error(`There was a problem writing to the target directory ${this.projectPath}`)
273 | }
274 | }
275 |
276 | isNameMatch (name) {
277 | const tester = new RegExp(`${name}$`, 'i')
278 | return tester.test(this.packageName)
279 | }
280 |
281 | /**
282 | * Read plugin data from pluginPath
283 | * @param {Object} options
284 | * @param {string} options.pluginPath Path to source directory
285 | * @param {string} [options.projectPath=process.cwd()] Optional path to potential installation project
286 | * @returns {Target}
287 | */
288 | static async fromPath ({
289 | pluginPath,
290 | projectPath = process.cwd()
291 | }) {
292 | const target = new Target({
293 | name: pluginPath,
294 | cwd: projectPath
295 | })
296 | await target.fetchLocalSourceInfo()
297 | return target
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/lib/integration/getBowerRegistryConfig.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import { findUpSync } from 'find-up'
3 |
4 | export default function getBowerRegistryConfig ({ cwd = process.cwd() } = {}) {
5 | function getConfig () {
6 | if (process.env.ADAPT_REGISTRY) {
7 | return process.env.ADAPT_REGISTRY
8 | }
9 | const configPath = findUpSync('.bowerrc', { cwd })
10 | if (configPath) {
11 | // a manifest exists, load it
12 | const config = fs.readJSONSync(configPath)
13 | return config.registry
14 | }
15 | // use the default Adapt registry
16 | return 'http://adapt-bower-repository.herokuapp.com/'
17 | }
18 | // normalize to https://github.com/bower/spec/blob/master/config.md
19 | const config = getConfig()
20 | let normalized = {}
21 | switch (typeof config) {
22 | case 'string':
23 | normalized = {
24 | register: config,
25 | search: [config]
26 | }
27 | break
28 | case 'object':
29 | Object.assign(normalized, config)
30 | break
31 | }
32 | if (typeof normalized.search === 'string') normalized.search = [normalized.search].filter(Boolean)
33 | return normalized
34 | }
35 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | import readline from 'readline'
2 | import chalk from 'chalk'
3 |
4 | export default {
5 | isLoggingProgress: false,
6 | logProgress (...args) {
7 | this.isLoggingProgress = true
8 | readline.cursorTo(process.stdout, 0)
9 | this.write(args.join(' '))
10 | },
11 | warn (...args) {
12 | this.log(chalk.yellow(...args))
13 | },
14 | error (...args) {
15 | this.log(chalk.red(...args))
16 | },
17 | log (...args) {
18 | if (this.isLoggingProgress) {
19 | this.isLoggingProgress = false
20 | readline.cursorTo(process.stdout, 0)
21 | this.write(args.join(' '))
22 | this.write('\n')
23 | return
24 | }
25 | console.log.apply(console, args)
26 | },
27 | write: process.stdout.write.bind(process.stdout)
28 | }
29 |
--------------------------------------------------------------------------------
/lib/util/JSONReadValidate.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import JSONLint from 'json-lint'
3 |
4 | /**
5 | * @param {string} filepath
6 | * @param {function} [done]
7 | * @returns {Promise}
8 | */
9 | export async function readValidateJSON (filepath, done) {
10 | try {
11 | const data = await fs.readFile(filepath, 'utf8')
12 | validateJSON(data, filepath)
13 | done?.(null, JSON.parse(data))
14 | return JSON.parse(data)
15 | } catch (err) {
16 | done?.(err.message)
17 | }
18 | }
19 |
20 | export function readValidateJSONSync (filepath) {
21 | const data = fs.readFileSync(filepath, 'utf-8')
22 | validateJSON(data, filepath)
23 | return JSON.parse(data)
24 | }
25 |
26 | function validateJSON (jsonData, filepath) {
27 | const lint = JSONLint(jsonData)
28 | if (!lint.error) return
29 | let errorMessage = 'JSON parsing error: ' + lint.error + ', line: ' + lint.line + ', character: ' + lint.character
30 | if (filepath) {
31 | errorMessage += ', file: \'' + filepath + '\''
32 | }
33 | throw new Error(errorMessage)
34 | }
35 |
--------------------------------------------------------------------------------
/lib/util/constants.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 |
3 | export const ADAPT_ALLOW_PRERELEASE = process.env.ADAPT_ALLOW_PRERELEASE !== 'false'
4 |
5 | export const ADAPT_FRAMEWORK = process.env.ADAPT_FRAMEWORK || 'https://github.com/adaptlearning/adapt_framework'
6 |
7 | export const ADAPT_COMPONENT = process.env.ADAPT_COMPONENT || 'https://github.com/adaptlearning/adapt-component'
8 |
9 | export const ADAPT_QUESTION = process.env.ADAPT_QUESTION || 'https://github.com/adaptlearning/adapt-questionComponent'
10 |
11 | export const ADAPT_EXTENSION = process.env.ADAPT_EXTENSION || 'https://github.com/adaptlearning/adapt-extension'
12 |
13 | export const ADAPT_DEFAULT_USER_AGENT = process.env.ADAPT_DEFAULT_USER_AGENT || 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'
14 |
15 | export const HOME_DIRECTORY = [
16 | process.env.HOME,
17 | (process.env.HOMEDRIVE + process.env.HOMEPATH),
18 | process.env.USERPROFILE,
19 | '/tmp',
20 | '/temp'
21 | ].filter(fs.existsSync)[0]
22 |
23 | /** @type {string} */
24 | export const PLUGIN_TYPES = [
25 | 'component',
26 | 'extension',
27 | 'menu',
28 | 'theme'
29 | ]
30 |
31 | /** @type {Object} */
32 | export const PLUGIN_TYPE_FOLDERS = {
33 | component: 'components',
34 | extension: 'extensions',
35 | menu: 'menu',
36 | theme: 'theme'
37 | }
38 |
39 | /** @type {string} */
40 | export const PLUGIN_DEFAULT_TYPE = PLUGIN_TYPES[0]
41 |
--------------------------------------------------------------------------------
/lib/util/createPromptTask.js:
--------------------------------------------------------------------------------
1 | import inquirer from 'inquirer'
2 |
3 | export async function createPromptTask (question) {
4 | question = Object.assign({}, { name: 'question' }, question)
5 | const confirmation = await inquirer.prompt([question])
6 | return confirmation.question
7 | }
8 |
--------------------------------------------------------------------------------
/lib/util/download.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { v4 as uuid } from 'uuid'
3 | import fs from 'fs-extra'
4 | import path from 'path'
5 | import urljoin from 'url-join'
6 | import fetch from 'download'
7 | import { HOME_DIRECTORY } from './constants.js'
8 | import gh from 'parse-github-url'
9 |
10 | export default async function download ({
11 | repository,
12 | branch,
13 | tmp,
14 | cwd,
15 | logger
16 | } = {}) {
17 | if (!branch && !repository) throw new Error('Repository details are required.')
18 | const repositoryName = gh(repository).name
19 | logger?.write(chalk.cyan(`downloading ${repositoryName} to ${cwd}\t`))
20 | tmp = (tmp || path.join(HOME_DIRECTORY, '.adapt', 'tmp', uuid()))
21 | const downloadFileName = await new Promise((resolve, reject) => {
22 | let downloadFileName = ''
23 | const url = urljoin(repository, 'archive', branch + '.zip')
24 | fetch(url, tmp, {
25 | extract: true
26 | })
27 | .on('response', response => {
28 | const disposition = response.headers['content-disposition']
29 | if (disposition?.indexOf('attachment') === -1) return
30 | const regex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
31 | const matches = regex.exec(disposition)
32 | if (!matches?.[1]) return
33 | downloadFileName = matches[1].replace(/['"]/g, '')
34 | })
35 | .on('error', reject)
36 | .then(() => resolve(downloadFileName))
37 | })
38 | const sourceFileName = downloadFileName
39 | ? path.parse(downloadFileName).name
40 | : `${repositoryName}-${branch}`
41 | const sourcePath = path.join(tmp, sourceFileName)
42 | await fs.copy(sourcePath, cwd)
43 | await fs.rm(tmp, { recursive: true })
44 | logger?.log(' ', 'done!')
45 | }
46 |
--------------------------------------------------------------------------------
/lib/util/errors.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ERROR_COURSE_DIR: {
3 | code: 0,
4 | message: 'Commands must be run in an Adapt project directory'
5 | },
6 | ERROR_INCOMPATIBLE_VALID_REQUEST: {
7 | code: 1,
8 | message: 'No compatible version exists (requested version is valid)'
9 | },
10 | ERROR_INCOMPATIBLE_BAD_REQUEST: {
11 | code: 2,
12 | message: 'No compatible version exists (requested version is invalid)'
13 | },
14 | ERROR_INCOMPATIBLE: {
15 | code: 3,
16 | message: 'No compatible version exists'
17 | },
18 | ERROR_COMPATIBLE_INC_REQUEST: {
19 | code: 4,
20 | message: 'Incompatible version requested (compatible version exists)'
21 | },
22 | ERROR_COMPATIBLE_BAD_REQUEST: {
23 | code: 5,
24 | message: 'Requested version is invalid'
25 | },
26 | ERROR_UNINSTALL: {
27 | code: 6,
28 | message: 'The plugin could not be uninstalled'
29 | },
30 | ERROR_NOT_FOUND: {
31 | code: 7,
32 | message: 'The plugin could not be found'
33 | },
34 | ERROR_NOTHING_TO_UPDATE: {
35 | code: 8,
36 | message: 'Could not resolve any plugins to update'
37 | },
38 | ERROR_UPDATE_INCOMPATIBLE: {
39 | code: 9,
40 | message: 'Incompatible update requested'
41 | },
42 | ERROR_INSTALL_ERROR: {
43 | code: 10,
44 | message: 'Unknown installation error'
45 | },
46 | ERROR_UPDATE_ERROR: {
47 | code: 11,
48 | message: 'Unknown update error'
49 | },
50 | ERROR_NO_RELEASES: {
51 | code: 12,
52 | message: 'No published releases'
53 | },
54 | ERROR_NO_UPDATE: {
55 | code: 13,
56 | message: 'No update available'
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/util/extract.js:
--------------------------------------------------------------------------------
1 | import { v4 as uuid } from 'uuid'
2 | import path from 'path'
3 | import decompress from 'decompress'
4 | import { HOME_DIRECTORY } from './constants.js'
5 |
6 | export default async function extract ({
7 | sourcePath,
8 | cwd
9 | } = {}) {
10 | const rootPath = path.join(HOME_DIRECTORY, '.adapt', 'tmp', uuid()).replace(/\\/g, '/')
11 | const files = await decompress(path.join(cwd, sourcePath), rootPath, {
12 | filter: file => !file.path.endsWith('/')
13 | })
14 | const rootDirectories = Object.keys(files.reduce((memo, file) => { memo[file.path.split(/\\|\//g)[0]] = true; return memo }, {}))
15 | let copyPath = rootPath
16 | if (rootDirectories.length === 1) {
17 | const rootDirectory = files[0].path.split(/\\|\//g)[0]
18 | copyPath = path.join(rootPath, rootDirectory)
19 | }
20 | return {
21 | rootPath,
22 | copyPath
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/util/getDirNameFromImportMeta.js:
--------------------------------------------------------------------------------
1 | export default function getDirNameFromImportMeta (importMeta) {
2 | const __dirname = (process.platform === 'win32')
3 | ? new URL('.', importMeta.url).pathname.slice(1)
4 | : new URL('.', import.meta.url).pathname
5 | return __dirname
6 | }
7 |
--------------------------------------------------------------------------------
/lib/util/promises.js:
--------------------------------------------------------------------------------
1 | import { eachOfLimit, eachOfSeries } from 'async'
2 | /**
3 | * Execute all promises in parallel, call progress at each promise to allow
4 | * progress reporting
5 | * @param {Array} array
6 | * @param {function} iterator
7 | * @param {function} progress
8 | * @returns
9 | */
10 | export async function eachOfLimitProgress (array, iterator, progress) {
11 | let currentIndex = 0
12 | progress(0)
13 | await eachOfLimit(array, 8, async item => {
14 | currentIndex++
15 | progress(parseInt((currentIndex * 100) / array.length))
16 | return iterator(item)
17 | })
18 | }
19 |
20 | /**
21 | * Execute iterator against each item in the array waiting for the iterator returned
22 | * to finish before continuing, calling progress as each stage
23 | * @param {Array} array
24 | * @param {function} iterator
25 | * @param {function} progress
26 | * @returns
27 | */
28 | export async function eachOfSeriesProgress (array, iterator, progress) {
29 | let currentIndex = 0
30 | progress(0)
31 | await eachOfSeries(array, async item => {
32 | currentIndex++
33 | progress(parseInt((currentIndex * 100) / array.length))
34 | return iterator(item)
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adapt-cli",
3 | "version": "3.3.2",
4 | "description": "Command line tools for Adapt",
5 | "main": "./lib/api.js",
6 | "type": "module",
7 | "dependencies": {
8 | "async": "^3.2.3",
9 | "bower": "^1.8.13",
10 | "bower-endpoint-parser": "^0.2.2",
11 | "chalk": "^2.4.1",
12 | "decompress": "^4.2.1",
13 | "download": "^8.0.0",
14 | "find-up": "^6.2.0",
15 | "fs-extra": "^10.0.0",
16 | "globs": "^0.1.4",
17 | "inquirer": "^7.3.3",
18 | "json-lint": "^0.1.0",
19 | "lodash-es": "^4.17.21",
20 | "node-fetch": "^3.2.10",
21 | "parse-github-url": "^1.0.2",
22 | "semver": "^7.3.5",
23 | "speakingurl": "^14.0.1",
24 | "url-join": "^4.0.0",
25 | "uuid": "^8.3.2"
26 | },
27 | "license": "GPL-3.0",
28 | "preferGlobal": true,
29 | "bin": {
30 | "adapt": "./bin/adapt.js"
31 | },
32 | "devDependencies": {
33 | "eslint": "^7.31.0",
34 | "eslint-config-standard": "^16.0.3",
35 | "eslint-plugin-import": "^2.23.4",
36 | "eslint-plugin-node": "^11.1.0",
37 | "eslint-plugin-promise": "^5.1.0",
38 | "@semantic-release/commit-analyzer": "^9.0.2",
39 | "@semantic-release/git": "^10.0.1",
40 | "@semantic-release/github": "^8.0.5",
41 | "@semantic-release/npm": "^9.0.1",
42 | "@semantic-release/release-notes-generator": "^10.0.3",
43 | "conventional-changelog-eslint": "^3.0.9",
44 | "semantic-release": "^19.0.3"
45 | },
46 | "release": {
47 | "plugins": [
48 | [
49 | "@semantic-release/commit-analyzer",
50 | {
51 | "preset": "eslint"
52 | }
53 | ],
54 | [
55 | "@semantic-release/release-notes-generator",
56 | {
57 | "preset": "eslint"
58 | }
59 | ],
60 | "@semantic-release/npm",
61 | "@semantic-release/github",
62 | [
63 | "@semantic-release/git",
64 | {
65 | "assets": [
66 | "package.json",
67 | "bower.json"
68 | ],
69 | "message": "Chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
70 | }
71 | ]
72 | ]
73 | }
74 | }
75 |
--------------------------------------------------------------------------------