From 9af5c2050c6f2c5c2f9da5b396a1d6d20a4c9aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20S=C3=A1gi?= Date: Mon, 24 Oct 2016 16:38:19 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 14 ++ .gitignore | 1 + LICENSE.md | 24 +++ README.md | 53 ++++++ index.js | 3 + install/pacman.js | 66 ++++++++ install/pacman.json | 26 +++ install/post-install.js | 45 +++++ lib/common.js | 281 +++++++++++++++++++++++++++++++ package.json | 28 +++ registry/aurelia-editables.json | 60 +++++++ registry/aurelia-interactjs.json | 12 ++ registry/bootstrap.json | 14 ++ registry/jquery.json | 7 + yarn.lock | 93 ++++++++++ 15 files changed, 727 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 index.js create mode 100644 install/pacman.js create mode 100644 install/pacman.json create mode 100644 install/post-install.js create mode 100644 lib/common.js create mode 100644 package.json create mode 100644 registry/aurelia-editables.json create mode 100644 registry/aurelia-interactjs.json create mode 100644 registry/bootstrap.json create mode 100644 registry/jquery.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a9c0e67 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# 2 space indentation +[**.*] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c2f95b2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) 2016 Marton Sagi, contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9246734 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +#**Experimental:** this project is a work in progress. + +# aurelia-cli-pacman + +> Aurelia-cli-pacman is a simple package management helper for projects using [aurelia-cli](http://github.com/aurelia/cli). It supports npm package installation/removal, and configuration of pre-defined bundle dependencies in `aurelia.json`. This project's main goal is to enhance the package/plugin configuration process while using `aurelia-cli` for development. + +## 1. Installation + +Since it's an extension to aurelia-cli, it cannot be used with JSPM or WebPack. + +``` +npm install aurelia-cli-pacman --save +``` + +### 1.1 `pacman` helper task for aurelia-cli + +Since aurelia-cli is still in alpha stage and `install` command is not yet implemented, I've created this custom cli task to enhance configuration of plugin dependencies in `aurelia.json`. It adds a pre-configured set of dependencies to `aurelia.json`, if there's any. +A post-install npm script takes care of placing this new `pacman.ts|js` task into `aurelia_project/tasks` folder. + +## 2. Usage + +| Parameters | Description | +| ------------------- | ----------- | +| --install, i | Install npm package and sets bundle dependencies. Calls `npm install --save` | +| --uninstall, u | Uninstall npm package and removes bundle dependencies. Calls `npm uninstall --save` | +| --bundle, b | Set bundle section to be modified | +| --force, f | Overwrite previously set dependencies (applies only to dependencies of specified package! It won't delete the whole bundle setting.) | + +Run `au pacman` helper: + +``` +au pacman --install [--bunde ] [--force] +au pacman i aurelia-validation --bunde plugin-bundle.js --force + +au pacman u aurelia-validation +``` + +**Note:** tested on Windows platform only. + +### 2.1 Pre-defined bundle dependencies + +There is a small dependency collection for several basic aurelia plugins and other npm packages in `./registry` folder. + +## 3. Dependencies + +* aurelia-cli +* fs-extra + + +## 4. Platform Support + +This extension can be used with **NodeJS** only. It's executed within `aurelia-cli` infrastructure. + diff --git a/index.js b/index.js new file mode 100644 index 0000000..885e6f4 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +"use strict"; + +exports.PacMan = require('./lib/common').PacMan; diff --git a/install/pacman.js b/install/pacman.js new file mode 100644 index 0000000..843963c --- /dev/null +++ b/install/pacman.js @@ -0,0 +1,66 @@ +/** + * Mini cli helper for package management + * - Installs/Uninstalls npm packages + * - Configures bundle correctly + * + * It's pure ES6 to support Babel/Typescript projects as well + * + * Usage: + * au pacman --install/i [--bundle ] [--force] + * au pacman --uninstall/u [--bundle ] + */ + +import * as fs from 'fs-extra'; +import {CLIOptions} from 'aurelia-cli'; +import {PacMan} from 'aurelia-cli-pacman'; + +/** + * Reads aurelia.json + * + * Using this, because default import statement would + * add a "default" member to the original object + * causing problems at saving + * + * @return {Promise|Promise} + */ +let getProject = () => { + return new Promise((resolve, reject) => { + let path = 'aurelia_project/aurelia.json'; + fs.readJson(path, (err, content) => { + if (err) { + reject(err); + } else { + resolve(content); + } + }); + }); +}; + +/** + * Execute + */ +export default () => { + // package manager + let pacMan = new PacMan(CLIOptions); + + // collect given parameters + let cliParams = pacMan.getCliParams(); + + if (!cliParams.action) { + console.log(`Invalid or no action given. Please use one of these:\n`); + for (let action of allowedActions) { + console.log(` au pacman --${action.join('/')} `); + } + return; + } + + let tasks = [getProject(), pacMan.getDependencies(cliParams.pkg)]; + + return Promise + .all(tasks) + .then(result => { + return pacMan[cliParams.action](cliParams.pkg) + .then(ok => pacMan.configure(cliParams, ...result)); + }) + .catch(err => { throw new Error(err); }); +}; diff --git a/install/pacman.json b/install/pacman.json new file mode 100644 index 0000000..9710660 --- /dev/null +++ b/install/pacman.json @@ -0,0 +1,26 @@ +{ + "name": "pacman", + "description": "cli extension for package management. It helps you to install/uninstall packages and to configure bundle dependencies.", + "flags": [ + { + "name": "install", + "description": "Installs npm package and sets bundle dependencies", + "type": "string" + }, + { + "name": "uninstall", + "description": "Uninstalls npm package and removes bundle dependencies", + "type": "string" + }, + { + "name": "bundle", + "description": "Sets bundle section to be modified", + "type": "string" + }, + { + "name": "force", + "description": "Overwrite previously set dependencies. This applies only to dependencies of the given package!", + "type": "boolean" + } + ] +} diff --git a/install/post-install.js b/install/post-install.js new file mode 100644 index 0000000..1615632 --- /dev/null +++ b/install/post-install.js @@ -0,0 +1,45 @@ +"use strict"; + +try { + let fs = require('fs-extra'), + path = require('path'), + projectFolder = '../../aurelia_project/', + projectFile = 'aurelia.json', + installName = 'pacman'; + + // fs-extra is installed as devDependency for CLI projects + // if it isn't there, it's probably not CLI + if (fs) { + // check again if it's an aurelia-cli project for sure + fs.exists(projectFolder, function (exists) { + if (exists === true) { + fs.readJson(projectFolder + projectFile, function (err, project) { + if (err) { + return console.log('Could not install ' + projectFile, err); + } else { + // determinate transpiler to set correct file extension + let filename = installName + project.transpiler.fileExtension, + source = './install/' + installName, + dest = projectFolder + 'tasks/'; + + fs.copy(source + '.js', dest + filename, function (err) { + if (err) { + return console.log('Could not install ' + filename, err); + } else { + fs.copy(source + '.json', dest + installName + '.json', function (err) { + if (err) { + return console.log('Could not install ' + dest + installName + '.json', err); + } else { + return console.log(dest + filename + ' has been installed.'); + } + }); + } + }); + } + }); + } + }); + } +} catch (e) { + console.log('aurelia-interactjs: post installation step skipped.\n', e); +} diff --git a/lib/common.js b/lib/common.js new file mode 100644 index 0000000..28df325 --- /dev/null +++ b/lib/common.js @@ -0,0 +1,281 @@ +/** + * Mini cli extension for package management + * - Installs/Uninstalls npm packages + * - Configures bundle correctly + * + * It's referenced in 'pacman' custom aurelia-cli task + */ + +"use strict"; + +const fs = require('fs-extra'), + path = require('path'), + npm = require('npm'); + +/** + * NPM api helper + * + * @param action string + * @param pkgName string + * @return {Promise} + */ +let NPMLoader = function (action, pkgName) { + return new Promise(function (resolve, reject) { + npm.load({ + loaded: false + }, function (err) { + if (err) { + reject(err); + } else { + npm.config.set('save', true); + npm.commands[action]([pkgName], function (err, data) { + // log the error or data + if (err) { + reject(err); + } else { + console.log(`[NPM] '${pkgName}' has been ${action}ed.`); + resolve(true); + } + }); + npm.on("log", function (message) { + // log the progress of the installation + console.log(message); + }); + } + }); + }); +}; + +/** + * Package manager class + * + * Adds/removes npm packages + * Configures bundle section of aurelia.json + */ +exports.PacMan = class { + + /** + * Constructor + * @param CLIOptions + */ + constructor(CLIOptions) { + this.CLIOptions = CLIOptions; + this.registryPath = '../registry/'; + this.allowedActions = [['install', 'i'], ['uninstall', 'u']]; + } + + /** + * Install npm package + * + * @param pkgName string package name + * @return {Promise} + */ + install(pkgName) { + return NPMLoader('install', pkgName); + } + + /** + * Uninstall npm package + * + * @param pkgName string packgae name + * @return {Promise} + */ + uninstall(pkgName) { + return NPMLoader('uninstall', pkgName); + } + + + /** + * Configures package dependencies + * Edits aurelia.json to add/remove pre-configured dependencies for specified package + * + * @void + */ + configure(cliParams, project, deps) { + if (!deps || deps.length === 0) { + console.log(`[INFO] There are no dependencies to configure for ${cliParams.pkg} in aurelia.json. Exiting...`); + return; + } + + let bundle = null, + bundles = project.build.bundles; + + if (bundles.length === 0) { + throw new Error("aurelia.json: bundles section is missing."); + } + + let bundleName = cliParams.bundle || 'vendor-bundle.js'; + + bundle = bundles.find(item => item.name === bundleName); + + if (!bundle) { + console.log(`[INFO] Bundle '${bundleName}' could not be found. Looking for default bundles...`); + + // There are 2 sections by default, second is usually the vendor-bundle.js + // Although, some developers prefer to merge everything into a single bundle + let index = bundles.length > 1 ? 1 : 0; + bundle = bundles[index]; + + // this should not be reached ever, but never say never :) + if (!bundle) { + throw new Error('Default bundle could not be found either. Check aurelia.json configuration.'); + } + + bundleName = bundle.name; + } + + if (!bundle.dependencies) { + if (cliParams.action === 'uninstall') { + console.log(`[INFO] No dependencies found in ${bundleName}. Exiting...`); + return; + } + bundle.dependencies = []; + } + + console.log(`[INFO] Bundle found: ${bundleName}. Configuring new dependencies in aurelia.json...`); + for (let dep of deps) { + let name = dep.name || dep, + check = bundle.dependencies.find(item => (item.name || item) === name); + + if (!check) { + if (cliParams.action === 'install') { + console.log(`[NEW] '${name}' has been configured.`); + bundle.dependencies.push(dep); + } + } else { + let i = bundle.dependencies.indexOf(check); + + if (cliParams.action === 'install') { + if (cliParams.force) { + bundle.dependencies[i] = dep; + console.log(`[MOD] '${name}' has been modified.`); + } else { + console.log(`[SKIP] '${name}' has already been configured.`); + } + } else { + bundle.dependencies.splice(i, 1); + console.log(`[DEL] '${name}' has been removed.`); + } + } + } + + console.log('[INFO] Saving changes to aurelia.json file...'); + let aureliaProjectFile = 'aurelia_project/aurelia.json', + aureliaProjectFileBackup = `${aureliaProjectFile}.${Date.now()}.bak`; + + fs.copy(aureliaProjectFile, aureliaProjectFileBackup, function (err) { + if (err) { + console.log('[ERROR] An error occurred while duplicating aurelia.json.', err); + } else { + console.log(`[INFO] Backup of aurelia.json has been created: ${aureliaProjectFileBackup}`); + fs.writeJson(aureliaProjectFile, project, (err) => { + if (err) { + console.log('[ERROR] An error occurred while updating aurelia.json.', err); + } else { + console.log(`[OK] ${aureliaProjectFile} has been updated.`); + console.log(`\n\n[OK] ${cliParams.pkg} has been configured successfully.`); + } + }); + } + }); + }; + + /** + * Simple wrapper for built-in CLIOptions + * + * @param name + * @param shortcut + * @returns {any|null} + */ + getCliParam(name, shortcut) { + if (this.CLIOptions.hasFlag(name, shortcut)) { + return this.CLIOptions.getFlagValue(name, shortcut) || null; + } + }; + + /** + * Collect given CLI parameters + * + * @return {any} + */ + getCliParams() { + let options = {}, + actionParam = this.getAction(); + + options.action = actionParam[0]; + options.pkg = actionParam[1] || null; + options.bundle = this.getCliParam('bundle', 'b'); + options.force = this.CLIOptions.hasFlag('force', 'f'); + + return options; + }; + + /** + * Determinate action to execute (install/uninstall) + * + * @return Array + */ + getAction() { + for (let action of this.allowedActions) { + if (this.CLIOptions.hasFlag(action[0], action[1])) { + return [action[0], this.getCliParam(action[0], action[1])]; + } + } + + return [null, null]; + }; + + /** + * Search for pre-defined dependencies in local 'registry' folder + * + * @param pkgName string package name + * @return {Promise} + */ + getDependencies(pkgName) { + let t = this; + return new Promise(function (resolve, reject) { + t.exists(pkgName) + .then(function (exists) { + if (exists === true) { + fs.readJson(t.getFilename(pkgName), function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } else { + resolve(null); + } + }) + .catch(function (e) { + reject(e); + }); + }); + } + + /** + * Determinate filepath of pre-defined json settings file. + * + * @param pkgName + * @return {Promise.<*>|*} + */ + getFilename(pkgName) { + return path.resolve(__dirname, `${this.registryPath + pkgName}.json`); + } + + /** + * Check whether a pre-defined configuration exists + * + * @param pkgName string package name + * @return {Promise} + */ + exists(pkgName) { + let t = this; + return new Promise(function (resolve, reject) { + fs.exists(t.getFilename(pkgName), function (exists) { + resolve(exists); + }); + }); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d9f913 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "aurelia-cli-pacman", + "version": "0.0.1", + "author": "Marton Sagi ", + "description": "Extension to provide aurelia-cli with package management capabilities", + "homepage": "https://github.com/martonsagi/aurelia-cli-pacman", + "license": "MIT", + "bugs": { + "url": "https://github.com/martonsagi/aurelia-cli-pacman/issues" + }, + "keywords": [ + "aurelia", + "cli", + "pacman", + "installation" + ], + "main": "index", + "repository": { + "type": "git", + "url": "https://github.com/martonsagi/aurelia-cli-pacman" + }, + "scripts": { + "postinstall": "node ./install/post-install.js" + }, + "dependencies": { + "fs-extra": "^0.30.0" + } +} diff --git a/registry/aurelia-editables.json b/registry/aurelia-editables.json new file mode 100644 index 0000000..765a335 --- /dev/null +++ b/registry/aurelia-editables.json @@ -0,0 +1,60 @@ +[ + { + "name": "interact", + "path": "../node_modules/interact.js/dist", + "main": "interact" + }, + { + "name": "aurelia-interactjs", + "path": "../node_modules/aurelia-interactjs/dist/amd", + "main": "index" + }, + "aurelia-http-client", + "underscore", + { + "name": "jquery", + "path": "../node_modules/jquery/dist", + "main": "jquery.min" + }, + { + "name": "bootstrap", + "path": "../node_modules/bootstrap/dist", + "main": "js/bootstrap.min", + "deps": ["jquery"], + "exports": "$" + }, + { + "name": "i18next", + "path": "../node_modules/i18next/dist/umd", + "main": "i18next" + }, + { + "name": "aurelia-i18n", + "path": "../node_modules/aurelia-i18n/dist/amd", + "main": "aurelia-i18n" + }, + { + "name": "i18next-xhr-backend", + "path": "../node_modules/i18next-xhr-backend/dist/umd", + "main": "i18nextXHRBackend" + }, + { + "name": "aurelia-validation", + "path": "../node_modules/aurelia-validation/dist/amd", + "main": "aurelia-validation" + }, + { + "name": "aurelia-ui-virtualization", + "path": "../node_modules/aurelia-ui-virtualization/dist/amd", + "main": "aurelia-ui-virtualization" + }, + { + "name": "aurelia-editables", + "path": "../node_modules/aurelia-editables/dist/amd", + "main": "aurelia-editables", + "resources": [ + "**/*.html", + "**/*.css" + ] + } +] diff --git a/registry/aurelia-interactjs.json b/registry/aurelia-interactjs.json new file mode 100644 index 0000000..f2dcecc --- /dev/null +++ b/registry/aurelia-interactjs.json @@ -0,0 +1,12 @@ +[ + { + "name": "interact", + "path": "../node_modules/interact.js/dist", + "main": "interact" + }, + { + "name": "aurelia-interactjs", + "path": "../node_modules/aurelia-interactjs/dist/amd", + "main": "index" + } +] diff --git a/registry/bootstrap.json b/registry/bootstrap.json new file mode 100644 index 0000000..ab7f825 --- /dev/null +++ b/registry/bootstrap.json @@ -0,0 +1,14 @@ +[ + { + "name": "jquery", + "path": "../node_modules/jquery/dist", + "main": "jquery.min" + }, + { + "name": "bootstrap", + "path": "../node_modules/bootstrap/dist", + "main": "js/bootstrap.min", + "deps": ["jquery"], + "exports": "$" + } +] \ No newline at end of file diff --git a/registry/jquery.json b/registry/jquery.json new file mode 100644 index 0000000..ba3fa6c --- /dev/null +++ b/registry/jquery.json @@ -0,0 +1,7 @@ +[ + { + "name": "jquery", + "path": "../node_modules/jquery/dist", + "main": "jquery.min" + } +] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..a16d169 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,93 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +fs-extra: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.0.5: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +klaw@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.0.tgz#8857bfbc1d824badf13d3d0241d8bbe46fb12f73" + +minimatch@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +rimraf@^2.2.8: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +