adapt-cli 2.1.13 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.json +14 -0
  3. package/README.md +150 -150
  4. package/bin/adapt.js +3 -0
  5. package/json/help-create/question.json +9 -0
  6. package/json/help-create.json +2 -1
  7. package/lib/api.js +260 -0
  8. package/lib/cli.js +61 -44
  9. package/lib/commands/authenticate.js +18 -0
  10. package/lib/commands/create/component.js +55 -72
  11. package/lib/commands/create/course.js +25 -80
  12. package/lib/commands/create/question.js +18 -0
  13. package/lib/commands/create.js +80 -85
  14. package/lib/commands/devinstall.js +35 -97
  15. package/lib/commands/help.js +31 -52
  16. package/lib/commands/install.js +16 -907
  17. package/lib/commands/ls.js +9 -24
  18. package/lib/commands/register.js +10 -195
  19. package/lib/commands/rename.js +13 -128
  20. package/lib/commands/search.js +10 -28
  21. package/lib/commands/uninstall.js +9 -136
  22. package/lib/commands/unregister.js +11 -95
  23. package/lib/commands/update.js +12 -867
  24. package/lib/commands/version.js +12 -14
  25. package/lib/integration/AdaptFramework/build.js +39 -0
  26. package/lib/integration/AdaptFramework/clone.js +27 -0
  27. package/lib/integration/AdaptFramework/deleteSrcCore.js +9 -0
  28. package/lib/integration/AdaptFramework/deleteSrcCourse.js +9 -0
  29. package/lib/integration/AdaptFramework/download.js +21 -0
  30. package/lib/integration/AdaptFramework/erase.js +34 -0
  31. package/lib/integration/AdaptFramework/getLatestVersion.js +79 -0
  32. package/lib/integration/AdaptFramework/npmInstall.js +21 -0
  33. package/lib/integration/AdaptFramework.js +19 -0
  34. package/lib/integration/Plugin.js +403 -0
  35. package/lib/integration/PluginManagement/autenticate.js +56 -0
  36. package/lib/integration/PluginManagement/install.js +222 -0
  37. package/lib/integration/PluginManagement/print.js +52 -0
  38. package/lib/integration/PluginManagement/register.js +130 -0
  39. package/lib/integration/PluginManagement/rename.js +101 -0
  40. package/lib/integration/PluginManagement/schemas.js +8 -0
  41. package/lib/integration/PluginManagement/search.js +46 -0
  42. package/lib/integration/PluginManagement/uninstall.js +141 -0
  43. package/lib/integration/PluginManagement/unregister.js +101 -0
  44. package/lib/integration/PluginManagement/update.js +224 -0
  45. package/lib/integration/PluginManagement.js +21 -0
  46. package/lib/integration/Project.js +146 -0
  47. package/lib/integration/Target.js +296 -0
  48. package/lib/integration/getBowerRegistryConfig.js +34 -0
  49. package/lib/logger.js +28 -0
  50. package/lib/util/JSONReadValidate.js +34 -0
  51. package/lib/util/constants.js +38 -0
  52. package/lib/util/createPromptTask.js +7 -0
  53. package/lib/util/download.js +45 -0
  54. package/lib/util/errors.js +58 -0
  55. package/lib/util/extract.js +24 -0
  56. package/lib/util/getDirNameFromImportMeta.js +6 -0
  57. package/lib/util/promises.js +36 -0
  58. package/package.json +20 -29
  59. package/TESTING.md +0 -25
  60. package/bin/adapt +0 -3
  61. package/gruntfile.js +0 -18
  62. package/lib/AdaptConsoleApplication.js +0 -19
  63. package/lib/CommandParser.js +0 -19
  64. package/lib/CommandTranslator.js +0 -16
  65. package/lib/ConsoleRenderer.js +0 -10
  66. package/lib/Constants.js +0 -68
  67. package/lib/JsonLoader.js +0 -40
  68. package/lib/JsonWriter.js +0 -21
  69. package/lib/PackageMeta.js +0 -41
  70. package/lib/Plugin.js +0 -53
  71. package/lib/PluginTypeResolver.js +0 -47
  72. package/lib/Project.js +0 -89
  73. package/lib/RendererHelpers.js +0 -41
  74. package/lib/RepositoryDownloader.js +0 -64
  75. package/lib/Slug.js +0 -5
  76. package/lib/VersionChecker.js +0 -7
  77. package/lib/commands/create/index.js +0 -6
  78. package/lib/commands/index.js +0 -16
  79. package/lib/commands/install/InstallLog.js +0 -32
  80. package/lib/commands/install/InstallTarget.js +0 -259
  81. package/lib/commands/install/extend.js +0 -31
  82. package/lib/download.js +0 -101
  83. package/lib/errors.js +0 -58
  84. package/lib/promise/authenticate.js +0 -58
  85. package/lib/promise/build.js +0 -20
  86. package/lib/promise/cloneInstall.js +0 -35
  87. package/lib/promise/confirmBuild.js +0 -6
  88. package/lib/promise/exec.js +0 -39
  89. package/lib/promise/getRepository.js +0 -26
  90. package/lib/promise/highest.js +0 -109
  91. package/lib/promise/install.js +0 -31
  92. package/lib/promise/installAdaptDependencies.js +0 -30
  93. package/lib/promise/installNodeDependencies.js +0 -28
  94. package/lib/promise/removeTemporaryDownload.js +0 -8
  95. package/lib/promise/replaceTextContent.js +0 -10
  96. package/lib/promise/uninstallPackage.js +0 -15
  97. package/lib/promise/update.js +0 -33
  98. package/lib/promise/util.js +0 -16
  99. package/test/fixtures/adapt-with-plugins.json +0 -6
  100. package/test/specs/command_translation_concerns.js +0 -13
  101. package/test/specs/create_command_concerns.js +0 -22
  102. package/test/specs/create_concerns.js +0 -30
  103. package/test/specs/install_concerns.js +0 -31
  104. package/test/specs/installing_compatible_plugins_concerns.js +0 -126
  105. package/test/specs/installing_incompatible_plugins_concerns.js +0 -103
  106. package/test/specs/ls_concerns.js +0 -28
  107. package/test/specs/plugin_name_concerns.js +0 -82
  108. package/test/specs/project_concerns.js +0 -128
  109. package/test/specs/registration_concerns.js +0 -31
  110. package/test/specs/repository_downloader_concerns.js +0 -55
  111. package/test/specs/search_concerns.js +0 -30
  112. package/test/specs/type_resolution_concerns.js +0 -71
  113. package/test/specs/uninstall_command_concerns.js +0 -64
  114. package/test/specs/uninstall_concerns.js +0 -31
@@ -0,0 +1,403 @@
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
+ const canCheckSourceAgainstProject = (this.latestSourceVersion && this.projectVersion)
83
+ if (!canCheckSourceAgainstProject) return null
84
+ const isLatestVersion = (this.projectVersion === this.latestSourceVersion)
85
+ const isLatestMatchedVersion = (this.projectVersion === this.matchedVersion)
86
+ const isProjectVersionGreater = semver.gt(this.projectVersion, this.matchedVersion)
87
+ return (isLatestVersion || isLatestMatchedVersion || isProjectVersionGreater)
88
+ }
89
+
90
+ /**
91
+ * the most recent version of the plugin
92
+ * @returns {string|null}
93
+ */
94
+ get latestSourceVersion () {
95
+ return (this._sourceInfo?.version || null)
96
+ }
97
+
98
+ /**
99
+ * the installed version of the plugin
100
+ * @returns {string|null}
101
+ */
102
+ get projectVersion () {
103
+ return (this._projectInfo?.version || null)
104
+ }
105
+
106
+ /**
107
+ * a list of tags denoting the source versions of the plugin
108
+ * @returns {[string]}
109
+ */
110
+ get sourceVersions () {
111
+ return this._versionsInfo
112
+ }
113
+
114
+ /**
115
+ * plugin will be or was installed from a local source
116
+ * @returns {boolean}
117
+ */
118
+ get isLocalSource () {
119
+ return Boolean(this.sourcePath || this?._projectInfo?._wasInstalledFromPath)
120
+ }
121
+
122
+ /**
123
+ * check if source path is a zip
124
+ * @returns {boolean}
125
+ */
126
+ get isLocalSourceZip () {
127
+ return Boolean(this.isLocalSource && (this.sourcePath?.includes('.zip') || this._projectInfo?._source?.includes('.zip')))
128
+ }
129
+
130
+ /** @returns {boolean} */
131
+ get isVersioned () {
132
+ return Boolean(this.sourceVersions?.length)
133
+ }
134
+
135
+ /**
136
+ * is a contrib plugin
137
+ * @returns {boolean}
138
+ */
139
+ get isContrib () {
140
+ return /^adapt-contrib/.test(this.packageName)
141
+ }
142
+
143
+ /**
144
+ * whether querying the server or disk for plugin information worked
145
+ * @returns {boolean}
146
+ */
147
+ get isPresent () {
148
+ return Boolean(this._projectInfo || this._sourceInfo)
149
+ }
150
+
151
+ /**
152
+ * has user requested version
153
+ * @returns {boolean}
154
+ */
155
+ get hasUserRequestVersion () {
156
+ return (this.requestedVersion !== '*')
157
+ }
158
+
159
+ /**
160
+ * the supplied a constraint is valid and supported by the plugin
161
+ * @returns {boolean|null}
162
+ */
163
+ get hasValidRequestVersion () {
164
+ return (this.latestSourceVersion)
165
+ ? semver.validRange(this.requestedVersion, semverOptions) &&
166
+ (this.isVersioned
167
+ ? semver.maxSatisfying(this.sourceVersions, this.requestedVersion, semverOptions) !== null
168
+ : semver.satisfies(this.latestSourceVersion, this.requestedVersion)
169
+ )
170
+ : null
171
+ }
172
+
173
+ /** @returns {boolean} */
174
+ get hasFrameworkCompatibleVersion () {
175
+ return (this.latestCompatibleSourceVersion !== null)
176
+ }
177
+
178
+ async fetchSourceInfo () {
179
+ if (this.isLocalSource) return await this.fetchLocalSourceInfo()
180
+ await this.fetchBowerInfo()
181
+ }
182
+
183
+ async fetchLocalSourceInfo () {
184
+ if (this._sourceInfo) return this._sourceInfo
185
+ this._sourceInfo = null
186
+ if (!this.isLocalSource) throw new Error('Plugin name or version must be a path to the source')
187
+ if (this.isLocalSourceZip) throw new Error('Cannot install from zip files')
188
+ this._sourceInfo = await new Promise((resolve, reject) => {
189
+ // get bower.json data
190
+ const paths = [
191
+ path.resolve(this.cwd, `${this.sourcePath}/bower.json`)
192
+ ]
193
+ const bowerJSON = paths.reduce((bowerJSON, bowerJSONPath) => {
194
+ if (bowerJSON) return bowerJSON
195
+ if (!fs.existsSync(bowerJSONPath)) return null
196
+ return fs.readJSONSync(bowerJSONPath)
197
+ }, null)
198
+ resolve(bowerJSON)
199
+ })
200
+ if (!this._sourceInfo) return
201
+ this.name = this._sourceInfo.name
202
+ this.matchedVersion = this.latestSourceVersion
203
+ this.packageName = this.name
204
+ }
205
+
206
+ async fetchBowerInfo () {
207
+ if (this._sourceInfo) return this._sourceInfo
208
+ this._sourceInfo = null
209
+ if (this.isLocalSource) return
210
+ const perform = async (attemptCount = 0) => {
211
+ try {
212
+ return await new Promise((resolve, reject) => {
213
+ bower.commands.info(`${this.packageName}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
214
+ .on('end', resolve)
215
+ .on('error', reject)
216
+ })
217
+ } catch (err) {
218
+ const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
219
+ if (isFinished) return null
220
+ return await perform(attemptCount++)
221
+ }
222
+ }
223
+ const info = await perform()
224
+ if (!info) return
225
+ this._sourceInfo = info.latest
226
+ this._versionsInfo = info.versions.filter(version => semverOptions.includePrerelease ? true : !semver.prerelease(version))
227
+ }
228
+
229
+ async fetchProjectInfo () {
230
+ if (this._projectInfo) return this._projectInfo
231
+ this._projectInfo = null
232
+ this._projectInfo = await new Promise((resolve, reject) => {
233
+ // get bower.json data
234
+ globs([
235
+ `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/.bower.json`,
236
+ `${this.cwd.replace(/\\/g, '/')}/src/*/${this.packageName}/bower.json`
237
+ ], (err, matches) => {
238
+ if (err) return resolve(null)
239
+ const tester = new RegExp(`/${this.packageName}/`, 'i')
240
+ const match = matches.find(match => tester.test(match))
241
+ if (!match) {
242
+ // widen the search
243
+ globs([
244
+ `${this.cwd.replace(/\\/g, '/')}/src/**/.bower.json`,
245
+ `${this.cwd.replace(/\\/g, '/')}/src/**/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) return resolve(null)
251
+ this.projectPath = path.resolve(match, '../')
252
+ resolve(fs.readJSONSync(match))
253
+ })
254
+ return
255
+ }
256
+ this.projectPath = path.resolve(match, '../')
257
+ resolve(fs.readJSONSync(match))
258
+ })
259
+ })
260
+ if (!this._projectInfo) return
261
+ this.name = this._projectInfo.name
262
+ this.packageName = this.name
263
+ }
264
+
265
+ async findCompatibleVersion (framework) {
266
+ const getBowerVersionInfo = async (version) => {
267
+ const perform = async (attemptCount = 0) => {
268
+ try {
269
+ return await new Promise((resolve, reject) => {
270
+ bower.commands.info(`${this.packageName}@${version}`, null, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
271
+ .on('end', resolve)
272
+ .on('error', reject)
273
+ })
274
+ } catch (err) {
275
+ const isFinished = (err?.code === 'ENOTFOUND' || attemptCount >= BOWER_MAX_TRY)
276
+ if (isFinished) return null
277
+ return await perform(attemptCount++)
278
+ }
279
+ }
280
+ return await perform()
281
+ }
282
+ const getMatchingVersion = async () => {
283
+ if (this.isLocalSource) {
284
+ const info = this.projectVersion ? this._projectInfo : this._sourceInfo
285
+ const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(info.version, this.requestedVersion, semverOptions)
286
+ const satisfiesFramework = semver.satisfies(framework, info.framework)
287
+ if (satisfiesFramework && satisfiesConstraint) this.latestCompatibleSourceVersion = info.version
288
+ return info.version
289
+ }
290
+
291
+ if (!this.isPresent) return null
292
+
293
+ // check if the latest version is compatible
294
+ const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(this._sourceInfo.version, this.requestedVersion, semverOptions)
295
+ const satisfiesFramework = semver.satisfies(framework, this._sourceInfo.framework, semverOptions)
296
+ if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = this.latestSourceVersion
297
+ if (satisfiesConstraint && satisfiesFramework) {
298
+ return this.latestSourceVersion
299
+ }
300
+
301
+ if (!this.isVersioned) return null
302
+
303
+ // find the highest version that is compatible with the framework and constraint
304
+ const searchVersionInfo = async (framework, versionIndex = 0) => {
305
+ const versioninfo = await getBowerVersionInfo(this.sourceVersions[versionIndex])
306
+ // give up if there is any failure to obtain version info
307
+ if (!this.isPresent) return null
308
+ // check that the proposed plugin is compatible with the contraint and installed framework
309
+ const satisfiesConstraint = !this.hasValidRequestVersion || semver.satisfies(versioninfo.version, this.requestedVersion, semverOptions)
310
+ const satisfiesFramework = semver.satisfies(framework, versioninfo.framework, semverOptions)
311
+ if (!this.latestCompatibleSourceVersion && satisfiesFramework) this.latestCompatibleSourceVersion = versioninfo.version
312
+ const checkNext = (!satisfiesFramework || !satisfiesConstraint)
313
+ const hasNoMoreVersions = (versionIndex + 1 >= this.sourceVersions.length)
314
+ if (checkNext && hasNoMoreVersions) return null
315
+ if (checkNext) return await searchVersionInfo(framework, versionIndex + 1)
316
+ return versioninfo.version
317
+ }
318
+ return await searchVersionInfo(framework)
319
+ }
320
+ this.matchedVersion = await getMatchingVersion()
321
+ this.canBeUpdated = (this.projectVersion && this.matchedVersion) && (this.projectVersion !== this.matchedVersion)
322
+ }
323
+
324
+ /**
325
+ * @returns {string}
326
+ */
327
+ async getType () {
328
+ if (this._type) return this._type
329
+ const info = await this.getInfo()
330
+ const foundAttributeType = PLUGIN_TYPES.find(type => info[type])
331
+ const foundKeywordType = info.keywords
332
+ .map(keyword => {
333
+ const typematches = PLUGIN_TYPES.filter(type => keyword?.toLowerCase()?.includes(type))
334
+ return typematches.length ? typematches[0] : null
335
+ })
336
+ .filter(Boolean)[0]
337
+ return (this._type = foundAttributeType || foundKeywordType || PLUGIN_DEFAULT_TYPE)
338
+ }
339
+
340
+ async getTypeFolder () {
341
+ const type = await this.getType()
342
+ return PLUGIN_TYPE_FOLDERS[type]
343
+ }
344
+
345
+ async getInfo () {
346
+ if (this._projectInfo) return this._projectInfo
347
+ if (!this._sourceInfo) await this.fetchSourceInfo()
348
+ return this._sourceInfo
349
+ }
350
+
351
+ async getRepositoryUrl () {
352
+ if (this._repositoryUrl) return this._repositoryUrl
353
+ if (this.isLocalSource) return
354
+ const url = await new Promise((resolve, reject) => {
355
+ bower.commands.lookup(this.packageName, { cwd: this.cwd, registry: this.BOWER_REGISTRY_CONFIG })
356
+ .on('end', resolve)
357
+ .on('error', reject)
358
+ })
359
+ return (this._repositoryUrl = url)
360
+ }
361
+
362
+ /** @returns {string} */
363
+ toString () {
364
+ const isAny = (this.projectVersion === '*' || this.projectVersion === null)
365
+ return `${this.packageName}${isAny ? '' : `@${this.projectVersion}`}`
366
+ }
367
+
368
+ async getSchemaPaths () {
369
+ if (this.isLocalSource) await this.fetchLocalSourceInfo()
370
+ else if (this.project) await this.fetchProjectInfo()
371
+ else throw new Error(`Cannot fetch schemas from remote plugin: ${this.name}`)
372
+ const pluginPath = this.projectPath ?? this.sourcePath
373
+ return new Promise((resolve, reject) => {
374
+ return globs(path.resolve(this.cwd, pluginPath, '**/*.schema.json'), (err, matches) => {
375
+ if (err) return reject(err)
376
+ resolve(matches)
377
+ })
378
+ })
379
+ }
380
+
381
+ /**
382
+ * Read plugin data from pluginPath
383
+ * @param {Object} options
384
+ * @param {string} options.pluginPath Path to source directory
385
+ * @param {string} [options.projectPath=process.cwd()] Optional path to potential installation project
386
+ * @returns {Plugin}
387
+ */
388
+ static async fromPath ({
389
+ pluginPath,
390
+ projectPath = process.cwd()
391
+ }) {
392
+ const plugin = new Plugin({
393
+ name: pluginPath,
394
+ cwd: projectPath
395
+ })
396
+ await plugin.fetchLocalSourceInfo()
397
+ return plugin
398
+ }
399
+
400
+ static get instances () {
401
+ return (Plugin._instances = Plugin._instances || [])
402
+ }
403
+ }
@@ -0,0 +1,56 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import request from 'request'
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
+ return new Promise((resolve, reject) => {
40
+ request({
41
+ uri: `${BOWER_REGISTRY_CONFIG.register}authenticate/${username}/${pluginName}?access_token=${token}`,
42
+ method: 'GET',
43
+ headers: { 'User-Agent': 'adapt-cli' },
44
+ followRedirect: false
45
+ }, (err, res, body) => {
46
+ if (err) return reject(err)
47
+ if (res.statusCode !== 200) reject(new Error(`The server responded with ${res.statusCode}`))
48
+ try {
49
+ const bodyJSON = JSON.parse(body)
50
+ resolve({ username, token, pluginName, ...bodyJSON })
51
+ } catch (err) {
52
+ reject(err)
53
+ }
54
+ })
55
+ })
56
+ }
@@ -0,0 +1,222 @@
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('<info>')} Installing plugins ${percentage}% complete`)
44
+ )
45
+ logger?.log(`${chalk.bold.cyan('<info>')} 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('<info>')} Getting plugin info ${percentage}% complete`)
96
+ )
97
+ logger?.log(`${chalk.bold.cyan('<info>')} Getting plugin info 100% complete`)
98
+ await eachOfLimitProgress(
99
+ targets,
100
+ target => target.findCompatibleVersion(frameworkVersion),
101
+ percentage => logger?.logProgress?.(`${chalk.bold.cyan('<info>')} Finding compatible source versions ${percentage}% complete`)
102
+ )
103
+ logger?.log(`${chalk.bold.cyan('<info>')} Finding compatible source versions 100% complete`)
104
+ await eachOfLimitProgress(
105
+ targets,
106
+ target => target.markInstallable(),
107
+ percentage => logger?.logProgress?.(`${chalk.bold.cyan('<info>')} Marking installable ${percentage}% complete`)
108
+ )
109
+ logger?.log(`${chalk.bold.cyan('<info>')} 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
+ : { name: `latest version [${target.latestSourceVersion}]`, value: 'l' },
133
+ { name: 'skip', value: 's' }
134
+ ].filter(Boolean)
135
+ const result = await createPromptTask({ message: chalk.reset(target.packageName), choices, type: 'list', default: 's' })
136
+ const installMasterBranch = (result === 'm')
137
+ const installRequested = (result === 'r')
138
+ const installLatest = result === 'l'
139
+ const skipped = result === 's'
140
+ if (installMasterBranch) target.markMasterForInstallation()
141
+ if (installRequested) target.markRequestedForInstallation()
142
+ if (installLatest && target.hasFrameworkCompatibleVersion) target.markLatestCompatibleForInstallation()
143
+ if (installLatest && !target.hasFrameworkCompatibleVersion) target.markLatestForInstallation()
144
+ if (skipped) target.markSkipped()
145
+ }
146
+ function add (list, header, prompt) {
147
+ if (!list.length) return
148
+ return {
149
+ header: chalk.cyan('<info> ') + header,
150
+ list,
151
+ prompt
152
+ }
153
+ }
154
+ const allQuestions = [
155
+ add(targets.filter(target => !target.hasFrameworkCompatibleVersion), 'There is no compatible version of the following plugins:', checkVersion),
156
+ add(targets.filter(target => target.hasFrameworkCompatibleVersion && !target.hasValidRequestVersion), 'The version requested is invalid, there are newer compatible versions of the following plugins:', checkVersion),
157
+ add(targets.filter(target => target.hasFrameworkCompatibleVersion && target.hasValidRequestVersion && !target.isApplyLatestCompatibleVersion), 'There are newer compatible versions of the following plugins:', checkVersion)
158
+ ].filter(Boolean)
159
+ if (allQuestions.length === 0) return
160
+ for (const question of allQuestions) {
161
+ logger?.log(question.header)
162
+ await eachOfSeries(question.list, question.prompt)
163
+ }
164
+ }
165
+
166
+ /**
167
+ * @param {Object} options
168
+ * @param {Project} options.project
169
+ * @param {[Target]} options.targets
170
+ */
171
+ async function updateManifest ({ project, targets, manifestDependencies, isInteractive }) {
172
+ if (targets.filter(target => target.isInstallSuccessful).length === 0) return
173
+ if (difference(targets.filter(target => target.isInstallSuccessful).map(target => target.packageName), Object.keys(manifestDependencies)).length === 0) return
174
+ if (isInteractive) {
175
+ const shouldUpdate = await createPromptTask({
176
+ message: chalk.white('Update the manifest (adapt.json)?'),
177
+ type: 'confirm',
178
+ default: true
179
+ })
180
+ if (!shouldUpdate) return
181
+ }
182
+ targets.forEach(target => target.isInstallSuccessful && project.add(target))
183
+ }
184
+
185
+ /**
186
+ * @param {Object} options
187
+ * @param {[Target]} options.targets
188
+ */
189
+ function summariseDryRun ({ logger, targets }) {
190
+ const toBeInstalled = targets.filter(target => target.isToBeInstalled)
191
+ const toBeSkipped = targets.filter(target => !target.isToBeInstalled || target.isSkipped)
192
+ const missing = targets.filter(target => target.isMissing)
193
+ summarise(logger, toBeSkipped, packageNamePrinter, 'The following plugins will be skipped:')
194
+ summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
195
+ summarise(logger, toBeInstalled, versionPrinter, 'The following plugins will be installed:')
196
+ }
197
+
198
+ /**
199
+ * @param {Object} options
200
+ * @param {[Target]} options.targets
201
+ */
202
+ function summariseInstallation ({ logger, targets, dev }) {
203
+ const installSucceeded = targets.filter(target => target.isInstallSuccessful)
204
+ const installSkipped = targets.filter(target => !target.isToBeInstalled || target.isSkipped)
205
+ const installErrored = targets.filter(target => target.isInstallFailure)
206
+ const missing = targets.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, installSkipped, packageNamePrinter, 'The following plugins were skipped:')
211
+ summarise(logger, missing, packageNamePrinter, 'There was a problem locating the following plugins:')
212
+ summarise(logger, installErrored, errorPrinter, 'The following plugins could not be installed:')
213
+ if (noneInstalled) logger?.log(chalk.cyanBright('None of the requested plugins could be installed'))
214
+ else if (allInstalledSuccessfully) summarise(logger, installSucceeded, dev ? packageNamePrinter : versionPrinter, 'All requested plugins were successfully installed. Summary of installation:')
215
+ else if (someInstalledSuccessfully) summarise(logger, installSucceeded, dev ? packageNamePrinter : versionPrinter, 'The following plugins were successfully installed:')
216
+ }
217
+
218
+ function summarise (logger, list, iterator, header) {
219
+ if (!list || !iterator || list.length === 0) return
220
+ logger?.log(chalk.cyanBright(header))
221
+ list.forEach(item => iterator(item, logger))
222
+ }