├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report---.md │ ├── feature-request---.md │ └── question---.md ├── pull_request_template.md └── workflows │ ├── dev.yml │ ├── production.yml │ ├── reusable-build.yml │ ├── reusable-publish-npm.yml │ ├── reusable-publish-v2.yml │ ├── reusable-tests.yml │ └── staging.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prebuild └── built-v16.16.0-linux-armv7 ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ESIM_PROVISION.md ├── LICENSE ├── README.md ├── RELEASE.md ├── assets ├── 50-particle.rules ├── banner.txt ├── keys │ ├── ec.pub.der │ ├── rsa.pub-padded.der │ └── rsa.pub.der ├── knownApps │ ├── argon │ │ └── tinker │ │ │ └── tinker-0.8.0-rc.27-argon.bin │ ├── b5som │ │ └── tinker │ │ │ └── tinker-1.5.0-b5som.bin │ ├── boron │ │ └── tinker │ │ │ └── tinker-0.8.0-rc.27-boron.bin │ ├── bsom │ │ └── tinker │ │ │ └── tinker-1.1.0-rc.1-bsom.bin │ ├── core │ │ └── tinker │ │ │ └── core_tinker.bin │ ├── electron │ │ ├── tinker-usb-debugging │ │ │ └── tinker-usb-debugging-0.6.0-electron.bin │ │ └── tinker │ │ │ └── electron_tinker.bin │ ├── p1 │ │ └── tinker │ │ │ └── tinker-0.4.5-p1.bin │ ├── p2 │ │ └── tinker │ │ │ └── tinker-5.0.0-p2.bin │ ├── photon │ │ └── tinker │ │ │ └── tinker-0.4.5-photon.bin │ ├── tracker │ │ └── tinker │ │ │ └── tracker-tinker@1.5.4-rc.1.bin │ └── xenon │ │ └── tinker │ │ └── tinker-0.8.0-rc.27-xenon.bin ├── logicFunction │ ├── @types │ │ ├── particle_core.d.ts │ │ └── particle_encoding.d.ts │ ├── logic_function_name.js.template │ └── logic_function_name.logic.json.template ├── qdl │ ├── darwin │ │ ├── arm64 │ │ │ ├── liblzma.5.dylib │ │ │ ├── libusb-1.0.0.dylib │ │ │ └── qdl │ │ └── x64 │ │ │ ├── liblzma.5.dylib │ │ │ ├── libusb-1.0.0.dylib │ │ │ └── qdl │ ├── firehose │ │ └── prog_firehose_ddr.elf │ ├── linux │ │ ├── arm64 │ │ │ ├── libusb-1.0.so.0 │ │ │ └── qdl │ │ └── x64 │ │ │ ├── libusb-1.0.so.0 │ │ │ └── qdl │ ├── read_gpt.xml │ └── win32 │ │ └── x64 │ │ ├── libbz2-1.dll │ │ ├── libiconv-2.dll │ │ ├── liblzma-5.dll │ │ ├── libusb-1.0.dll │ │ ├── libxml2-2.dll │ │ ├── qdl.exe │ │ └── zlib1.dll └── templates │ ├── deviceList.hbs │ └── eventFeed.hbs ├── development.md ├── installer ├── unix │ └── install-cli └── windows │ ├── .gitignore │ ├── ParticleCLISetup.nsi │ ├── Plugins │ ├── amd64-unicode │ │ ├── INetC.dll │ │ └── nsJSON.dll │ └── x86-ansi │ │ ├── INetC.dll │ │ └── nsJSON.dll │ ├── README.md │ ├── assets │ ├── particle.bmp │ └── particle.ico │ ├── licenses.txt │ ├── utils.nsh │ └── welcome.txt ├── npm-shrinkwrap.json ├── package.json ├── scripts ├── generate-manifest.js └── test-wrapper-update.sh ├── settings.js ├── src ├── app │ ├── cli.js │ ├── command-processor.js │ ├── command-processor.test.js │ ├── prompts.js │ ├── ui.js │ ├── ui.test.js │ ├── update-check.js │ └── update-check.test.js ├── cli │ ├── alias.js │ ├── apiclient.js │ ├── app.js │ ├── binary.js │ ├── bundle.js │ ├── bundle.test.js │ ├── cloud.js │ ├── cloud.test.js │ ├── config.js │ ├── device-protection.js │ ├── device-protection.test.js │ ├── doctor.js │ ├── esim.js │ ├── flash.js │ ├── function.js │ ├── function.test.js │ ├── help.js │ ├── index.js │ ├── keys.js │ ├── keys.test.js │ ├── library.js │ ├── library_add.js │ ├── library_add.test.js │ ├── library_delete.js │ ├── library_init.js │ ├── library_init.test.js │ ├── library_install.js │ ├── library_install.test.js │ ├── library_list.js │ ├── library_migrate.js │ ├── library_migrate.test.js │ ├── library_publish.js │ ├── library_search.js │ ├── library_ui.js │ ├── library_upload.js │ ├── library_view.js │ ├── logic-function.js │ ├── preprocess.js │ ├── preprocess.test.js │ ├── product.js │ ├── product.test.js │ ├── project.js │ ├── project_init.js │ ├── publish.js │ ├── publish.test.js │ ├── secrets.js │ ├── serial.js │ ├── subscribe.js │ ├── subscribe.test.js │ ├── tachyon.js │ ├── token.js │ ├── udp.js │ ├── update-cli.js │ ├── update.js │ ├── usb.js │ ├── usb.test.js │ ├── variable.js │ ├── variable.test.js │ ├── version.js │ ├── webhook.js │ ├── whoami.js │ └── wifi.js ├── cmd │ ├── README_binary.md │ ├── api.js │ ├── app.js │ ├── backup-restore-tachyon.js │ ├── base.js │ ├── base.test.js │ ├── binary.js │ ├── binary.test.js │ ├── bundle.js │ ├── bundle.test.js │ ├── bundle2.test.js │ ├── cloud.js │ ├── cloud.test.js │ ├── config.js │ ├── device-protection.js │ ├── device-protection.test.js │ ├── device-util.js │ ├── doctor.js │ ├── download-tachyon-package.js │ ├── esim.js │ ├── esim.test.js │ ├── flash.js │ ├── flash.test.js │ ├── function.js │ ├── function.test.js │ ├── identify-tachyon.js │ ├── index.js │ ├── keys.js │ ├── keys.test.js │ ├── logic-function.js │ ├── logic-function.test.js │ ├── preprocess.js │ ├── preprocess.test.js │ ├── product.js │ ├── publish.js │ ├── secrets.js │ ├── serial.js │ ├── serial.test.js │ ├── setup-tachyon.js │ ├── subscribe.js │ ├── token.js │ ├── udev.js │ ├── udp.js │ ├── update-cli.js │ ├── update-cli.test.js │ ├── update.js │ ├── usb-util.js │ ├── usb.js │ ├── usb.test.js │ ├── variable.js │ ├── variable.test.js │ ├── webhook.js │ ├── whoami.js │ ├── whoami.test.js │ ├── wifi.js │ ├── wifi.test.js │ └── wireless.js ├── index.js └── lib │ ├── api-cache.js │ ├── api-cache.test.js │ ├── api-client.js │ ├── connect │ ├── darwin.js │ ├── executor.js │ ├── executor.test.js │ ├── linux.js │ ├── windows.js │ └── windows.test.js │ ├── dependency-walker.js │ ├── dependency-walker.test.js │ ├── device-error-handler.js │ ├── device-os-version-util.js │ ├── device-os-version-util.test.js │ ├── device-protection-helper.js │ ├── device-specs.js │ ├── download-manager.js │ ├── download-manager.test.js │ ├── file-types.js │ ├── flash-helper.js │ ├── flash-helper.test.js │ ├── has-supported-node.js │ ├── has-supported-node.test.js │ ├── json-result.js │ ├── json-result.test.js │ ├── keys-specs.js │ ├── known-apps.js │ ├── known-apps.test.js │ ├── log.js │ ├── logic-function.js │ ├── logic-function.test.js │ ├── openurl.js │ ├── output.js │ ├── particle-cache.js │ ├── particle-cache.test.js │ ├── platform.js │ ├── platform.test.js │ ├── prompts.js │ ├── qdl.js │ ├── secrets.js │ ├── secrets.test.js │ ├── serial-batch-parser.js │ ├── serial-batch-parser.test.js │ ├── serial-trigger.js │ ├── serial-trigger.test.js │ ├── spinner-mixin.js │ ├── sso.js │ ├── sso.test.js │ ├── supported-countries.js │ ├── tachyon-utils.js │ ├── template-processor.js │ ├── template-processor.test.js │ ├── ui │ ├── index.js │ └── index.test.js │ ├── unindent.js │ ├── unindent.test.js │ ├── utilities.js │ ├── utilities.test.js │ ├── wifi-manager.js │ └── wifi-scanner.js └── test ├── .eslintrc ├── README.md ├── __fixtures__ ├── binaries │ ├── argon-bootloader-610.bin │ ├── argon-bootloader-620.bin │ ├── argon-system-part1@4.1.0.bin │ ├── argon-tinker-620.bin │ ├── argon_stroby.bin │ ├── boron_blank.bin │ ├── core_tinker.bin │ ├── electron_tinker.bin │ ├── p1_tinker-0.4.5.bin │ ├── p1_tinker.bin │ ├── photon_tinker-0.4.5.bin │ ├── photon_tinker.bin │ └── product_firmware.bin ├── esim │ ├── binaries │ │ └── esim-firmware-b5som.bin │ ├── input.json │ └── output.json ├── libraries │ └── valid │ │ └── 0.0.2 │ │ ├── LICENSE │ │ ├── README.md │ │ ├── doc │ │ └── firmware-code-conventions.md │ │ ├── examples │ │ ├── blink-an-led-slower │ │ │ └── blink-an-led-slower.cpp │ │ ├── blink-an-led-very-slow │ │ │ └── blink-an-led-very-slow.cpp │ │ └── blink-an-led │ │ │ └── blink-an-led.cpp │ │ ├── library.properties │ │ ├── src │ │ ├── a-c-example.c │ │ ├── test-library-publish.cpp │ │ ├── test-library-publish.h │ │ └── uber-library-example │ │ │ └── uber-library-example.h │ │ └── test │ │ └── unit │ │ ├── RUNNING_TESTS.md │ │ ├── makefile │ │ └── test1.cpp ├── logic_functions │ ├── lf1_proj │ │ ├── code.js │ │ ├── config.json │ │ └── sample │ │ │ └── data.json │ ├── lf2_proj │ │ ├── code.js │ │ ├── code2.js │ │ └── config.json │ ├── lf3_proj │ │ ├── code.js │ │ └── config.json │ ├── logicFunc1.json │ └── logicFunc2.json ├── pkg │ ├── .gitignore │ └── package.json ├── projects │ ├── blank │ │ ├── README.md │ │ ├── project.properties │ │ └── src │ │ │ └── blank.ino │ ├── extended │ │ ├── project.properties │ │ └── src │ │ │ ├── app.ino │ │ │ └── helper │ │ │ ├── helper.cpp │ │ │ └── helper.h │ ├── fail │ │ └── fail.cpp │ ├── legacy-exclude │ │ ├── app.ino │ │ ├── helper.cpp │ │ ├── helper.h │ │ ├── nope.cpp │ │ ├── nope.h │ │ └── particle.ignore │ ├── legacy-flat │ │ ├── app.ino │ │ ├── helper.cpp │ │ └── helper.h │ ├── lib-with-example │ │ ├── lib │ │ │ └── Particle_TEST_E2E_CLI_LIB │ │ │ │ ├── examples │ │ │ │ └── one.ino │ │ │ │ ├── library.properties │ │ │ │ └── src │ │ │ │ └── Particle_TEST_E2E_CLI_LIB.h │ │ ├── project.properties │ │ └── src │ │ │ └── app.ino │ ├── multiple-header-extensions │ │ ├── project.properties │ │ └── src │ │ │ ├── app.ino │ │ │ └── helper │ │ │ ├── h0.cpp │ │ │ ├── h0.h │ │ │ ├── h1.cpp │ │ │ ├── h1.hpp │ │ │ ├── h2.cpp │ │ │ ├── h2.hxx │ │ │ ├── h3.cpp │ │ │ └── h3.hh │ ├── proj-ignore │ │ ├── lib │ │ │ └── helper │ │ │ │ └── src │ │ │ │ ├── helper.cpp │ │ │ │ ├── helper.def │ │ │ │ ├── helper.h │ │ │ │ └── helper2.cpp │ │ ├── particle.ignore │ │ ├── project.properties │ │ └── src │ │ │ └── app.ino │ ├── proj-include │ │ ├── lib │ │ │ └── helper │ │ │ │ └── src │ │ │ │ ├── helper.cpp │ │ │ │ ├── helper.def │ │ │ │ └── helper.h │ │ ├── particle.include │ │ ├── project.properties │ │ └── src │ │ │ └── app.ino │ ├── stroby-no-sources │ │ ├── README.md │ │ └── src │ │ │ └── .gitkeep │ ├── stroby │ │ ├── README.md │ │ ├── project.properties │ │ └── src │ │ │ └── stroby.ino │ └── symlink │ │ ├── main-project │ │ ├── app.ino │ │ ├── project.properties │ │ └── shared │ │ └── shared-project │ │ └── sub_dir │ │ ├── helper.cpp │ │ └── helper.h ├── third_party_ota │ ├── app-with-assets.bin │ ├── bundle-asset-hash-no-match.zip │ ├── bundle-missing-assets.zip │ ├── bundle.zip │ ├── invalid-bundle.zip │ ├── invalid_bin │ │ └── app.txt │ ├── invalid_no_assets │ │ └── app.bin │ ├── nested_dir │ │ └── assets │ │ │ ├── .special_file1 │ │ │ ├── .special_file2 │ │ │ ├── file1.txt │ │ │ ├── file2.txt │ │ │ └── more_assets │ │ │ └── file3.txt │ ├── projects │ │ └── stroby-with-assets │ │ │ ├── README.md │ │ │ ├── assets │ │ │ ├── cat.txt │ │ │ ├── house.txt │ │ │ └── water.txt │ │ │ ├── project.properties │ │ │ └── src │ │ │ └── stroby.ino │ ├── tinker_argon_541.bin │ ├── valid-no-proj-prop │ │ ├── app.bin │ │ └── assets │ │ │ ├── cat.txt │ │ │ ├── house.txt │ │ │ └── water.txt │ ├── valid-no-prop │ │ ├── app.bin │ │ ├── otaAssets │ │ │ ├── cat.txt │ │ │ ├── house.txt │ │ │ └── water.txt │ │ └── project.properties │ ├── valid │ │ ├── app.bin │ │ ├── bundle_app_1719510886367.zip │ │ ├── otaAssets │ │ │ ├── cat.txt │ │ │ ├── house.txt │ │ │ └── water.txt │ │ └── project.properties │ └── zero_assets │ │ ├── app.bin │ │ ├── assets │ │ └── .gitkeep │ │ └── project.properties └── wiring │ ├── .gitignore │ ├── one │ └── input.ino │ └── two │ └── input.ino ├── __mocks__ └── serial.mock.js ├── __root__ ├── .gitignore └── README.md ├── e2e ├── __root__.e2e.js ├── binary.e2e.js ├── bundle.e2e.js ├── call.e2e.js ├── cloud.e2e.js ├── compile.e2e.js ├── config.e2e.js ├── device-protection.e2e.js ├── device.e2e.js ├── doctor.e2e.js ├── flash-protected.e2e.js ├── flash.e2e.js ├── function.e2e.js ├── get.e2e.js ├── help.e2e.js ├── identify.e2e.js ├── keys.e2e.js ├── library.e2e.js ├── list.e2e.js ├── logic-function.e2e.js ├── login.e2e.js ├── logout.e2e.js ├── monitor.e2e.js ├── preprocess.e2e.js ├── product.e2e.js ├── project.e2e.js ├── publish.e2e.js ├── secrets.e2e.js ├── serial-protected.e2e.js ├── serial.e2e.js ├── subscribe.e2e.js ├── token.e2e.js ├── udp.e2e.js ├── update-protected.e2e.js ├── update.e2e.js ├── usb-protected.e2e.js ├── usb.e2e.js ├── variable.e2e.js ├── version.e2e.js ├── webhook.e2e.js ├── whoami.e2e.js ├── wifi-protected.e2e.js └── wifi.e2e.js ├── integration ├── access_token.js ├── command-processor.integration.js ├── library.integration.js ├── library_init.integration.js └── library_install.integration.js ├── lib ├── ansi-strip.js ├── capture-matches.js ├── cli.js ├── env.js ├── fs.js ├── mocha-utils.js └── open-ssl.js ├── secrets └── fixtures.js └── setup.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-particle'], 3 | parserOptions: { 4 | ecmaVersion: 13, 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | commonjs: true, 9 | es6: true, 10 | node: true, 11 | mocha: true, 12 | worker: true 13 | }, 14 | rules: { 15 | 'no-console': 'off', 16 | 'valid-jsdoc': 'off', 17 | 'max-depth': ['warn', 5], 18 | 'max-statements': ['warn', 40] 19 | } 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report \U0001F41E" 3 | about: "Something isn't working as expected? Report it here \U0001F44D" 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | 21 | ## Description 22 | 23 | Describe the problem you are experiencing. 24 | 25 | 26 | ### Steps to reproduce 27 | 28 | What are the minimal set of steps required to reproduce the issue? 29 | 30 | 31 | ### Expected result 32 | 33 | What should happen? 34 | 35 | 36 | ### Actual result 37 | 38 | What happened? 39 | 40 | 41 | ### Environment 42 | 43 | Tell us about your setup: 44 | 45 | * OS: 46 | * Node (run `node -v`): 47 | * NPM (run `npm -v`): 48 | * Particle CLI (run `particle version`): 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request \U0001F4A1" 3 | about: Suggest a new idea for the project ✨ 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | 21 | ## Description 22 | 23 | Brief explanation of the feature. 24 | 25 | 26 | ### Example 27 | 28 | If the proposal involves a new or changed API, include a basic code example. Omit this section if it's not applicable. 29 | 30 | 31 | ### Motivation 32 | 33 | Why should we do this? What use cases does it support? How does it improve the project? 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Question \U0001F914" 3 | about: Usage question or discussion about the Particle CLI. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | 21 | ## Summary 22 | 23 | 24 | ## Details / Background Info 25 | 26 | 27 | ### Environment (if relevant) 28 | 29 | * OS: 30 | * Node (run `node -v`): 31 | * NPM (run `npm -v`): 32 | * Particle CLI (run `particle version`): 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | ## Description 20 | 21 | 24 | 25 | 26 | ## How to Test 27 | 28 | 31 | 32 | 33 | ## Related Issues / Discussions 34 | 35 | 48 | 49 | 50 | ## Completeness 51 | 52 | - [x] User is totes amazing for contributing! 53 | - [ ] Contributor has signed [CLA](https://docs.google.com/a/particle.io/forms/d/1_2P-vRKGUFg5bmpcKLHO_qNZWGi5HKYnfrrkd-sbZoA/viewform) 54 | - [ ] Problem and solution clearly stated 55 | - [ ] Tests have been provided 56 | - [ ] Docs have been updated 57 | - [ ] CI is passing 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests on Branch 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | - '!staging' 7 | jobs: 8 | call-tests: 9 | uses: ./.github/workflows/reusable-tests.yml 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to production 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | call-tests: 8 | uses: ./.github/workflows/reusable-tests.yml 9 | secrets: inherit 10 | with: 11 | skipE2E: true 12 | call-build: 13 | uses: ./.github/workflows/reusable-build.yml 14 | secrets: inherit 15 | needs: call-tests 16 | call-publish-v2: 17 | uses: ./.github/workflows/reusable-publish-v2.yml 18 | secrets: inherit 19 | needs: call-build 20 | with: 21 | environment: production 22 | build_run_id: ${{ github.run_id }} 23 | call-publish-npm: 24 | uses: ./.github/workflows/reusable-publish-npm.yml 25 | secrets: inherit 26 | needs: call-publish-v2 27 | -------------------------------------------------------------------------------- /.github/workflows/reusable-publish-npm.yml: -------------------------------------------------------------------------------- 1 | name: Build Package 2 | 3 | on: [workflow_call] 4 | 5 | jobs: 6 | publish-npm: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Set up Node.js 11 | uses: actions/setup-node@v3 12 | with: 13 | node-version: '16' 14 | - name: Configure NPM Token 15 | run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_PUBLISH_TOKEN }} 16 | - name: Install dependencies 17 | run: npm install 18 | - name: Publish package 19 | run: npm publish 20 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to staging 2 | on: 3 | push: 4 | branches: 5 | - 'staging' 6 | jobs: 7 | call-tests: 8 | uses: ./.github/workflows/reusable-tests.yml 9 | secrets: inherit 10 | with: 11 | skipE2E: true 12 | call-build: 13 | uses: ./.github/workflows/reusable-build.yml 14 | secrets: inherit 15 | needs: call-tests 16 | call-publish: 17 | uses: ./.github/workflows/reusable-publish-v2.yml 18 | secrets: inherit 19 | needs: call-build 20 | with: 21 | environment: staging 22 | build_run_id: ${{ github.run_id }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.tgz 10 | 11 | pids 12 | logs 13 | results 14 | coverage 15 | 16 | npm-debug.log 17 | node_modules 18 | 19 | .DS_Store 20 | es6 21 | accept/tmp 22 | 23 | .nyc_output 24 | .env 25 | .idea/ 26 | build/ 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | **/* 3 | 4 | # except us :) 5 | !src/**/* 6 | !assets/**/* 7 | !settings.js 8 | !CHANGELOG.md 9 | !CONTRIBUTING.md 10 | !README.md 11 | 12 | # but don't forget to also ignore us 13 | .DS_Store* 14 | .env 15 | installer/**/* 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prebuild/built-v16.16.0-linux-armv7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/.prebuild/built-v16.16.0-linux-armv7 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | 3 | All contributors must first sign the [Particle Individual Contributor License Agreement (CLA)](https://docs.google.com/a/particle.io/forms/d/1_2P-vRKGUFg5bmpcKLHO_qNZWGi5HKYnfrrkd-sbZoA/viewform), which is based on the Google CLA, and provides the Particle team a license to re-distribute your contributions. 4 | 5 | Whenever possible, please follow these guidelines for contributions: 6 | 7 | - Keep each pull request small and focused on a single feature or bug fix. 8 | - Familiarize yourself with the code base, and follow the formatting principles adhered to in the surrounding code. 9 | - Wherever possible, provide unit tests for your contributions. 10 | -------------------------------------------------------------------------------- /ESIM_PROVISION.md: -------------------------------------------------------------------------------- 1 | # Provision a blank eSIM 2 | 3 | ### Pre-req 4 | 5 | 1. Input JSON (A JSON file that has a list of EIDs and their respective RSP URLs) 6 | 2. Output (Folder where output logs will be stored. Defaults to `esim_loading_logs` if not set) 7 | 3. lpa tool (lpa tool binary - differently built for mac and windows) 8 | 4. binaries (Folder with the user binaries) 9 | 10 | ## Setup 11 | 12 | ### Local folder setup on computer 13 | 14 | Put your files in this structure (for example) 15 | 16 | ``` 17 | /kigen-resources 18 | ├── /binaries 19 | │ ├── esim-firmware-b5som.bin 20 | │ ├── esim-firmware-msom.bin 21 | ├── input.json 22 | ├── custom_output_folder/ 23 | ├── lpa 24 | │ ├── mac 25 | │ ├── ├── lpa 26 | │ ├── windows 27 | │ ├── ├── lpa.exe 28 | ``` 29 | 30 | ### Device Setup 31 | 32 | 1. Connect your device(s) to the computer 33 | 2. Run this command 34 | ``` 35 | particle.js esim provision --input /path/to/input.json --lpa /path/to/lpa-tool --binary /path/to/binaries --bulk true 36 | ``` 37 | 38 | ### Expected Outcome 39 | First, the device(s) are flashed. Once the download process starts on a given device, device will turn its LED into yellow. If the download worked, LED turns green. If the download failed, LED turns red. 40 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Releasing a new version 2 | 3 | - Checkout the `master` branch 4 | 5 | - Files have been updated and committed prior to this step. 6 | 7 | - `npm version ` 8 | 9 | - This will bump the version in `package.json`, build the distribution files, prompt you to update the `CHANGELOG.md`, and make a "vX.Y.Z" commit for you. 10 | - e.g. going from 1.28.1 to 1.28.2 use `npm version patch` 11 | - e.g. going from 1.28.2 to 1.29.0 use `npm version minor` 12 | 13 | - `git push origin master --follow-tags` 14 | 15 | - This will push the commits and tag created in the previous steps. 16 | 17 | - GitHub Actions will publish to npm when the build succeeds. 18 | 19 | - Create a release on GitHub with the notes from the `CHANGELOG.md` 20 | 21 | ## Create a test version on staging 22 | 23 | - Run the following command: 24 | 25 | - `git push origin ${branch}:staging -f` 26 | 27 | - In case you need to change the version, you can run the following command: 28 | 29 | - `npm version ` 30 | - Make sure to remove the created tag with `git tag -d vX.Y.Z` to prevent publishing to production. 31 | - Then, push the changes to staging again. 32 | - Once you are happy with the changes, you can proceed to merge the changes to master, remember to remove the version commit you just created. 33 | - The executables will be available on `binaries.staging.particle.io/particle-cli/` for testing. 34 | -------------------------------------------------------------------------------- /assets/50-particle.rules: -------------------------------------------------------------------------------- 1 | # UDEV Rules for Particle boards 2 | # 3 | # This will allow reflashing with DFU-util without using sudo 4 | # 5 | # This file must be placed at: 6 | # 7 | # /etc/udev/rules.d/50-particle.rules (preferred location) 8 | # 9 | # To install, type this command in a terminal: 10 | # sudo cp 50-particle.rules /etc/udev/rules.d/50-particle.rules 11 | # 12 | # After this file is installed, physically unplug and reconnect the 13 | # Particle device. 14 | # 15 | # Core 16 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="607[df]", GROUP="plugdev", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1" 17 | # Gen 2 (Photon/P1/Electron), Gen 3 (Argon/Boron/Xenon, SoM) 18 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="2b04", ATTRS{idProduct}=="[cd]0??", GROUP="plugdev", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1" 19 | # Particle Programmer Shield v1.0 20 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", GROUP="plugdev", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1" 21 | # Particle Debugger 22 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0d28", ATTRS{idProduct}=="0204", GROUP="plugdev", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1" 23 | # Tachyon in EDL mode 24 | SUBSYSTEM=="usb", ATTRS{idProduct}=="9008", ATTRS{idVendor}=="05c6", GROUP="plugdev", MODE="0666" 25 | # 26 | # If you share your linux system with other users, or just don't like the 27 | # idea of write permission for everybody, you can replace MODE:="0666" with 28 | # OWNER:="yourusername" to create the device owned by you, or with 29 | # GROUP:="somegroupname" and mange access using standard unix groups. 30 | # 31 | # 32 | # If using USB Serial you get a new device each time (Ubuntu >9.10) 33 | # eg: /dev/ttyACM0, ttyACM1, ttyACM2, ttyACM3, ttyACM4, etc 34 | # apt-get remove --purge modemmanager (reboot may be necessary) 35 | # 36 | # CREDITS: 37 | # 38 | # Edited by Julien Vanier 39 | # 40 | # This file is derived from the Teensy UDEV rules 41 | # http://www.pjrc.com/teensy/49-teensy.rules 42 | # 43 | -------------------------------------------------------------------------------- /assets/banner.txt: -------------------------------------------------------------------------------- 1 | _ __ _ _ _ 2 | | '_ \ __ _ _ __| |_(_) ___| | ___ 3 | | |_) |/ _` | '__| __| |/ __| |/ _ \ 4 | | __/| (_| | | | |_| | (__| | __/ 5 | |_| \__,_|_| \__|_|\___|_|\___| 6 | https://particle.io 7 | -------------------------------------------------------------------------------- /assets/keys/ec.pub.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/keys/ec.pub.der -------------------------------------------------------------------------------- /assets/keys/rsa.pub-padded.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/keys/rsa.pub-padded.der -------------------------------------------------------------------------------- /assets/keys/rsa.pub.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/keys/rsa.pub.der -------------------------------------------------------------------------------- /assets/knownApps/argon/tinker/tinker-0.8.0-rc.27-argon.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/argon/tinker/tinker-0.8.0-rc.27-argon.bin -------------------------------------------------------------------------------- /assets/knownApps/b5som/tinker/tinker-1.5.0-b5som.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/b5som/tinker/tinker-1.5.0-b5som.bin -------------------------------------------------------------------------------- /assets/knownApps/boron/tinker/tinker-0.8.0-rc.27-boron.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/boron/tinker/tinker-0.8.0-rc.27-boron.bin -------------------------------------------------------------------------------- /assets/knownApps/bsom/tinker/tinker-1.1.0-rc.1-bsom.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/bsom/tinker/tinker-1.1.0-rc.1-bsom.bin -------------------------------------------------------------------------------- /assets/knownApps/core/tinker/core_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/core/tinker/core_tinker.bin -------------------------------------------------------------------------------- /assets/knownApps/electron/tinker-usb-debugging/tinker-usb-debugging-0.6.0-electron.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/electron/tinker-usb-debugging/tinker-usb-debugging-0.6.0-electron.bin -------------------------------------------------------------------------------- /assets/knownApps/electron/tinker/electron_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/electron/tinker/electron_tinker.bin -------------------------------------------------------------------------------- /assets/knownApps/p1/tinker/tinker-0.4.5-p1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/p1/tinker/tinker-0.4.5-p1.bin -------------------------------------------------------------------------------- /assets/knownApps/p2/tinker/tinker-5.0.0-p2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/p2/tinker/tinker-5.0.0-p2.bin -------------------------------------------------------------------------------- /assets/knownApps/photon/tinker/tinker-0.4.5-photon.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/photon/tinker/tinker-0.4.5-photon.bin -------------------------------------------------------------------------------- /assets/knownApps/tracker/tinker/tracker-tinker@1.5.4-rc.1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/tracker/tinker/tracker-tinker@1.5.4-rc.1.bin -------------------------------------------------------------------------------- /assets/knownApps/xenon/tinker/tinker-0.8.0-rc.27-xenon.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/knownApps/xenon/tinker/tinker-0.8.0-rc.27-xenon.bin -------------------------------------------------------------------------------- /assets/logicFunction/@types/particle_encoding.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'particle:encoding' { 2 | namespace Encoding { 3 | /** 4 | * Converts a byte array to a UTF-8 string 5 | * @param input The byte array to convert 6 | * @returns {string} The converted string 7 | */ 8 | export function bytesToString(input: number[]): string; 9 | /** 10 | * Converts a UTF-8 string to a byte array 11 | * @param input The string to convert 12 | * @returns {number[]} The byte array representing this string 13 | */ 14 | export function stringToBytes(input: string): number[]; 15 | /** 16 | * Encodes a string or byte array to base64 (RFC 3548) 17 | * @param input The string or byte array to encode 18 | * @returns {string} The base64 encoded string 19 | */ 20 | export function base64Encode(input: string | number[]): string; 21 | /** 22 | * Decodes a base64 (RFC 3548) string to a byte array 23 | * @param input The base64 string to decode 24 | * @returns {number[]} The decoded byte array 25 | */ 26 | export function base64Decode(input: string): number[]; 27 | /** 28 | * Encodes a string or byte array to base85 (RFC1924) 29 | * @param input The string or byte array to encode 30 | * @returns {string} The base85 encoded string 31 | */ 32 | export function base85Encode(input: string | number[]): string; 33 | /** 34 | * Decodes a base85 (RFC1924) string to a byte array 35 | * @param input The base85 string to decode 36 | * @returns {number[]} The decoded byte array 37 | */ 38 | export function base85Decode(input: string): number[]; 39 | } 40 | 41 | export = Encoding; 42 | } 43 | -------------------------------------------------------------------------------- /assets/logicFunction/logic_function_name.js.template: -------------------------------------------------------------------------------- 1 | import Particle from 'particle:core'; 2 | 3 | export default function process({ functionInfo, trigger, event }) { 4 | // Add your code here 5 | } 6 | -------------------------------------------------------------------------------- /assets/logicFunction/logic_function_name.logic.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "schemas/logic_function.schema.json", 3 | "logic_function": { 4 | "name": "${name}", 5 | "description": "${description}", 6 | "source": { 7 | "type": "JavaScript" 8 | }, 9 | "enabled": true, 10 | "logic_triggers": [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/qdl/darwin/arm64/liblzma.5.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/arm64/liblzma.5.dylib -------------------------------------------------------------------------------- /assets/qdl/darwin/arm64/libusb-1.0.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/arm64/libusb-1.0.0.dylib -------------------------------------------------------------------------------- /assets/qdl/darwin/arm64/qdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/arm64/qdl -------------------------------------------------------------------------------- /assets/qdl/darwin/x64/liblzma.5.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/x64/liblzma.5.dylib -------------------------------------------------------------------------------- /assets/qdl/darwin/x64/libusb-1.0.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/x64/libusb-1.0.0.dylib -------------------------------------------------------------------------------- /assets/qdl/darwin/x64/qdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/darwin/x64/qdl -------------------------------------------------------------------------------- /assets/qdl/firehose/prog_firehose_ddr.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/firehose/prog_firehose_ddr.elf -------------------------------------------------------------------------------- /assets/qdl/linux/arm64/libusb-1.0.so.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/linux/arm64/libusb-1.0.so.0 -------------------------------------------------------------------------------- /assets/qdl/linux/arm64/qdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/linux/arm64/qdl -------------------------------------------------------------------------------- /assets/qdl/linux/x64/libusb-1.0.so.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/linux/x64/libusb-1.0.so.0 -------------------------------------------------------------------------------- /assets/qdl/linux/x64/qdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/linux/x64/qdl -------------------------------------------------------------------------------- /assets/qdl/read_gpt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/qdl/win32/x64/libbz2-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/libbz2-1.dll -------------------------------------------------------------------------------- /assets/qdl/win32/x64/libiconv-2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/libiconv-2.dll -------------------------------------------------------------------------------- /assets/qdl/win32/x64/liblzma-5.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/liblzma-5.dll -------------------------------------------------------------------------------- /assets/qdl/win32/x64/libusb-1.0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/libusb-1.0.dll -------------------------------------------------------------------------------- /assets/qdl/win32/x64/libxml2-2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/libxml2-2.dll -------------------------------------------------------------------------------- /assets/qdl/win32/x64/qdl.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/qdl.exe -------------------------------------------------------------------------------- /assets/qdl/win32/x64/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/assets/qdl/win32/x64/zlib1.dll -------------------------------------------------------------------------------- /assets/templates/deviceList.hbs: -------------------------------------------------------------------------------- 1 | {{#each data}} 2 | {{~#if connected}}{{#chalk 'cyan' 'bold'}}{{defaultValue name ''}}{{/chalk}}{{else}}{{#chalk 'cyan' 'dim'}}{{defaultValue name ''}}{{/chalk}}{{/if}} [{{id}}] ({{lookup @root.platformsById platform_id}}) is {{printIf connected 'online' 'offline'}} 3 | {{#if variables.length}} 4 | Variables: 5 | {{#each variables}} 6 | {{@key}} ({{this}}) 7 | {{/each}} 8 | {{/if}} 9 | {{#if functions.length}} 10 | Functions: 11 | {{#each functions}} 12 | int {{this}}(String args) 13 | {{/each}} 14 | {{/if}} 15 | {{/each}} -------------------------------------------------------------------------------- /assets/templates/eventFeed.hbs: -------------------------------------------------------------------------------- 1 | {{#with data}}{{moment published_at 'DD MMM YYYY HH:mm:ss.SSS'}} {{coreid}} {{{name}}}:{{tab}}{{{data}}} 2 | {{/with}} -------------------------------------------------------------------------------- /installer/windows/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | ParticleCLISetup.exe 3 | Thumbs.db 4 | *.p12 5 | ParticleDriversSetup.exe 6 | -------------------------------------------------------------------------------- /installer/windows/Plugins/amd64-unicode/INetC.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/Plugins/amd64-unicode/INetC.dll -------------------------------------------------------------------------------- /installer/windows/Plugins/amd64-unicode/nsJSON.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/Plugins/amd64-unicode/nsJSON.dll -------------------------------------------------------------------------------- /installer/windows/Plugins/x86-ansi/INetC.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/Plugins/x86-ansi/INetC.dll -------------------------------------------------------------------------------- /installer/windows/Plugins/x86-ansi/nsJSON.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/Plugins/x86-ansi/nsJSON.dll -------------------------------------------------------------------------------- /installer/windows/README.md: -------------------------------------------------------------------------------- 1 | # Particle CLI Installer 2 | 3 | Includes a version of the CLI with all Javascript files and Node packaged as a single file. 4 | 5 | *This installer is based on the CLI installer by Daniel Sullivan. Thanks!* 6 | 7 | ## Releasing the installer 8 | 9 | - GitHub Actions builds and signs the executable when the staging branch is pushed. 10 | - Download the installer from binaries.staging.particle.io and test it . 11 | - After testing those installers, you can trigger a new release from particle-cli root repo. 12 | 13 | ## Compile installer 14 | 15 | - Download and install nsis version 3 16 | - Run `npm run generate:win-installer` from root of particle-cli repo to get `ParticleCLISetup.exe` 17 | 18 | ## Included components 19 | 20 | See [licences.txt](/installer/windows/licenses.txt) for more info on the open source licenses. 21 | 22 | 23 | ## Release Urls 24 | - Download the staging installer from 25 | - Download the latest installer from 26 | - Download the installer for a specific version from 27 | -------------------------------------------------------------------------------- /installer/windows/assets/particle.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/assets/particle.bmp -------------------------------------------------------------------------------- /installer/windows/assets/particle.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/installer/windows/assets/particle.ico -------------------------------------------------------------------------------- /installer/windows/welcome.txt: -------------------------------------------------------------------------------- 1 | This installer downloads the Particle Command Line Interface (CLI) and its dependencies. 2 | 3 | After this initial installation the CLI will keep itself up to date by installing new versions as they are released. 4 | 5 | -------------------------------------------------------------------------------- /scripts/test-wrapper-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "::::::::::::::::::::::::::::::::::::::::::::" 5 | echo ":::: Updating Local CLI Wrapper Package ::::" 6 | echo "::::::::::::::::::::::::::::::::::::::::::::" 7 | 8 | shopt -s dotglob 9 | shopt -s nullglob 10 | PWD=$(pwd) 11 | particle_dir=~/.particle 12 | 13 | echo 14 | echo ":::: Packaging CLI via npm :::::::::::::::::" 15 | 16 | npm pack 17 | 18 | echo 19 | 20 | tgz_files=(${PWD}/particle-cli-*.tgz) 21 | 22 | if [ ${#tgz_files[@]} -eq 1 ]; then 23 | pkg=${tgz_files[0]} 24 | else 25 | PS3='Select your package (.tgz): ' 26 | select file in "${tgz_files[@]}" 27 | do 28 | pkg=${file} 29 | break 30 | done 31 | fi 32 | 33 | user_dirs=(${particle_dir}/node-*/) 34 | 35 | if [ ${#user_dirs[@]} -eq 1 ]; then 36 | node_dir=${user_dirs[0]} 37 | else 38 | PS3='Please pick your node install directory: ' 39 | select dir in "${user_dirs[@]}" 40 | do 41 | node_dir=${dir} 42 | break 43 | done 44 | fi 45 | 46 | echo 47 | echo ":::: Installing CLI package ::::::::::::::::" 48 | echo ":::: Destination: ${node_dir}" 49 | echo ":::: Package: ${pkg}" 50 | 51 | node_bin_dir=${node_dir}/bin 52 | 53 | export PATH="$node_bin_dir:$PATH" 54 | 55 | echo 56 | echo ':::: Using Configuration :::::::::::::::::::' 57 | echo ":::: PATH: ${PATH}" 58 | echo ":::: node: $(node -v)" 59 | echo ":::: npm: $(npm -v)" 60 | 61 | cd ${particle_dir} 62 | npm_install_log=$(npm install ${pkg} --loglevel=verbose --color=always 2>&1 | tee /dev/tty) 63 | 64 | if (echo $npm_install_log | grep --silent "No prebuilt binaries found") 65 | then 66 | echo ":::: Error: It appears prebuild binaries for native modules are not available!" 67 | exit 1 68 | fi 69 | 70 | echo 71 | echo ":::: done!" 72 | 73 | -------------------------------------------------------------------------------- /src/app/prompts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | username(username) { 3 | return { 4 | type: 'input', 5 | name: 'username', 6 | message: 'Please enter your email address:', 7 | default: username, 8 | validate: (value) => { 9 | if (!value) { 10 | return 'You need an email address to log in'; 11 | } 12 | return true; 13 | } 14 | }; 15 | }, 16 | 17 | password(msg) { 18 | return { 19 | type: 'password', 20 | name: 'password', 21 | message: msg || 'Please enter your password:', 22 | validate: (value) => { 23 | if (!value) { 24 | return 'You need a password to log in'; 25 | } 26 | return true; 27 | } 28 | }; 29 | }, 30 | 31 | requestTransfer() { 32 | return { 33 | type: 'confirm', 34 | name: 'transfer', 35 | message: 'That device belongs to someone else. Would you like to request a transfer?', 36 | default: true 37 | }; 38 | }, 39 | 40 | areYouSure(msg) { 41 | return { 42 | type: 'confirm', 43 | name: 'sure', 44 | message: `Are you sure ${msg}?`, 45 | default: false 46 | }; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/app/ui.js: -------------------------------------------------------------------------------- 1 | const { Spinner } = require('cli-spinner'); 2 | const inquirer = require('inquirer'); 3 | const log = require('../lib/log'); 4 | 5 | Spinner.setDefaultSpinnerString(Spinner.spinners[7]); 6 | 7 | // TODO: migrate all usage prompt in src/app/ui to src/lib/ui 8 | module.exports.prompt = async (question) => { 9 | if (!global.isInteractive){ 10 | throw new Error('Prompts are not allowed in non-interactive mode'); 11 | } 12 | return inquirer.prompt(question); 13 | }; 14 | 15 | module.exports.spin = async (promise, str) => { 16 | let spinner; 17 | 18 | if (!global.isInteractive){ 19 | return promise; 20 | } 21 | 22 | if (global.verboseLevel > 1){ 23 | log.debug(str); 24 | return promise; 25 | } 26 | 27 | spinner = new Spinner(str); 28 | spinner.start(); 29 | return promise.finally(() => spinner.stop(true)); 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/app/update-check.js: -------------------------------------------------------------------------------- 1 | const settings = require('../../settings'); 2 | const childProcess = require('node:child_process'); 3 | 4 | 5 | module.exports = async (skip, force, command) => { 6 | if (!process.pkg) { // running from source 7 | return; 8 | } 9 | 10 | const now = Date.now(); 11 | const lastCheck = settings.profile_json.last_version_check || 0; 12 | const skipUpdates = !!(settings.profile_json.enableUpdates === false || skip || command === 'update-cli'); 13 | 14 | if ((now - lastCheck >= settings.updateCheckInterval) || force){ 15 | settings.profile_json.last_version_check = now; 16 | settings.saveProfileData(); 17 | if (skipUpdates) { 18 | return; 19 | } 20 | childProcess.spawn(process.execPath, [process.argv[1], 'update-cli'], { 21 | detached: true, 22 | stdio: 'ignore', 23 | windowsHide: true 24 | }).unref(); 25 | } 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /src/cli/alias.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | function alias(category, aliasName, path) { 3 | const cmd = root.find(path); 4 | if (cmd) { 5 | commandProcessor.createCommand(category, aliasName, cmd.description, cmd.options); 6 | } 7 | } 8 | 9 | alias(root, 'login', ['cloud', 'login']); 10 | alias(root, 'logout', ['cloud', 'logout']); 11 | alias(root, 'list', ['cloud', 'list']); 12 | alias(root, 'nyan', ['cloud', 'nyan']); 13 | alias(root, 'call', ['function', 'call']); 14 | alias(root, 'get', ['variable', 'get']); 15 | alias(root, 'monitor', ['variable', 'monitor']); 16 | 17 | alias(root, 'compile', ['cloud', 'compile']); 18 | 19 | alias(root, 'identify', ['serial', 'identify']); 20 | 21 | const device = commandProcessor.createCategory(root, 'device', 'Manipulate a device'); 22 | alias(device, 'add', ['cloud', 'claim']); 23 | alias(device, 'remove', ['cloud', 'remove']); 24 | alias(device, 'rename', ['cloud', 'name']); 25 | alias(device, 'doctor', ['doctor']); 26 | }; 27 | -------------------------------------------------------------------------------- /src/cli/apiclient.js: -------------------------------------------------------------------------------- 1 | const settings = require('../../settings'); 2 | 3 | // todo - this is a bad coupling since it calls back up to the enclosing app (via settings) 4 | // the access token and other contextual items should be passed to the command as exteranl context (injection) 5 | 6 | module.exports.buildAPIClient = (apiJS) => { 7 | return apiJS.client({ auth: settings.access_token }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/cli/binary.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const binary = commandProcessor.createCategory(root, 'binary', 'Inspect binaries'); 3 | 4 | commandProcessor.createCommand(binary, 'inspect', 'Describe binary contents', { 5 | params: '', 6 | handler: (args) => { 7 | const BinaryCommand = require('../cmd/binary'); 8 | return new BinaryCommand().inspectBinary(args.params.filename); 9 | }, 10 | examples: { 11 | '$0 $command firmware.bin': 'Describe contents of firmware.bin' 12 | } 13 | }); 14 | 15 | commandProcessor.createCommand(binary, 'enable-device-protection', 'Create a protected bootloader binary', { 16 | params: '', 17 | options: { 18 | 'saveTo': { 19 | description: 'Specify the filename for the protected binary' 20 | } 21 | }, 22 | handler: (args) => { 23 | const BinaryCommand = require('../cmd/binary'); 24 | return new BinaryCommand().createProtectedBinary({ saveTo: args.saveTo, file: args.params.file, verbose: true }); 25 | }, 26 | examples: { 27 | '$0 $command bootloader.bin': 'Provide bootloader binary to protect' 28 | } 29 | }); 30 | 31 | commandProcessor.createCommand(binary, 'list-assets', 'Lists assets present in an application binary', { 32 | params: '', 33 | handler: (args) => { 34 | const BinaryCommand = require('../cmd/binary'); 35 | return new BinaryCommand().listAssetsFromApplication(args.params.file); 36 | }, 37 | examples: { 38 | '$0 $command app-with-assets.bin': 'Show the list of assets in the application binary' 39 | } 40 | }); 41 | 42 | commandProcessor.createCommand(binary, 'strip-assets', 'Remove assets from application binary', { 43 | params: '', 44 | handler: (args) => { 45 | const BinaryCommand = require('../cmd/binary'); 46 | return new BinaryCommand().stripAssetsFromApplication(args.params.file); 47 | }, 48 | examples: { 49 | '$0 $command app-with-assets.bin': 'Remove assets from the application binary' 50 | } 51 | }); 52 | 53 | return binary; 54 | }; 55 | -------------------------------------------------------------------------------- /src/cli/bundle.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'bundle', 'Creates a bundle of application binary and assets', { 3 | params: '', 4 | options: { 5 | 'saveTo': { 6 | description: 'Specify the filename for the compiled binary' 7 | }, 8 | 'assets': { 9 | description: 'Optional. Specify the assets directory using --assets /path/to/assets or --assets /path/to/project.properties. If not specified, assets are obtained from the assetOtaDir property in the project.properties file' 10 | } 11 | }, 12 | handler: (args) => { 13 | const BundleCommands = require('../cmd/bundle'); 14 | return new BundleCommands().createBundle(args); 15 | }, 16 | examples: { 17 | '$0 $command myApp.bin': 'Creates a bundle of application binary and assets. The assets are obtained from the project.properties in the current directory', 18 | '$0 $command myApp.bin --assets /path/to/assets': 'Creates a bundle of application binary and assets. The assets are obtained from /path/to/assets directory', 19 | '$0 $command myApp.bin --assets /path/to/project.properties': 'Creates a bundle of application binary and assets. The assets are picked up from the provided project.properties file', 20 | '$0 $command myApp.bin --assets /path/ --saveTo myApp.zip': 'Creates a bundle of application binary and assets, and saves it to the myApp.zip file', 21 | '$0 $command myApp.bin --saveTo myApp.zip': 'Creates a bundle of application binary and assets as specified in the assetOtaDir if available, and saves the bundle to the myApp.zip file' 22 | }, 23 | epilogue: 'Add assetOtaDir=assets to your project.properties file to bundle assets from the asset directory. The assets path should be relative to the project root.' 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/cli/config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'config', 'Configure and switch between multiple accounts', { 3 | params: '[profile] [setting] [value]', 4 | options: { 5 | 'list': { 6 | boolean: true, 7 | description: 'Display available configurations' 8 | } 9 | }, 10 | handler: (args) => { 11 | const ConfigCommands = require('../cmd/config'); 12 | return new ConfigCommands().configSwitch(args.params.profile, args.params.setting, args.params.value, args); 13 | }, 14 | examples: { 15 | '$0 $command company': 'Switch to a profile called company', 16 | '$0 $command particle': 'Switch back to the default profile', 17 | '$0 $command set apiUrl http://localhost:9090': 'Change the apiUrl setting for the current profile', 18 | '$0 $command set proxyUrl http://proxy:8080': 'Change the proxyUrl setting for the current profile' 19 | } 20 | }); 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/cli/device-protection.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const deviceProtection = commandProcessor.createCategory(root, 'device-protection', 'Manage Device Protection'); 3 | 4 | commandProcessor.createCommand(deviceProtection, 'status', 'Gets the current Device Protection status', { 5 | options: { 6 | device: { 7 | description: 'Device ID or name', 8 | alias: 'd' 9 | } 10 | }, 11 | handler: (args) => { 12 | const DeviceProtectionCommands = require('../cmd/device-protection'); 13 | return new DeviceProtectionCommands().getStatus(args); 14 | }, 15 | examples: { 16 | '$0 $command': 'Gets the current Device Protection status' 17 | } 18 | }); 19 | 20 | commandProcessor.createCommand(deviceProtection, 'disable', 'Disables Device Protection', { 21 | options: { 22 | d: { 23 | description: 'Device ID or name', 24 | alias: 'device' 25 | } 26 | }, 27 | handler: (args) => { 28 | const DeviceProtectionCommands = require('../cmd/device-protection'); 29 | return new DeviceProtectionCommands().disableProtection(args); 30 | }, 31 | examples: { 32 | '$0 $command': 'Puts a Protected Device to Service Mode', 33 | }, 34 | epilogue: 'A Protected Device in Service Mode allows any command to be performed on it that can be performed on an Open Device like flashing firmware or serial monitor.' 35 | }); 36 | 37 | commandProcessor.createCommand(deviceProtection, 'enable', 'Enables Device Protection', { 38 | options: { 39 | file: { 40 | description: 'File to use for Device Protection' 41 | }, 42 | d: { 43 | description: 'Device ID or name', 44 | alias: 'device' 45 | } 46 | }, 47 | handler: (args) => { 48 | const DeviceProtectionCommands = require('../cmd/device-protection'); 49 | return new DeviceProtectionCommands().enableProtection(args); 50 | }, 51 | examples: { 52 | '$0 $command': 'Turns an Open Device into a Protected Device' 53 | } 54 | }); 55 | 56 | return deviceProtection; 57 | }; 58 | 59 | -------------------------------------------------------------------------------- /src/cli/doctor.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'doctor', 'NOT SUPPORTED. Go to the device doctor tool at docs.particle.io/tools/doctor', { 3 | handler: () => { 4 | const DoctorCommand = require('../cmd/doctor'); 5 | return new DoctorCommand().deviceDoctor(); 6 | } 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/cli/function.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const func = commandProcessor.createCategory(root, 'function', 'Call functions on your device'); 3 | 4 | commandProcessor.createCommand(func, 'list', 'Show functions provided by your device(s)', { 5 | handler: (args) => { 6 | const FunctionCommand = require('../cmd/function'); 7 | return new FunctionCommand(args).listFunctions(); 8 | } 9 | }); 10 | 11 | commandProcessor.createCommand(func, 'call', 'Call a particular function on a device', { 12 | params: ' [argument]', 13 | options: { 14 | 'product': { 15 | description: 'Target a device within the given Product ID or Slug' 16 | } 17 | }, 18 | handler: (args) => { 19 | const FunctionCommand = require('../cmd/function'); 20 | return new FunctionCommand(args).callFunction(args); 21 | 22 | }, 23 | examples: { 24 | '$0 $command coffee brew': 'Call the `brew` function on the `coffee` device', 25 | '$0 $command board digitalWrite D7=HIGH': 'Call the `digitalWrite` function with argument `D7=HIGH` on the `board` device', 26 | '$0 $command 0123456789abcdef01234567 brew --product 12345': 'Call the `brew` function on the device with id `0123456789abcdef01234567` within product `12345`' 27 | } 28 | }); 29 | 30 | return func; 31 | }; 32 | -------------------------------------------------------------------------------- /src/cli/help.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root, app }) => { 2 | commandProcessor.createCommand(root, 'help', false, { 3 | params: '[command...]', 4 | handler: (argv) => { 5 | let cmd = argv.params.command; 6 | cmd.push('--help'); 7 | 8 | return app.runCommand(cmd); 9 | } 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/cli/library_add.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const log = require('../lib/log'); 3 | const { spin } = require('../app/ui'); 4 | const { buildAPIClient } = require('./apiclient'); 5 | const { LibraryAddCommand, LibraryAddCommandSite } = require('../cmd'); 6 | 7 | 8 | class CLILibraryAddCommandSite extends LibraryAddCommandSite { 9 | constructor(argv, apiClient){ 10 | super(); 11 | this._apiClient = apiClient; 12 | [this.name, this.version='latest'] = argv.params.name.split('@'); 13 | this.dir = argv.params.dir || process.cwd(); 14 | } 15 | 16 | apiClient(){ 17 | return this._apiClient; 18 | } 19 | 20 | libraryIdent(){ 21 | // todo - shouldn't this be a promise? 22 | return { 23 | name: this.name, 24 | version: this.version 25 | }; 26 | } 27 | 28 | projectDir(){ 29 | return this.dir; 30 | } 31 | 32 | fetchingLibrary(promise, name){ 33 | return spin(promise, `Adding library ${chalk.blue(name)}`); 34 | } 35 | 36 | async addedLibrary(name, version){ 37 | log.success(`Library ${chalk.blue(name)} ${version} has been added to the project.`); 38 | log.success(`To get started using this library, run ${chalk.bold('particle library view '+name)} to view the library documentation and sources.`); 39 | } 40 | } 41 | 42 | 43 | module.exports.command = (apiJS, argv) => { 44 | const site = new CLILibraryAddCommandSite(argv, buildAPIClient(apiJS)); 45 | const cmd = new LibraryAddCommand(); 46 | return site.run(cmd); 47 | }; 48 | 49 | -------------------------------------------------------------------------------- /src/cli/library_add.test.js: -------------------------------------------------------------------------------- 1 | // The following test struggles to complete in a timely fashion 2 | // commenting out for now until we can figure out how to avoid the timeouts 3 | // we periodacally see: 4 | // ``` 5 | // 1) LibraryAddCommand 6 | // adds a library to a project: 7 | // Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/distiller/project/src/cli/library_add.test.js) 8 | // at listOnTimeout (node:internal/timers:559:17) 9 | // at processTimers (node:internal/timers:502:7) 10 | // ``` 11 | // 12 | // const fs = require('fs'); 13 | // const path = require('path'); 14 | // const mockfs = require('mock-fs'); 15 | // const { expect, sinon } = require('../../test/setup'); 16 | // const { LibraryAddCommand, LibraryAddCommandSite } = require('../cmd'); 17 | // 18 | // describe('LibraryAddCommand', () => { 19 | // const sandbox = sinon.createSandbox(); 20 | // let testSite; 21 | 22 | // beforeEach(() => { 23 | // testSite = sandbox.createStubInstance(LibraryAddCommandSite); 24 | // mockfs(); 25 | // }); 26 | 27 | // afterEach(() => { 28 | // sandbox.restore(); 29 | // mockfs.restore(); 30 | // }); 31 | 32 | // it('adds a library to a project', async () => { 33 | // const name = 'neopixel'; 34 | // const version = '1.0.0'; 35 | // const dir = '.'; 36 | 37 | // fs.writeFileSync(path.join(dir, 'project.properties'), ''); 38 | 39 | // const apiClient = { library: sandbox.stub() }; 40 | 41 | // testSite.projectDir.withArgs().returns(dir); 42 | // testSite.apiClient.withArgs().resolves(apiClient); 43 | // testSite.libraryIdent.withArgs().returns({ name }); 44 | // testSite.addedLibrary.withArgs(name, version).resolves(); 45 | // testSite.fetchingLibrary.withArgs().callsFake(promise => promise); 46 | 47 | // const neopixelLatest = { name, version }; 48 | 49 | // apiClient.library.withArgs(name, { version: 'latest' }).resolves(neopixelLatest); 50 | 51 | // const cmd = new LibraryAddCommand(); 52 | // const result = await cmd.run({}, testSite); 53 | // const expectedDependency = /dependencies.neopixel=1\.0\.0/; 54 | // const savedProperties = fs.readFileSync(`${dir}/project.properties`, 'utf8'); 55 | // expect(savedProperties).to.match(expectedDependency); 56 | // expect(result).to.not.exist; 57 | // }); 58 | // }); 59 | 60 | -------------------------------------------------------------------------------- /src/cli/library_delete.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const { spin } = require('../app/ui'); 3 | const log = require('../lib/log'); 4 | const { buildAPIClient } = require('./apiclient'); 5 | const { LibraryDeleteCommandSite, LibraryDeleteCommand } = require('../cmd'); 6 | 7 | 8 | class CLILibraryDeleteCommandSite extends LibraryDeleteCommandSite { 9 | 10 | constructor(argv, apiClient) { 11 | super(); 12 | this._apiClient = apiClient; 13 | this.argv = argv; 14 | } 15 | 16 | libraryIdent() { 17 | return this.argv.params.name; 18 | } 19 | 20 | apiClient() { 21 | return this._apiClient; 22 | } 23 | 24 | notifyStart(promise, lib) { 25 | return spin(promise, `Deleting library ${chalk.green(lib)}...`); 26 | } 27 | 28 | notifyComplete(promise, library, error) { 29 | if (error) { 30 | // this leads to the message being printed twice 31 | // log.error(error); 32 | } else { 33 | log.success(`Library ${chalk.green(library)} deleted.`); 34 | } 35 | } 36 | } 37 | 38 | module.exports.CLILibraryDeleteCommandSite = CLILibraryDeleteCommandSite; 39 | module.exports.command = (apiJS, argv) => { 40 | const site = new CLILibraryDeleteCommandSite(argv, buildAPIClient(apiJS)); 41 | const cmd = new LibraryDeleteCommand(); 42 | return site.run(cmd); 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /src/cli/library_init.js: -------------------------------------------------------------------------------- 1 | const { LibraryInitCommandSite, LibraryInitCommand } = require('../cmd'); 2 | const UI = require('../lib/ui'); 3 | const inquirer = require('inquirer'); 4 | 5 | class CLILibraryInitCommandSite extends LibraryInitCommandSite { 6 | constructor(argv) { 7 | super(); 8 | this.ui = new UI(); 9 | this.argv = argv; 10 | } 11 | 12 | options() { 13 | return this.argv; 14 | } 15 | 16 | arguments() { 17 | return this.argv.params || []; 18 | } 19 | 20 | prompter() { 21 | return inquirer.prompt; 22 | } 23 | 24 | outputStreamer() { 25 | return this.ui.stdout; 26 | } 27 | 28 | } 29 | 30 | 31 | module.exports.CLILibraryInitCommandSite = CLILibraryInitCommandSite; 32 | module.exports.command = (argv) => { 33 | // todo - can we avoid the global dependency on process.cwd() 34 | // the cli itself should provide an environment, including cwd(). 35 | const site = new CLILibraryInitCommandSite(argv, process.cwd()); 36 | const cmd = new LibraryInitCommand(); 37 | return site.run(cmd); 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /src/cli/library_migrate.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | ****************************************************************************** 3 | Copyright (c) 2016 Particle Industries, Inc. All rights reserved. 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation, either 8 | version 3 of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this program; if not, see . 17 | ****************************************************************************** 18 | */ 19 | 20 | const { expect, sinon } = require('../../test/setup'); 21 | const { CLILibraryTestMigrateCommandSite } = require('./library_migrate'); 22 | 23 | 24 | describe('library command', () => { 25 | describe('CLILibraryTestMigrateCommandSite', () => { 26 | const argv = { params: {} }; 27 | const sut = new CLILibraryTestMigrateCommandSite(argv, __dirname); 28 | 29 | it('calls handleError when notifyEnd is called with a non-zero 3rd parameter', () => { 30 | 31 | sut.handleError = sinon.spy(); 32 | sut.notifyEnd('lib', undefined, '123'); 33 | 34 | expect(sut.handleError).to.be.calledWith('lib', '123'); 35 | }); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /src/cli/library_search.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const log = require('../lib/log'); 3 | const { spin } = require('../app/ui'); 4 | const { buildAPIClient } = require('./apiclient'); 5 | const { formatLibrary } = require('./library_ui'); 6 | const { LibrarySearchCommandSite, LibrarySearchCommand } = require('../cmd'); 7 | const { JSONResult } = require('../lib/json-result'); 8 | 9 | 10 | class CLILibrarySearchCommandSite extends LibrarySearchCommandSite { 11 | constructor(argv, apiClient) { 12 | super(); 13 | this._apiClient = apiClient; 14 | this.argv = argv; 15 | } 16 | 17 | searchString() { 18 | return this.argv.params.name; 19 | } 20 | 21 | apiClient() { 22 | return this._apiClient; 23 | } 24 | 25 | notifyListLibrariesStart(promise, filter) { 26 | const { json } = this.argv; 27 | 28 | if (json){ 29 | return promise; 30 | } 31 | return spin(promise, `Searching for libraries matching ${chalk.green(filter)}`); 32 | } 33 | 34 | notifyListLibrariesComplete(promise, filter, libraries, error) { 35 | const { json } = this.argv; 36 | 37 | if (error){ 38 | throw error; 39 | } 40 | 41 | if (json){ 42 | return console.log( 43 | this.createJSONResult(filter, libraries) 44 | ); 45 | } 46 | 47 | const count = libraries ? libraries.length : 0; 48 | const library = count === 1 ? 'library' : 'libraries'; 49 | log.success(`Found ${count} ${library} matching ${chalk.green(filter)}`); 50 | for (let idx in libraries) { 51 | const lib = libraries[idx]; 52 | console.log(formatLibrary(lib)); 53 | } 54 | } 55 | 56 | createJSONResult(filter, libraries){ 57 | return new JSONResult({ filter }, libraries).toString(); 58 | } 59 | } 60 | 61 | 62 | module.exports.CLILibrarySearchCommandSite = CLILibrarySearchCommandSite; 63 | module.exports.command = (apiJS, argv) => { 64 | const site = new CLILibrarySearchCommandSite(argv, buildAPIClient(apiJS)); 65 | const cmd = new LibrarySearchCommand(); 66 | return site.run(cmd) 67 | .catch(error => { 68 | error.asJSON = argv.json; 69 | throw error; 70 | }); 71 | }; 72 | 73 | -------------------------------------------------------------------------------- /src/cli/library_ui.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | 4 | module.exports.formatLibrary = (library, excludeBadges=[]) => { 5 | let badges = []; 6 | 7 | if (library.official && !excludeBadges.official) { 8 | badges.push(chalk.green('[official] ')); 9 | } else { 10 | if (library.verified && !excludeBadges.verified) { 11 | badges.push(chalk.green('[verified] ')); 12 | } 13 | } 14 | 15 | if (library.visibility==='private' && !excludeBadges.private) { 16 | badges.push(chalk.blue('[private] ')); 17 | } else { 18 | if (library.mine && !excludeBadges.mine) { 19 | badges.push(chalk.blue('[mine] ')); 20 | } 21 | } 22 | 23 | const badgesText = badges.join(''); 24 | const version = library.version; 25 | const defaultSentence = ''; 26 | const formatted = chalk.blue(library.name)+' '+version+' '+badgesText+chalk.grey(library.installs || 0)+' '+ `${library.sentence || defaultSentence}`; 27 | return formatted; 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /src/cli/library_upload.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const log = require('../lib/log'); 3 | const { spin } = require('../app/ui'); 4 | const { convertApiError } = require('../cmd/api'); 5 | const { buildAPIClient } = require('./apiclient'); 6 | const { LibraryContributeCommand, LibraryContributeCommandSite } = require('../cmd'); 7 | 8 | 9 | class CLILibraryContributeCommandSite extends LibraryContributeCommandSite { 10 | constructor(argv, dir, apiClient) { 11 | super(); 12 | this.argv = argv; 13 | this.dir = dir; 14 | this._apiClient = apiClient; 15 | } 16 | 17 | libraryDirectory() { 18 | return this.dir; 19 | } 20 | 21 | apiClient() { 22 | return this._apiClient; 23 | } 24 | 25 | dryRun() { 26 | return this.argv.dryRun; 27 | } 28 | 29 | error(error) { 30 | throw convertApiError(error); 31 | } 32 | 33 | validatingLibrary(promise, directory) { 34 | return spin(promise, `Validating library at ${chalk.bold(directory)}`); 35 | } 36 | 37 | contributingLibrary(promise, library) { 38 | return spin(promise, `Uploading library ${chalk.green(library.name)}`); 39 | } 40 | 41 | contributeComplete(library) { 42 | return log.success(`Library ${chalk.green(library.name)} was successfully uploaded.\n` + 43 | `Add it to your project with ${chalk.bold('particle library add ' + library.name)}`); 44 | } 45 | } 46 | 47 | 48 | module.exports.CLILibraryContributeCommandSite = CLILibraryContributeCommandSite; 49 | module.exports.command = (apiJS, argv) => { 50 | const site = new CLILibraryContributeCommandSite(argv, process.cwd(), buildAPIClient(apiJS)); 51 | const cmd = new LibraryContributeCommand(); 52 | return site.run(cmd); 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /src/cli/preprocess.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'preprocess', 'Preprocess a Wiring file (ino) into a C++ file (cpp)', { 3 | params: '', 4 | options: { 5 | 'name': { 6 | description: 'Filename and path to include in the preprocessed file. Default to the input file name' 7 | }, 8 | 'saveTo': { 9 | description: 'Filename for the preprocessed file' 10 | } 11 | }, 12 | handler: (args) => { 13 | const PreprocessCommand = require('../cmd/preprocess'); 14 | return new PreprocessCommand().preprocess(args.params.file, args); 15 | }, 16 | examples: { 17 | '$0 $command app.ino': 'Preprocess app.ino and save it to app.cpp', 18 | '$0 $command - --name app.ino --saveTo -': 'Preprocess from standard input and save output to standard output. Useful for scripts' 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/cli/preprocess.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const commandProcessor = require('../app/command-processor'); 3 | const preprocess = require('./preprocess'); 4 | 5 | 6 | describe('preprocess command-line interface', () => { 7 | let root; 8 | beforeEach(() => { 9 | createApp(); 10 | addCommand(); 11 | }); 12 | 13 | function createApp() { 14 | root = commandProcessor.createAppCategory(); 15 | } 16 | 17 | function addCommand() { 18 | preprocess({ commandProcessor, root }); 19 | } 20 | 21 | it('parses the arguments', () => { 22 | const argv = commandProcessor.parse(root, ['preprocess', 'app.ino']); 23 | expect(argv.clierror).to.be.undefined; 24 | expect(argv.params).to.have.property('file').equal('app.ino'); 25 | }); 26 | 27 | it('fails when file is missing', () => { 28 | const argv = commandProcessor.parse(root, ['preprocess']); 29 | expect(argv.clierror).to.have.property('message', 'Parameter \'file\' is required.'); 30 | }); 31 | 32 | it('accepts an output file', () => { 33 | const argv = commandProcessor.parse(root, ['preprocess', 'app.ino', '--saveTo', 'processed.cpp']); 34 | expect(argv.clierror).to.be.undefined; 35 | expect(argv).to.have.property('saveTo').equal('processed.cpp'); 36 | }); 37 | 38 | it('accepts an input name', () => { 39 | const argv = commandProcessor.parse(root, ['preprocess', 'app.ino', '--name', 'file.ino']); 40 | expect(argv.clierror).to.be.undefined; 41 | expect(argv).to.have.property('name').equal('file.ino'); 42 | }); 43 | 44 | it('includes help with examples', () => { 45 | const termWidth = null; // don't right-align option type labels so testing is easier 46 | commandProcessor.parse(root, ['preprocess', '--help'], termWidth); 47 | commandProcessor.showHelp((helpText) => { 48 | expect(helpText).to.include('Preprocess a Wiring file (ino) into a C++ file (cpp)'); 49 | expect(helpText).to.include('Options:'); 50 | expect(helpText).to.include(' --name Filename and path to include in the preprocessed file. Default to the input file name [string]'); 51 | expect(helpText).to.include(' --saveTo Filename for the preprocessed file [string]'); 52 | expect(helpText).to.include('Examples:'); 53 | expect(helpText).to.include('particle preprocess app.ino'); 54 | expect(helpText).to.include('particle preprocess - --name app.ino --saveTo -'); 55 | }); 56 | }); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /src/cli/project.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const project = commandProcessor.createCategory(root, 'project', 'Manage application projects'); 3 | 4 | commandProcessor.createCommand(project, 'create', 'Create a new project in the current or specified directory', { 5 | options: { 6 | 'name' : { 7 | description: 'provide a name for the project' 8 | } 9 | }, 10 | params: '[dir]', 11 | handler: (...args) => require('./project_init').command(...args) 12 | }); 13 | 14 | return project; 15 | }; 16 | -------------------------------------------------------------------------------- /src/cli/publish.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'publish', 'Publish an event to the cloud', { 3 | params: ' [data]', 4 | options: { 5 | 'product': { 6 | description: 'Publish to the given Product ID or Slug\'s stream' 7 | } 8 | }, 9 | handler: (args) => { 10 | const PublishCommand = require('../cmd/publish'); 11 | return new PublishCommand(args).publishEvent(args); 12 | }, 13 | examples: { 14 | '$0 $command temp 25.0': 'Publish a temp event to your private event stream', 15 | '$0 $command temp 25.0 --product 12345': 'Publish a temp event to your product 12345\'s event stream' 16 | } 17 | }); 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /src/cli/subscribe.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'subscribe', 'Listen to device event stream', { 3 | params: '[event]', 4 | options: { 5 | 'all': { 6 | boolean: true, 7 | description: 'Listen to all events instead of just those from my devices' 8 | }, 9 | 'device': { 10 | describe: 'Listen to events from this device only' 11 | }, 12 | 'until': { 13 | describe: 'Listen until we see an event exactly matching this data' 14 | }, 15 | 'max': { 16 | number: true, 17 | describe: 'Listen until we see this many events' 18 | }, 19 | 'product': { 20 | description: 'Target a device within the given Product ID or Slug' 21 | } 22 | }, 23 | handler: (args) => { 24 | const SubscribeCommand = require('../cmd/subscribe'); 25 | return new SubscribeCommand(args).startListening(args); 26 | }, 27 | examples: { 28 | '$0 $command': 'Subscribe to all event published by my devices', 29 | '$0 $command update': 'Subscribe to events starting with `update` from my devices', 30 | '$0 $command --product 12345': 'Subscribe to all events published by devices within product `12345`', 31 | '$0 $command --device blue': 'Subscribe to all events published by device `blue`', 32 | '$0 $command --all': 'Subscribe to public events and all events published by my devices', 33 | '$0 $command --until data': 'Subscribe to all events and exit when an event has data matching `data`', 34 | '$0 $command --max 4': 'Subscribe to all events and exit after seeing `4` events' 35 | } 36 | }); 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/cli/token.js: -------------------------------------------------------------------------------- 1 | const AccessTokenCommands = require('../cmd/token'); 2 | 3 | module.exports = ({ commandProcessor, root }) => { 4 | const token = commandProcessor.createCategory(root, 'token', 'Manage access tokens (require username/password)'); 5 | 6 | commandProcessor.createCommand(token, 'revoke', 'Revoke an access token', { 7 | params: '', 8 | options: { 9 | 'force': { 10 | boolean: true, 11 | description: 'Force deleting access token used by this CLI' 12 | } 13 | }, 14 | handler: (args) => { 15 | return new AccessTokenCommands().revokeAccessToken(args.params.tokens, args); 16 | } 17 | }); 18 | 19 | commandProcessor.createCommand(token, 'create', 'Create a new access token', { 20 | options: { 21 | 'expires-in': { 22 | description: 'Create a token valid for this many seconds. When omitted, the Particle API assigns a default expiration.', 23 | number: true 24 | }, 25 | 'never-expires': { 26 | description: "Create a token that doesn't expire. Useful for a token that will be used by a cloud application for making Particle API requests.", 27 | boolean: true 28 | }, 29 | }, 30 | handler: (args) => { 31 | return new AccessTokenCommands().createAccessToken({ expiresIn: args['expires-in'], neverExpires: args['never-expires'] }); 32 | } 33 | }); 34 | 35 | return token; 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /src/cli/udp.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const udp = commandProcessor.createCategory(root, 'udp', 'Talk UDP to repair devices, run patches, check Wi-Fi, and more!'); 3 | 4 | commandProcessor.createCommand(udp, 'send', 'Sends a UDP packet to the specified host and port', { 5 | params: ' ', 6 | handler: (args) => { 7 | const UdpCommand = require('../cmd/udp'); 8 | return new UdpCommand().sendUdpPacket({ host: args.params.host, port: args.params.port, message: args.params.message }); 9 | } 10 | }); 11 | 12 | commandProcessor.createCommand(udp, 'listen', 'Listens for UDP packets on an optional port (default 5549)', { 13 | params: '[port]', 14 | handler: (args) => { 15 | const UdpCommand = require('../cmd/udp'); 16 | return new UdpCommand(args).listenUdp({ port: args.params.port }); 17 | } 18 | }); 19 | 20 | return udp; 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/cli/update-cli.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'update-cli', 'Update the Particle CLI to the latest version', { 3 | options: { 4 | 'enable-updates': { 5 | boolean: true, 6 | description: 'Enable automatic update checks' 7 | }, 8 | 'disable-updates': { 9 | boolean: true, 10 | description: 'Disable automatic update checks' 11 | }, 12 | 'version': { 13 | description: 'Update to a specific version' 14 | } 15 | }, 16 | handler: (args) => { 17 | const UpdateCliCommand = require('../cmd/update-cli'); 18 | return new UpdateCliCommand().update(args); 19 | } 20 | }); 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/cli/update.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'update', 'Update Device OS on a device via USB', { 3 | params: '[device]', 4 | options: { 5 | 'target': { 6 | description: 'The Device OS version to update. Defaults to latest version.', 7 | } 8 | }, 9 | handler: (args) => { 10 | const UpdateCommand = require('../cmd/update'); 11 | return new UpdateCommand().updateDevice(args.params.device, args); 12 | }, 13 | examples: { 14 | '$0 $command': 'Update Device OS on the device connected over USB', 15 | '$0 $command red': 'Update Device OS on device red', 16 | '$0 $command --target 5.0.0 blue': 'Update Device OS on device blue to version 5.0.0' 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/cli/variable.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | const variable = commandProcessor.createCategory(root, 'variable', 'Retrieve and monitor variables on your device'); 3 | 4 | const timeOption = { 5 | 'time': { 6 | boolean: true, 7 | description: 'Show the time when the variable was received' 8 | } 9 | }; 10 | 11 | commandProcessor.createCommand(variable, 'list', 'Show variables provided by your device(s)', { 12 | handler: (args) => { 13 | const VariableCommand = require('../cmd/variable'); 14 | return new VariableCommand(args).listVariables(); 15 | } 16 | }); 17 | 18 | commandProcessor.createCommand(variable, 'get', 'Retrieve a value from your device', { 19 | params: '[device] [variableName]', 20 | options: Object.assign({}, timeOption, { 21 | 'product': { 22 | description: 'Target a device within the given Product ID or Slug' 23 | } 24 | }), 25 | handler: (args) => { 26 | const VariableCommand = require('../cmd/variable'); 27 | return new VariableCommand(args).getValue(args); 28 | }, 29 | examples: { 30 | '$0 $command basement temperature': 'Read the `temperature` variable from the device `basement`', 31 | '$0 $command 0123456789abcdef01234567 temperature --product 12345': 'Read the `temperature` variable from the device with id `0123456789abcdef01234567` within product `12345`', 32 | '$0 $command all temperature': 'Read the `temperature` variable from all my devices' 33 | } 34 | }); 35 | 36 | commandProcessor.createCommand(variable, 'monitor', 'Connect and display messages from a device', { 37 | params: '[device] [variableName]', 38 | options: Object.assign({}, timeOption, { 39 | 'delay': { 40 | number: true, 41 | description: 'Interval in milliseconds between variable fetches', 42 | nargs: 1 43 | } 44 | }), 45 | handler: (args) => { 46 | const VariableCommand = require('../cmd/variable'); 47 | return new VariableCommand(args).monitorVariables(args); 48 | }, 49 | examples: { 50 | '$0 $command up temp --delay 2000': 'Read the temp variable from the device up every 2 seconds' 51 | } 52 | }); 53 | 54 | return variable; 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /src/cli/version.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root, app }) => { 2 | commandProcessor.createCommand(root, 'version', false, { 3 | handler: () => app.runCommand(['--version']), 4 | }); 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /src/cli/whoami.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ commandProcessor, root }) => { 2 | commandProcessor.createCommand(root, 'whoami', 'prints signed-in username', { 3 | handler: () => { 4 | const WhoAmICommand = require('../cmd/whoami'); 5 | return new WhoAmICommand().getUsername(); 6 | } 7 | }); 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /src/cmd/README_binary.md: -------------------------------------------------------------------------------- 1 | # binary.js 2 | 3 | ## Overview 4 | 5 | `binary.js` is a command module in the `particle-cli` tool that provides functionality related to inspecting binary files and/or bundles that may contain binary files and assets. 6 | 7 | ## Usage 8 | 9 | To use the `binary.js` command module, you can run the following commands: 10 | 11 | - `particle binary inspect <.bin file>`: Inspects a binary file and displays detailed information such as filename, crc, prefixInfo, suffixInfo, and other relevant metadata. 12 | 13 | - `particle binary inspect <.bin file with baked-in dependencies>`: Inspects a binary file and displays detailed information such as filename, crc, prefixInfo, suffixInfo, and other relevant metadata such as TLV of the (asset) files. 14 | 15 | - `particle binary inspect <.zip file>`: Extracts and inspects contents from the zip file 16 | 17 | ## Command-Line Options 18 | 19 | - `--verbose` or `-v`: Increases how much logging to display 20 | 21 | - `--quiet` or `-q`: Decreases how much logging to display 22 | 23 | ## Examples 24 | 25 | 1. Inspecting a binary file such as tinker.bin: 26 | 27 | ``` 28 | particle binary inspect p2_app.bin 29 | 30 | > particle-cli@3.10.2 start 31 | > node ./src/index.js binary inspect /path/to/p2_app.bin 32 | 33 | p2_app.bin 34 | CRC is ok (6e2abf80) 35 | Compiled for p2 36 | This is an application module number 1 at version 6 37 | It depends on a system module number 1 at version 5302 38 | ``` 39 | 40 | 2. Inspecting a zip file whose app firmware has baked-in asset dependencies: 41 | 42 | ``` 43 | $ npm start -- binary inspect /path/to/bundle.zip 44 | 45 | > particle-cli@3.10.2 start 46 | > node ./src/index.js binary inspect /path/to/bundle.zip 47 | 48 | app.bin 49 | CRC is ok (7fa30408) 50 | Compiled for argon 51 | This is an application module number 2 at version 6 52 | It is firmware for product id 12 at version 3 53 | It depends on a system module number 1 at version 4006 54 | It depends on assets: 55 | cat.txt (hash b0f0d8ff8cc965a7b70b07e0c6b4c028f132597196ae9c70c620cb9e41344106) 56 | house.txt (hash a78fb0e7df9977ffd3102395254ae92dd332b46a616e75ff4701e75f91dd60d3) 57 | water.txt (hash 3b0c25d6b8af66da115b30018ae94fbe3f04ac056fa60d1150131128baf8c591) 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /src/cmd/base.js: -------------------------------------------------------------------------------- 1 | const { errors: { usageError } } = require('../app/command-processor'); 2 | const UI = require('../lib/ui'); 3 | 4 | const DEVICE_ID_PTN = /^[0-9a-f]{24}$/i; 5 | 6 | 7 | module.exports = class CLICommandBase { 8 | constructor({ 9 | stdin = process.stdin, 10 | stdout = process.stdout, 11 | stderr = process.stderr, 12 | quiet = false 13 | } = {}) { 14 | this.stdin = stdin; 15 | this.stdout = stdout; 16 | this.stderr = stderr; 17 | this.quiet = !!quiet; 18 | this.ui = new UI({ stdin, stdout, stderr, quiet }); 19 | } 20 | 21 | isDeviceId(x){ 22 | return DEVICE_ID_PTN.test(x); 23 | } 24 | 25 | async showUsageError(msg){ 26 | throw usageError(msg); 27 | } 28 | 29 | async showProductDeviceNameUsageError(device){ 30 | await this.showUsageError( 31 | `\`device\` must be an id when \`--product\` flag is set - received: ${device}` 32 | ); 33 | } 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /src/cmd/base.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const CLICommandBase = require('./base'); 3 | const UI = require('../lib/ui'); 4 | 5 | 6 | describe('CLI Command Base Class', () => { 7 | let cmd; 8 | 9 | beforeEach(() => { 10 | cmd = new CLICommandBase(); 11 | }); 12 | 13 | it('Initializes', () => { 14 | expect(cmd).to.have.property('stdin', process.stdin); 15 | expect(cmd).to.have.property('stdout', process.stdout); 16 | expect(cmd).to.have.property('stderr', process.stderr); 17 | expect(cmd).to.have.property('quiet', false); 18 | expect(cmd).to.have.property('ui').that.is.an.instanceof(UI); 19 | expect(cmd).to.respondTo('showUsageError'); 20 | expect(cmd).to.respondTo('isDeviceId'); 21 | }); 22 | 23 | it('Determines if input is a device id', () => { 24 | expect(cmd.isDeviceId(null)).to.equal(false); 25 | expect(cmd.isDeviceId(undefined)).to.equal(false); 26 | expect(cmd.isDeviceId('')).to.equal(false); 27 | expect(cmd.isDeviceId(666)).to.equal(false); 28 | expect(cmd.isDeviceId('nope')).to.equal(false); 29 | expect(cmd.isDeviceId(123456789123456789123456)).to.equal(false); // eslint-disable-line no-loss-of-precision 30 | expect(cmd.isDeviceId('0123456789abcdef0123456')).to.equal(false); 31 | expect(cmd.isDeviceId('0123456789abcdef01234567')).to.equal(true); 32 | expect(cmd.isDeviceId('0123456789ABCDEF01234567')).to.equal(true); 33 | }); 34 | 35 | it('Shows a usage error', async () => { 36 | const promise = cmd.showUsageError('test'); 37 | 38 | expect(promise).to.be.an.instanceof(Promise); 39 | 40 | let error; 41 | 42 | try { 43 | await promise; 44 | } catch (e){ 45 | error = e; 46 | } 47 | 48 | expect(error).to.be.an.instanceof(Error); 49 | expect(error).to.have.property('message', 'test'); 50 | }); 51 | 52 | it('Shows a product device name usage error', async () => { 53 | const promise = cmd.showProductDeviceNameUsageError('my-device-name'); 54 | 55 | expect(promise).to.be.an.instanceof(Promise); 56 | 57 | let error; 58 | 59 | try { 60 | await promise; 61 | } catch (e){ 62 | error = e; 63 | } 64 | 65 | expect(error).to.be.an.instanceof(Error); 66 | expect(error).to.have.property('message', '`device` must be an id when `--product` flag is set - received: my-device-name'); 67 | }); 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /src/cmd/bundle2.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/src/cmd/bundle2.test.js -------------------------------------------------------------------------------- /src/cmd/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const settings = require('../../settings'); 3 | const utilities = require('../lib/utilities'); 4 | 5 | 6 | module.exports = class ConfigCommand { 7 | constructor(options) { 8 | this.options = options; 9 | } 10 | 11 | configSwitch(profile, setting, value, { list }) { 12 | if (list) { 13 | this.listProfiles(); 14 | } else if (setting) { 15 | this.changeSetting(profile === 'set' ? settings.profile : profile, setting, value); 16 | } else if (profile) { 17 | this.switchProfile(profile); 18 | } else { 19 | this.showProfile(); 20 | } 21 | } 22 | 23 | switchProfile(profile) { 24 | settings.switchProfile(profile); 25 | } 26 | 27 | changeSetting(profile, name, value) { 28 | settings.override(profile, name, value); 29 | } 30 | 31 | showProfile() { 32 | console.log('Current profile: ' + settings.profile); 33 | console.log('Using API: ' + settings.apiUrl); 34 | if (settings.proxyUrl) { 35 | console.log('Proxy URL: ' + settings.proxyUrl); 36 | } 37 | console.log('Access token: ' + settings.access_token); 38 | } 39 | 40 | listProfiles() { 41 | const particleDir = settings.ensureFolder(); 42 | const files = utilities.globList(null, [ 43 | path.join(particleDir, '*.config.json') 44 | ]); 45 | 46 | if (files.length > 0) { 47 | console.log('Available config files: '); 48 | for (let i = 0; i < files.length; i++) { 49 | 50 | //strip the path 51 | const filename = path.basename(files[i]); 52 | 53 | //strip the extension 54 | const name = filename.replace('.config.json', ''); 55 | 56 | console.log((i + 1) + '.) ' + name); 57 | } 58 | } else { 59 | console.log('No configuration files found.'); 60 | } 61 | } 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /src/cmd/device-util.js: -------------------------------------------------------------------------------- 1 | const { platformForId } = require('../lib/platform'); 2 | const semver = require('semver'); 3 | 4 | /** 5 | * Check if the string can represent a valid device ID. 6 | * 7 | * @param {String} str A string. 8 | * @return {Boolean} 9 | */ 10 | module.exports.isDeviceId = (str) => { 11 | return /^[0-9a-f]{24}$/i.test(str); 12 | }; 13 | 14 | /** 15 | * Format device info. 16 | * 17 | * @param {Object} device Device info. 18 | * @param {String} device.id Device ID. 19 | * @param {String} device.type Device type, e.g. 'Photon'. 20 | * @param {String} [device.name] Device name. 21 | * @return {String} 22 | */ 23 | module.exports.formatDeviceInfo = ({ id, type, name = null }) => { 24 | return `${name || ''} [${id}] (${type})`; 25 | }; 26 | 27 | /** 28 | * Get device attributes. 29 | * 30 | * @param {Object} options Options. 31 | * @param {String} options.id Device ID or name. 32 | * @param {Object} options.api API client. 33 | * @param {String} options.auth Access token. 34 | * @param {String} [options.displayName] Device name as shown to the user. 35 | * @param {Boolean} [options.dontThrow] Return 'null' instead of throwing an error if the device cannot be found. 36 | * @param {Promise} 37 | */ 38 | module.exports.getDevice = ({ id, api, auth, displayName = null, dontThrow = false }) => { 39 | return api.getDevice({ deviceId: id, auth }) 40 | .then(res => res.body) 41 | .catch(error => { 42 | if (error.statusCode === 403 || error.statusCode === 404) { 43 | if (dontThrow) { 44 | return null; 45 | } 46 | throw new Error(`Device not found: ${displayName || id}`); 47 | } 48 | throw error; 49 | }); 50 | }; 51 | 52 | module.exports.validateDFUSupport = ({ device, ui }) => { 53 | const platform = platformForId(device.platformId); 54 | if (!device.isInDfuMode && (!semver.valid(device.firmwareVersion) || semver.lt(device.firmwareVersion, '2.0.0')) && platform.generation === 2) { 55 | ui.logDFUModeRequired({ showVersionWarning: true }); 56 | throw new Error('Put the device in DFU mode and try again'); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/cmd/doctor.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | module.exports = class DoctorCommand { 4 | deviceDoctor(){ 5 | console.log(`${chalk.bold.white('particle device doctor')} is no longer supported.`); 6 | console.log(`Go to the device doctor tool at ${chalk.bold.cyan('docs.particle.io/tools/doctor')}.\n`); 7 | } 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /src/cmd/esim.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | const ESimCommands = require('./esim'); 4 | 5 | describe('ESimCommands', () => { 6 | let esim; 7 | let cacheStub; 8 | 9 | beforeEach(() => { 10 | esim = new ESimCommands(); 11 | esim.inputJsonData = { 12 | provisioning_data: [ 13 | { profiles: [{ iccid: '123' }] }, 14 | { profiles: [{ iccid: '456' }] }, 15 | { profiles: [{ iccid: '789' }] } 16 | ] 17 | }; 18 | cacheStub = sinon.stub(esim.cache, 'get'); 19 | }); 20 | 21 | afterEach(() => { 22 | sinon.restore(); 23 | }); 24 | 25 | describe('_generateAvailableProvisioningData', () => { 26 | 27 | it('should populate availableProvisioningData with all profiles when cache is empty', async () => { 28 | cacheStub.returns([]); 29 | await esim._generateAvailableProvisioningData(); 30 | expect(esim.availableProvisioningData.size).to.equal(3); 31 | expect(esim.availableProvisioningData.has(0)).to.be.true; 32 | expect(esim.availableProvisioningData.has(1)).to.be.true; 33 | expect(esim.availableProvisioningData.has(2)).to.be.true; 34 | }); 35 | 36 | it('should remove provisioned profiles from availableProvisioningData', async () => { 37 | cacheStub.returns([[{ iccid: '456' }]]); 38 | await esim._generateAvailableProvisioningData(); 39 | expect(esim.availableProvisioningData.size).to.equal(2); 40 | expect(esim.availableProvisioningData.has(0)).to.be.true; 41 | expect(esim.availableProvisioningData.has(1)).to.be.false; 42 | expect(esim.availableProvisioningData.has(2)).to.be.true; 43 | }); 44 | 45 | it('should remove all profiles from availableProvisioningData if all are provisioned', async () => { 46 | cacheStub.returns([[{ iccid: '123' }], [{ iccid: '456' }], [{ iccid: '789' }]]); 47 | await esim._generateAvailableProvisioningData(); 48 | expect(esim.availableProvisioningData.size).to.equal(0); 49 | }); 50 | 51 | it('should handle corrupted cache data gracefully', async () => { 52 | cacheStub.returns('{not valid json}'); 53 | await esim._generateAvailableProvisioningData(); 54 | expect(esim.availableProvisioningData.size).to.equal(3); 55 | }); 56 | 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/cmd/function.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const VError = require('verror'); 3 | const ParticleAPI = require('./api'); 4 | const LegacyApiClient = require('../lib/api-client'); 5 | const settings = require('../../settings'); 6 | const CLICommandBase = require('./base'); 7 | 8 | const { normalizedApiError } = LegacyApiClient; 9 | 10 | 11 | module.exports = class FunctionCommand extends CLICommandBase { 12 | constructor(...args){ 13 | super(...args); 14 | } 15 | 16 | listFunctions(){ 17 | const api = new LegacyApiClient(); 18 | api.ensureToken(); 19 | 20 | return api.getAllAttributes() 21 | .then(devices => this.ui.logDeviceDetail(devices, { fnsOnly: true })) 22 | .catch(err => { 23 | throw new VError(normalizedApiError(err), 'Error while listing variables'); 24 | }); 25 | } 26 | 27 | callFunction({ product, params: { device, function: fn, argument: arg } }){ 28 | if (product){ 29 | if (!this.isDeviceId(device)){ 30 | return this.showProductDeviceNameUsageError(device); 31 | } 32 | } 33 | 34 | let msg = `Calling function ${fn} from device ${device}`; 35 | 36 | if (product){ 37 | msg += ` in product ${product}`; 38 | } 39 | 40 | const fetchVar = createAPI().callFunction(device, fn, arg, product); 41 | return this.ui.showBusySpinnerUntilResolved(msg, fetchVar) 42 | .then(res => { 43 | if (!res || !Object.prototype.hasOwnProperty.call(res, 'return_value')){ 44 | throw res; 45 | } 46 | this.ui.stdout.write(`${res.return_value}${os.EOL}`); 47 | }) 48 | .catch(error => { 49 | let message = `Error calling function: \`${fn}\``; 50 | 51 | if (error && error.statusCode === 404){ 52 | message = `Function call failed: Function \`${fn}\` not found`; 53 | } 54 | throw createAPIErrorResult({ error, message }); 55 | }); 56 | } 57 | }; 58 | 59 | // UTILS ////////////////////////////////////////////////////////////////////// 60 | function createAPI(){ 61 | return new ParticleAPI(settings.apiUrl, { 62 | accessToken: settings.access_token 63 | }); 64 | } 65 | 66 | function createAPIErrorResult({ error: e, message, json }){ 67 | const error = new VError(normalizedApiError(e), message); 68 | error.asJSON = json; 69 | return error; 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/cmd/identify-tachyon.js: -------------------------------------------------------------------------------- 1 | const CLICommandBase = require('./base'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | 5 | const { 6 | getEDLDevice, 7 | getTachyonInfo 8 | } = require('../lib/tachyon-utils'); 9 | 10 | 11 | module.exports = class IdentifyTachyonCommand extends CLICommandBase { 12 | constructor({ ui } = {}) { 13 | super(); 14 | this.ui = ui || this.ui; 15 | } 16 | 17 | async identify() { 18 | const device = await getEDLDevice({ ui: this.ui }); 19 | const outputLog = path.join(os.tmpdir(), `tachyon_${device.id}_identify_${Date.now()}.log`); 20 | 21 | try { 22 | const tachyonInfo = await getTachyonInfo({ outputLog, ui: this.ui, device }); 23 | this.printIdentification(tachyonInfo); 24 | } catch (error) { 25 | this.ui.stdout.write(`An error ocurred while trying to identify your tachyon ${os.EOL}`); 26 | this.ui.stdout.write(`Error: ${error.message} ${os.EOL}`); 27 | this.ui.stdout.write(`Verify your logs ${outputLog} for more information ${os.EOL}`); 28 | } 29 | } 30 | 31 | printIdentification({ deviceId, region, manufacturingData, osVersion }) { 32 | this.ui.stdout.write(`Device ID: ${deviceId}${os.EOL}`); 33 | this.ui.stdout.write(`Region: ${region}${os.EOL}`); 34 | this.ui.stdout.write(`Manufacturing data: ${manufacturingData}${os.EOL}`); 35 | this.ui.stdout.write(`OS Version: ${osVersion}${os.EOL}`); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/cmd/index.js: -------------------------------------------------------------------------------- 1 | const ParticleCmds = require('particle-commands'); 2 | 3 | module.exports = ParticleCmds; 4 | 5 | -------------------------------------------------------------------------------- /src/cmd/publish.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const VError = require('verror'); 3 | const settings = require('../../settings'); 4 | const { normalizedApiError } = require('../lib/api-client'); 5 | const ParticleAPI = require('./api'); 6 | const CLICommandBase = require('./base'); 7 | 8 | 9 | module.exports = class PublishCommand extends CLICommandBase { 10 | constructor(...args){ 11 | super(...args); 12 | } 13 | 14 | publishEvent({ product, params: { event, data } }){ 15 | let epilogue = `private event: ${event}`; 16 | 17 | if (product){ 18 | epilogue += ` to product: ${product}`; 19 | } 20 | 21 | const publishEvent = createAPI().publishEvent(event, data, product); 22 | return this.ui.showBusySpinnerUntilResolved(`Publishing ${epilogue}`, publishEvent) 23 | .then(() => this.ui.stdout.write(`Published ${epilogue}${os.EOL}${os.EOL}`)) 24 | .catch(error => { 25 | const message = 'Error publishing event'; 26 | throw createAPIErrorResult({ error, message }); 27 | }); 28 | } 29 | }; 30 | 31 | 32 | // UTILS ////////////////////////////////////////////////////////////////////// 33 | function createAPI(){ 34 | return new ParticleAPI(settings.apiUrl, { 35 | accessToken: settings.access_token 36 | }); 37 | } 38 | 39 | function createAPIErrorResult({ error: e, message, json }){ 40 | const error = new VError(normalizedApiError(e), message); 41 | error.asJSON = json; 42 | return error; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/cmd/udp.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | 3 | 4 | module.exports = class UdpCommands { 5 | sendUdpPacket({ host, port, message }) { 6 | const client = dgram.createSocket('udp4'); 7 | const buf = new Buffer(message); 8 | 9 | console.log('Sending "' + message + '" to', host, 'at port', port); 10 | return new Promise((resolve, reject) => { 11 | client.send(buf, 0, buf.length, port, host, (err) => { 12 | if (err) { 13 | console.log('error during send ' + err); 14 | reject(); 15 | } else { 16 | console.log('Sent.'); 17 | resolve(); 18 | } 19 | client.close(); 20 | }); 21 | }); 22 | } 23 | 24 | listenUdp({ port }) { 25 | port = port || 5549; 26 | 27 | const udpSocket = dgram.createSocket('udp4'); 28 | 29 | udpSocket.on('listening', () => { 30 | console.log('Listening for UDP packets on port '+port+' ...'); 31 | }); 32 | 33 | udpSocket.on('message', (msg, rinfo) => { 34 | console.log('['+rinfo.address+'] '+msg.toString()); 35 | }); 36 | 37 | udpSocket.bind(port); 38 | } 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /src/cmd/whoami.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const VError = require('verror'); 3 | const settings = require('../../settings'); 4 | const ApiClient = require('../lib/api-client'); 5 | const spinnerMixin = require('../lib/spinner-mixin'); 6 | 7 | const arrow = chalk.green('>'); 8 | 9 | 10 | module.exports = class WhoAmICommand { 11 | constructor() { 12 | spinnerMixin(this); 13 | } 14 | 15 | getUsername(){ 16 | const api = new ApiClient(); 17 | 18 | return Promise.resolve() 19 | .then(() => { 20 | api.ensureToken(); 21 | 22 | this.newSpin('Checking...').start(); 23 | 24 | return api.getUser(); 25 | }) 26 | .then(user => { 27 | const username = settings.username || user.username || 'unknown username'; 28 | 29 | this.stopSpin(); 30 | 31 | console.log(arrow, username); 32 | return username; 33 | }) 34 | .catch(error => { 35 | this.stopSpin(); 36 | 37 | if (error instanceof VError){ 38 | throw error; 39 | } 40 | throw new VError('Failed to find username! Try: `particle login`'); 41 | }); 42 | } 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | global.verboseLevel = 1; 4 | 5 | const hasValidNodeInstall = require('./lib/has-supported-node'); 6 | const CLI = require('./app/cli'); 7 | 8 | 9 | if (hasValidNodeInstall()){ 10 | new CLI().run(process.argv); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/lib/api-cache.js: -------------------------------------------------------------------------------- 1 | const ParticleCache = require('./particle-cache'); 2 | 3 | class ApiCache { 4 | constructor(api) { 5 | this.api = api; 6 | this.cache = new ParticleCache(); 7 | } 8 | 9 | async getDeviceOsVersions(platformId, version) { 10 | const key = this.cache._generateKey('device_os_version', { platformId, version }); 11 | try { 12 | const deviceOsVersion = await this.api.getDeviceOsVersions(platformId, version); 13 | this.cache.set(key, deviceOsVersion); 14 | return deviceOsVersion; 15 | } catch (error) { 16 | if (isInternetConnectionError(error)) { 17 | const cachedDeviceOsVersion = this.cache.get(key); 18 | if (cachedDeviceOsVersion) { 19 | return cachedDeviceOsVersion; 20 | } 21 | throw new Error(`Device OS version not found in cache for platform: ${platformId} version: ${version} and there was an internet connection error`); 22 | 23 | } 24 | throw error; 25 | } 26 | } 27 | 28 | async getDevice({ deviceId: id, auth }) { 29 | const key = this.cache._generateKey('device', { deviceIdOrName: id }); 30 | try { 31 | const device = await this.api.getDevice({ deviceId: id, auth }); 32 | this.cache.set(key, device); 33 | return device; 34 | } catch (error) { 35 | if (isInternetConnectionError(error)) { 36 | const cachedDevice = this.cache.get(key); 37 | if (cachedDevice) { 38 | return cachedDevice; 39 | } 40 | throw new Error(`Device ${id} not found in cache and there was an internet connection error`); 41 | } 42 | throw error; 43 | } 44 | } 45 | } 46 | 47 | function isInternetConnectionError(error) { 48 | return error.message.includes('ECONNREFUSED') || error.message.includes('ENOTFOUND') || error.message.includes('Network error'); 49 | } 50 | 51 | const proxyHandler = { 52 | get: (target, prop) => { 53 | if (typeof target[prop] === 'function') { 54 | return target[prop].bind(target); 55 | } else if (prop === 'api') { 56 | return createApiCache(target.api); 57 | } else if (typeof target.api[prop] === 'function') { 58 | return target.api[prop].bind(target.api); 59 | } else { 60 | return target[prop]; 61 | } 62 | } 63 | }; 64 | 65 | function createApiCache(api) { 66 | const apiCache = new ApiCache(api); 67 | return new Proxy(apiCache, proxyHandler); 68 | } 69 | 70 | 71 | module.exports = createApiCache; 72 | -------------------------------------------------------------------------------- /src/lib/connect/executor.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn; 2 | const extend = require('xtend'); 3 | 4 | 5 | module.exports.systemExecutor = (cmdArgs) => { 6 | const { runCommand } = module.exports; 7 | 8 | return new Promise((resolve, reject) => { 9 | runCommand(cmdArgs[0], cmdArgs.splice(1), (err, code, stdout, stderr) => { 10 | if (err || stderr || code){ 11 | reject({ err, stderr, stdout, code }); 12 | } else { 13 | resolve(stdout); 14 | } 15 | }); 16 | }); 17 | }; 18 | 19 | /*** 20 | * Executes a command, collecting the output from stdout and stderr. 21 | * @param cmd 22 | * @param args 23 | * @param cb callback that receives (error, exitCode, stdout, stderr) 24 | */ 25 | module.exports.runCommand = (cmd, args, cb) => { 26 | // set locale so we can be sure of consistency of command execution 27 | const env = extend(process.env, { LANG: 'en', LC_ALL: 'en', LC_MESSAGES: 'en' }); 28 | 29 | const argArray = Array.isArray(args) ? args : args.split(' '); 30 | 31 | const s = spawn(cmd, argArray, { 32 | stdio: ['ignore', 'pipe', 'pipe'], 33 | env 34 | }); 35 | 36 | let stdout = ''; 37 | s.stdout.on('data', (data) => { 38 | stdout += data; 39 | }); 40 | 41 | let stderr = ''; 42 | s.stderr.on('data', (data) => { 43 | stderr += data; 44 | }); 45 | 46 | s.on('error', (error) => { 47 | cb(error, null, stdout, stderr); 48 | }); 49 | 50 | s.on('close', (code) => { 51 | cb(null, code, stdout, stderr); 52 | }); 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /src/lib/connect/executor.test.js: -------------------------------------------------------------------------------- 1 | const { expect, sinon } = require('../../../test/setup'); 2 | const executor = require('./executor'); 3 | 4 | 5 | describe('wifi executor', () => { 6 | const sandbox = sinon.createSandbox(); 7 | 8 | afterEach(() => { 9 | sandbox.restore(); 10 | }); 11 | 12 | describe('System Executor', () => { 13 | const { systemExecutor } = executor; 14 | let fakeCmdArgs, fakeStdOut, fakeStdErr, fakeCode; 15 | 16 | beforeEach(() => { 17 | sandbox.stub(executor, 'runCommand'); 18 | fakeCmdArgs = ['a', 'b', 'c']; 19 | fakeStdOut = 'ok'; 20 | fakeStdErr = ''; 21 | fakeCode = 0; 22 | }); 23 | 24 | it('runs executor', async () => { 25 | executor.runCommand.callsArgWith(2, null, fakeCode, fakeStdOut, fakeStdErr); 26 | const stdout = await systemExecutor(fakeCmdArgs); 27 | 28 | expect(stdout).to.equal(fakeStdOut); 29 | expect(executor.runCommand).to.have.property('callCount', 1); 30 | expect(executor.runCommand.firstCall.args[0]).to.equal('a'); 31 | expect(executor.runCommand.firstCall.args[1]).to.eql(['b', 'c']); 32 | expect(executor.runCommand.firstCall.args[2]).to.be.a('function'); 33 | }); 34 | 35 | it('rejects with data when executor fails', async () => { 36 | const error = new Error('nope'); 37 | let data; 38 | 39 | try { 40 | executor.runCommand.callsArgWith(2, error, fakeCode, fakeStdOut, fakeStdErr); 41 | await systemExecutor(fakeCmdArgs); 42 | } catch (d){ 43 | data = d; 44 | } 45 | 46 | expect(data).to.eql({ code: fakeCode, err: error, stderr: fakeStdErr, stdout: fakeStdOut }); 47 | expect(executor.runCommand).to.have.property('callCount', 1); 48 | expect(executor.runCommand.firstCall.args[0]).to.equal('a'); 49 | expect(executor.runCommand.firstCall.args[1]).to.eql(['b', 'c']); 50 | expect(executor.runCommand.firstCall.args[2]).to.be.a('function'); 51 | }); 52 | }); 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /src/lib/connect/linux.js: -------------------------------------------------------------------------------- 1 | const wifiCli = '/usr/bin/nmcli'; 2 | 3 | const runCommand = require('./executor').runCommand; 4 | 5 | function getCurrentNetwork(cb) { 6 | const currentNetworkParams = '--terse --fields NAME,TYPE connection show --active'; 7 | 8 | runCommand(wifiCli, currentNetworkParams, (err, code, stdout, stderr) => { 9 | if (err || stderr || code) { 10 | return cb(err || stderr || code); 11 | } 12 | 13 | const wifiType = '802-11-wireless'; 14 | const lines = stdout.split('\n'); 15 | for (let i = 0; i < lines.length; i++) { 16 | const fields = lines[i].split(':'); 17 | const ssid = fields[0]; 18 | const type = fields[1]; 19 | if (type === wifiType) { 20 | return cb(null, ssid); 21 | } 22 | } 23 | 24 | cb(); 25 | }); 26 | } 27 | 28 | function connect(opts, cb) { 29 | function reconnect() { 30 | const connectionDoesNotExistError = 10; 31 | const reconnectParams = 'connection up id ' + opts.ssid; 32 | runCommand(wifiCli, reconnectParams, (err, code, stdout, stderr) => { 33 | if (code === connectionDoesNotExistError) { 34 | return newConnect(); 35 | } else if (err || stderr) { 36 | return cb(err || stderr); 37 | } 38 | 39 | cb(null, opts); 40 | }); 41 | } 42 | 43 | function newConnect() { 44 | let newConnectParams = 'device wifi connect ' + opts.ssid; 45 | if (opts.password) { 46 | newConnectParams += ' password ' + opts.password; 47 | } 48 | 49 | runCommand(wifiCli, newConnectParams, (err, code, stdout, stderr) => { 50 | if (err || stderr || code) { 51 | return cb(err || stderr || code); 52 | } 53 | 54 | cb(null, opts); 55 | }); 56 | } 57 | 58 | reconnect(); 59 | } 60 | 61 | module.exports = { 62 | connect: connect, 63 | getCurrentNetwork: getCurrentNetwork 64 | }; 65 | -------------------------------------------------------------------------------- /src/lib/device-error-handler.js: -------------------------------------------------------------------------------- 1 | const deviceControlError = { 2 | 'LIBUSB_ERROR_IO': 'Input/output error', 3 | 'LIBUSB_ERROR_INVALID_PARAM': 'Invalid parameter', 4 | 'LIBUSB_ERROR_ACCESS': 'Access denied (insufficient permissions)', 5 | 'LIBUSB_ERROR_NO_DEVICE': 'No such device (it may have been disconnected)', 6 | 'LIBUSB_ERROR_NOT_FOUND': 'Entity not found', 7 | 'LIBUSB_ERROR_BUSY': 'Resource busy', 8 | 'LIBUSB_ERROR_TIMEOUT': 'Operation timed out', 9 | 'LIBUSB_ERROR_OVERFLOW': 'Overflow', 10 | 'LIBUSB_ERROR_PIPE': 'Pipe error', 11 | 'LIBUSB_ERROR_INTERRUPTED': 'System call interrupted (perhaps due to signal)', 12 | 'LIBUSB_ERROR_NO_MEM': 'Insufficient memory', 13 | 'LIBUSB_ERROR_NOT_SUPPORTED': 'Operation not supported or unimplemented on this platform', 14 | 'LIBUSB_TRANSFER_STALL': 'Operation was stalled (device is unable to respond to request)', 15 | 'LIBUSB_TRANSFER_TIMED_OUT': 'Transfer timed out', 16 | 'LIBUSB_ERROR_OTHER': 'Other error', 17 | }; 18 | 19 | 20 | module.exports = { 21 | deviceControlError 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/device-protection-helper.js: -------------------------------------------------------------------------------- 1 | // This helper module is written mainly for the device protection module to not mess with the flash module directly 2 | // and vice versa. This acts as a bridge between the two modules. 3 | const settings = require('../../settings'); 4 | const ParticleApi = require('../cmd/api'); 5 | const createApiCache = require('../lib/api-cache'); 6 | const os = require('os'); 7 | 8 | async function getProtectionStatus(device) { 9 | const s = await device.getProtectionState(); 10 | return s; 11 | } 12 | 13 | async function disableDeviceProtection(device) { 14 | const { api, auth } = _particleApi(); 15 | const deviceId = device.id; 16 | 17 | try { 18 | let r = await api.unprotectDevice({ deviceId, action: 'prepare', auth }); 19 | const serverNonce = Buffer.from(r.server_nonce, 'base64'); 20 | 21 | const { deviceNonce, deviceSignature, devicePublicKeyFingerprint } = await device.unprotectDevice({ action: 'prepare', serverNonce }); 22 | 23 | r = await api.unprotectDevice({ 24 | deviceId, 25 | action: 'confirm', 26 | serverNonce: serverNonce.toString('base64'), 27 | deviceNonce: deviceNonce.toString('base64'), 28 | deviceSignature: deviceSignature.toString('base64'), 29 | devicePublicKeyFingerprint: devicePublicKeyFingerprint.toString('base64'), 30 | auth 31 | }); 32 | 33 | const serverSignature = Buffer.from(r.server_signature, 'base64'); 34 | const serverPublicKeyFingerprint = Buffer.from(r.server_public_key_fingerprint, 'base64'); 35 | 36 | await device.unprotectDevice({ action: 'confirm', serverSignature, serverPublicKeyFingerprint }); 37 | } catch (error) { 38 | if (error.message === 'Device public key was not found') { 39 | throw new Error(`Server key mismatch while putting device in Service Mode. Check that device is accessible through ${settings.apiUrl || 'https://api.particle.io'}.${os.EOL}`); 40 | } 41 | } 42 | } 43 | 44 | async function turnOffServiceMode(device) { 45 | await device.unprotectDevice({ action: 'reset' }); 46 | } 47 | 48 | function _particleApi() { 49 | const auth = settings.access_token; 50 | const api = new ParticleApi(settings.apiUrl, { accessToken: auth } ); 51 | const apiCache = createApiCache(api); 52 | return { api: apiCache, auth }; 53 | } 54 | 55 | module.exports = { 56 | getProtectionStatus, 57 | disableDeviceProtection, 58 | turnOffServiceMode 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/file-types.js: -------------------------------------------------------------------------------- 1 | const sourcePatterns = [ 2 | '**/*.h', 3 | '**/*.hpp', 4 | '**/*.hh', 5 | '**/*.hxx', 6 | '**/*.ino', 7 | '**/*.cpp', 8 | '**/*.c', 9 | '**/build.mk', 10 | 'project.properties' 11 | ]; 12 | 13 | const binaryExtensions = [ 14 | '.bin', 15 | '.zip' 16 | ]; 17 | 18 | const binaryPatterns = binaryExtensions.map(ext => `*${ext}`); 19 | 20 | module.exports = { 21 | sourcePatterns, 22 | binaryExtensions, 23 | binaryPatterns 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/has-supported-node.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const semver = require('semver'); 3 | const packageJson = require('../../package.json'); 4 | 5 | 6 | module.exports = function hasSupportedNode(options) { 7 | options = options || {}; 8 | var version = options.version || process.version; 9 | var json = options.json || packageJson; 10 | var exit = options.exit || process.exit; 11 | var _console = options.console || console; 12 | var requirement = _.get(json, 'engines.node'); 13 | 14 | if (!semver.satisfies(version, requirement)) { 15 | _console.error('The Particle CLI requires Node ' + requirement); 16 | exit(1); 17 | return false; 18 | } 19 | return true; 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /src/lib/has-supported-node.test.js: -------------------------------------------------------------------------------- 1 | const { expect, sinon } = require('../../test/setup'); 2 | const hasSupportedNode = require('./has-supported-node'); 3 | 4 | 5 | describe('NodeJS Support Check', () => { 6 | const sandbox = sinon.createSandbox(); 7 | let fakes; 8 | 9 | beforeEach(() => { 10 | fakes = { 11 | exit: () => {}, 12 | console: { 13 | error: () => {} 14 | } 15 | }; 16 | sandbox.stub(fakes, 'exit'); 17 | sandbox.stub(fakes.console, 'error'); 18 | }); 19 | 20 | afterEach(() => { 21 | sandbox.restore(); 22 | }); 23 | 24 | it('does not exit for this test', () => { 25 | const { console, exit } = fakes; 26 | 27 | hasSupportedNode({ console, exit }); 28 | 29 | expect(exit).to.have.property('callCount', 0); 30 | expect(console.error).to.have.property('callCount', 0); 31 | }); 32 | 33 | it('does not exit when version requirement is satisfied', () => { 34 | const { console, exit } = fakes; 35 | const json = { 36 | engines: { 37 | node: '>= 4.4' 38 | } 39 | }; 40 | 41 | hasSupportedNode({ version: '8.4.0', json, console, exit }); 42 | 43 | expect(exit).to.have.property('callCount', 0); 44 | expect(console.error).to.have.property('callCount', 0); 45 | }); 46 | 47 | it('exit when version requirement is not satisfied', () => { 48 | const { console, exit } = fakes; 49 | const json = { 50 | engines: { 51 | node: '>= 4.4' 52 | } 53 | }; 54 | 55 | hasSupportedNode({ version: '0.12.0', json, console, exit }); 56 | 57 | expect(exit).to.have.property('callCount', 1); 58 | expect(exit.firstCall.args).to.eql([1]); 59 | expect(console.error).to.have.property('callCount', 1); 60 | expect(console.error.firstCall.args).to.eql(['The Particle CLI requires Node >= 4.4']); 61 | }); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /src/lib/json-result.js: -------------------------------------------------------------------------------- 1 | const version = '1.0.0'; 2 | 3 | 4 | module.exports.JSONResult = class JSONResult { 5 | constructor(meta, data = {}){ 6 | this.meta = Object.assign({ version }, meta); 7 | this.data = data; 8 | } 9 | 10 | toString(){ 11 | return JSON.stringify(this, null, 4); 12 | } 13 | 14 | toJSON(){ 15 | const { meta, data } = this; 16 | return { meta, data }; 17 | } 18 | }; 19 | 20 | module.exports.JSONErrorResult = class JSONErrorResult extends Error { 21 | constructor(cause = new Error('Something went wrong')){ 22 | super(cause.message); 23 | this.cause = cause; 24 | this.meta = { version }; 25 | } 26 | 27 | toString(){ 28 | return JSON.stringify(this, null, 4); 29 | } 30 | 31 | toJSON(){ 32 | const { meta, cause } = this; 33 | const names = Object.getOwnPropertyNames(cause); 34 | const error = {}; 35 | 36 | for (const name of names){ 37 | error[name] = this[name]; 38 | } 39 | 40 | return { meta, error }; 41 | } 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /src/lib/keys-specs.js: -------------------------------------------------------------------------------- 1 | const keysDctOffsets = { 2 | generation1: { 3 | tcpServerKey: { 4 | address: 0x00001000, 5 | size: 2048, 6 | format: 'der', 7 | alt: 1, 8 | alg: 'rsa', 9 | addressOffset: 384, 10 | portOffset: 450 11 | }, 12 | tcpPrivateKey: { 13 | address: 0x00002000, 14 | size: 1024, 15 | format: 'der', 16 | alt: 1, 17 | alg: 'rsa' 18 | } 19 | }, 20 | laterGenerations: { 21 | tcpServerKey: { 22 | address: 2082, 23 | size: 512, 24 | format: 'der', 25 | alt: 1, 26 | alg: 'rsa', 27 | addressOffset: 384, 28 | portOffset: 450 29 | }, 30 | udpServerKey: { 31 | address: 3298, 32 | size: 320, 33 | format: 'der', 34 | alt: 1, 35 | alg: 'ec', 36 | addressOffset: 192, 37 | portOffset: 258 38 | }, 39 | tcpPrivateKey: { 40 | address: 34, 41 | size: 612, 42 | format: 'der', 43 | alt: 1, 44 | alg: 'rsa' 45 | }, 46 | udpPrivateKey: { 47 | address: 3106, 48 | size: 192, 49 | format: 'der', 50 | alt: 1, 51 | alg: 'ec' 52 | } 53 | } 54 | }; 55 | 56 | module.exports = { 57 | keysDctOffsets 58 | }; 59 | -------------------------------------------------------------------------------- /src/lib/known-apps.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | 5 | // Walk the assets/knownApps directory to find all known apps 6 | function knownAppNames() { 7 | const knownAppsPath = path.join(__dirname, '../../assets/knownApps'); 8 | const names = new Set(); 9 | fs.readdirSync(knownAppsPath).forEach((platform) => { 10 | const platformPath = path.join(knownAppsPath, platform); 11 | const stat = fs.statSync(platformPath); 12 | if (!stat.isDirectory()) { 13 | return; 14 | } 15 | 16 | fs.readdirSync(platformPath).forEach((appName) => { 17 | const appPath = path.join(platformPath, appName); 18 | const stat = fs.statSync(appPath); 19 | if (!stat.isDirectory()) { 20 | return; 21 | } 22 | names.add(appName); 23 | }); 24 | }); 25 | 26 | return Array.from(names); 27 | } 28 | 29 | // Walk the assets/knownApps/${name} directory to find known app binaries for this platform 30 | function knownAppsForPlatform(name) { 31 | const platformKnownAppsPath = path.join(__dirname, '../../assets/knownApps', name); 32 | try { 33 | return fs.readdirSync(platformKnownAppsPath).reduce((knownApps, appName) => { 34 | try { 35 | const appPath = path.join(platformKnownAppsPath, appName); 36 | const binaries = fs.readdirSync(appPath); 37 | const appBinary = binaries.filter(filename => filename.match(/\.bin$/))[0]; 38 | if (appBinary) { 39 | knownApps[appName] = path.join(appPath, appBinary); 40 | } 41 | } catch (e) { 42 | // ignore errors 43 | } 44 | 45 | return knownApps; 46 | }, {}); 47 | } catch (e) { 48 | // no known apps for this platform 49 | return {}; 50 | } 51 | } 52 | 53 | module.exports = { 54 | knownAppNames, 55 | knownAppsForPlatform 56 | }; 57 | -------------------------------------------------------------------------------- /src/lib/known-apps.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const { knownAppNames, knownAppsForPlatform } = require('./known-apps'); 3 | 4 | describe('Known Apps', () => { 5 | describe('knownAppsNames', () => { 6 | it('returns all the known apps', () => { 7 | const apps = knownAppNames(); 8 | 9 | expect(apps.sort()).to.eql(['tinker', 'tinker-usb-debugging']); 10 | }); 11 | }); 12 | 13 | describe('knownAppsForPlatform', () => { 14 | it('returns the known apps for Photon', () => { 15 | const apps = knownAppsForPlatform('photon'); 16 | 17 | expect(Object.keys(apps)).to.eql(['tinker']); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/openurl.js: -------------------------------------------------------------------------------- 1 | // Copy of https://github.com/rauschma/openurl with additional error handling 2 | const spawn = require('child_process').spawn; 3 | const path = require('path'); 4 | 5 | let command; 6 | 7 | switch (process.platform) { 8 | case 'darwin':{ 9 | command = 'open'; 10 | break; 11 | } 12 | case 'win32':{ 13 | const binPath = `${process.env.SYSTEMROOT || process.env.windir || 'C:\\Windows'}`; 14 | command = path.join(binPath, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell'); 15 | break; 16 | } 17 | case 'linux': { 18 | command = 'xdg-open'; 19 | break; 20 | } 21 | default: 22 | throw new Error('Unsupported platform: ' + process.platform); 23 | } 24 | 25 | /** 26 | * Error handling is deliberately minimal, as this function is to be easy to use for shell scripting 27 | * 28 | * @param url The URL to open 29 | * @param callback A function with a single error argument. Optional. 30 | */ 31 | 32 | function open(url, callback) { 33 | const args = process.platform === 'win32' ? ['Start', url] : [url]; 34 | const child = spawn(command, args); 35 | child.on('error', (error) => { 36 | callback(error); 37 | }); 38 | let errorText = ''; 39 | child.stderr.setEncoding('utf8'); 40 | child.stderr.on('data', (data)=> { 41 | errorText += data; 42 | }); 43 | child.stderr.on('end', () => { 44 | if (errorText.length > 0) { 45 | const error = new Error(errorText); 46 | if (callback) { 47 | callback(error); 48 | } else { 49 | throw error; 50 | } 51 | } else if (callback) { 52 | callback(); 53 | } 54 | }); 55 | } 56 | 57 | module.exports = { 58 | open 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | formatItems(items, formatter, lines) { 3 | items.forEach((item, index, array) => { 4 | let output = formatter(item, index, array); 5 | if (Array.isArray(output)) { 6 | lines.push.apply(lines, output); 7 | } else { 8 | lines.push(output); 9 | } 10 | return lines; 11 | }); 12 | }, 13 | 14 | stringFormatter(item) { 15 | return ''+item; 16 | }, 17 | 18 | print(lines) { 19 | console.log(lines.join('\n')); 20 | } 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/lib/particle-cache.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const settings = require('../../settings'); 3 | const fs = require('fs-extra'); 4 | const crypto = require('crypto'); 5 | 6 | 7 | class ParticleCache { 8 | constructor() { 9 | const particleDir = settings.ensureFolder(); 10 | this.path = path.join(particleDir, 'cli-cache'); 11 | } 12 | 13 | get(key) { 14 | try { 15 | return fs.readJsonSync(path.join(this.path, `${key}.json`)); 16 | } catch (error) { 17 | return null; 18 | } 19 | 20 | } 21 | 22 | set(key, value) { 23 | fs.outputJsonSync(path.join(this.path, `${key}.json`), value); 24 | } 25 | 26 | _generateKey(requestName, options) { 27 | return `${requestName}_${hashOptions(options)}`; 28 | } 29 | } 30 | 31 | function hashOptions(options) { 32 | let optionsString = ''; 33 | const optionKeys = Object.keys(options); 34 | for (const optionKey of optionKeys) { 35 | const sanitizedValue = options[optionKey].toString().replace(/[^a-zA-Z0-9\-_]/g, '-'); 36 | optionsString += `${optionKey}_${sanitizedValue}`; 37 | } 38 | return crypto.createHash('md5').update(optionsString).digest('hex'); 39 | } 40 | 41 | module.exports = ParticleCache; 42 | -------------------------------------------------------------------------------- /src/lib/particle-cache.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const ParticleCache = require('./particle-cache'); 3 | const { PATH_TMP_DIR } = require('../../test/lib/env'); 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | 7 | describe('Cache', () => { 8 | const originalEnv = process.env; 9 | beforeEach(() => { 10 | process.env = { 11 | ...originalEnv, 12 | home: PATH_TMP_DIR, 13 | }; 14 | }); 15 | 16 | afterEach(async() => { 17 | process.env = originalEnv; 18 | await fs.remove(path.join(PATH_TMP_DIR, '.particle/')); 19 | }); 20 | 21 | it('should create a cache file', () => { 22 | const cache = new ParticleCache(); 23 | const requestOptions = { test: 'test' }; 24 | const key = cache._generateKey('test', requestOptions); 25 | cache.set(key, { deviceId: 'abc123', platformId: 6 }); 26 | const result = cache.get(key); 27 | expect(result).to.eql({ deviceId: 'abc123', platformId: 6 }); 28 | }); 29 | 30 | it('should create a cache file with names with special characters', () => { 31 | const cache = new ParticleCache(); 32 | const requestOptions = { 33 | platformId: 6, 34 | deviceName: 'my_device@!$%&()[]{}\\|;:\'",<>?/`~', 35 | }; 36 | const key = cache._generateKey('device', requestOptions); 37 | cache.set(key, { deviceId: 'abc123' }); 38 | const result = cache.get(key); 39 | expect(result).to.eql({ deviceId: 'abc123' }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/lib/platform.js: -------------------------------------------------------------------------------- 1 | const deviceConstants = require('@particle/device-constants'); 2 | 3 | /** 4 | * Array of description objects for all supported platforms. 5 | */ 6 | const PLATFORMS = Object.values(deviceConstants).filter(p => p.public); 7 | 8 | const PLATFORMS_BY_ID = PLATFORMS.reduce((map, p) => map.set(p.id, p), new Map()); 9 | 10 | /** 11 | * Supported platform IDs. 12 | * 13 | * @enum {Number} 14 | * @property {Number} CORE 15 | * @property {Number} PHOTON 16 | * @property {Number} P1 17 | * @property {Number} ELECTRON 18 | * @property {Number} ARGON 19 | * @property {Number} BORON 20 | * @property {Number} XENON 21 | * @property {Number} ESOMX 22 | * @property {Number} BSOM 23 | * @property {Number} B5SOM 24 | * @property {Number} TRACKER 25 | * @property {Number} TRACKERM 26 | * @property {Number} P2 27 | * @property {Number} MSOM 28 | */ 29 | const PlatformId = PLATFORMS.reduce((out, p) => { 30 | out[p.name.toUpperCase()] = p.id; 31 | return out; 32 | }, {}); 33 | 34 | /** 35 | * Get the platform description. 36 | * 37 | * @param {Number} id The platform ID. 38 | * @throws Throws an error if `id` is not a known platform ID. 39 | */ 40 | function platformForId(id) { 41 | const p = PLATFORMS_BY_ID.get(id); 42 | if (!p) { 43 | throw new Error(`Unknown platform ID: ${id}`); 44 | } 45 | return p; 46 | } 47 | 48 | /** 49 | * Check if a platform ID is known. 50 | * 51 | * @param {Number} id The platform ID. 52 | * @returns {Boolean} `true` if the platform ID is known, otherwise `false`. 53 | */ 54 | function isKnownPlatformId(id) { 55 | return PLATFORMS_BY_ID.has(id); 56 | } 57 | 58 | module.exports = { 59 | PLATFORMS, 60 | PlatformId, 61 | platformForId, 62 | isKnownPlatformId 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/platform.test.js: -------------------------------------------------------------------------------- 1 | const { PLATFORMS, PlatformId, platformForId, isKnownPlatformId } = require('./platform'); 2 | const { expect } = require('../../test/setup'); 3 | 4 | const deviceConstants = require('@particle/device-constants'); 5 | 6 | const supportedPlatforms = Object.values(deviceConstants).filter(p => p.public); 7 | 8 | describe('Platform utilities', () => { 9 | describe('PLATFORMS', () => { 10 | it('contains description objects for all supported platforms', () => { 11 | expect(PLATFORMS).to.deep.equal(supportedPlatforms); 12 | }); 13 | }); 14 | 15 | describe('PlatformId', () => { 16 | it('has an entry for each supported platform', () => { 17 | for (const p of supportedPlatforms) { 18 | expect(PlatformId[p.name.toUpperCase()]).to.equal(p.id); 19 | } 20 | }); 21 | }); 22 | 23 | describe('platformForId()', () => { 24 | it('returns a platform description for a given platform ID', () => { 25 | for (const p of supportedPlatforms) { 26 | const p2 = platformForId(p.id); 27 | expect(p2).to.deep.equal(p); 28 | } 29 | }); 30 | 31 | it('throws an error if the argument is not a known platform ID', () => { 32 | expect(() => platformForId(1000)).to.throw('Unknown platform ID: 1000'); 33 | }); 34 | }); 35 | 36 | describe('isKnownPlatformId()', () => { 37 | it('returns true if the argument is a known platform ID', () => { 38 | for (const p of supportedPlatforms) { 39 | expect(isKnownPlatformId(p.id)).to.be.true; 40 | } 41 | }); 42 | 43 | it('returns false if the argument is not a known platform ID', () => { 44 | expect(isKnownPlatformId(1000)).to.be.false; 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/lib/secrets.js: -------------------------------------------------------------------------------- 1 | async function list({ api, org } = {}) { 2 | const response = await api.listSecrets({ orgSlug: org }); 3 | return response.secrets?.length ? formatSecretList(response.secrets) : []; 4 | } 5 | 6 | async function create({ api, org, name, value } = {}) { 7 | // validate name 8 | const regex =/^[A-Z_][A-Z0-9_]*$/; 9 | if (!regex.test(name)) { 10 | throw new Error('Keys may include only uppercase letters, digits, and underscores, and must not begin with a digit.'); 11 | } 12 | if (!value) { 13 | throw new Error('value is required'); 14 | } 15 | const response = await api.createSecret({ orgSlug: org, name, value }); 16 | if (response.secret) { 17 | return formatSecret(response); 18 | } else { 19 | throw new Error('Unable to create secret'); 20 | } 21 | } 22 | 23 | async function get({ api, org, name }) { 24 | const response = await api.getSecret({ api, orgSlug: org, name }); 25 | return formatSecret(response); 26 | } 27 | async function update({ api, org, name, value } = {}) { 28 | if (!value) { 29 | throw new Error('value is required'); 30 | } 31 | const response = await api.updateSecret({ orgSlug: org, name, value }); 32 | return formatSecret(response); 33 | } 34 | 35 | async function remove({ api, org, name } = {}) { 36 | const response = await api.removeSecret({ orgSlug: org, name }); 37 | if (!response.error) { 38 | return true; 39 | } 40 | } 41 | 42 | async function formatSecret(secretResponse) { 43 | const secret = secretResponse.secret; 44 | return { 45 | name: secret.name, 46 | createdAt: secret.created_at, 47 | updatedAt: secret.updated_at, 48 | lastAccessedAt: secret.last_accessed_at, 49 | logicFunctions: secret.logic_functions, 50 | integrations: secret.integrations, 51 | }; 52 | } 53 | 54 | async function formatSecretList(secretList) { 55 | return secretList.map((secret) => { 56 | return { 57 | name: secret.name, 58 | createdAt: secret.created_at, 59 | updatedAt: secret.updated_at, 60 | lastAccessedAt: secret.last_accessed_at, 61 | usageCount: secret.integrations.length + secret.logic_functions.length, 62 | }; 63 | }); 64 | } 65 | module.exports = { 66 | list, 67 | create, 68 | update, 69 | remove, 70 | get 71 | }; 72 | -------------------------------------------------------------------------------- /src/lib/serial-batch-parser.js: -------------------------------------------------------------------------------- 1 | const Transform = require('stream').Transform; 2 | const Buffer = require('safe-buffer').Buffer; 3 | 4 | 5 | /** 6 | * A parser that waits for a given length of time for a response and emits the batch of 7 | * data that was received within that time. 8 | */ 9 | module.exports = class SerialBatchParser extends Transform { 10 | constructor(options){ 11 | super(); 12 | options = options || {}; 13 | this.batchTimeout = options.timeout || 250; 14 | this.batchTimer = null; 15 | this.buffer = Buffer.alloc(0); 16 | this.setTimeoutFunctions(global.setTimeout, global.clearTimeout); 17 | } 18 | 19 | setTimeoutFunctions(setTimeout, clearTimeout){ 20 | this.setTimeout = setTimeout; 21 | this.clearTimeout = clearTimeout; 22 | } 23 | 24 | _transform(chunk, encoding, cb){ 25 | this.buffer = Buffer.concat([this.buffer, chunk]); 26 | this.updateTimer(); 27 | cb(); 28 | } 29 | 30 | _flush(cb){ 31 | this.pushBatch(); 32 | cb(); 33 | } 34 | 35 | pushBatch(){ 36 | let batch = this.buffer; 37 | this.buffer = Buffer.alloc(0); 38 | this.push(batch); 39 | } 40 | 41 | updateTimer(){ 42 | this.clearTimeout(this.batchTimer); 43 | this.batchTimer = this.setTimeout( 44 | this.pushBatch.bind(this), 45 | this.batchTimeout 46 | ); 47 | } 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /src/lib/serial-batch-parser.test.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const Readable = require('stream').Readable; 3 | const { expect, sinon } = require('../../test/setup'); 4 | const SerialBatchParser = require('./serial-batch-parser'); 5 | 6 | 7 | describe('SerialBatchParser', () => { 8 | const sandbox = sinon.createSandbox(); 9 | let fakes; 10 | 11 | beforeEach(() => { 12 | fakes = { 13 | setTimeout: () => {}, 14 | clearTimeout: () => {} 15 | }; 16 | sandbox.stub(fakes, 'setTimeout'); 17 | sandbox.stub(fakes, 'clearTimeout'); 18 | }); 19 | 20 | afterEach(() => { 21 | sandbox.restore(); 22 | }); 23 | 24 | it('waits for the timeout and returns the batch of output.', async () => { 25 | const timeout = 50; 26 | const timer = 'timer'; 27 | const stream = createMockStream(); 28 | const parser = new SerialBatchParser({ timeout }); 29 | 30 | parser.setTimeoutFunctions(fakes.setTimeout, fakes.clearTimeout); 31 | fakes.setTimeout.returns(timer); 32 | stream.pipe(parser); 33 | stream.push('abc'); 34 | 35 | await Promise.resolve(); // wait a tic 36 | 37 | expect(fakes.clearTimeout).to.have.been.calledWith(null); 38 | expect(fakes.setTimeout).has.been.calledWith(sinon.match.func, timeout); 39 | expect(parser.read()).to.eq(null); 40 | 41 | fakes.setTimeout.reset(); 42 | fakes.clearTimeout.reset(); 43 | stream.push('def'); 44 | 45 | await Promise.resolve(); // wait a tic 46 | 47 | expect(fakes.clearTimeout).to.have.been.calledWith(timer); 48 | expect(fakes.setTimeout).has.been.calledWith(sinon.match.func, timeout); 49 | expect(parser.read()).to.equal(null); 50 | 51 | // now emulate the passage of time by calling the function passed to setTimeout 52 | const timeoutCallback = fakes.setTimeout.args[0][0]; 53 | fakes.setTimeout.reset(); 54 | fakes.clearTimeout.reset(); 55 | 56 | timeoutCallback(); 57 | 58 | expect(fakes.clearTimeout).to.have.not.been.called; 59 | expect(fakes.setTimeout).has.not.been.called; 60 | expect(parser.read().toString()).to.eq('abcdef'); 61 | }); 62 | 63 | function createMockStream(){ 64 | function MockStream(){ 65 | Readable.call(this); 66 | } 67 | util.inherits(MockStream, Readable); 68 | MockStream.prototype._read = () => {}; 69 | return new MockStream(); 70 | } 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /src/lib/serial-trigger.test.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const Transform = require('stream').Transform; 3 | const MockSerial = require('../../test/__mocks__/serial.mock'); 4 | const SerialTrigger = require('./serial-trigger'); 5 | 6 | function PassthroughStream() { 7 | Transform.call(this); 8 | } 9 | util.inherits(PassthroughStream, Transform); 10 | PassthroughStream.prototype._transform = function _transform(chunk, encoding, cb) { 11 | this.push(chunk); 12 | process.nextTick(cb); 13 | }; 14 | 15 | PassthroughStream.prototype._flush = function _flush(cb) { 16 | process.nextTick(cb); 17 | }; 18 | 19 | describe('SerialTrigger', () => { 20 | it('should trigger when prompt is at beginning of line', (done) => { 21 | var port = new MockSerial(); 22 | var stream = new PassthroughStream(); 23 | port.pipe(stream); 24 | var st = new SerialTrigger(port, stream); 25 | st.addTrigger('SSID: ', () => { 26 | st.stop(); 27 | done(); 28 | }); 29 | st.start(); 30 | port.push('SSID: '); 31 | }); 32 | 33 | it('should not trigger when data does not match', (done) => { 34 | var port = new MockSerial(); 35 | var stream = new PassthroughStream(); 36 | port.pipe(stream); 37 | var st = new SerialTrigger(port, stream); 38 | var to = setTimeout(() => { 39 | done(); 40 | }, 100); 41 | st.addTrigger('SSID: ', () => { 42 | clearTimeout(to); 43 | st.stop(); 44 | done(new Error('triggered when it should not')); 45 | }); 46 | st.start(); 47 | port.push('ASDF: '); 48 | }); 49 | 50 | it('should write the response from the trigger', (done) => { 51 | var port = new MockSerial(); 52 | var stream = new PassthroughStream(); 53 | port.pipe(stream); 54 | var st = new SerialTrigger(port, stream); 55 | port.on('drain', () => { 56 | if (port.data !== 'particle') { 57 | return done(new Error('Response data does not match')); 58 | } 59 | st.stop(); 60 | port.removeAllListeners(); 61 | done(); 62 | }); 63 | st.addTrigger('SSID: ', (cb) => { 64 | cb('particle'); 65 | }); 66 | st.start(true); 67 | port.push('SSID: '); 68 | }); 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /src/lib/spinner-mixin.js: -------------------------------------------------------------------------------- 1 | const Spinner = require('cli-spinner').Spinner; 2 | 3 | 4 | module.exports = function spinnerMixin(obj) { 5 | Object.assign(obj, { 6 | newSpin(str) { 7 | this.__spin = new Spinner(str); 8 | return this.__spin; 9 | }, 10 | startSpin() { 11 | if (!this.__spin){ 12 | return; 13 | } 14 | this.__spin.start(); 15 | }, 16 | stopSpin() { 17 | if (!this.__spin){ 18 | return; 19 | } 20 | this.__spin.stop(true); 21 | }, 22 | stopSpinAfterPromise(promise) { 23 | return promise.then((value) => { 24 | this.stopSpin(); 25 | return value; 26 | }, (error) => { 27 | this.stopSpin(); 28 | throw error; 29 | }); 30 | }, 31 | showBusySpinnerUntilResolved(message, promise){ 32 | this.newSpin(message); 33 | this.startSpin(); 34 | return this.stopSpinAfterPromise(promise); 35 | } 36 | }); 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/lib/supported-countries.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | supportedCountries : [ 3 | { name: 'Australia', value: 'AUS' }, 4 | { name: 'Austria', value: 'AUT' }, 5 | { name: 'Belgium', value: 'BEL' }, 6 | { name: 'Bulgaria', value: 'BGR' }, 7 | { name: 'Canada', value: 'CAN' }, 8 | { name: 'Cyprus', value: 'CYP' }, 9 | { name: 'Czech Republic', value: 'CZE' }, 10 | { name: 'Denmark', value: 'DNK' }, 11 | { name: 'Estonia', value: 'EST' }, 12 | { name: 'Finland', value: 'FIN' }, 13 | { name: 'France', value: 'FRA' }, 14 | { name: 'Germany', value: 'DEU' }, 15 | { name: 'Greece', value: 'GRC' }, 16 | { name: 'Hungary', value: 'HUN' }, 17 | { name: 'Iceland', value: 'ISL' }, 18 | { name: 'India', value: 'IND' }, 19 | { name: 'Ireland', value: 'IRL' }, 20 | { name: 'Italy', value: 'ITA' }, 21 | { name: 'Lithuania', value: 'LTU' }, 22 | { name: 'Luxembourg', value: 'LUX' }, 23 | { name: 'Japan', value: 'JPN' }, 24 | { name: 'Mexico', value: 'MEX' }, 25 | { name: 'Netherlands', value: 'NLD' }, 26 | { name: 'New Zealand', value: 'NZL' }, 27 | { name: 'Norway', value: 'NOR' }, 28 | { name: 'Poland', value: 'POL' }, 29 | { name: 'Portugal', value: 'PRT' }, 30 | { name: 'Romania', value: 'ROU' }, 31 | { name: 'Slovakia', value: 'SVK' }, 32 | { name: 'Slovenia', value: 'SVN' }, 33 | { name: 'South Africa', value: 'ZAF' }, 34 | { name: 'South Korea', value: 'KOR' }, 35 | { name: 'Spain', value: 'ESP' }, 36 | { name: 'Sweden', value: 'SWE' }, 37 | { name: 'Switzerland', value: 'CHE' }, 38 | { name: 'United Kingdom', value: 'GBR' }, 39 | { name: 'United States', value: 'USA' }, 40 | { name: 'Other Country', value: 'OTHER' } // This is a catch-all for unsupported countries 41 | ] 42 | }; 43 | -------------------------------------------------------------------------------- /src/lib/template-processor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | async function loadTemplateFiles({ templatePath, contentReplacements, fileNameReplacements }){ 5 | if (!await fs.pathExists(templatePath)){ 6 | throw new Error('Template not found'); 7 | } 8 | const templateFiles = await fs.readdir(templatePath); 9 | const files = []; 10 | for (const file of templateFiles){ 11 | const filePath = path.join(templatePath, file); 12 | const stats = await fs.stat(filePath); 13 | if (stats.isDirectory()){ 14 | const subFiles = await loadTemplateFiles({ templatePath: filePath, contentReplacements, fileNameReplacements }); 15 | files.push(...subFiles); 16 | } else { 17 | const file = await copyTemplateToString({ filePath, contentReplacements, fileNameReplacements }); 18 | files.push(file); 19 | } 20 | } 21 | return files; 22 | } 23 | 24 | async function copyTemplateToString({ filePath, contentReplacements, fileNameReplacements }) { 25 | // open the file 26 | const file = await fs.readFile(filePath, 'utf8'); 27 | // replace the content 28 | const content = replace(file, contentReplacements); 29 | // replace the file name 30 | let fileName = filePath.replace('.template', ''); 31 | const replacement = fileNameReplacements.find((replacement) => { 32 | return fileName.includes(replacement.template); 33 | }); 34 | 35 | if (replacement){ 36 | fileName = fileName.replace(replacement.template, replacement.fileName); 37 | } 38 | return { 39 | fileName: fileName, 40 | content: content 41 | }; 42 | } 43 | 44 | function replace(content, replacements){ 45 | let result = content; 46 | for (const key in replacements){ 47 | const value = replacements[key]; 48 | result = result.replace(new RegExp(`\\$\{${key}}`, 'g'), value); 49 | } 50 | return result; 51 | } 52 | 53 | module.exports = { 54 | loadTemplateFiles 55 | }; 56 | -------------------------------------------------------------------------------- /src/lib/template-processor.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const { loadTemplateFiles } = require('./template-processor'); 3 | const { PATH_TMP_DIR } = require('../../test/lib/env'); 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | 7 | describe('template-processor', () => { 8 | afterEach(async () => { 9 | await fs.emptyDir(PATH_TMP_DIR); 10 | }); 11 | describe('loadTemplateFiles', () => { 12 | it('copies template files to destination', async () => { 13 | const templatePath = path.join(__dirname, '..', '..', 'assets', 'logicFunction'); 14 | const replacements = { 15 | name: 'My Logic Function', 16 | description: 'My Logic Function Description', 17 | }; 18 | const fileNameReplacements = [ 19 | { template: 'logic_function_name', fileName: 'my-logic-function' }, 20 | ]; 21 | const files = await loadTemplateFiles({ 22 | templatePath, 23 | contentReplacements: replacements, 24 | fileNameReplacements, 25 | }); 26 | 27 | const namedFiles = files.filter(file => file.fileName.includes('my-logic-function')); 28 | expect(namedFiles).to.have.length(2); 29 | }); 30 | it('throws an error if the template does not exist', async () => { 31 | try { 32 | await loadTemplateFiles({ templatePath: 'invalid-template' }); 33 | } catch (e) { 34 | expect(e.message).to.equal('Template not found'); 35 | } 36 | }); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /src/lib/unindent.js: -------------------------------------------------------------------------------- 1 | module.exports = function unindent(string) { 2 | const match = string.match(/\n(\s*)/m); 3 | if (!match) { 4 | return string; 5 | } 6 | 7 | const re = new RegExp(`^${match[1]}`, 'gm'); 8 | return string.replace(re, '').replace(/^\n/, '').replace(/\n[ \t]*$/, ''); 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/lib/unindent.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../../test/setup'); 2 | const unindent = require('./unindent'); 3 | 4 | 5 | describe('unindent', () => { 6 | it('does not change a single line string', () => { 7 | const string = ' foo'; 8 | expect(unindent(string)).to.equal(string); 9 | }); 10 | 11 | it('removes leading and trailing newlines', () => { 12 | const string = ` 13 | foo 14 | `; 15 | expect(unindent(string)).to.equal('foo'); 16 | }); 17 | it('removes leading tabs', () => { 18 | const string = ` 19 | foo 20 | `; 21 | expect(unindent(string)).to.equal('foo'); 22 | }); 23 | it('removes leading spaces', () => { 24 | const string = ` 25 | foo 26 | `; 27 | expect(unindent(string)).to.equal('foo'); 28 | }); 29 | it('removes the same amount of indent from every line', () => { 30 | const string = ` 31 | foo 32 | bar 33 | fred 34 | `; 35 | expect(unindent(string)).to.equal('foo\n bar\nfred'); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /src/lib/wifi-manager.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const _ = require('lodash'); 3 | const connect = { 4 | 'darwin': require('./connect/darwin'), 5 | 'linux': require('./connect/linux'), 6 | 'win32': require('./connect/windows') 7 | }; 8 | 9 | 10 | module.exports = class WiFiManager { 11 | constructor(opts) { 12 | if (opts) { 13 | // TODO: something fancy with the interfaces. 14 | // Some users will need to be able to customize this. 15 | } 16 | 17 | this.platform = os.platform(); 18 | this.osConnect = connect[this.platform]; 19 | // todo - allow the connector to actively check for preconditions, specific OS version support etc 20 | this.supported = { 21 | getCurrentNetwork: !!(this.osConnect && this.osConnect.getCurrentNetwork), 22 | connect: !!(this.osConnect && this.osConnect.connect) 23 | }; 24 | 25 | this.__cache = undefined; 26 | } 27 | 28 | getCurrentNetwork(cb) { 29 | if (!this.supported.getCurrentNetwork) { 30 | // default to nothing 31 | // todo - why not raise an error? 32 | return cb(); 33 | } 34 | this.osConnect.getCurrentNetwork(cb); 35 | } 36 | 37 | connect(opts, cb) { 38 | 39 | let self = this; 40 | let ap; 41 | 42 | if (!opts) { 43 | opts = {}; 44 | } 45 | if (!opts.mac && !opts.ssid) { 46 | cb(new Error('Must specify either ssid or mac of network with which to connect.')); 47 | } 48 | 49 | if (opts.mac) { 50 | 51 | if (!(ap = this.__lookupMAC(opts.mac))) { 52 | return this.scan(null, recheck); 53 | } 54 | opts.ssid = ap.ssid; 55 | return self.__connect(opts, cb); 56 | } 57 | 58 | self.__connect(opts, cb); 59 | 60 | function recheck(err) { 61 | 62 | if (err) { 63 | return cb(new Error('Unknown MAC address and unable to perform a Wi-Fi scan.')); 64 | } 65 | if (!(ap = self.__lookupMAC(opts.mac))) { 66 | return cb(new Error('Unable to locate SSID matching provided MAC address.')); 67 | } 68 | opts.ssid = ap.ssid; 69 | return self.__connect(opts, cb); 70 | } 71 | } 72 | 73 | // actually connect via OS-dependent binary execution 74 | __connect(opts, cb) { 75 | if (!this.supported.connect) { 76 | return cb(new Error('Unsupported platform. Don\'t know how to automatically connect to Wi-Fi on ' + this.platform)); 77 | } 78 | this.osConnect.connect(opts, cb); 79 | } 80 | 81 | __lookupMAC(mac) { 82 | return _.find(this.__cache, 'mac', mac); 83 | } 84 | }; 85 | 86 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "max-statements": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/__fixtures__/binaries/argon-bootloader-610.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/argon-bootloader-610.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/argon-bootloader-620.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/argon-bootloader-620.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/argon-system-part1@4.1.0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/argon-system-part1@4.1.0.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/argon-tinker-620.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/argon-tinker-620.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/argon_stroby.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/argon_stroby.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/boron_blank.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/boron_blank.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/core_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/core_tinker.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/electron_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/electron_tinker.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/p1_tinker-0.4.5.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/p1_tinker-0.4.5.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/p1_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/p1_tinker.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/photon_tinker-0.4.5.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/photon_tinker-0.4.5.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/photon_tinker.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/photon_tinker.bin -------------------------------------------------------------------------------- /test/__fixtures__/binaries/product_firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/binaries/product_firmware.bin -------------------------------------------------------------------------------- /test/__fixtures__/esim/binaries/esim-firmware-b5som.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/esim/binaries/esim-firmware-b5som.bin -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Joe Goggins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/doc/firmware-code-conventions.md: -------------------------------------------------------------------------------- 1 | ### Firmware Code Conventions 2 | 3 | In general or where unspecified, use node.js + [npm](https://www.npmjs.org/doc/misc/npm-coding-style.html) for inspiration while respecting the pragmatic realities of embedded programming. Specifically: 4 | 5 | - Use `all-lower-hyphen-css-case` for multiword filenames. 6 | - Use `UpperCamelCase` for class names (things that you'd pass to "new") and namespaces 7 | - Use `lowerCamelCase` for multiword identifiers when they refer to objects, functions, methods, members, or anything not specified in this section. 8 | - Use `CAPS_SNAKE_CASE` for constants, things that should never change and are rarely used. 9 | 10 | When using an acronym like `LED` or `JSON` in a class name, file name, or other context above, don't necessarily use all caps. Instead, let the convention drive the spelling. For example, a class that blinks LEDs would be called `LedBlinker`, and live in a file called `led-blinker.cpp` 11 | 12 | - Use two spaces for indentation. 13 | - Prefix variables or functions with an underscore (`_`) when indended for very narrow, restricted, local usage. 14 | - Functions or methods that begin with the name `begin`, are meant to be called in the `setup()` function. 15 | - Curly Brackets should go on the next line after a class or function. 16 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/examples/blink-an-led-slower/blink-an-led-slower.cpp: -------------------------------------------------------------------------------- 1 | // IMPORTANT: When including a library in a firmware app, a sub dir prefix is needed 2 | // before the particular .h file. 3 | #include "uber-library-example.h" 4 | 5 | // Initialize objects from the lib; be sure not to call anything 6 | // that requires hardware be initialized here, put those in setup() 7 | UberLibraryExample::Pin outputPin(D7); 8 | 9 | void setup() { 10 | // Call functions on initialized library objects that require hardware 11 | // to be wired up correct and available. 12 | outputPin.beginInPinMode(OUTPUT); 13 | } 14 | 15 | void loop() { 16 | // Use the library's initialized objects and functions 17 | outputPin.modulateAtFrequency(50); 18 | } 19 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/examples/blink-an-led-very-slow/blink-an-led-very-slow.cpp: -------------------------------------------------------------------------------- 1 | #include "uber-library-example.h" 2 | UberLibraryExample::Pin outputPin(D7); 3 | void setup() { 4 | outputPin.beginInPinMode(OUTPUT); 5 | } 6 | 7 | void loop() { 8 | outputPin.modulateAtFrequency(3000); 9 | } 10 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/examples/blink-an-led/blink-an-led.cpp: -------------------------------------------------------------------------------- 1 | // IMPORTANT: When including a library in a firmware app, a sub dir prefix is needed 2 | // before the particular .h file. 3 | #include "test-library-publish.h" 4 | 5 | // Initialize objects from the lib; be sure not to call anything 6 | // that requires hardware be initialized here, put those in setup() 7 | UberLibraryExample::Pin outputPin(D7); 8 | 9 | void setup() { 10 | // Call functions on initialized library objects that require hardware 11 | // to be wired up correct and available. 12 | outputPin.beginInPinMode(OUTPUT); 13 | } 14 | 15 | void loop() { 16 | // Use the library's initialized objects and functions 17 | outputPin.modulateAtFrequency(300); 18 | } 19 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/library.properties: -------------------------------------------------------------------------------- 1 | name=test-library-publish 2 | version=0.0.2 3 | license=MIT 4 | author=Joe Goggins 5 | sentence=A simple library that illustrates coding conventions of a Spark Library 6 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/src/a-c-example.c: -------------------------------------------------------------------------------- 1 | int some_helper() { 2 | return 1; 3 | } 4 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/src/test-library-publish.cpp: -------------------------------------------------------------------------------- 1 | // NOTE/NUANCE: When including WITHIN a library, no sub-dir prefix is needed. 2 | #include "test-library-publish.h" 3 | 4 | // Constructor 5 | UberLibraryExample::Pin::Pin(int _number) 6 | { 7 | number = _number; 8 | state = LOW; 9 | } 10 | 11 | // Initializers that should be called in the `setup()` function 12 | void UberLibraryExample::Pin::beginInPinMode(PinMode _pinMode) 13 | { 14 | pinMode(number, _pinMode); 15 | } 16 | 17 | // Main API functions that the library provides 18 | // typically called in `loop()` or `setup()` functions 19 | void UberLibraryExample::Pin::modulateAtFrequency(int _ms) 20 | { 21 | setHigh(); 22 | delay(_ms); 23 | setLow(); 24 | delay(_ms); 25 | } 26 | 27 | // Getters 28 | int UberLibraryExample::Pin::getNumber() 29 | { 30 | return number; 31 | } 32 | bool UberLibraryExample::Pin::getState() 33 | { 34 | return state; 35 | } 36 | bool UberLibraryExample::Pin::getMode() 37 | { 38 | return mode; 39 | } 40 | bool UberLibraryExample::Pin::isHigh() 41 | { 42 | return state == HIGH ? true : false; 43 | } 44 | 45 | // Setters 46 | void UberLibraryExample::Pin::setHigh() 47 | { 48 | state = HIGH; 49 | setActualPinState(); 50 | } 51 | void UberLibraryExample::Pin::setLow() 52 | { 53 | state = LOW; 54 | setActualPinState(); 55 | } 56 | void UberLibraryExample::Pin::setActualPinState() 57 | { 58 | digitalWrite(number, state); 59 | } 60 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/src/test-library-publish.h: -------------------------------------------------------------------------------- 1 | #ifndef _UBER_LIBRARY_EXAMPLE 2 | #define _UBER_LIBRARY_EXAMPLE 3 | 4 | // Make library cross-compatiable 5 | // with Arduino, GNU C++ for tests, and Spark. 6 | //#if defined(ARDUINO) && ARDUINO >= 100 7 | //#include "Arduino.h" 8 | //#elif defined(SPARK) 9 | //#include "application.h" 10 | //#endif 11 | 12 | // TEMPORARY UNTIL the stuff that supports the code above is deployed to the build IDE 13 | #include "application.h" 14 | namespace UberLibraryExample 15 | { 16 | class Pin 17 | { 18 | private: 19 | int number; 20 | int mode; 21 | bool state; 22 | public: 23 | Pin(int _number); 24 | void beginInPinMode(PinMode _pinMode); 25 | void modulateAtFrequency(int _ms); 26 | int getNumber(); 27 | bool getState(); 28 | bool getMode(); 29 | bool isHigh(); 30 | void setHigh(); 31 | void setLow(); 32 | void setActualPinState(); 33 | }; 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/src/uber-library-example/uber-library-example.h: -------------------------------------------------------------------------------- 1 | #include "../uber-library-example.h" -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/test/unit/RUNNING_TESTS.md: -------------------------------------------------------------------------------- 1 | How to Run Tests 2 | === 3 | 4 | We're currently not sure what the best way to do [test driven embedded development](http://pragprog.com/book/jgade/test-driven-development-for-embedded-c) for Spark is. Here's what we know about how a Spark library should be unit tested: 5 | 6 | Desired Developer Experience 7 | --- 8 | 9 | The user of a library to be able to do: 10 | 11 | git clone the-library.git 12 | cd the-library/firmware/test 13 | make test 14 | 15 | And see a bunch of test output like this: 16 | 17 | running unit tests ...... 18 | test/test1.cpp:343: error: Failure in UberLibraryExample.blabla: false 19 | 20 | - Unit tests should run on a regular computer, not a Spark Core. 21 | - A developer should be able to cd into `firmware/test` and type `make` to run the unit tests (as illustrated above) 22 | - The tests should stub out Spark specific functionality so the code compiles and runs using on a 23 | standard GNU C++ compiler. 24 | - When the compiled binary is run, it should print out a "." for each successful test. Failing tests should have a descriptive name and point to the file and line number of the test. 25 | - When any test fails, the binary should return a non-zero exit code. 26 | 27 | How to help out 28 | --- 29 | 30 | If you have ideas about how to do this, please start a conversation at [community.spark.io](http://community.spark.io), or fork this repository and issue a pull request that illustrates how to do unit testing of a Spark Library. 31 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/test/unit/makefile: -------------------------------------------------------------------------------- 1 | # TODO: Figure out how to stub out the fact that 2 | # a libraries main .cpp file does `#include "uber-library-example/uber-library-example.h"` 3 | # 4 | CPP = g++ 5 | RM = rm -f 6 | MKDIR = mkdir -p 7 | INCLUDE_DIRS += .. 8 | all: 9 | $(CPP) -I $(INCLUDE_DIRS) test1.cpp 10 | ./a.out 11 | -------------------------------------------------------------------------------- /test/__fixtures__/libraries/valid/0.0.2/test/unit/test1.cpp: -------------------------------------------------------------------------------- 1 | // GNU c string lib 2 | #include 3 | 4 | // Stubs from Spark land 5 | #define D7 7 6 | typedef enum PinMode 7 | { 8 | OUTPUT, 9 | INPUT, 10 | INPUT_PULLUP, 11 | INPUT_PULLDOWN, 12 | AF_OUTPUT_PUSHPULL, //Used internally for Alternate Function Output PushPull(TIM, UART, SPI etc) 13 | AF_OUTPUT_DRAIN, //Used internally for Alternate Function Output Drain(I2C etc). External pullup resistors required. 14 | AN_INPUT //Used internally for ADC Input 15 | } PinMode; 16 | 17 | #include "uber-library-example.h" 18 | 19 | int main() 20 | { 21 | // TODO: Figure out how run the library's example code and do unit tests against it. 22 | // UberLibraryExample::Pin outputPin(D7); 23 | // outputPin.beginInPinMode(OUTPUT); 24 | // outputPin.modulateAtFrequency(300); 25 | // UberLibraryExample::Pin pin(D7); 26 | std::cout << "ok" << std::endl; 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf1_proj/code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('particle:core').FunctionContext} context 3 | */ 4 | export default function main() { 5 | // Add your code here 6 | console.log('Hello from logic function!'); 7 | } 8 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf1_proj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "schemas/logic_function.schema.json", 3 | "logic_function": { 4 | "name": "my logic function", 5 | "description": "my description", 6 | "source": { 7 | "type": "JavaScript" 8 | }, 9 | "enabled": true, 10 | "logic_triggers": [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf1_proj/sample/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf2_proj/code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('particle:core').FunctionContext} context 3 | */ 4 | export default function main() { 5 | // Add your code here 6 | console.log('Hello from logic function!'); 7 | } 8 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf2_proj/code2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('particle:core').FunctionContext} context 3 | */ 4 | export default function main() { 5 | // Add your code here 6 | console.log('Hello from logic function!'); 7 | } 8 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf2_proj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "schemas/logic_function.schema.json", 3 | "logic_function": { 4 | "name": "my logic function", 5 | "description": "my description", 6 | "source": { 7 | "type": "JavaScript" 8 | }, 9 | "enabled": true, 10 | "logic_triggers": [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf3_proj/code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('particle:core').FunctionContext} context 3 | */ 4 | export default function main() { 5 | // Add your code here 6 | console.log('Hello from logic function!'); 7 | } 8 | -------------------------------------------------------------------------------- /test/__fixtures__/logic_functions/lf3_proj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "schemas/logic_function.schema.json", 3 | "logic_function": { 4 | "name": "newlf", 5 | "description": "my description", 6 | "source": { 7 | "type": "JavaScript" 8 | }, 9 | "enabled": true, 10 | "logic_triggers": [{ 11 | "type": "Event", 12 | "event_name": "Name of the event", 13 | "product_id": 26346, 14 | "enabled": true 15 | }] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/__fixtures__/pkg/.gitignore: -------------------------------------------------------------------------------- 1 | # all directory contents are ignored 2 | * 3 | 4 | # except me :) 5 | !.gitignore 6 | 7 | -------------------------------------------------------------------------------- /test/__fixtures__/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-pkg", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/blank/project.properties: -------------------------------------------------------------------------------- 1 | name=blank 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/blank/src/blank.ino: -------------------------------------------------------------------------------- 1 | // -------------- 2 | // Blank user app 3 | // -------------- 4 | SYSTEM_MODE(SEMI_AUTOMATIC); 5 | 6 | String name = "blank"; 7 | int version = 1; 8 | 9 | void setup() { 10 | Particle.variable("name", &name, STRING); 11 | Particle.variable("version", &version, INT); 12 | Particle.function("check", check); 13 | Particle.connect(); 14 | } 15 | 16 | void loop() { 17 | delay(500); 18 | Particle.publish("heartbeat", "ok", 60, PRIVATE); 19 | } 20 | 21 | int check(String){ 22 | return 200; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/extended/project.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/projects/extended/project.properties -------------------------------------------------------------------------------- /test/__fixtures__/projects/extended/src/app.ino: -------------------------------------------------------------------------------- 1 | // Demo app with a nested structure 2 | #include "helper/helper.h" 3 | 4 | void setup() { 5 | if (DOIT) { 6 | Particle.function("test", testFn); 7 | } 8 | } 9 | 10 | int testFn(String) { 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/extended/src/helper/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/extended/src/helper/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/fail/fail.cpp: -------------------------------------------------------------------------------- 1 | 2 | asdfjasfjdkl -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/app.ino: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | void setup() { 4 | if (DOIT) { 5 | Particle.function("test", testFn); 6 | } 7 | } 8 | 9 | int testFn(String) { 10 | return 0; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/nope.cpp: -------------------------------------------------------------------------------- 1 | #include "nope.h" 2 | 3 | const int NOPE = 1; 4 | 5 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/nope.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern const int NOPE; 4 | 5 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-exclude/particle.ignore: -------------------------------------------------------------------------------- 1 | nope.cpp 2 | nope.h 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-flat/app.ino: -------------------------------------------------------------------------------- 1 | // Demo app with a flat structure 2 | #include "helper.h" 3 | 4 | void setup() { 5 | if (DOIT) { 6 | Particle.function("test", testFn); 7 | } 8 | } 9 | 10 | int testFn(String) { 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-flat/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/legacy-flat/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/lib-with-example/lib/Particle_TEST_E2E_CLI_LIB/examples/one.ino: -------------------------------------------------------------------------------- 1 | #include "Particle_TEST_E2E_CLI_LIB.h" 2 | 3 | Particle_TEST_E2E_CLI_LIB test = MYLIB(0); 4 | 5 | void setup() { 6 | Particle.function("getX", getX); 7 | } 8 | 9 | void loop(){ 10 | } 11 | 12 | int getX(String){ 13 | uint8_t x = test.incr(); 14 | return x; 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/lib-with-example/lib/Particle_TEST_E2E_CLI_LIB/library.properties: -------------------------------------------------------------------------------- 1 | name=Particle_TEST_E2E_CLI_LIB 2 | version=1.0.0 3 | license=MIT 4 | author=Me 5 | sentence=A test library 6 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/lib-with-example/lib/Particle_TEST_E2E_CLI_LIB/src/Particle_TEST_E2E_CLI_LIB.h: -------------------------------------------------------------------------------- 1 | #ifndef _Particle_TEST_E2E_CLI_LIB_H_ 2 | #define _Particle_TEST_E2E_CLI_LIB_H_ 3 | 4 | class Particle_TEST_E2E_CLI_LIB { 5 | 6 | public: 7 | uint8_t x = 0; 8 | 9 | Particle_TEST_E2E_CLI_LIB(){}; 10 | Particle_TEST_E2E_CLI_LIB(uint8_t xVal) : x(xVal){} 11 | 12 | uint8_t incr(){ 13 | if (x > 255){ 14 | x = 0; 15 | } else { 16 | x = x + 1; 17 | } 18 | return x; 19 | } 20 | 21 | }; 22 | 23 | #endif 24 | 25 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/lib-with-example/project.properties: -------------------------------------------------------------------------------- 1 | name=lib-with-example 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/lib-with-example/src/app.ino: -------------------------------------------------------------------------------- 1 | #include "Particle_TEST_E2E_CLI_LIB.h" 2 | 3 | SYSTEM_MODE(SEMI_AUTOMATIC); 4 | 5 | int version = 1; 6 | String name = "lib-with-example"; 7 | String deviceID = System.deviceID(); 8 | String deviceShortID = deviceID.substring(0, 6); 9 | Particle_TEST_E2E_CLI_LIB test = Particle_TEST_E2E_CLI_LIB(0); 10 | 11 | void setup() { 12 | Particle.variable("name", &name, STRING); 13 | Particle.variable("version", &version, INT); 14 | Particle.function("check", check); 15 | Particle.function("report", report); 16 | Particle.connect(); 17 | } 18 | 19 | void loop(){ 20 | } 21 | 22 | int check(String){ 23 | return 200; 24 | } 25 | 26 | int report(String){ 27 | uint8_t x = test.incr(); 28 | return x; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/project.properties: -------------------------------------------------------------------------------- 1 | name=multiple-header-extensions 2 | 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/app.ino: -------------------------------------------------------------------------------- 1 | // Demo app with a nested structure 2 | #include "helper/h0.h" 3 | #include "helper/h1.hpp" 4 | #include "helper/h2.hxx" 5 | #include "helper/h3.hh" 6 | 7 | void setup() { 8 | if (DOIT_0) { 9 | Particle.function("test", testFn); 10 | } 11 | delay(1000); 12 | if (DOIT_1) { 13 | Particle.function("test", testFn); 14 | } 15 | delay(1000); 16 | if (DOIT_2) { 17 | Particle.function("test", testFn); 18 | } 19 | delay(1000); 20 | if (DOIT_3) { 21 | Particle.function("test", testFn); 22 | } 23 | } 24 | 25 | int testFn(String) { 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h0.cpp: -------------------------------------------------------------------------------- 1 | #include "h0.h" 2 | 3 | const int DOIT_0 = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h0.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern const int DOIT_0; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h1.cpp: -------------------------------------------------------------------------------- 1 | #include "h1.hpp" 2 | 3 | const int DOIT_1 = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h1.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern const int DOIT_1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h2.cpp: -------------------------------------------------------------------------------- 1 | #include "h2.hxx" 2 | 3 | const int DOIT_2 = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h2.hxx: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern const int DOIT_2; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h3.cpp: -------------------------------------------------------------------------------- 1 | #include "h3.hh" 2 | 3 | const int DOIT_3 = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/multiple-header-extensions/src/helper/h3.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern const int DOIT_3; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/lib/helper/src/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/lib/helper/src/helper.def: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/projects/proj-ignore/lib/helper/src/helper.def -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/lib/helper/src/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/lib/helper/src/helper2.cpp: -------------------------------------------------------------------------------- 1 | const int DOIT2 = 0; 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/particle.ignore: -------------------------------------------------------------------------------- 1 | **/helper2.cpp -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/project.properties: -------------------------------------------------------------------------------- 1 | name=legacy-include 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-ignore/src/app.ino: -------------------------------------------------------------------------------- 1 | // Demo app with a flat structure 2 | #include "helper.h" 3 | 4 | void setup() { 5 | if (DOIT) { 6 | Particle.function("test", testFn); 7 | } 8 | } 9 | 10 | int testFn(String) { 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/lib/helper/src/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/lib/helper/src/helper.def: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/projects/proj-include/lib/helper/src/helper.def -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/lib/helper/src/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/particle.include: -------------------------------------------------------------------------------- 1 | **/*.def -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/project.properties: -------------------------------------------------------------------------------- 1 | name=legacy-include 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/proj-include/src/app.ino: -------------------------------------------------------------------------------- 1 | // Demo app with a flat structure 2 | #include "helper.h" 3 | 4 | void setup() { 5 | if (DOIT) { 6 | Particle.function("test", testFn); 7 | } 8 | } 9 | 10 | int testFn(String) { 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/stroby-no-sources/src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/projects/stroby-no-sources/src/.gitkeep -------------------------------------------------------------------------------- /test/__fixtures__/projects/stroby/project.properties: -------------------------------------------------------------------------------- 1 | name=stroby 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/stroby/src/stroby.ino: -------------------------------------------------------------------------------- 1 | // ------------ 2 | // Strobe an LED 3 | // ------------ 4 | 5 | SYSTEM_MODE(SEMI_AUTOMATIC); 6 | 7 | String name = "stroby"; 8 | String deviceID = System.deviceID(); 9 | String deviceShortID = deviceID.substring(0, 6); 10 | int version = 42; 11 | int blinkState = 0; 12 | int led1 = D0; // DO for gen2, D7 for gen3 13 | 14 | void setup() { 15 | pinMode(led1, OUTPUT); 16 | Particle.variable("name", &name, STRING); 17 | Particle.variable("version", &version, INT); 18 | Particle.variable("blinking", &blinkState, INT); 19 | Particle.function("check", check); 20 | Particle.function("stop", stop); 21 | Particle.function("start", start); 22 | Particle.function("toggle", toggleBlink); 23 | Particle.connect(); 24 | } 25 | 26 | void loop(){ 27 | if (blinkState == 0){ 28 | return; 29 | } 30 | 31 | digitalWrite(led1, HIGH); 32 | Serial.printlnf("%s - active", deviceShortID.c_str()); 33 | Particle.publish(deviceShortID, "active", 60, PUBLIC); 34 | Particle.publish("led", "ON", 60, PRIVATE); 35 | delay(500); 36 | digitalWrite(led1, LOW); 37 | Particle.publish("led", "OFF", 60, PRIVATE); 38 | delay(500); 39 | } 40 | 41 | int start(String){ 42 | blinkState = 1; 43 | Serial.printlnf("%s - start", deviceShortID.c_str()); 44 | return blinkState; 45 | } 46 | 47 | int stop(String){ 48 | blinkState = 0; 49 | Serial.printlnf("%s - stop", deviceShortID.c_str()); 50 | return blinkState; 51 | } 52 | 53 | int toggleBlink(String){ 54 | if (blinkState == 1){ 55 | stop(""); 56 | } else { 57 | start(""); 58 | } 59 | return blinkState; 60 | } 61 | 62 | int check(String){ 63 | return 200; 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/symlink/main-project/app.ino: -------------------------------------------------------------------------------- 1 | #include "shared/sub_dir/helper.h" 2 | // Demo app with an include that points to a symlink 3 | 4 | void setup() { 5 | if (DOIT) { 6 | Particle.function("test", testFn); 7 | } 8 | } 9 | 10 | int testFn(String) { 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/symlink/main-project/project.properties: -------------------------------------------------------------------------------- 1 | name=main-project 2 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/symlink/main-project/shared: -------------------------------------------------------------------------------- 1 | ../shared-project -------------------------------------------------------------------------------- /test/__fixtures__/projects/symlink/shared-project/sub_dir/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | 3 | const int DOIT = 1; 4 | -------------------------------------------------------------------------------- /test/__fixtures__/projects/symlink/shared-project/sub_dir/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | extern const int DOIT; 3 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/app-with-assets.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/app-with-assets.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/bundle-asset-hash-no-match.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/bundle-asset-hash-no-match.zip -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/bundle-missing-assets.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/bundle-missing-assets.zip -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/bundle.zip -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/invalid-bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/invalid-bundle.zip -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/invalid_bin/app.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/invalid_bin/app.txt -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/invalid_no_assets/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/invalid_no_assets/app.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/nested_dir/assets/.special_file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/nested_dir/assets/.special_file1 -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/nested_dir/assets/.special_file2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/nested_dir/assets/.special_file2 -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/nested_dir/assets/file1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/nested_dir/assets/file1.txt -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/nested_dir/assets/file2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/nested_dir/assets/file2.txt -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/nested_dir/assets/more_assets/file3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/nested_dir/assets/more_assets/file3.txt -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/projects/stroby-with-assets/assets/cat.txt: -------------------------------------------------------------------------------- 1 | meow 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/projects/stroby-with-assets/assets/house.txt: -------------------------------------------------------------------------------- 1 | house 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/projects/stroby-with-assets/assets/water.txt: -------------------------------------------------------------------------------- 1 | drip 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/projects/stroby-with-assets/project.properties: -------------------------------------------------------------------------------- 1 | name=stroby 2 | assetOtaDir=assets 3 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/projects/stroby-with-assets/src/stroby.ino: -------------------------------------------------------------------------------- 1 | // ------------ 2 | // Strobe an LED 3 | // ------------ 4 | 5 | SYSTEM_MODE(SEMI_AUTOMATIC); 6 | 7 | String name = "stroby"; 8 | String deviceID = System.deviceID(); 9 | String deviceShortID = deviceID.substring(0, 6); 10 | int version = 42; 11 | int blinkState = 0; 12 | int led1 = D0; // DO for gen2, D7 for gen3 13 | 14 | void setup() { 15 | pinMode(led1, OUTPUT); 16 | Particle.variable("name", &name, STRING); 17 | Particle.variable("version", &version, INT); 18 | Particle.variable("blinking", &blinkState, INT); 19 | Particle.function("check", check); 20 | Particle.function("stop", stop); 21 | Particle.function("start", start); 22 | Particle.function("toggle", toggleBlink); 23 | Particle.connect(); 24 | } 25 | 26 | void loop(){ 27 | if (blinkState == 0){ 28 | return; 29 | } 30 | 31 | digitalWrite(led1, HIGH); 32 | Serial.printlnf("%s - active", deviceShortID.c_str()); 33 | Particle.publish(deviceShortID, "active", 60, PUBLIC); 34 | Particle.publish("led", "ON", 60, PRIVATE); 35 | delay(500); 36 | digitalWrite(led1, LOW); 37 | Particle.publish("led", "OFF", 60, PRIVATE); 38 | delay(500); 39 | } 40 | 41 | int start(String){ 42 | blinkState = 1; 43 | Serial.printlnf("%s - start", deviceShortID.c_str()); 44 | return blinkState; 45 | } 46 | 47 | int stop(String){ 48 | blinkState = 0; 49 | Serial.printlnf("%s - stop", deviceShortID.c_str()); 50 | return blinkState; 51 | } 52 | 53 | int toggleBlink(String){ 54 | if (blinkState == 1){ 55 | stop(""); 56 | } else { 57 | start(""); 58 | } 59 | return blinkState; 60 | } 61 | 62 | int check(String){ 63 | return 200; 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/tinker_argon_541.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/tinker_argon_541.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-proj-prop/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/valid-no-proj-prop/app.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-proj-prop/assets/cat.txt: -------------------------------------------------------------------------------- 1 | meow 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-proj-prop/assets/house.txt: -------------------------------------------------------------------------------- 1 | house 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-proj-prop/assets/water.txt: -------------------------------------------------------------------------------- 1 | drip 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-prop/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/valid-no-prop/app.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-prop/otaAssets/cat.txt: -------------------------------------------------------------------------------- 1 | meow 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-prop/otaAssets/house.txt: -------------------------------------------------------------------------------- 1 | house 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-prop/otaAssets/water.txt: -------------------------------------------------------------------------------- 1 | drip 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid-no-prop/project.properties: -------------------------------------------------------------------------------- 1 | name=valid 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/valid/app.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/bundle_app_1719510886367.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/valid/bundle_app_1719510886367.zip -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/otaAssets/cat.txt: -------------------------------------------------------------------------------- 1 | meow 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/otaAssets/house.txt: -------------------------------------------------------------------------------- 1 | house 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/otaAssets/water.txt: -------------------------------------------------------------------------------- 1 | drip 2 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/valid/project.properties: -------------------------------------------------------------------------------- 1 | name=valid 2 | assetOtaDir=otaAssets 3 | -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/zero_assets/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/zero_assets/app.bin -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/zero_assets/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/particle-cli/1438765f07c21f282781905166c60e6c42596ee4/test/__fixtures__/third_party_ota/zero_assets/assets/.gitkeep -------------------------------------------------------------------------------- /test/__fixtures__/third_party_ota/zero_assets/project.properties: -------------------------------------------------------------------------------- 1 | name=zero_assets 2 | assetOtaDir=assets 3 | -------------------------------------------------------------------------------- /test/__fixtures__/wiring/.gitignore: -------------------------------------------------------------------------------- 1 | output.cpp 2 | -------------------------------------------------------------------------------- /test/__fixtures__/wiring/one/input.ino: -------------------------------------------------------------------------------- 1 | ino file 2 | -------------------------------------------------------------------------------- /test/__mocks__/serial.mock.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Duplex = require('stream').Duplex; 3 | 4 | function MockSerial() { 5 | Duplex.call(this); 6 | this.isOpen = false; 7 | this.data = ''; 8 | } 9 | util.inherits(MockSerial, Duplex); 10 | 11 | MockSerial.prototype._read = function _read() { 12 | }; 13 | 14 | MockSerial.prototype.write = function write(chunk) { 15 | this.data += chunk; 16 | }; 17 | 18 | MockSerial.prototype.drain = function drain(cb = () => {}) { 19 | this.emit('drain'); 20 | process.nextTick(cb); 21 | }; 22 | 23 | MockSerial.prototype.flush = function flush(cb) { 24 | this.emit('flush'); 25 | process.nextTick(cb); 26 | }; 27 | 28 | MockSerial.prototype.open = function open(cb) { 29 | this.isOpen = true; 30 | process.nextTick(cb); 31 | }; 32 | 33 | 34 | MockSerial.prototype.close = function close(cb) { 35 | this.isOpen = false; 36 | if (cb) { 37 | process.nextTick(cb); 38 | } 39 | }; 40 | 41 | module.exports = MockSerial; 42 | -------------------------------------------------------------------------------- /test/__root__/.gitignore: -------------------------------------------------------------------------------- 1 | # all directory contents are ignored 2 | * 3 | 4 | # except us :) 5 | !/.gitignore 6 | !/README.md 7 | -------------------------------------------------------------------------------- /test/__root__/README.md: -------------------------------------------------------------------------------- 1 | This directory is used by the end-to-end tests instead of your default `$HOME` directory. Directory contents are ignored. 2 | -------------------------------------------------------------------------------- /test/e2e/doctor.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | 4 | 5 | describe('Doctor Commands [@device]', () => { 6 | const help = [ 7 | 'NOT SUPPORTED. Go to the device doctor tool at docs.particle.io/tools/doctor', 8 | 'Usage: particle doctor [options]', 9 | '', 10 | 'Global Options:', 11 | ' -v, --verbose Increases how much logging to display [count]', 12 | ' -q, --quiet Decreases how much logging to display [count]' 13 | ]; 14 | 15 | it('Shows `help` content', async () => { 16 | const { stdout, stderr, exitCode } = await cli.run(['help', 'doctor']); 17 | 18 | expect(stdout).to.equal(''); 19 | expect(stderr.split('\n')).to.include.members(help); 20 | expect(exitCode).to.equal(0); 21 | }); 22 | 23 | it('Shows `help` content when run with `--help` flag', async () => { 24 | const { stdout, stderr, exitCode } = await cli.run(['doctor', '--help']); 25 | 26 | expect(stdout).to.equal(''); 27 | expect(stderr.split('\n')).to.include.members(help); 28 | expect(exitCode).to.equal(0); 29 | }); 30 | 31 | it('is not longer supported', async() => { 32 | const { stdout, stderr, exitCode } = await cli.run(['doctor', '--help']); 33 | 34 | expect(stdout).to.equal('particle device doctor is no longer supported.\nGo to the device doctor tool at docs.particle.io/tools/doctor.'); 35 | expect(stderr).to.equal(''); 36 | expect(exitCode).to.equal(0); 37 | }); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /test/e2e/identify.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | const { 4 | DEVICE_ID 5 | } = require('../lib/env'); 6 | 7 | 8 | describe('Identify Commands [@device]', () => { 9 | const help = [ 10 | 'Ask for and display device ID via serial', 11 | 'Usage: particle identify [options]', 12 | '', 13 | 'Global Options:', 14 | ' -v, --verbose Increases how much logging to display [count]', 15 | ' -q, --quiet Decreases how much logging to display [count]', 16 | '', 17 | 'Options:', 18 | ' --port Use this serial port instead of auto-detecting. Useful if there are more than 1 connected device [string]' 19 | ]; 20 | 21 | before(async () => { 22 | await cli.setTestProfileAndLogin(); 23 | await cli.startListeningMode(); 24 | }); 25 | 26 | after(async () => { 27 | await cli.stopListeningMode(); 28 | await cli.waitUntilOnline(); 29 | await cli.logout(); 30 | await cli.setDefaultProfile(); 31 | }); 32 | 33 | it('Shows `help` content', async () => { 34 | const { stdout, stderr, exitCode } = await cli.run(['help', 'identify']); 35 | 36 | expect(stdout).to.equal(''); 37 | expect(stderr.split('\n')).to.include.members(help); 38 | expect(exitCode).to.equal(0); 39 | }); 40 | 41 | it('Shows `help` content when run with `--help` flag', async () => { 42 | const { stdout, stderr, exitCode } = await cli.run(['identify', '--help']); 43 | 44 | expect(stdout).to.equal(''); 45 | expect(stderr.split('\n')).to.include.members(help); 46 | expect(exitCode).to.equal(0); 47 | }); 48 | 49 | it('Identifies device', async () => { 50 | const { stdout, stderr, exitCode } = await cli.run('identify'); 51 | 52 | expect(stdout).to.include(`Your device id is ${DEVICE_ID}`); 53 | expect(stdout).to.include('Your system firmware version is'); 54 | expect(stderr).to.equal(''); 55 | expect(exitCode).to.equal(0); 56 | }); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /test/e2e/list.e2e.js: -------------------------------------------------------------------------------- 1 | const capitalize = require('lodash/capitalize'); 2 | const { expect } = require('../setup'); 3 | const cli = require('../lib/cli'); 4 | const { 5 | DEVICE_ID, 6 | DEVICE_NAME, 7 | DEVICE_PLATFORM_NAME 8 | } = require('../lib/env'); 9 | 10 | 11 | describe('List Commands', () => { 12 | const help = [ 13 | 'Display a list of your devices, as well as their variables and functions', 14 | 'Usage: particle list [options] [filter]', 15 | '', 16 | 'Global Options:', 17 | ' -v, --verbose Increases how much logging to display [count]', 18 | ' -q, --quiet Decreases how much logging to display [count]', 19 | '', 20 | '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, linux), a device ID or name' 21 | ]; 22 | 23 | before(async () => { 24 | await cli.setTestProfileAndLogin(); 25 | }); 26 | 27 | after(async () => { 28 | await cli.logout(); 29 | await cli.setDefaultProfile(); 30 | }); 31 | 32 | it('Shows `help` content', async () => { 33 | const { stdout, stderr, exitCode } = await cli.run(['help', 'list']); 34 | 35 | expect(stdout).to.equal(''); 36 | expect(stderr.split('\n')).to.include.members(help); 37 | expect(exitCode).to.equal(0); 38 | }); 39 | 40 | it('Shows `help` content when run with `--help` flag', async () => { 41 | const { stdout, stderr, exitCode } = await cli.run(['list', '--help']); 42 | 43 | expect(stdout).to.equal(''); 44 | expect(stderr.split('\n')).to.include.members(help); 45 | expect(exitCode).to.equal(0); 46 | }); 47 | 48 | it('Lists devices', async () => { 49 | const platform = capitalize(DEVICE_PLATFORM_NAME); 50 | const { stdout, stderr, exitCode } = await cli.run('list'); 51 | 52 | expect(stdout).to.include(`${DEVICE_NAME} [${DEVICE_ID}] (${platform})`); 53 | expect(stderr).to.equal(''); 54 | expect(exitCode).to.equal(0); 55 | }); 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /test/e2e/logout.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | const { USERNAME } = require('../lib/env'); 4 | 5 | 6 | describe('Logout Commands', () => { 7 | const help = [ 8 | 'Log out of your session and clear your saved access token', 9 | 'Usage: particle logout [options]', 10 | '', 11 | 'Global Options:', 12 | ' -v, --verbose Increases how much logging to display [count]', 13 | ' -q, --quiet Decreases how much logging to display [count]' 14 | ]; 15 | 16 | before(async () => { 17 | await cli.setTestProfileAndLogin(); 18 | }); 19 | 20 | after(async () => { 21 | await cli.logout(); 22 | await cli.setDefaultProfile(); 23 | }); 24 | 25 | it('Shows `help` content', async () => { 26 | const { stdout, stderr, exitCode } = await cli.run(['help', 'logout']); 27 | 28 | expect(stdout).to.equal(''); 29 | expect(stderr.split('\n')).to.include.members(help); 30 | expect(exitCode).to.equal(0); 31 | }); 32 | 33 | it('Shows `help` content when run with `--help` flag', async () => { 34 | const { stdout, stderr, exitCode } = await cli.run(['logout', '--help']); 35 | 36 | expect(stdout).to.equal(''); 37 | expect(stderr.split('\n')).to.include.members(help); 38 | expect(exitCode).to.equal(0); 39 | }); 40 | 41 | it('Signs out', async () => { 42 | const { stdout, stderr, exitCode } = await cli.run(['logout']); 43 | 44 | expect(stdout).to.include(`You have been logged out from ${USERNAME}`); 45 | expect(stderr).to.equal(''); 46 | expect(exitCode).to.equal(0); 47 | }); 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /test/e2e/udp.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const { delay } = require('../lib/mocha-utils'); 3 | const cli = require('../lib/cli'); 4 | 5 | 6 | describe('UDP Commands', () => { 7 | const help = [ 8 | 'Talk UDP to repair devices, run patches, check Wi-Fi, and more!', 9 | 'Usage: particle udp ', 10 | 'Help: particle help udp ', 11 | '', 12 | 'Commands:', 13 | ' send Sends a UDP packet to the specified host and port', 14 | ' listen Listens for UDP packets on an optional port (default 5549)', 15 | '', 16 | 'Global Options:', 17 | ' -v, --verbose Increases how much logging to display [count]', 18 | ' -q, --quiet Decreases how much logging to display [count]' 19 | ]; 20 | 21 | it('Shows `help` content', async () => { 22 | const { stdout, stderr, exitCode } = await cli.run(['help', 'udp']); 23 | 24 | expect(stdout).to.equal(''); 25 | expect(stderr.split('\n')).to.include.members(help); 26 | expect(exitCode).to.equal(0); 27 | }); 28 | 29 | it('Shows `help` content when run without arguments', async () => { 30 | const { stdout, stderr, exitCode } = await cli.run('udp'); 31 | 32 | expect(stdout).to.equal(''); 33 | expect(stderr.split('\n')).to.include.members(help); 34 | expect(exitCode).to.equal(0); 35 | }); 36 | 37 | it('Shows `help` content when run with `--help` flag', async () => { 38 | const { stdout, stderr, exitCode } = await cli.run(['udp', '--help']); 39 | 40 | expect(stdout).to.equal(''); 41 | expect(stderr.split('\n')).to.include.members(help); 42 | expect(exitCode).to.equal(0); 43 | }); 44 | 45 | it('Sends and receives packet', async () => { 46 | const port = 3000; 47 | const host = '127.0.0.1'; 48 | const subprocess = cli.run(['udp', 'listen', port]); 49 | 50 | await delay(1000); 51 | await cli.run(['udp', 'send', host, port, 'hello']); 52 | await delay(1000); 53 | subprocess.cancel(); // CTRL-C 54 | 55 | const { all, isCanceled } = await subprocess; 56 | 57 | expect(all).to.include(`Listening for UDP packets on port ${port}`); 58 | expect(all).to.include(`[${host}] hello`); 59 | expect(isCanceled).to.equal(true); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /test/e2e/update-protected.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | const { 4 | DEVICE_ID, 5 | DEVICE_NAME, DEVICE_PLATFORM_NAME 6 | } = require('../lib/env'); 7 | const stripAnsi = require('strip-ansi'); 8 | const { delay } = require('../lib/mocha-utils'); 9 | 10 | 11 | describe('Update Commands for Protected Devices [@device]', () => { 12 | before(async () => { 13 | await cli.setTestProfileAndLogin(); 14 | }); 15 | 16 | after(async () => { 17 | await cli.logout(); 18 | await cli.setDefaultProfile(); 19 | }); 20 | 21 | it('Updates to latest default Device OS version', async () => { 22 | const { stdout: stdoutPBefore } = await cli.run(['device-protection', 'status']); 23 | const { stdout, stderr, exitCode } = await cli.run(['update']); 24 | const { stdout: stdoutPAfter } = await cli.run(['device-protection', 'status']); 25 | 26 | expect(stripAnsi(stdout)).to.include(`Updating ${DEVICE_PLATFORM_NAME} ${DEVICE_ID} to latest Device OS version`); 27 | expect(stripAnsi(stdout)).to.include('Update success!'); 28 | expect(stderr).to.equal(''); 29 | expect(exitCode).to.equal(0); 30 | 31 | await delay(5000); 32 | await cli.waitUntilOnline(); 33 | const cmd = await cli.run(['usb', 'list']); 34 | 35 | expect(cmd.stdout).to.include(DEVICE_ID); 36 | expect(cmd.stdout).to.include(DEVICE_NAME); 37 | expect((stdoutPBefore.split('\n'))[0]).to.include('Protected Device'); 38 | expect((stdoutPBefore.split('\n'))[0]).to.not.include('Service Mode'); 39 | expect((stdoutPAfter.split('\n'))[0]).to.include('Protected Device'); 40 | expect((stdoutPAfter.split('\n'))[0]).to.not.include('Service Mode'); 41 | }); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /test/e2e/update.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | const { 4 | DEVICE_ID, 5 | DEVICE_NAME, DEVICE_PLATFORM_NAME 6 | } = require('../lib/env'); 7 | const stripAnsi = require('strip-ansi'); 8 | 9 | describe('Update Commands [@device]', () => { 10 | const help = [ 11 | 'Update Device OS on a device via USB', 12 | 'Usage: particle update [options] [device]', 13 | '', 14 | 'Global Options:', 15 | ' -v, --verbose Increases how much logging to display [count]', 16 | ' -q, --quiet Decreases how much logging to display [count]', 17 | '', 18 | 'Options:', 19 | ' --target The Device OS version to update. Defaults to latest version. [string]', 20 | '', 21 | 'Examples:', 22 | ' particle update Update Device OS on the device connected over USB', 23 | ' particle update red Update Device OS on device red', 24 | ' particle update --target 5.0.0 blue Update Device OS on device blue to version 5.0.0', 25 | ]; 26 | 27 | before(async () => { 28 | await cli.setTestProfileAndLogin(); 29 | }); 30 | 31 | after(async () => { 32 | await cli.logout(); 33 | await cli.setDefaultProfile(); 34 | }); 35 | 36 | it('Shows `help` content', async () => { 37 | const { stdout, stderr, exitCode } = await cli.run(['help', 'update']); 38 | 39 | expect(stdout).to.equal(''); 40 | expect(stderr.split('\n')).to.include.members(help); 41 | expect(exitCode).to.equal(0); 42 | }); 43 | 44 | it('Shows `help` content when run with `--help` flag', async () => { 45 | const { stdout, stderr, exitCode } = await cli.run(['update', '--help']); 46 | 47 | expect(stdout).to.equal(''); 48 | expect(stderr.split('\n')).to.include.members(help); 49 | expect(exitCode).to.equal(0); 50 | }); 51 | 52 | it('Updates to latest default Device OS version', async () => { 53 | const { stdout, stderr, exitCode } = await cli.run(['update']); 54 | 55 | expect(stripAnsi(stdout)).to.include(`Updating ${DEVICE_PLATFORM_NAME} ${DEVICE_ID} to latest Device OS version`); 56 | expect(stdout).to.include('Update success!'); 57 | expect(stderr).to.equal(''); 58 | expect(exitCode).to.equal(0); 59 | 60 | await cli.waitUntilOnline(); 61 | const cmd = await cli.run(['usb', 'list']); 62 | 63 | expect(cmd.stdout).to.include(DEVICE_ID); 64 | expect(cmd.stdout).to.include(DEVICE_NAME); 65 | }); 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /test/e2e/version.e2e.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | const pkgJSON = require('../../package.json'); 3 | const { expect } = require('../setup'); 4 | const cli = require('../lib/cli'); 5 | 6 | 7 | describe('Version Commands', () => { 8 | it('Prints the version', async () => { 9 | const { stdout, stderr, exitCode } = await cli.run('version'); 10 | 11 | expect(stdout).to.equal(pkgJSON.version); 12 | expect(!!semver.valid(stdout)).to.equal(true); 13 | expect(stderr).to.equal(''); 14 | expect(exitCode).to.equal(0); 15 | }); 16 | 17 | it('Prints the version when run with `--version` flag', async () => { 18 | const { stdout, stderr, exitCode } = await cli.run('--version'); 19 | 20 | expect(stdout).to.equal(pkgJSON.version); 21 | expect(!!semver.valid(stdout)).to.equal(true); 22 | expect(stderr).to.equal(''); 23 | expect(exitCode).to.equal(0); 24 | }); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /test/e2e/whoami.e2e.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('../setup'); 2 | const cli = require('../lib/cli'); 3 | const { 4 | USERNAME 5 | } = require('../lib/env'); 6 | const stripAnsi = require('strip-ansi'); 7 | 8 | 9 | describe('Whoami Commands', () => { 10 | const help = [ 11 | 'prints signed-in username', 12 | 'Usage: particle whoami [options]', 13 | '', 14 | 'Global Options:', 15 | ' -v, --verbose Increases how much logging to display [count]', 16 | ' -q, --quiet Decreases how much logging to display [count]' 17 | ]; 18 | 19 | before(async () => { 20 | await cli.setTestProfileAndLogin(); 21 | }); 22 | 23 | after(async () => { 24 | await cli.logout(); 25 | await cli.setDefaultProfile(); 26 | }); 27 | 28 | it('Shows `help` content', async () => { 29 | const { stdout, stderr, exitCode } = await cli.run(['help', 'whoami']); 30 | 31 | expect(stdout).to.equal(''); 32 | expect(stderr.split('\n')).to.include.members(help); 33 | expect(exitCode).to.equal(0); 34 | }); 35 | 36 | it('Shows `help` content when run with `--help` flag', async () => { 37 | const { stdout, stderr, exitCode } = await cli.run(['whoami', '--help']); 38 | 39 | expect(stdout).to.equal(''); 40 | expect(stderr.split('\n')).to.include.members(help); 41 | expect(exitCode).to.equal(0); 42 | }); 43 | 44 | it('Reports currently signed-in username', async () => { 45 | const { stdout, stderr, exitCode } = await cli.run('whoami'); 46 | 47 | expect(stdout).to.include(USERNAME); 48 | expect(stderr).to.equal(''); 49 | expect(exitCode).to.equal(0); 50 | }); 51 | 52 | it('Errors when user is signed-out', async () => { 53 | await cli.logout(); 54 | const { stdout, stderr, exitCode } = await cli.run('whoami'); 55 | 56 | expect(stripAnsi(stdout)).to.equal('You\'re not logged in. Please login using particle login before using this command'); 57 | expect(stderr).to.equal(''); 58 | expect(exitCode).to.equal(1); 59 | }); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /test/integration/access_token.js: -------------------------------------------------------------------------------- 1 | function accessTokenFromSettings() { 2 | const settings = require('../../settings'); 3 | settings.whichProfile(); 4 | settings.loadOverrides(); 5 | return settings.access_token; 6 | } 7 | 8 | let token = undefined; 9 | function fetchAccessToken(){ 10 | if (token===undefined) { 11 | token = process.env.ACCESS_TOKEN || accessTokenFromSettings() || null; 12 | } 13 | return token; 14 | } 15 | 16 | function itHasAccessToken(){ 17 | return fetchAccessToken() ? it : xit; 18 | } 19 | 20 | 21 | module.exports.fetchAccessToken = fetchAccessToken; 22 | module.exports.itHasAccessToken = itHasAccessToken; 23 | -------------------------------------------------------------------------------- /test/integration/command-processor.integration.js: -------------------------------------------------------------------------------- 1 | /* 2 | ****************************************************************************** 3 | Copyright (c) 2016 Particle Industries, Inc. All rights reserved. 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation, either 8 | version 3 of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this program; if not, see . 17 | ****************************************************************************** 18 | */ 19 | 20 | const { expect } = require('../setup'); 21 | const CLI = require('../../src/app/cli'); 22 | const commandProcessor = require('../../src/app/command-processor'); 23 | 24 | 25 | describe('command line parsing', () => { 26 | describe('global flags', () => { 27 | describe('verbosity', () => { 28 | const rootCategory = new CLI().rootCategory; 29 | 30 | function assertVerbosity(argv, expected) { 31 | if (argv.clierror) { 32 | throw argv.clierror; 33 | } 34 | if (expected!==undefined) { 35 | expect(global).to.have.property('verboseLevel').equal(expected); 36 | } else { 37 | expect(global).to.not.have.property('verboseLevel'); 38 | } 39 | } 40 | 41 | it('is 1 by default', () => { 42 | const argv = commandProcessor.parse(rootCategory, []); 43 | assertVerbosity(argv, 1); 44 | }); 45 | 46 | it('is 2 for a single v flag', () => { 47 | const argv = commandProcessor.parse(rootCategory, ['-v']); 48 | assertVerbosity(argv, 2); 49 | }); 50 | 51 | it('is 3 for a double v flag', () => { 52 | const argv = commandProcessor.parse(rootCategory, ['-vv']); 53 | assertVerbosity(argv, 3); 54 | }); 55 | 56 | it('is 0 with one quiet', () => { 57 | const argv = commandProcessor.parse(rootCategory, ['-q']); 58 | assertVerbosity(argv, 0); 59 | }); 60 | 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/integration/library_init.integration.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | ****************************************************************************** 4 | Copyright (c) 2016 Particle Industries, Inc. All rights reserved. 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation, either 9 | version 3 of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this program; if not, see . 18 | ****************************************************************************** 19 | */ 20 | const { expect } = require('../setup'); 21 | const fs = require('fs-extra'); 22 | const path = require('path'); 23 | const libraryCommands = require('../../src/cli/library'); 24 | const commandProcessor = require('../../src/app/command-processor'); 25 | const { PATH_TMP_DIR } = require('../lib/env'); 26 | const { delay } = require('../lib/mocha-utils'); 27 | 28 | describe('library init', () => { 29 | 30 | after(async () => { 31 | await fs.remove(path.join(PATH_TMP_DIR, 'lib')); 32 | }); 33 | 34 | it('can run library init without prompts', async function libraryCreate(){ 35 | this.timeout(18*1000); 36 | const root = commandProcessor.createAppCategory(); 37 | 38 | libraryCommands({ commandProcessor, root }); 39 | 40 | await fs.ensureDir(path.join(PATH_TMP_DIR, 'lib')); 41 | const argv = commandProcessor.parse(root, ['library', 'create', '--name', 'foobar', 42 | '--version=1.2.3', '--author=mrbig', '--dir', path.join(PATH_TMP_DIR, 'lib')]); 43 | 44 | expect(argv.clicommand).to.be.ok; 45 | 46 | await argv.clicommand.exec(argv); 47 | await delay(1000); // wait for the generator to finish 48 | expect(fs.existsSync(path.join(PATH_TMP_DIR, 'lib', 'library.properties'))).to.equal(true); 49 | expect(fs.existsSync(path.join(PATH_TMP_DIR, 'lib','src', 'foobar.cpp'))).to.equal(true); 50 | expect(fs.existsSync(path.join(PATH_TMP_DIR, 'lib', 'src', 'foobar.h'))).to.equal(true); 51 | }); 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /test/lib/ansi-strip.js: -------------------------------------------------------------------------------- 1 | // source: https://github.com/chalk/ansi-regex 2 | const ptn = new RegExp( 3 | [ 4 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', 5 | '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' 6 | ].join('|'), 7 | 'g' 8 | ); 9 | 10 | module.exports = (str) => str.replace(ptn, ''); 11 | 12 | -------------------------------------------------------------------------------- /test/lib/capture-matches.js: -------------------------------------------------------------------------------- 1 | module.exports = (str, regex) => { 2 | let output = []; 3 | let matches; 4 | 5 | while ((matches = regex.exec(str))){ 6 | output.push(matches[1]); 7 | } 8 | 9 | return output; 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /test/lib/fs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const { PATH_PARTICLE_PROFILE } = require('./env'); 4 | 5 | 6 | module.exports = Object.assign({}, fs); // TODO (mirande): better approach? 7 | 8 | module.exports.getDirectoryContents = async (dir, options = {}, contents = [], rootDepth) => { 9 | const { getDirectoryContents, readdir, stat } = module.exports; 10 | const { maxDepth } = options; 11 | const files = await readdir(dir); 12 | 13 | rootDepth = rootDepth || dir.split(path.sep).length; 14 | 15 | for (let file of files) { 16 | const filepath = path.join(dir, file); 17 | const stats = await stat(filepath); 18 | 19 | contents.push(filepath); 20 | 21 | if (stats.isDirectory()){ 22 | const depth = filepath.split(path.sep).length - rootDepth; 23 | 24 | if (depth <= maxDepth){ 25 | contents = await getDirectoryContents(filepath, options, contents, rootDepth); 26 | } 27 | } 28 | } 29 | 30 | return contents; 31 | }; 32 | 33 | module.exports.getParticleAccessToken = async () => { 34 | const { readFile } = module.exports; 35 | const json = await readFile(PATH_PARTICLE_PROFILE, 'utf8'); 36 | const { access_token: token } = JSON.parse(json); 37 | return token; 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /test/lib/mocha-utils.js: -------------------------------------------------------------------------------- 1 | // 2 | // TODO (mirande): cli commands should have an option to wait for an operation 3 | // to complete (e.g. `--wait`) 4 | module.exports.runForAtLeast = (secs, fn) => { 5 | const { delay } = module.exports; 6 | 7 | return async () => { 8 | const end = Date.now() + (secs * 1000); 9 | const result = await fn(); 10 | 11 | if (end > Date.now()){ 12 | await delay(end - Date.now()); 13 | } 14 | return result; 15 | }; 16 | }; 17 | 18 | // TODO (mirande): use @particle/async-utils 19 | module.exports.delay = (ms, value) => { 20 | return new Promise((resolve) => setTimeout(() => resolve(value), ms)); 21 | }; 22 | 23 | // TODO (mirande): figure out a better approach. this allows us to verify 24 | // log output without supressing mocha's success / error messages but is a 25 | // bit awkward 26 | module.exports.withConsoleStubs = (sandbox, fn) => { 27 | return () => { 28 | let result; 29 | 30 | sandbox.spy(process.stdout, 'write'); 31 | 32 | if (process.stdout.isTTY){ 33 | sandbox.stub(process.stdout, 'isTTY').get(() => false); 34 | } 35 | 36 | sandbox.spy(process.stderr, 'write'); 37 | 38 | if (process.stderr.isTTY){ 39 | sandbox.stub(process.stderr, 'isTTY').get(() => false); 40 | } 41 | 42 | try { 43 | result = fn(); 44 | } catch (error){ 45 | sandbox.restore(); 46 | throw error; 47 | } 48 | 49 | if (result && typeof result.finally === 'function'){ 50 | return result.finally(() => sandbox.restore()); 51 | } 52 | sandbox.restore(); 53 | return result; 54 | }; 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /test/lib/open-ssl.js: -------------------------------------------------------------------------------- 1 | const execa = require('execa'); 2 | const { 3 | PATH_REPO_DIR 4 | } = require('./env'); 5 | 6 | 7 | module.exports.run = (args = [], options = {}) => { 8 | const opts = Object.assign({ 9 | cwd: PATH_REPO_DIR, 10 | reject: false 11 | }, options); 12 | 13 | args = Array.isArray(args) ? args : [args]; 14 | return execa('openssl', [...args], opts); 15 | }; 16 | 17 | module.exports.debug = (...args) => { 18 | const subprocess = module.exports.run(...args); 19 | subprocess.stdout.pipe(process.stdout); 20 | subprocess.stderr.pipe(process.stderr); 21 | return subprocess; 22 | }; 23 | 24 | module.exports.exists = async () => { 25 | const { version } = module.exports; 26 | 27 | try { 28 | await version(); 29 | } catch (error){ 30 | return !error; /* false */ 31 | } 32 | return true; 33 | }; 34 | 35 | module.exports.ensureExists = async () => { 36 | const { exists } = module.exports; 37 | 38 | if (!await exists()){ 39 | throw new OpenSSLError('Unable to locate `openssl` - please ensure it is installed and working'); 40 | } 41 | }; 42 | 43 | module.exports.version = () => { 44 | const { run } = module.exports; 45 | return run(['version'], { reject: true }); 46 | }; 47 | 48 | 49 | // ERRORS ///////////////////////////////////////////////////////////////////// 50 | class OpenSSLError extends Error { 51 | constructor(message){ 52 | super(message); 53 | Error.captureStackTrace(this, OpenSSLError); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | // Set up the Mocha test framework with the Chai assertion library and 2 | // the testdouble library for mocks and stubs (previously Sinon mock library) 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | 8 | chai.use(sinonChai); 9 | chai.use(chaiAsPromised); 10 | const expect = chai.expect; 11 | 12 | module.exports = { 13 | chai, 14 | sinon, 15 | expect 16 | }; 17 | --------------------------------------------------------------------------------