diff --git a/assets/qdl/darwin/arm64/qdl b/assets/qdl/darwin/arm64/qdl index f49b8f569..2c9716310 100755 Binary files a/assets/qdl/darwin/arm64/qdl and b/assets/qdl/darwin/arm64/qdl differ diff --git a/assets/qdl/darwin/x64/qdl b/assets/qdl/darwin/x64/qdl index 0130980b6..0a690cd9d 100755 Binary files a/assets/qdl/darwin/x64/qdl and b/assets/qdl/darwin/x64/qdl differ diff --git a/assets/qdl/linux/arm64/qdl b/assets/qdl/linux/arm64/qdl index 4b1b94463..eedc90148 100755 Binary files a/assets/qdl/linux/arm64/qdl and b/assets/qdl/linux/arm64/qdl differ diff --git a/assets/qdl/linux/x64/qdl b/assets/qdl/linux/x64/qdl index 55162239f..f22007998 100755 Binary files a/assets/qdl/linux/x64/qdl and b/assets/qdl/linux/x64/qdl differ diff --git a/assets/qdl/win32/x64/qdl.exe b/assets/qdl/win32/x64/qdl.exe index 73c8724ec..be206f471 100644 Binary files a/assets/qdl/win32/x64/qdl.exe and b/assets/qdl/win32/x64/qdl.exe differ diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 80f504ff9..14a7ad22b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -6,10 +6,10 @@ "packages": { "": { "name": "particle-cli", - "version": "3.30.3", + "version": "3.31.0", "license": "Apache-2.0", "dependencies": { - "@particle/device-constants": "^3.7.0", + "@particle/device-constants": "^3.8.0", "binary-version-reader": "^2.5.1", "chalk": "^2.4.2", "cli-progress": "^3.12.0", @@ -23,6 +23,7 @@ "jose": "^4.13.1", "lodash": "^4.17.15", "moment": "^2.24.0", + "node-fetch": "^2.7.0", "node-wifiscanner2": "^1.2.1", "particle-api-js": "^11.1.0", "particle-commands": "^1.0.1", @@ -32,6 +33,7 @@ "safe-buffer": "^5.2.0", "semver": "^7.5.2", "serialport": "^10.4.0", + "sha512crypt-node": "^1.0.2", "softap-setup": "^4.1.0", "temp": "^0.9.1", "unzipper": "^0.12.3", @@ -565,9 +567,9 @@ } }, "node_modules/@particle/device-constants": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.7.0.tgz", - "integrity": "sha512-B6lPf5L0Zgffg/dbgkTAj2Wyk7AAKFqy7rMmL3pqiY0m/gYsVDhvPoqUeyeFKg8jIIkjGKjXHjLywGWBZ/8V1Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.8.0.tgz", + "integrity": "sha512-SyphIgh64z25S7aRMheenBvQ57+5Ta7BfY1hLI4NaAbyFu9NWLwceimK0442cChB1U1JgpekSTTO93d1cP1gAw==", "engines": { "node": ">=12.x", "npm": "8.x" @@ -962,6 +964,11 @@ "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.7.tgz", "integrity": "sha512-PCKxgvnJ8wo2gCW8o96CmxDqG2okgsNDfa2xBCbHKvdmPRBTBLz6ClyRXtU+ylwvsqTYaLtJoxnGRG8nkcjd9A==" }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1083,6 +1090,18 @@ "node": ">=4" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/app-root-path": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", @@ -1403,6 +1422,17 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/binary-version-reader": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.5.1.tgz", @@ -1491,7 +1521,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1502,8 +1531,7 @@ "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, "node_modules/browserify-zlib": { "version": "0.1.4", @@ -1721,6 +1749,32 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2420,7 +2474,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, "engines": { "node": ">=6" } @@ -2669,57 +2722,12 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3108,7 +3116,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3326,6 +3333,19 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -3417,9 +3437,10 @@ "dev": true }, "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3439,7 +3460,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3502,7 +3522,6 @@ "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, "engines": { "node": ">=4.x" } @@ -3706,7 +3725,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, "bin": { "he": "bin/he" } @@ -4108,6 +4126,17 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -4170,7 +4199,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4190,7 +4218,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4228,7 +4255,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -4308,6 +4334,17 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -4670,6 +4707,20 @@ "node": ">=0.10.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -5287,6 +5338,17 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -5878,6 +5940,34 @@ "node": ">=8" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -6493,7 +6583,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -7286,6 +7375,14 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -7394,6 +7491,17 @@ "node": ">=10" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz", @@ -7677,6 +7785,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serialport": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-10.4.0.tgz", @@ -7735,88 +7851,510 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "node_modules/sha512crypt-node": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sha512crypt-node/-/sha512crypt-node-1.0.2.tgz", + "integrity": "sha512-SmZnsrnp3T/+h/nd+/uAe4CKz2KgPBG0enskKE2qoEjzUUUM/1MYAPgBZRAHWAq6XEQl3XzSpS+bzl2rWWABnA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "mocha": "^9.1.3" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "sha512crypt-demo": "demo.js" + }, + "engines": { + "node": "*" } }, - "node_modules/signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "node_modules/sha512crypt-node/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "engines": { + "node": ">=6" } }, - "node_modules/sinon": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", - "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", - "deprecated": "16.1.1", - "dev": true, + "node_modules/sha512crypt-node/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.3", - "diff": "^3.5.0", - "lolex": "^4.2.0", - "nise": "^1.5.2", - "supports-color": "^5.5.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/sinon-chai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", + "node_modules/sha512crypt-node/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/sha512crypt-node/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/sha512crypt-node/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/sha512crypt-node/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sha512crypt-node/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sha512crypt-node/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sha512crypt-node/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sha512crypt-node/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/sha512crypt-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/sha512crypt-node/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/sha512crypt-node/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/sha512crypt-node/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/sha512crypt-node/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sha512crypt-node/node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/sha512crypt-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/sha512crypt-node/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sha512crypt-node/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sha512crypt-node/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/sha512crypt-node/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sha512crypt-node/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/sha512crypt-node/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/sha512crypt-node/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sha512crypt-node/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/sha512crypt-node/node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "deprecated": "16.1.1", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + } + }, + "node_modules/sinon-chai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", "dev": true, "peerDependencies": { @@ -8535,7 +9073,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -8961,6 +9498,11 @@ "node": ">=0.4.0" } }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==" + }, "node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -9270,7 +9812,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, @@ -9707,9 +10248,9 @@ } }, "@particle/device-constants": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.7.0.tgz", - "integrity": "sha512-B6lPf5L0Zgffg/dbgkTAj2Wyk7AAKFqy7rMmL3pqiY0m/gYsVDhvPoqUeyeFKg8jIIkjGKjXHjLywGWBZ/8V1Q==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.8.0.tgz", + "integrity": "sha512-SyphIgh64z25S7aRMheenBvQ57+5Ta7BfY1hLI4NaAbyFu9NWLwceimK0442cChB1U1JgpekSTTO93d1cP1gAw==" }, "@particle/device-os-protobuf": { "version": "2.6.1", @@ -9996,6 +10537,11 @@ "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.7.tgz", "integrity": "sha512-PCKxgvnJ8wo2gCW8o96CmxDqG2okgsNDfa2xBCbHKvdmPRBTBLz6ClyRXtU+ylwvsqTYaLtJoxnGRG8nkcjd9A==" }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -10089,6 +10635,15 @@ "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "app-root-path": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", @@ -10357,6 +10912,11 @@ "chainsaw": "~0.1.0" } }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" + }, "binary-version-reader": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.5.1.tgz", @@ -10433,7 +10993,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -10441,8 +11000,7 @@ "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, "browserify-zlib": { "version": "0.1.4", @@ -10606,6 +11164,21 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -11178,8 +11751,7 @@ "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, "escape-string-regexp": { "version": "1.0.5", @@ -11332,38 +11904,11 @@ "argparse": "^2.0.1" } }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "path-exists": { "version": "4.0.0", @@ -11673,7 +12218,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -11857,6 +12401,12 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -11935,9 +12485,9 @@ "dev": true }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11951,7 +12501,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -11998,8 +12547,7 @@ "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" }, "gunzip-maybe": { "version": "1.4.1", @@ -12139,8 +12687,7 @@ "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "hoek": { "version": "2.16.3", @@ -12442,6 +12989,14 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -12491,8 +13046,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -12506,7 +13060,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -12536,8 +13089,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-object": { "version": "1.0.1", @@ -12596,6 +13148,11 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -12908,6 +13465,14 @@ "strip-bom": "^2.0.0" } }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -13410,6 +13975,11 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==" + }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -13888,6 +14458,22 @@ "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", "dev": true }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -14402,8 +14988,7 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pify": { "version": "2.3.0", @@ -15019,6 +15604,14 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -15118,6 +15711,14 @@ } } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, "regenerator-runtime": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz", @@ -15345,6 +15946,14 @@ } } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, "serialport": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-10.4.0.tgz", @@ -15391,6 +16000,292 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "sha512crypt-node": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sha512crypt-node/-/sha512crypt-node-1.0.2.tgz", + "integrity": "sha512-SmZnsrnp3T/+h/nd+/uAe4CKz2KgPBG0enskKE2qoEjzUUUM/1MYAPgBZRAHWAq6XEQl3XzSpS+bzl2rWWABnA==", + "requires": { + "mocha": "^9.1.3" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + } + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -16029,7 +16924,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -16398,6 +17292,11 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==" + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -16660,8 +17559,7 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "zip-stream": { "version": "4.1.0", diff --git a/package.json b/package.json index a1c567fe0..13cc91b94 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ } ], "dependencies": { - "@particle/device-constants": "^3.7.0", + "@particle/device-constants": "^3.8.0", "binary-version-reader": "^2.5.1", "chalk": "^2.4.2", "cli-progress": "^3.12.0", @@ -66,6 +66,7 @@ "jose": "^4.13.1", "lodash": "^4.17.15", "moment": "^2.24.0", + "node-fetch": "^2.7.0", "node-wifiscanner2": "^1.2.1", "particle-api-js": "^11.1.0", "particle-commands": "^1.0.1", @@ -75,6 +76,7 @@ "safe-buffer": "^5.2.0", "semver": "^7.5.2", "serialport": "^10.4.0", + "sha512crypt-node": "^1.0.2", "softap-setup": "^4.1.0", "temp": "^0.9.1", "unzipper": "^0.12.3", diff --git a/settings.js b/settings.js index e4e690878..e2f7d039b 100644 --- a/settings.js +++ b/settings.js @@ -39,7 +39,9 @@ let settings = { cloudKnownApps: { 'tinker': true - } + }, + + tachyonMeta: 'https://tachyon-ci.particle.io/meta' }; function envValue(varName, defaultValue) { diff --git a/src/cli/cloud.test.js b/src/cli/cloud.test.js index 8d68f5245..0335b9ba5 100644 --- a/src/cli/cloud.test.js +++ b/src/cli/cloud.test.js @@ -64,7 +64,7 @@ describe('Cloud Command-Line Interface', () => { 'Display a list of your devices, as well as their variables and functions', 'Usage: particle cloud list [options] [filter]', '', - 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2), a device ID or name', + 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2, tachyon), a device ID or name', '' ].join('\n')); }); @@ -308,7 +308,7 @@ describe('Cloud Command-Line Interface', () => { ' particle cloud compile photon Compile the source code in the current directory in the cloud for a `photon`', ' particle cloud compile electron project --saveTo electron.bin Compile the source code in the project directory in the cloud for an `electron` and save it to a file named `electron.bin`', '', - 'Param deviceType can be: core, c, photon, p, p1, electron, e, argon, a, boron, b, xenon, x, esomx, bsom, b5som, tracker, assettracker, trackerm, p2, photon2, msom, muon, electron2', + 'Param deviceType can be: core, c, photon, p, p1, electron, e, argon, a, boron, b, xenon, x, esomx, bsom, b5som, tracker, assettracker, trackerm, p2, photon2, msom, muon, electron2, tachyon', '' ].join('\n')); }); diff --git a/src/cli/index.js b/src/cli/index.js index f76d491f4..535cb06bd 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -28,6 +28,7 @@ const webhook = require('./webhook'); const whoami = require('./whoami'); const wifi = require('./wifi'); const usb = require('./usb'); +const tachyon = require('./tachyon'); /** * The default function export from this module registers all the available commands. @@ -64,6 +65,7 @@ module.exports = function registerAllCommands(context) { publish(context); serial(context); subscribe(context); + tachyon(context); token(context); udp(context); updateCli(context); diff --git a/src/cli/tachyon.js b/src/cli/tachyon.js new file mode 100644 index 000000000..fa7bc0904 --- /dev/null +++ b/src/cli/tachyon.js @@ -0,0 +1,80 @@ +module.exports = ({ commandProcessor, root }) => { + const tachyon = commandProcessor.createCategory(root, 'tachyon', 'Setup Particle devices'); + + commandProcessor.createCommand(tachyon, 'setup', 'Setup a Tachyon device', { + options: { + skip_flashing_os: { + description: 'Skip flashing the Operating System', + boolean: true + }, + version: { + description: 'Version to download package for (default: latest). Can include a directory or a local zip file', + type: 'string' + }, + load_config: { + description: 'Path to a config file to use for setup', + type: 'string' + }, + save_config: { + description: 'Path to dump the config file to after setup', + type: 'string' + } + }, + handler: (args) => { + const SetupTachyonCommands = require('../cmd/setup-tachyon'); + return new SetupTachyonCommands().setup(args); + }, + examples: { + '$0 $command': 'Setup a Tachyon device' + } + }); + + commandProcessor.createCommand(tachyon, 'download-package', 'Download a Tachyon package', { + options: { + region: { + description: 'Region to download package for', + type: 'string', + }, + version: { + description: 'Version to download package for', + type: 'string' + } + }, + handler: (args) => { + const DownloadTachyonPackageCommand = require('../cmd/download-tachyon-package'); + return new DownloadTachyonPackageCommand().download(args); + }, + examples: { + '$0 $command --region': 'Download a Tachyon package for the US region and version 1.0.0', + '$0 $command --region NA --version 1.0.0': 'Download a Tachyon package for the North America region and version 1.0.0' + } + }); + + commandProcessor.createCommand(tachyon, 'clean-cache', 'Clean the Tachyon package cache', { + options: { + region: { + description: 'Region to download package for', + type: 'string', + }, + version: { + description: 'Version to download package for', + type: 'string' + }, + all: { + description: 'Clean all cached packages', + boolean: true + } + }, + handler: (args) => { + const CleanPackageCacheCommand = require('../cmd/download-tachyon-package'); + return new CleanPackageCacheCommand().cleanUp(args); + }, + examples: { + '$0 $command --region NA --version 1.0.0': 'Clean the Tachyon package cache for the North America region and version 1.0.0', + '$0 $command --all': 'Clean all cached packages', + } + }); + + return tachyon; +}; + diff --git a/src/cli/usb.test.js b/src/cli/usb.test.js index 6c07e3b2a..bb53cdeec 100644 --- a/src/cli/usb.test.js +++ b/src/cli/usb.test.js @@ -63,7 +63,7 @@ describe('USB Command-Line Interface', () => { ' --exclude-dfu Do not list devices which are in DFU mode [boolean]', ' --ids-only Print only device IDs [boolean]', '', - 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2), a device ID or name', + 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2, tachyon), a device ID or name', '' ].join('\n')); }); diff --git a/src/cli/wifi.js b/src/cli/wifi.js index 9f8ff5978..0b17b387f 100644 --- a/src/cli/wifi.js +++ b/src/cli/wifi.js @@ -1,7 +1,7 @@ const unindent = require('../lib/unindent'); module.exports = ({ commandProcessor, root }) => { - const wifi = commandProcessor.createCategory(root, 'wifi', 'Configure Wi-Fi credentials to your device (Supported on Gen 3+ devices).'); + const wifi = commandProcessor.createCategory(root, 'wifi', 'Configure Wi-Fi credentials to your device (Supported on Gen 3+ devices)'); commandProcessor.createCommand(wifi, 'add', 'Adds a Wi-Fi network to your device', { options: Object.assign({ diff --git a/src/cmd/api.js b/src/cmd/api.js index d37fed4fa..961b6af3e 100644 --- a/src/cmd/api.js +++ b/src/cmd/api.js @@ -232,6 +232,43 @@ module.exports = class ParticleApi { ); } + getOrgs() { + return this._wrap( + this.api.get({ + uri: '/v1/orgs', + auth: this.accessToken + }) + ); + } + + getRegistrationCode(productId) { + return this._wrap( + this.api.post({ + uri: `/v1/products/${productId}/registration_code`, + auth: this.accessToken + }) + ); + } + + createProduct({ name, type, org }) { + return this._wrap( + this.api.post({ + uri: `/v1${org ? `/orgs/${org}` : ''}/products`, + form: { name, type }, + auth: this.accessToken + }) + ); + } + + getProducts(org) { + return this._wrap( + this.api.get({ + uri: `/v1${org ? `/orgs/${org}` : ''}/products`, + auth: this.accessToken + }) + ); + } + getDevice({ deviceId: id }) { return this.api.getDevice({ deviceId: id, auth: this.accessToken }); } diff --git a/src/cmd/download-tachyon-package.js b/src/cmd/download-tachyon-package.js new file mode 100644 index 000000000..cd3d9658b --- /dev/null +++ b/src/cmd/download-tachyon-package.js @@ -0,0 +1,84 @@ +const CLICommandBase = require('./base'); +const DownloadManager = require('../lib/download-manager'); + +module.exports = class DownloadTachyonPackageCommand extends CLICommandBase { + constructor({ ui } = {}) { + super(); + this.ui = ui || this.ui; + } + + async _selectRegion() { + const regionMapping = { + 'NA (North America)': 'NA', + 'RoW (Rest of the World)': 'RoW' + }; + const question = [ + { + type: 'list', + name: 'region', + message: 'Select the region:', + choices: Object.keys(regionMapping), + }, + ]; + const { region } = await this.ui.prompt(question); + return regionMapping[region]; + } + + async _selectVersion() { + const question = [ + { + type: 'input', + name: 'version', + message: 'Enter the version number:', + default: 'latest', + }, + ]; + const answer = await this.ui.prompt(question); + return answer.version; + } + async download ({ region, version }) { + // prompt for region and version if not provided + if (!region) { + region = await this._selectRegion(); + } + if (!version) { + version = await this._selectVersion(); + } + const manager = new DownloadManager(this.ui); + const manifest = await manager.fetchManifest({ version }); + const build = manifest.builds.find((b) => b.region === region); + if (!build) { + throw new Error(`No build available for region: ${region}`); + } + const { artifact_url: url, sha256_checksum: expectedChecksum } = build.artifacts[0]; + const outputFileName = url.replace(/.*\//, ''); + const filePath = await manager.download({ url, outputFileName, expectedChecksum }); + this.ui.write(`Downloaded package to: ${filePath}`); + + return filePath; + } + + async cleanUp({ region, version, all }) { + const manager = new DownloadManager(this.ui); + if (all) { + await manager.cleanup({ cleanDownload: true, cleanInProgress: true }); + this.ui.write('Cleaned up all cached packages'); + } else { + if (!region) { + region = await this._selectRegion(); + } + if (!version) { + version = await this._selectVersion(); + } + const manifest = await manager.fetchManifest({ version }); + const build = manifest.builds.find((b) => b.region === region); + if (!build) { + throw new Error(`No build available for region: ${region}`); + } + const { artifact_url: url } = build.artifacts[0]; + const outputFileName = url.replace(/.*\//, ''); + await manager.cleanup({ cleanDownload: false, fileName: outputFileName }); + this.ui.write(`Cleaned up package cache for region: ${region} and version: ${version}`); + } + } +}; diff --git a/src/cmd/flash.js b/src/cmd/flash.js index a64717c38..46e91a94c 100644 --- a/src/cmd/flash.js +++ b/src/cmd/flash.js @@ -70,9 +70,8 @@ module.exports = class FlashCommand extends CLICommandBase { } } + //returns true if successful or false if failed async flashTachyon({ files, output }) { - this.ui.write(`${os.EOL}Ensure that only one device is connected to the computer before proceeding.${os.EOL}`); - let zipFile; let includeDir = ''; let updateFolder = ''; @@ -82,7 +81,7 @@ module.exports = class FlashCommand extends CLICommandBase { files = ['.']; } - const input = files[0]; + const [input, ...rest] = files; const stats = await fs.stat(input); let filesToProgram; @@ -97,17 +96,17 @@ module.exports = class FlashCommand extends CLICommandBase { const zipInfo = await this._extractFlashFilesFromZip(input); includeDir = zipInfo.baseDir; filesToProgram = zipInfo.filesToProgram.map((file) => path.join(includeDir, file)); + filesToProgram.push(...rest); } else { filesToProgram = files; } - this.ui.write(`Starting download. This may take several minutes...${os.EOL}`); if (output && !fs.existsSync(output)) { fs.mkdirSync(output); } const outputLog = path.join(output ? output : process.cwd(), `tachyon_flash_${Date.now()}.log`); try { - this.ui.write(`Logs are being written to: ${outputLog}${os.EOL}`); + this.ui.write(`Starting download. This may take several minutes. See logs at: ${outputLog}${os.EOL}`); const qdl = new QdlFlasher({ files: filesToProgram, includeDir, @@ -118,9 +117,11 @@ module.exports = class FlashCommand extends CLICommandBase { }); await qdl.run(); fs.appendFileSync(outputLog, 'Download complete.'); + return true; } catch (error) { this.ui.write('Download failed'); fs.appendFileSync(outputLog, 'Download failed with error: ' + error.message); + return false; } } diff --git a/src/cmd/setup-tachyon.js b/src/cmd/setup-tachyon.js new file mode 100644 index 000000000..38bdf5e3f --- /dev/null +++ b/src/cmd/setup-tachyon.js @@ -0,0 +1,547 @@ +const CLICommandBase = require('./base'); +const spinnerMixin = require('../lib/spinner-mixin'); +const fs = require('fs-extra'); +const ParticleApi = require('./api'); +const settings = require('../../settings'); +const createApiCache = require('../lib/api-cache'); +const ApiClient = require('../lib/api-client'); +const crypto = require('crypto'); +const temp = require('temp').track(); +const os = require('os'); +const FlashCommand = require('./flash'); +const CloudCommand = require('./cloud'); +const { sha512crypt } = require('sha512crypt-node'); +const DownloadManager = require('../lib/download-manager'); +const { platformForId } = require('../lib/platform'); + +module.exports = class SetupTachyonCommands extends CLICommandBase { + constructor({ ui } = {}) { + super(); + spinnerMixin(this); + const { api } = this._particleApi(); + this.api = api; + this.ui = ui || this.ui; + this._userConfiguration = this._userConfiguration.bind(this); + this._getSystemPassword = this._getSystemPassword.bind(this); + this._getWifi = this._getWifi.bind(this); + this._getKeys = this._getKeys.bind(this); + this._runStepWithTiming = this._runStepWithTiming.bind(this); + this._formatAndDisplaySteps = this._formatAndDisplaySteps.bind(this); + } + + async setup({ skip_flashing_os: skipFlashingOs, version, load_config: loadConfig, save_config: saveConfig }) { + try { + this.ui.write(` +=========================================================== + Particle Tachyon Setup Command +=========================================================== + +Welcome to the Particle Tachyon setup! This interactive command: + +- Flashes your Tachyon device +- Configures it (password, WiFi credentials etc...) +- Connects it to the internet and the Particle Cloud! + +**What you'll need:** + +1. Your Tachyon device +2. The Tachyon battery +3. A USB-C cable + +**Important:** +- This tool requires you to be logged into your Particle account. +- For more details, check out the documentation at: https://part.cl/setup-tachyon`); + + this._formatAndDisplaySteps("Okay—first up! Checking if you're logged in...", 1); + + await this._verifyLogin(); + + this.ui.write("...All set! You're logged in and ready to go!"); + + const region = 'NA'; //await this._selectRegion(); + + //if version is not provided, set to latest + if (!version) { + version = 'latest'; //await this._selectVersion(); + } + + let config = { systemPassword: null, wifi: null, sshPublicKey: null }; + + if ( !loadConfig ) { + config = await this._runStepWithTiming( + `Now lets capture some information about how you'd like your device to be configured when it first boots.${os.EOL}${os.EOL}` + + `First, you'll be asked to set a password for the root account on your Tachyon device.${os.EOL}` + + `Don't worry if you forget this—you can always reset your device later.${os.EOL}${os.EOL}` + + `Next, you'll be prompted to provide an optional Wi-Fi network.${os.EOL}` + + `While the 5G cellular connection will automatically connect, Wi-Fi is often much faster for use at home.${os.EOL}${os.EOL}` + + `Finally, you'll have the option to add an SSH key from your local disk.${os.EOL}` + + 'This is optional—you can still SSH into the device using a password. Adding the key just allows for password-free access.', + 2, + () => this._userConfiguration(), + 0 + ); + } else { + this.ui.write( + `${os.EOL}${os.EOL}Skipping to Step 3 - Using configuration file: ` + loadConfig + `${os.EOL}` + ); + } + + const product = await this._runStepWithTiming( + `Next, let's select a Particle organization that you are part of.${os.EOL}` + + `This organization will help manage the Tachyon device and keep things organized.${os.EOL}${os.EOL}` + + "Once you've selected an organization, you can then choose which product the device will belong to.", + 3, + () => this._selectProduct() + ); + + const packagePath = await this._runStepWithTiming( + `Next, we'll download the Tachyon Operating System image.${os.EOL}` + + `Heads up: it's a large file — 2.6GB! Don't worry, though—the download will resume${os.EOL}` + + `if it's interrupted. If you have to kill the CLI, it will pick up where it left. You can also${os.EOL}` + + "just let it run in the background. We'll wait for you to be ready when its time to flash the device.", + 4, + () => this._download({ region, version }) + ); + + const registrationCode = await this._runStepWithTiming( + `Great! The download is complete.${os.EOL}` + + "Now, let's register your product on the Particle platform.", + 5, + () => this._getRegistrationCode(product) + ); + + let configBlobPath = loadConfig; + if (configBlobPath) { + this.ui.write( + `${os.EOL}${os.EOL}Skipping Step 6 - Using configuration file: ` + loadConfig + `${os.EOL}` + ); + } else { + configBlobPath = await this._runStepWithTiming( + 'Creating the configuration file to write to the Tachyon device...', + 6, + () => this._createConfigBlob({ registrationCode, ...config }) + ); + } + const xmlPath = await this._createXmlFile(configBlobPath); + + if (saveConfig) { + this.ui.write(`${os.EOL}${os.EOL}Configuration file written here: ${saveConfig}${os.EOL}`); + fs.copyFileSync(configBlobPath, saveConfig); + } + + //what files to flash? + const filesToFlash = skipFlashingOs ? [xmlPath] : [packagePath, xmlPath]; + + const flashSuccessful = await this._runStepWithTiming( + `Okay—last step! We're now flashing the device with the configuration, including the password, Wi-Fi settings, and operating system.${os.EOL}` + + `Heads up: this is a large image and will take around 10 minutes to complete. Don't worry—we'll show a progress bar as we go!${os.EOL}${os.EOL}` + + `Before we get started, we need to power on your Tachyon board:${os.EOL}${os.EOL}` + + `1. Plug the USB-C cable into your computer and the Tachyon board.${os.EOL}` + + ` The red light should turn on!${os.EOL}${os.EOL}` + + `2. Put the Tachyon device into download mode:${os.EOL}` + + ` - Hold the button next to the red LED for 3 seconds.${os.EOL}` + + ` - When the light starts flashing yellow, release the button.${os.EOL}` + + ' Your device is now in flashing mode!', + 7, + () => this._flash(filesToFlash) + ); + + if (flashSuccessful) { + this._formatAndDisplaySteps( + `All done! Your Tachyon device is now booting into the operating system and will automatically connect to Wi-Fi.${os.EOL}${os.EOL}` + + `It will also:${os.EOL}` + + ` - Activate the built-in 5G modem${os.EOL}` + + ` - Connect to the Particle Cloud${os.EOL}` + + ` - Run all system services, including battery charging${os.EOL}${os.EOL}` + + 'For more information about Tachyon, visit our developer site at: developer.particle.io!', + 8 + ); + } else { + this.ui.write( + `${os.EOL}Flashing failed. Please unplug your device and rerun this. We're going to have to try it again.${os.EOL}` + + `If it continues to fail, please select a different USB port or visit https://part.cl/setup-tachyon and the setup link for more information.${os.EOL}` + ); + } + + } catch (error) { + throw new Error(`${os.EOL}There was an error setting up Tachyon:${os.EOL}${os.EOL} >> ${error.message}${os.EOL}`); + } + } + + async _runStepWithTiming(stepDesc, stepNumber, asyncTask, minDuration = 2000) { + this._formatAndDisplaySteps(stepDesc, stepNumber); + + const startTime = Date.now(); + + try { + const result = await asyncTask(); + const elapsed = Date.now() - startTime; + + if (elapsed < minDuration) { + await new Promise((resolve) => setTimeout(resolve, minDuration - elapsed)); + } + + return result; + } catch (err) { + throw new Error(`Step ${stepNumber} failed with the following error: ${err.message}`); + } + } + + async _formatAndDisplaySteps(text, step) { + // Display the formatted step + this.ui.write(`${os.EOL}===========================================================${os.EOL}`); + this.ui.write(`Step ${step}:${os.EOL}`); + this.ui.write(`${text}${os.EOL}`); + } + + async _verifyLogin() { + const api = new ApiClient(); + try { + api.ensureToken(); + } catch { + const cloudCommand = new CloudCommand(); + await cloudCommand.login(); + } + } + + async _selectRegion() { + const regionMapping = { + 'NA (North America)': 'NA', + 'RoW (Rest of the World)': 'RoW' + }; + const question = [ + { + type: 'list', + name: 'region', + message: 'Select the region:', + choices: Object.keys(regionMapping), + }, + ]; + const { region } = await this.ui.prompt(question); + return regionMapping[region]; + } + + async _selectVersion() { + const question = [ + { + type: 'input', + name: 'version', + message: 'Enter the version number:', + default: 'latest', + }, + ]; + const answer = await this.ui.prompt(question); + return answer.version; + } + + async _selectProduct() { + const { orgName, orgSlug } = await this._getOrg(); + + let productId = await this._getProduct(orgName, orgSlug); + + if (!productId) { + productId = await this._createProduct(orgSlug); + } + return productId; + } + + async _getOrg() { + const orgsResp = await this.api.getOrgs(); + const orgs = orgsResp.organizations; + + const orgName = orgs.length + ? await this._promptForOrg([...orgs.map(org => org.name), 'Sandbox']) + : 'Sandbox'; + + const orgSlug = orgName !== 'Sandbox' ? orgs.find(org => org.name === orgName).slug : null; + return { orgName, orgSlug }; + } + + async _promptForOrg(choices) { + const question = [ + { + type: 'list', + name: 'org', + message: 'Select an organization:', + choices, + }, + ]; + const { org } = await this.ui.prompt(question); + return org; + } + + async _getProduct(orgName, orgSlug) { + const productsResp = await this.api.getProducts(orgSlug); + + //if orgSlug is not null, filter for this org from product.organization_id + //if orgSlug is null, filter for an empty field in product.organization_id + let products = []; + if (orgSlug) { + products = productsResp.products.filter((product) => product.org === orgName); + } else { + products = productsResp.products.filter((product) => !product.org); + } + + products = products.filter((product) => platformForId(product.platform_id)?.name === 'tachyon'); + + if (!products.length) { + return null; // No products available + } + + const selectedProductName = await this._promptForProduct(products.map(product => product.name)); + + const selectedProduct = products.find(p => p.name === selectedProductName); + + return selectedProduct?.id || null; + } + + async _promptForProduct(choices) { + const question = [ + { + type: 'list', + name: 'product', + message: 'Select a product:', + choices, + }, + ]; + const { product } = await this.ui.prompt(question); + return product; + } + + async _createProduct() { + // It appears that CLI code base does not have a method to create a product readily available + // TODO: Discuss with the team to add a method to create a product + // For now though, we will return an error + throw new Error('No products available. Create a product in the console and return to continue.'); + } + + async _userConfiguration() { + const systemPassword = await this._getSystemPassword(); + + const wifi = await this._getWifi(); + + const sshPublicKey = await this._getKeys(); + + return { systemPassword, wifi, sshPublicKey }; + } + + async _download({ region, version }) { + + //before downloading a file, we need to check if 'version' is a local file or directory + //if it is a local file or directory, we need to return the path to the file + if (fs.existsSync(version)) { + return version; + } + + const manager = new DownloadManager(this.ui); + const manifest = await manager.fetchManifest({ version }); + const build = manifest?.builds.find(build => build.region === region); + if (!build) { + throw new Error('No builds available for the selected region'); + } + const artifact = build.artifacts[0]; + const url = artifact.artifact_url; + const outputFileName = url.replace(/.*\//, ''); + const expectedChecksum = artifact.sha256_checksum; + + return manager.download({ url, outputFileName, expectedChecksum }); + } + + async _getSystemPassword() { + const questions = [ + { + type: 'password', + name: 'password', + message: 'Password for the system account:', + validate: (value) => { + if (!value) { + return 'Enter a password for the root account'; + } + return true; + } + }, + { + type: 'password', + name: 'passwordConfirm', + message: 'Re-enter the password for the root account:', + validate: (value) => { + if (!value) { + return 'You need to confirm the password'; + } + return true; + } + } + ]; + const res = await this.ui.prompt(questions); + + //check if the passwords match + if (res.password !== res.passwordConfirm) { + throw new Error('Passwords do not match. Please try again.'); + } + + return res.password; + } + + async _getWifi() { + const question = [ + { + type: 'input', + name: 'setupWifi', + message: 'Would you like to set up WiFi for your device? (y/n):', + default: 'y', + } + ]; + const { setupWifi } = await this.ui.prompt(question); + if (setupWifi.toLowerCase() === 'y') { + return this._getWifiCredentials(); + } + + return null; + } + + async _getWifiCredentials() { + const questions = [ + { + type: 'input', + name: 'ssid', + message: 'Enter your WiFi SSID:' + }, + { + type: 'password', + name: 'password', + message: 'Enter your WiFi password:' + }, + { + type: 'password', + name: 'passwordConfirm', + message: 'Re-enter your WiFi password:' + }, + ]; + const res = await this.ui.prompt(questions); + + if (res.password !== res.passwordConfirm) { + throw new Error('Passwords do not match. Please try again.'); + } + + return { ssid: res.ssid, password: res.password }; + } + + async _getKeys() { + let question = [ + { + type: 'input', + name: 'addKey', + message: 'Would you like to add an SSH key to log in to your device? (y/n):', + default: 'y', + } + ]; + const { addKey } = await this.ui.prompt(question); + if (addKey.toLowerCase() !== 'y') { + return; + } + + question = [ + { + type: 'input', + name: 'sshKey', + message: 'Enter the path to your SSH public key:', + validate: (value) => { + if (!fs.existsSync(value)) { + return 'You need to provide a path to your SSH public key'; + } + return true; + } + }, + ]; + + const { sshKey } = await this.ui.prompt(question); + return fs.readFileSync(sshKey, 'utf8'); + } + + async _getRegistrationCode(product) { + const data = await this.api.getRegistrationCode(product); + return data.registration_code; + } + + async _createConfigBlob({ registrationCode, systemPassword, wifi, sshKey }) { + // Format the config and registration code into a config blob (JSON file, prefixed by the file size) + const config = { + registration_code: registrationCode, + system_password : this._generateShadowCompatibleHash(systemPassword), + }; + + if (wifi) { + config.wifi = wifi; + } + + if (sshKey) { + config.ssh_key = sshKey; + } + + // Write config JSON to a temporary file (generate a filename with the temp npm module) + // prefixed by the JSON string length as a 32 bit integer + let jsonString = JSON.stringify(config); + const buffer = Buffer.alloc(4 + Buffer.byteLength(jsonString)); + buffer.writeUInt32BE(Buffer.byteLength(jsonString), 0); + buffer.write(jsonString, 4); + + const tempFile = temp.openSync(); + fs.writeSync(tempFile.fd, buffer); + fs.closeSync(tempFile.fd); + + return tempFile.path; + } + + _generateShadowCompatibleHash(password) { + const salt = crypto.randomBytes(12).toString('base64'); + return sha512crypt(password, `$6$${salt}`); + } + + async _createXmlFile(configBlobPath) { + const xmlContent = [ + '', + '', + ' ', + '' + ].join(os.EOL); + + // Create a temporary file for the XML content + const tempFile = temp.openSync(); + fs.writeSync(tempFile.fd, xmlContent, 0, xmlContent.length, 0); + fs.closeSync(tempFile.fd); + return tempFile.path; + } + + async _flash(files) { + const question = { + type: 'confirm', + name: 'flash', + message: 'Is the device powered, its LED flashing yellow and a USB-C cable plugged in from your computer?', + default: true + }; + await this.ui.prompt(question); + + const flashCommand = new FlashCommand(); + return await flashCommand.flashTachyon({ files }); + } + + _particleApi() { + const auth = settings.access_token; + const api = new ParticleApi(settings.apiUrl, { accessToken: auth } ); + const apiCache = createApiCache(api); + return { api: apiCache, auth }; + } +}; diff --git a/src/lib/download-manager.js b/src/lib/download-manager.js new file mode 100644 index 000000000..f31866c5c --- /dev/null +++ b/src/lib/download-manager.js @@ -0,0 +1,224 @@ +const settings = require('../../settings'); +const fs = require('fs-extra'); +const path = require('path'); +const fetch = require('node-fetch'); +const UI = require('./ui'); +const crypto = require('crypto'); +const { delay } = require('./utilities'); + +class DownloadManager { + /** + * @param {UI} [ui] - The UI object to use for logging + */ + constructor(ui = new UI()) { + const particleDir = settings.ensureFolder(); + this.ui = ui; + this._baseDir = path.join(particleDir); + this._downloadDir = path.join(this._baseDir, 'downloads'); + this._ensureWorkDir(); + } + + get downloadDir() { + return this._downloadDir; + } + + _ensureWorkDir() { + try { + // Create the download directory if it doesn't exist + fs.mkdirSync(this.downloadDir, { recursive: true }); + } catch (error) { + this.ui.error(`Error creating directories: ${error.message}`); + throw error; + } + } + + async fetchManifest({ version = 'latest' }) { + const metadataUrl = `${settings.tachyonMeta}/tachyon-${encodeURIComponent(version)}.json`; + + try { + const response = await fetch(metadataUrl); + if (!response.ok) { + if (response.status === 404) { + throw new Error('Version file not found. Please check the version number and try again.'); + } else { + throw new Error('An error occurred while downloading the version file. Please try again later.'); + } + } + + return response.json(); + } catch (err) { + console.log(err); + throw new Error('Could not download the version file. Please check your internet connection.'); + } + } + + async download({ url, outputFileName, expectedChecksum, options = {} }) { + // Check cache + const cachedFile = await this._getCachedFile(outputFileName, expectedChecksum); + if (cachedFile) { + this.ui.write(`Using cached file: ${cachedFile}`); + return cachedFile; + } + const filePath = await this._downloadFile(url, outputFileName, options); + // Validate checksum after download completes + // Validate checksum after download completes + try { + if (expectedChecksum) { + await this._validateChecksum(filePath, expectedChecksum); + } + } catch (error) { + // Remove the invalid file + await fs.remove(filePath); + this.ui.write(`Removed invalid downloaded file: ${outputFileName}`); + throw error; + } + + return filePath; + } + + + async _downloadFile(url, outputFileName, options = {}) { + const { maxRetries = 5, timeout = 10000, waitTime = 5000 } = options; + const progressFilePath = path.join(this.downloadDir, `${outputFileName}.progress`); + const finalFilePath = path.join(this.downloadDir, outputFileName); + let attempt = 0; + while (attempt < maxRetries) { + try { + return await this._attemptDownload(url, outputFileName, progressFilePath, finalFilePath, timeout); + } catch (error) { + attempt++; + if (attempt >= maxRetries) { + throw new Error(`Failed to download file after ${maxRetries} attempts: ${error.message}`); + } + this.ui.write(`Retrying download for ${outputFileName} after waiting for ${waitTime}ms...`); + await delay(waitTime); + } + } + } + + async _attemptDownload(url, outputFileName, progressFilePath, finalFilePath, timeout) { + const progressBar = this.ui.createProgressBar(); + let downloadedBytes = 0; + if (fs.existsSync(progressFilePath)) { + downloadedBytes = fs.statSync(progressFilePath).size; + this.ui.write(`Resuming download file: ${outputFileName}`); + } + const headers = downloadedBytes > 0 ? { Range: `bytes=${downloadedBytes}-` } : {}; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + try { + const response = await fetch(url, { headers, signal: controller.signal }); + clearTimeout(timeoutId); + if (!response.ok && response.status !== 206) { + throw new Error(`Unexpected response status: ${response.status}`); + } + const totalBytes = parseInt(response.headers.get('content-length') || '0', 10) + downloadedBytes; + if (progressBar && totalBytes) { + progressBar.start(totalBytes, downloadedBytes, { description: `Downloading ${outputFileName} ...` }); + } + await this._streamToFile(response.body, progressFilePath, progressBar, downloadedBytes, timeout, controller); + fs.renameSync(progressFilePath, finalFilePath); + this.ui.write(`Download completed: ${finalFilePath}`); + return finalFilePath; + } finally { + if (progressBar) { + progressBar.stop(); + } + } + } + + async _streamToFile(stream, filePath, progressBar, downloadedBytes, timeout, controller) { + const writer = fs.createWriteStream(filePath, { flags: 'a' }); + return new Promise((resolve, reject) => { + let streamTimeout = setTimeout(() => { + controller.abort(); + reject(new Error('Stream timeout')); + }, timeout); + stream.on('data', (chunk) => { + clearTimeout(streamTimeout); + streamTimeout = setTimeout(() => { + controller.abort(); + reject(new Error('Stream timeout')); + }, timeout); + downloadedBytes += chunk.length; + if (progressBar) { + progressBar.increment(chunk.length); + } + }); + stream.pipe(writer); + stream.on('error', (err) => { + clearTimeout(streamTimeout); + reject(err); + }); + writer.on('finish', () => { + clearTimeout(streamTimeout); + resolve(); + }); + }); + } + + + async _getCachedFile(fileName, expectedChecksum) { + const cachedFilePath = path.join(this.downloadDir, fileName); + if (fs.existsSync(cachedFilePath)) { + if (expectedChecksum) { + try { + await this._validateChecksum(cachedFilePath, expectedChecksum); + return cachedFilePath; + } catch (error) { + this.ui.write(`Cached file checksum mismatch for ${fileName}`); + await fs.remove(cachedFilePath); // Remove the invalid cached file + this.ui.write(`Removed invalid cached file: ${fileName}`); + } + } + } + return null; + } + + async _validateChecksum(filePath, expectedChecksum) { + return this.ui.showBusySpinnerUntilResolved('Performing checksum validation...', new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256'); + const stream = fs.createReadStream(filePath); + + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => { + const fileChecksum = hash.digest('hex'); + if (fileChecksum !== expectedChecksum) { + reject(new Error(`Checksum validation failed for ${path.basename(filePath)}. Expected: ${expectedChecksum}, Got: ${fileChecksum}`)); + } else { + resolve(); + } + }); + stream.on('error', (error) => { + reject(new Error(`Error reading file for checksum validation: ${error.message}`)); + }); + })); + + } + + async cleanup({ fileName, cleanInProgress = false, cleanDownload = true } = {}) { + try { + if (fileName) { + // Remove specific file and its progress file + await fs.remove(path.join(this.downloadDir, fileName)); + await fs.remove(path.join(this.downloadDir, `${fileName}.progress`)); + } else if (cleanInProgress) { + // Remove all in-progress files + const files = (await fs.readdir(this.downloadDir)).filter(file => file.endsWith('.progress')); + await Promise.all(files.map(file => fs.remove(path.join(this.downloadDir, file)))); + files.forEach(file => this.ui.write(`Removed in-progress file: ${file}`)); + if (cleanDownload) { + await fs.remove(this.downloadDir); + } + } else if (cleanDownload) { + // Remove the entire download directory + await fs.remove(this.downloadDir); + } + } catch (error) { + this.ui.error(`Error cleaning up directory: ${error.message}`); + throw error; + } + } +} + +module.exports = DownloadManager; diff --git a/src/lib/download-manager.test.js b/src/lib/download-manager.test.js new file mode 100644 index 000000000..9a31dad50 --- /dev/null +++ b/src/lib/download-manager.test.js @@ -0,0 +1,195 @@ +const { expect, sinon } = require('../../test/setup'); +const DownloadManager = require('./download-manager'); +const nock = require('nock'); +const fs = require('fs-extra'); +const path = require('path'); +const { PATH_TMP_DIR } = require('../../test/lib/env'); +const UI = require('./ui'); + +describe('DownloadManager', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { + ...originalEnv, + home: PATH_TMP_DIR, + }; + }); + + afterEach(async () => { + sinon.restore(); + process.env = originalEnv; + await fs.remove(path.join(PATH_TMP_DIR, '.particle/downloads')); + }); + + describe('initialize', () => { + it('creates a download manager', () => { + const downloadManager = new DownloadManager(); + expect(downloadManager.downloadDir).to.eql(path.join(PATH_TMP_DIR,'.particle/downloads')); + expect(fs.existsSync(downloadManager.downloadDir)).to.be.true; + }); + }); + + describe('cleanup', () => { + it('removes the download directory', async () => { + const downloadManager = new DownloadManager(); + await fs.mkdirp(downloadManager.downloadDir); + expect(fs.existsSync(downloadManager.downloadDir)).to.be.true; + + await downloadManager.cleanup(); + expect(fs.existsSync(downloadManager.downloadDir)).to.be.false; + }); + + it('removes a specific file from the download directory', async () => { + const downloadManager = new DownloadManager(); + const testFile = path.join(downloadManager.downloadDir, 'test.bin'); + await fs.ensureFile(testFile); + expect(fs.existsSync(testFile)).to.be.true; + + await downloadManager.cleanup({ fileName: 'test.bin' }); + expect(fs.existsSync(testFile)).to.be.false; + }); + + it('removes only in-progress files', async () => { + const downloadManager = new DownloadManager(); + const progressFile = path.join(downloadManager.downloadDir, 'test.bin.progress'); + const completedFile = path.join(downloadManager.downloadDir, 'test.bin'); + + // Create both progress and completed files + await fs.ensureFile(progressFile); + await fs.ensureFile(completedFile); + + // Verify files exist + expect(fs.existsSync(progressFile)).to.be.true; + expect(fs.existsSync(completedFile)).to.be.true; + + // Remove only in-progress files + await downloadManager.cleanup({ cleanInProgress: true, cleanDownload: false }); + + // Verify the progress file is removed and the completed file remains + expect(fs.existsSync(progressFile)).to.be.false; + expect(fs.existsSync(completedFile)).to.be.true; + }); + }); + + describe('download', () => { + let downloadManager; + let ui; + beforeEach(() => { + ui = sinon.createStubInstance(UI, { + write: sinon.stub(), + error: sinon.stub(), + createProgressBar: sinon.stub(), + showBusySpinnerUntilResolved: sinon.stub().callsFake((text, promise) => promise), + }); + downloadManager = new DownloadManager(ui); + }); + it('downloads a file', async () => { + const url = 'https://example.com'; + const outputFileName = 'file.txt'; + const fileContent = 'This is a test file.'; + + // Mock the HTTP response + nock(url) + .get(`/${outputFileName}`) + .reply(200, fileContent); + + const result = await downloadManager.download({ url: `${url}/${outputFileName}`, outputFileName }); + console.log(result); + const finalFilePath = path.join(downloadManager.downloadDir, outputFileName); + expect(fs.existsSync(finalFilePath)).to.be.true; + const content = fs.readFileSync(finalFilePath, 'utf8'); + expect(content).to.equal(fileContent); + }); + + it('resumes a download', async () => { + const url = 'https://example.com'; + const outputFileName = 'file.txt'; + const initialContent = 'This is a '; + const remainingContent = 'resumed download test.'; + const tempFilePath = path.join(downloadManager.downloadDir, `${outputFileName}.progress`); + + // Create a partial file + fs.writeFileSync(tempFilePath, initialContent); + + // Mock the HTTP response with a range + nock(url, { reqheaders: { Range: 'bytes=10-' } }) + .get(`/${outputFileName}`) + .reply(206, remainingContent); + + await downloadManager.download({ url: `${url}/${outputFileName}`, outputFileName }); + + const finalFilePath = path.join(downloadManager.downloadDir, outputFileName); + const content = fs.readFileSync(finalFilePath, 'utf8'); + expect(content).to.equal(initialContent + remainingContent); + }); + + it('throws an error if the download fails', async () => { + const url = 'https://example.com'; + const outputFileName = 'file.txt'; + let error; + + // Mock the HTTP response to simulate a failure + nock(url) + .get(`/${outputFileName}`) + .reply(500); + + try { + const options = { + timeout:100, + maxRetries: 1, + waitTime: 0, + }; + await downloadManager.download({ url:`${url}/${outputFileName}`, outputFileName, options }); + throw new Error('Expected method to throw.'); + } catch (_error) { + error = _error; + } + expect(error.message).to.include('Unexpected response status: 500'); + }); + + it('throws an error if checksum does not match', async () => { + const url = 'https://example.com'; + const outputFileName = 'file.txt'; + const fileContent = 'This is a test file.'; + let error; + + // Mock the HTTP response + nock(url) + .get(`/${outputFileName}`) + .reply(200, fileContent); + + try { + await downloadManager.download({ url:`${url}/${outputFileName}`, outputFileName, expectedChecksum: 'invalidchecksum' }); + } catch (_error) { + error = _error; + } + const tempPath = path.join(downloadManager.downloadDir, `${outputFileName}.progress`); + const finalPath = path.join(downloadManager.downloadDir, `${outputFileName}`); + expect(error.message).to.include('Checksum validation failed for file.txt'); + expect(fs.existsSync(tempPath)).to.be.false; + expect(fs.existsSync(finalPath)).to.be.false; + }); + it('validates checksum and save the file', async () => { + const url = 'https://example.com'; + const outputFileName = 'file.txt'; + const fileContent = 'This is a test file.'; + const checksum = 'f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de'; + + // Mock the HTTP response + nock(url) + .get(`/${outputFileName}`) + .reply(200, fileContent); + + await downloadManager.download({ url: `${url}/${outputFileName}`, outputFileName, expectedChecksum: checksum }); + + const tempFilePath = path.join(downloadManager.downloadDir, `${outputFileName}.progress`); + const finalFilePath = path.join(downloadManager.downloadDir, outputFileName); + expect(fs.existsSync(finalFilePath)).to.be.true; + expect(fs.existsSync(tempFilePath)).to.be.false; + const content = fs.readFileSync(finalFilePath, 'utf8'); + expect(content).to.equal(fileContent); + }); + }); + +}); diff --git a/src/lib/platform.js b/src/lib/platform.js index cb7e0baa5..a5b11522b 100644 --- a/src/lib/platform.js +++ b/src/lib/platform.js @@ -3,7 +3,7 @@ const deviceConstants = require('@particle/device-constants'); /** * Array of description objects for all supported platforms. */ -const PLATFORMS = Object.values(deviceConstants).filter(p => p.public); +const PLATFORMS = Object.values(deviceConstants).filter(p => p.public || p.name === 'tachyon'); // TODO: revisit to make Tachyon platform public const PLATFORMS_BY_ID = PLATFORMS.reduce((map, p) => map.set(p.id, p), new Map()); diff --git a/src/lib/platform.test.js b/src/lib/platform.test.js index 26c8d5fe6..b97f4e13e 100644 --- a/src/lib/platform.test.js +++ b/src/lib/platform.test.js @@ -8,7 +8,8 @@ const supportedPlatforms = Object.values(deviceConstants).filter(p => p.public); describe('Platform utilities', () => { describe('PLATFORMS', () => { it('contains description objects for all supported platforms', () => { - expect(PLATFORMS).to.deep.equal(supportedPlatforms); + const tachyon = PLATFORMS.find(p => p.name === 'tachyon'); // tachyon is not public yet + expect(PLATFORMS).to.deep.equal([...supportedPlatforms, tachyon]); }); }); diff --git a/src/lib/qdl.js b/src/lib/qdl.js index e075d0b01..b9fa38358 100644 --- a/src/lib/qdl.js +++ b/src/lib/qdl.js @@ -100,11 +100,6 @@ class QdlFlasher { } processFlashingLogs(line) { - if (!this.preparingDownload) { - this.preparingDownload = true; - this.ui.stdout.write('Preparing for download...'); - } - if (line.includes('status=getProgramInfo')) { this.handleProgramInfo(line); } else if (line.includes('status=Start flashing module')) { @@ -115,6 +110,10 @@ class QdlFlasher { } handleProgramInfo(line) { + if (!this.preparingDownload) { + this.preparingDownload = true; + this.ui.stdout.write('Preparing to download files...'); + } const match = line.match(/sectors_total=(\d+)/); if (match) { this.totalSectorsInAllFiles += parseInt(match[1], 10); diff --git a/src/lib/utilities.test.js b/src/lib/utilities.test.js index af69acef4..153f9ab12 100644 --- a/src/lib/utilities.test.js +++ b/src/lib/utilities.test.js @@ -23,7 +23,8 @@ describe('Utilities', () => { 'trackerm': 28, 'p2': 32, 'msom': 35, - 'electron2': 37 + 'electron2': 37, + 'tachyon': 42 }); }); }); @@ -54,7 +55,8 @@ describe('Utilities', () => { 'photon2': 32, 'msom': 35, 'muon': 35, - 'electron2': 37 + 'electron2': 37, + 'tachyon': 42 }); }); }); @@ -76,7 +78,8 @@ describe('Utilities', () => { 28: 'Tracker-M', 32: 'Photon 2 / P2', 35: 'M-SoM', - 37: 'Electron 2' + 37: 'Electron 2', + 42: 'Tachyon' }); }); }); diff --git a/test/e2e/compile.e2e.js b/test/e2e/compile.e2e.js index c55eef41d..de6d4efa1 100644 --- a/test/e2e/compile.e2e.js +++ b/test/e2e/compile.e2e.js @@ -31,7 +31,7 @@ describe('Compile Commands', () => { ' particle compile photon Compile the source code in the current directory in the cloud for a `photon`', ' particle compile electron project --saveTo electron.bin Compile the source code in the project directory in the cloud for an `electron` and save it to a file named `electron.bin`', '', - 'Param deviceType can be: core, c, photon, p, p1, electron, e, argon, a, boron, b, xenon, x, esomx, bsom, b5som, tracker, assettracker, trackerm, p2, photon2, msom, muon, electron2', + 'Param deviceType can be: core, c, photon, p, p1, electron, e, argon, a, boron, b, xenon, x, esomx, bsom, b5som, tracker, assettracker, trackerm, p2, photon2, msom, muon, electron2, tachyon', ]; beforeEach(async () => { diff --git a/test/e2e/help.e2e.js b/test/e2e/help.e2e.js index 98681db88..6b59d3b39 100644 --- a/test/e2e/help.e2e.js +++ b/test/e2e/help.e2e.js @@ -36,6 +36,7 @@ describe('Help & Unknown Command / Argument Handling', () => { ' publish Publish an event to the cloud', ' serial Simple serial interface to your devices', ' subscribe Listen to device event stream', + ' tachyon Setup Particle devices', ' token Manage access tokens (require username/password)', ' udp Talk UDP to repair devices, run patches, check Wi-Fi, and more!', ' update Update Device OS on a device via USB', @@ -44,7 +45,7 @@ describe('Help & Unknown Command / Argument Handling', () => { ' variable Retrieve and monitor variables on your device', ' webhook Manage webhooks that react to device event streams', ' whoami prints signed-in username', - ' wifi Configure Wi-Fi credentials to your device (Supported on Gen 3+ devices).' + ' wifi Configure Wi-Fi credentials to your device (Supported on Gen 3+ devices)' ]; const allCmds = ['binary inspect', 'binary enable-device-protection', 'binary list-assets', 'binary strip-assets', 'binary', 'bundle', 'call', 'cloud list', @@ -66,9 +67,9 @@ describe('Help & Unknown Command / Argument Handling', () => { 'product device list', 'product device add', 'product device remove', 'product device', 'product', 'project create', 'project', 'publish', 'serial list', 'serial monitor', 'serial identify', 'serial wifi', - 'serial mac', 'serial inspect', 'serial flash', - 'serial', 'subscribe', 'token revoke', - 'token create', 'token', 'udp send', 'udp listen', 'udp', 'update', + 'serial mac', 'serial inspect', 'serial flash', 'serial', 'subscribe', + 'tachyon setup', 'tachyon download-package', 'tachyon clean-cache', 'tachyon', + 'token revoke', 'token create', 'token', 'udp send', 'udp listen', 'udp', 'update', 'update-cli', 'usb list', 'usb start-listening', 'usb listen', 'usb stop-listening', 'usb safe-mode', 'usb dfu', 'usb reset', 'usb setup-done', 'usb configure', 'usb cloud-status', 'usb network-interfaces', 'usb', @@ -109,9 +110,9 @@ describe('Help & Unknown Command / Argument Handling', () => { it('Shows `help` content when run with unknown flag', async () => { const { stdout, stderr, exitCode } = await cli.run('--WATNOPE'); - expect(stdout).to.equal('Unknown argument \'WATNOPE\''); expect(stderr.split('\n')).to.include.members(commandList); + expect(exitCode).to.equal(1); }); diff --git a/test/e2e/list.e2e.js b/test/e2e/list.e2e.js index 8cfbb8a6a..fccd71913 100644 --- a/test/e2e/list.e2e.js +++ b/test/e2e/list.e2e.js @@ -17,7 +17,7 @@ describe('List Commands', () => { ' -v, --verbose Increases how much logging to display [count]', ' -q, --quiet Decreases how much logging to display [count]', '', - 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2), a device ID or name' + 'Param filter can be: online, offline, a platform name (core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom, electron2, tachyon), a device ID or name' ]; before(async () => {