From f0a6c4fc39823fee7fbbfba685e4242f26aa48e6 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 4 Aug 2025 12:01:25 -0400 Subject: [PATCH 01/25] Add standards-compliant User-Agent header (#203) --- README.md | 16 ++++++++- src/RokuDeploy.spec.ts | 76 +++++++++++++++++++++++++++++++++++++++--- src/RokuDeploy.ts | 48 +++++++++++++++++++++++--- 3 files changed, 129 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 269c2c1..fc34116 100644 --- a/README.md +++ b/README.md @@ -388,9 +388,23 @@ Here are the available options. The defaults are shown to the right of the optio If true the previously installed dev channel will be deleted before installing the new one -Click [here](https://github.com/rokucommunity/roku-deploy/blob/8e1cbdfcccb38dad4a1361277bdaf5484f1c2bcd/src/RokuDeploy.ts#L897) to see the typescript interface for these options +Click [here](https://github.com/rokucommunity/roku-deploy/blob/master/src/RokuDeployOptions.ts) to see the typescript interface for these options +## User-agent +roku-deploy includes a User-Agent header to help you filter out unwanted network traffic from your network monitoring software of choice. The User-Agent header will be the name of the tool (`roku-deploy`) followed by the full version of the tool. If you're using a prerelease or temporary testing version, that information will also be included. Here are some examples: +``` +User-Agent: roku-deploy/3.12.6 +User-Agent: roku-deploy/4.0.0-alpha.12 +User-Agent: roku-deploy/3.12.7-some-branch-name-123.3958293294854 + +``` + +If for some strange reason we were unable to find the version number, then the `User-Agent` will be: +``` +User-Agent: roku-deploy/unknown +``` + ## Troubleshooting - if you see a `ESOCKETTIMEDOUT` error during deployment, this can be caused by an antivirus blocking network traffic, so consider adding a special exclusion for your Roku device. diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index b50cd6b..064c502 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -112,7 +112,7 @@ describe('index', () => { return {} as any; }); - let results = await rokuDeploy['doPostRequest']({}, true); + let results = await rokuDeploy['doPostRequest']({} as any, true); expect(results.body).to.equal(body); }); @@ -124,7 +124,7 @@ describe('index', () => { }); try { - await rokuDeploy['doPostRequest']({}, true); + await rokuDeploy['doPostRequest']({} as any, true); } catch (e) { expect(e).to.equal(error); return; @@ -140,7 +140,7 @@ describe('index', () => { }); try { - await rokuDeploy['doPostRequest']({}, true); + await rokuDeploy['doPostRequest']({} as any, true); } catch (e) { expect(e).to.be.instanceof(errors.InvalidDeviceResponseCodeError); return; @@ -155,7 +155,7 @@ describe('index', () => { return {} as any; }); - let results = await rokuDeploy['doPostRequest']({}, false); + let results = await rokuDeploy['doPostRequest']({} as any, false); expect(results.body).to.equal(body); }); }); @@ -3755,6 +3755,72 @@ describe('index', () => { }); }); + describe('setUserAgentIfMissing', () => { + const currentVersion = fsExtra.readJsonSync(`${__dirname}/../package.json`).version; + + it('getUserAgent caches package version', () => { + const spy = sinon.spy(fsExtra, 'readJsonSync'); + rokuDeploy['_packageVersion'] = undefined; + expect(rokuDeploy['getUserAgent']()).to.eql(`roku-deploy/${currentVersion}`); + expect(rokuDeploy['getUserAgent']()).to.eql(`roku-deploy/${currentVersion}`); + + expect(spy.callCount).to.equal(1); + }); + + it('getUserAgent caches failed package.json read', () => { + const stub = sinon.stub(fsExtra, 'readJsonSync').throws(new Error('Unable to read package.json')); + rokuDeploy['_packageVersion'] = undefined; + expect(rokuDeploy['getUserAgent']()).to.eql(`roku-deploy/unknown`); + expect(rokuDeploy['getUserAgent']()).to.eql(`roku-deploy/unknown`); + + expect(stub.callCount).to.equal(1); + rokuDeploy['_packageVersion'] = null; + }); + + it('currentVersion is valid', () => { + expect(currentVersion).to.exist.and.to.match(/^\d+\.\d+\.\d+.*/); + }); + + it('works when params is undefined', () => { + //undefined + expect( + rokuDeploy['setUserAgentIfMissing'](undefined) + ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + }); + + it('works when params has no header container', () => { + expect( + rokuDeploy['setUserAgentIfMissing']({} as any) + ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + }); + + it('works when params has empty header container', () => { + expect( + rokuDeploy['setUserAgentIfMissing']({} as any) + ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + }); + + it('works when params has existing header container with no user agent', () => { + expect( + rokuDeploy['setUserAgentIfMissing']({ headers: {} } as any) + ).to.eql({ headers: { 'User-Agent': `roku-deploy/${currentVersion}` } }); + }); + + it('works when params has existing header container with user agent', () => { + expect( + rokuDeploy['setUserAgentIfMissing']({ headers: { 'User-Agent': 'some-other-user-agent' } } as any) + ).to.eql({ headers: { 'User-Agent': 'some-other-user-agent' } }); + }); + + it('works when we fail to load package version', () => { + sinon.stub(fsExtra, 'readJsonSync').throws(new Error('Unable to read package.json')); + rokuDeploy['_packageVersion'] = undefined; + expect( + rokuDeploy['setUserAgentIfMissing']({} as any) + ).to.eql({ headers: { 'User-Agent': 'roku-deploy/unknown' } }); + }); + }); + describe('isUpdateCheckRequiredResponse', () => { it('matches on actual response from device', () => { const response = `\n\n \n \n Roku Development Kit \n\n \n\n\n
\n\n
\n\n \n \n\n\n
\n\n Please make sure that your Roku device is connected to internet, and running most recent software version (d=953108)\n\n
\n\n\n\n`; @@ -3787,7 +3853,7 @@ describe('index', () => { it('returns false on missing results', () => { expect( - rokuDeploy['isUpdateRequiredError']({ }) + rokuDeploy['isUpdateRequiredError']({}) ).to.be.false; }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 26ec742..ae3d0b1 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -684,12 +684,50 @@ export class RokuDeploy { return this.getToFile(requestOptions, pkgFilePath); } + /** + * Set the `User-Agent` header if missing from the request params, ensuring it's included in all requests made by roku-deploy + * @param params + * @returns + */ + private setUserAgentIfMissing(params: requestType.OptionsWithUrl) { + if (!params) { + params = {} as requestType.OptionsWithUrl; + } + if (!params.headers) { + params.headers = {}; + } + if (!params.headers['User-Agent']) { + params.headers['User-Agent'] = this.getUserAgent(); + } + return params; + } + + /** + * Get the user-agent string used for HTTP requests sent by this package + * @returns + */ + private getUserAgent() { + try { + if (this._packageVersion === undefined) { + this._packageVersion = _fsExtra.readJsonSync(`${__dirname}/../package.json`).version; + } + } catch (e) { + this._packageVersion = null; + } + return `roku-deploy/${this._packageVersion ?? 'unknown'}`; + } + + private _packageVersion: string; + /** * Centralized function for handling POST http requests * @param params */ - private async doPostRequest(params: any, verify = true) { + private async doPostRequest(params: requestType.OptionsWithUrl, verify = true) { let results: { response: any; body: any } = await new Promise((resolve, reject) => { + + this.setUserAgentIfMissing(params); + request.post(params, (err, resp, body) => { if (err) { return reject(err); @@ -709,6 +747,9 @@ export class RokuDeploy { */ private async doGetRequest(params: requestType.OptionsWithUrl) { let results: { response: any; body: any } = await new Promise((resolve, reject) => { + + this.setUserAgentIfMissing(params); + request.get(params, (err, resp, body) => { if (err) { return reject(err); @@ -1084,10 +1125,7 @@ export class RokuDeploy { let response = await this.doGetRequest({ url: url, - timeout: options.timeout, - headers: { - 'User-Agent': 'https://github.com/RokuCommunity/roku-deploy' - } + timeout: options.timeout }); try { const parsedContent = await xml2js.parseStringPromise(response.body, { From 77106a8251d5bf6551dc7c33ae73436e8a3f2012 Mon Sep 17 00:00:00 2001 From: Christian-Holbrook <118202694+Christian-Holbrook@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:03:13 -0600 Subject: [PATCH 02/25] chore: Support dispatch workflows (#198) --- .github/workflows/make-release-artifacts.yml | 17 +++++++++++++++-- .github/workflows/publish-release.yml | 19 +++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/make-release-artifacts.yml b/.github/workflows/make-release-artifacts.yml index ac6756f..48f9f5c 100644 --- a/.github/workflows/make-release-artifacts.yml +++ b/.github/workflows/make-release-artifacts.yml @@ -6,15 +6,27 @@ on: - reopened - opened - synchronize + workflow_dispatch: + inputs: + tag: + type: string + description: 'The release tag that should be referenced (i.e. `v1.2.3`)' + required: true + force: + type: boolean + description: 'Force the release artifacts to be created and uploaded' + required: false + default: false jobs: run: - if: startsWith( github.head_ref, 'release/') + if: github.event_name == 'workflow_dispatch' || startsWith( github.head_ref, 'release/') uses: rokucommunity/workflows/.github/workflows/make-release-artifacts.yml@master with: - branch: ${{ github.event.pull_request.head.ref }} + branch: ${{ github.event.inputs.tag || github.event.pull_request.head.ref }} node-version: "16.20.2" artifact-paths: "./*.tgz" # "*.vsix" + force: ${{ github.event.inputs.force == 'true' }} secrets: inherit success-or-skip: @@ -23,3 +35,4 @@ jobs: runs-on: ubuntu-latest steps: - run: if [ "${{ needs.run.result }}" = "failure" ]; then exit 1; fi + diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1d6887d..48f6fba 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -3,16 +3,23 @@ name: Publish Release on: pull_request: types: - - closed + - closed paths: - - 'package.json' - - 'package-lock.json' + - "package.json" + - "package-lock.json" + workflow_dispatch: + inputs: + tag: + type: string + description: "The release tag that should be published (i.e. `v1.2.3`)" + required: true jobs: run: - if: startsWith( github.head_ref, 'release/') && (github.event.pull_request.merged == true) + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release/') uses: rokucommunity/workflows/.github/workflows/publish-release.yml@master with: - release-type: "npm" # "vsce" - ref: ${{ github.event.pull_request.merge_commit_sha }} + release-type: "npm" # or "vsce" + ref: ${{ github.event.inputs.tag || github.event.pull_request.merge_commit_sha }} secrets: inherit + From b4bc173540f47f9cb829c7d64e42e09611d5a483 Mon Sep 17 00:00:00 2001 From: "rokucommunity-bot[bot]" <93661887+rokucommunity-bot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:22:36 +0000 Subject: [PATCH 03/25] 3.13.0 (#205) Co-authored-by: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7311d6b..a75d07b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.13.0](https://github.com/rokucommunity/roku-deploy/compare/3.12.6...v3.13.0) - 2025-08-04 +### Added + - Add standards-compliant User-Agent header ([#203](https://github.com/rokucommunity/roku-deploy/pull/203)) + + + ## [3.12.6](https://github.com/rokucommunity/roku-deploy/compare/3.12.5...v3.12.6) - 2025-06-03 ### Changed - chore: upgrade to the `undent` package instead of `dedent` ([#192](https://github.com/rokucommunity/roku-deploy/pull/196)) diff --git a/package-lock.json b/package-lock.json index bea2777..82d530a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.12.6", + "version": "3.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.12.6", + "version": "3.13.0", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 4e236c9..8ea2153 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.12.6", + "version": "3.13.0", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From cc51f75e1ae5f69fe4ecfe74a9e10c21d94a2650 Mon Sep 17 00:00:00 2001 From: Christian-Holbrook <118202694+Christian-Holbrook@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:32:52 -0600 Subject: [PATCH 04/25] Add 'rebootDevice' and 'checkForUpdate' functionality for supported OS versions (#208) --- package-lock.json | 69 ++++++++++++++---------------------------- package.json | 2 ++ src/RokuDeploy.spec.ts | 53 ++++++++++++++++++++++++++++++++ src/RokuDeploy.ts | 43 ++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82d530a..23d7889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "moment": "^2.29.1", "parse-ms": "^2.1.0", "postman-request": "^2.88.1-postman.40", + "semver": "^7.7.3", "temp-dir": "^2.0.0", "xml2js": "^0.5.0" }, @@ -38,6 +39,7 @@ "@types/mocha": "^9.0.0", "@types/node": "^16.11.3", "@types/q": "^1.5.8", + "@types/semver": "^7.7.1", "@types/sinon": "^10.0.4", "@types/xml2js": "^0.4.5", "@typescript-eslint/eslint-plugin": "5.1.0", @@ -910,6 +912,13 @@ "form-data": "^2.5.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sinon": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.4.tgz", @@ -3246,18 +3255,6 @@ "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -4423,13 +4420,10 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5144,12 +5138,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -5924,6 +5912,12 @@ "form-data": "^2.5.0" } }, + "@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, "@types/sinon": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.4.tgz", @@ -7646,15 +7640,6 @@ } } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -8517,13 +8502,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" }, "serialize-javascript": { "version": "6.0.0", @@ -9068,12 +9049,6 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 8ea2153..ec4752c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "moment": "^2.29.1", "parse-ms": "^2.1.0", "postman-request": "^2.88.1-postman.40", + "semver": "^7.7.3", "temp-dir": "^2.0.0", "xml2js": "^0.5.0" }, @@ -44,6 +45,7 @@ "@types/mocha": "^9.0.0", "@types/node": "^16.11.3", "@types/q": "^1.5.8", + "@types/semver": "^7.7.1", "@types/sinon": "^10.0.4", "@types/xml2js": "^0.4.5", "@typescript-eslint/eslint-plugin": "5.1.0", diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 064c502..6eeb08f 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -2130,6 +2130,59 @@ describe('index', () => { }); }); + describe('plugin_swup', () => { + function mockGetDeviceInfo(swVersion: string) { + sinon.stub(rokuDeploy as any, 'getDeviceInfo').callsFake((params) => { + return { 'software-version': swVersion }; + }); + } + it('should send a request to the plugin_swup endpoint for a reboot', async () => { + mockGetDeviceInfo('15.0.4'); + let stub = mockDoPostRequest(); + let result = await rokuDeploy.rebootDevice(options); + expect(result).not.to.be.undefined; + expect(stub.args[0][0].url).to.include(`/plugin_swup`); + expect(stub.args[0][0].formData.mysubmit).to.include('Reboot'); + }); + + it('should send a request to the plugin_swup endpoint to check for update', async () => { + mockGetDeviceInfo('15.0.4'); + let stub = mockDoPostRequest(); + let result = await rokuDeploy.checkForUpdate(options); + expect(result).not.to.be.undefined; + expect(stub.args[0][0].url).to.include(`/plugin_swup`); + expect(stub.args[0][0].formData.mysubmit).to.include('CheckUpdate'); + }); + + it('should fail to reboot when sw version is just below minimum (15.0.3)', async () => { + mockGetDeviceInfo('15.0.3'); + await assertThrowsAsync(async () => { + await rokuDeploy.rebootDevice(options); + }); + }); + + it('should fail to reboot when software-version is null', async () => { + mockGetDeviceInfo(null); + await assertThrowsAsync(async () => { + await rokuDeploy.rebootDevice(options); + }); + }); + + it('should fail to check for updates when sw version is just below minimum (15.0.3)', async () => { + mockGetDeviceInfo('15.0.3'); + await assertThrowsAsync(async () => { + await rokuDeploy.checkForUpdate(options); + }); + }); + + it('should fail to check for updates when software-version is null', async () => { + mockGetDeviceInfo(null); + await assertThrowsAsync(async () => { + await rokuDeploy.checkForUpdate(options); + }); + }); + }); + describe('deleteInstalledChannel', () => { it('attempts to delete any installed dev channel on the device', async () => { mockDoPostRequest(); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index ae3d0b1..68ea9fd 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -18,6 +18,7 @@ import * as tempDir from 'temp-dir'; import * as dayjs from 'dayjs'; import * as lodash from 'lodash'; import type { DeviceInfo, DeviceInfoRaw } from './DeviceInfo'; +import * as semver from 'semver'; export class RokuDeploy { @@ -1260,6 +1261,48 @@ export class RokuDeploy { const content = await zip.generateAsync({ type: 'nodebuffer', compressionOptions: { level: 2 } }); return this.fsExtra.outputFile(zipFilePath, content); } + + public async rebootDevice(options: RokuDeployOptions) { + options = this.getOptions(options); + + // Get device info to check software version + const deviceInfo = await this.getDeviceInfo(options as any); + const softwareVersion = deviceInfo['software-version']; + + // Check if device version is at least 15.0.4 + if (!softwareVersion || semver.lt(semver.coerce(softwareVersion), '15.0.4')) { + throw new Error(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for reboot operation`); + } + + return this.doPostRequest({ + ...this.generateBaseRequestOptions('plugin_swup', options), + formData: { + mysubmit: 'Reboot', + archive: '' + } + }); + } + + public async checkForUpdate(options: RokuDeployOptions) { + options = this.getOptions(options); + + // Get device info to check software version + const deviceInfo = await this.getDeviceInfo(options as any); + const softwareVersion = deviceInfo['software-version']; + + // Check if device version is at least 15.0.4 + if (!softwareVersion || semver.lt(semver.coerce(softwareVersion), '15.0.4')) { + throw new Error(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for check update operation`); + } + + return this.doPostRequest({ + ...this.generateBaseRequestOptions('plugin_swup', options), + formData: { + mysubmit: 'CheckUpdate', + archive: '' + } + }); + } } export interface ManifestData { From de2a92f640193d2982fc663f6a9900344563a7c3 Mon Sep 17 00:00:00 2001 From: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:43:00 +0000 Subject: [PATCH 05/25] Increment version to 3.14.0 --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a75d07b..d0657be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.14.0](https://github.com/rokucommunity/roku-deploy/compare/3.13.0...v3.14.0) - 2025-10-28 +### Added + - Add 'rebootDevice' and 'checkForUpdate' functionality for supported OS versions ([#208](https://github.com/rokucommunity/roku-deploy/pull/208)) + + + ## [3.13.0](https://github.com/rokucommunity/roku-deploy/compare/3.12.6...v3.13.0) - 2025-08-04 ### Added - Add standards-compliant User-Agent header ([#203](https://github.com/rokucommunity/roku-deploy/pull/203)) diff --git a/package-lock.json b/package-lock.json index 23d7889..c4d7ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.13.0", + "version": "3.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.13.0", + "version": "3.14.0", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index ec4752c..4df1ba1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.13.0", + "version": "3.14.0", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From 8fb5c9f8caaf96e12cd47cb3894e0a55504435fe Mon Sep 17 00:00:00 2001 From: Christian Holbrook Date: Tue, 28 Oct 2025 16:18:46 -0600 Subject: [PATCH 06/25] Add the OIDC permissions to the dispatching workflow --- .github/workflows/publish-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 48f6fba..ad2cc8d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -18,6 +18,9 @@ jobs: run: if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release/') uses: rokucommunity/workflows/.github/workflows/publish-release.yml@master + permissions: + id-token: write + contents: read with: release-type: "npm" # or "vsce" ref: ${{ github.event.inputs.tag || github.event.pull_request.merge_commit_sha }} From 0f798a8512384d522476fb6602af4ea7ef5fe8e0 Mon Sep 17 00:00:00 2001 From: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:51:28 +0000 Subject: [PATCH 07/25] Increment version to 3.14.1 --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0657be..9c98d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.14.1](https://github.com/rokucommunity/roku-deploy/compare/3.14.0...v3.14.1) - 2025-10-29 +### Added + - Add the OIDC permissions to the dispatching workflow ([#211](https://github.com/rokucommunity/roku-deploy/pull/211)) + + + ## [3.14.0](https://github.com/rokucommunity/roku-deploy/compare/3.13.0...v3.14.0) - 2025-10-28 ### Added - Add 'rebootDevice' and 'checkForUpdate' functionality for supported OS versions ([#208](https://github.com/rokucommunity/roku-deploy/pull/208)) diff --git a/package-lock.json b/package-lock.json index c4d7ed3..c69afaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.14.0", + "version": "3.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.14.0", + "version": "3.14.1", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 4df1ba1..df82a74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.14.0", + "version": "3.14.1", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From 342af88af7945fcbe43adbb965ecd921dd691e18 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 29 Oct 2025 12:55:07 -0400 Subject: [PATCH 08/25] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c98d99..2bf22e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.14.1](https://github.com/rokucommunity/roku-deploy/compare/3.14.0...v3.14.1) - 2025-10-29 -### Added - - Add the OIDC permissions to the dispatching workflow ([#211](https://github.com/rokucommunity/roku-deploy/pull/211)) +### Changed + - chore: add OIDC permissions to the dispatching workflow ([#211](https://github.com/rokucommunity/roku-deploy/pull/211)) @@ -496,3 +496,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0](https://github.com/RokuCommunity/roku-deploy/compare/v0.2.1...v1.0.0) - 2018-12-18 ### Added - support for negated globs + From 0dd4e6b3e5853eb48f347b7f87f258d297899f42 Mon Sep 17 00:00:00 2001 From: Christian Holbrook Date: Wed, 29 Oct 2025 14:17:33 -0600 Subject: [PATCH 09/25] Chore: Update publish-release permissions to content: write --- .github/workflows/publish-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ad2cc8d..d823eca 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -20,7 +20,7 @@ jobs: uses: rokucommunity/workflows/.github/workflows/publish-release.yml@master permissions: id-token: write - contents: read + contents: write with: release-type: "npm" # or "vsce" ref: ${{ github.event.inputs.tag || github.event.pull_request.merge_commit_sha }} From a33ea05b08f9469c2dd09b46f1ed5fb906addf02 Mon Sep 17 00:00:00 2001 From: Christian Holbrook Date: Wed, 29 Oct 2025 20:39:25 -0600 Subject: [PATCH 10/25] Add pull-request write permissions --- .github/workflows/publish-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d823eca..1d1ff42 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -21,6 +21,7 @@ jobs: permissions: id-token: write contents: write + pull-requests: write with: release-type: "npm" # or "vsce" ref: ${{ github.event.inputs.tag || github.event.pull_request.merge_commit_sha }} From e9c031de347303ac98183f9764a903e641ef23dc Mon Sep 17 00:00:00 2001 From: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:43:00 +0000 Subject: [PATCH 11/25] Increment version to 3.14.2 --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf22e9..92ee382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.14.2](https://github.com/rokucommunity/roku-deploy/compare/3.14.1...v3.14.2) - 2025-10-30 +### Added + - Add pull-request write permissions ([#214](https://github.com/rokucommunity/roku-deploy/pull/214)) + + + ## [3.14.1](https://github.com/rokucommunity/roku-deploy/compare/3.14.0...v3.14.1) - 2025-10-29 ### Changed - chore: add OIDC permissions to the dispatching workflow ([#211](https://github.com/rokucommunity/roku-deploy/pull/211)) diff --git a/package-lock.json b/package-lock.json index c69afaa..f71645d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.14.1", + "version": "3.14.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.14.1", + "version": "3.14.2", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index df82a74..36502a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.14.1", + "version": "3.14.2", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From 646c4dcaf3450acecef71fd4784be245ce3823b9 Mon Sep 17 00:00:00 2001 From: Christian-Holbrook <118202694+Christian-Holbrook@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:45:56 -0600 Subject: [PATCH 12/25] Update print statement from 'Hello' to 'Goodbye' --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ee382..9a4c473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.14.2](https://github.com/rokucommunity/roku-deploy/compare/3.14.1...v3.14.2) - 2025-10-30 -### Added - - Add pull-request write permissions ([#214](https://github.com/rokucommunity/roku-deploy/pull/214)) +### Changed + - chore: add pull-request write permissions ([#214](https://github.com/rokucommunity/roku-deploy/pull/214)) @@ -503,3 +503,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - support for negated globs + From 423bcaf3c884ed7626440fffbb9faca9e16b664f Mon Sep 17 00:00:00 2001 From: Christian Holbrook Date: Thu, 30 Oct 2025 12:35:19 -0600 Subject: [PATCH 13/25] Add new error that is thrown when the OS does not support the function --- src/Errors.ts | 7 +++++++ src/RokuDeploy.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Errors.ts b/src/Errors.ts index 1ca78cc..4218e5d 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -57,6 +57,13 @@ export class MissingRequiredOptionError extends Error { } } +export class UnsupportedFirmwareVersionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, UnsupportedFirmwareVersionError.prototype); + } +} + /** * This error is thrown when a Roku device refuses to accept connections because it requires the user to check for updates (even if no updates are actually available). */ diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 68ea9fd..54766bb 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1271,7 +1271,7 @@ export class RokuDeploy { // Check if device version is at least 15.0.4 if (!softwareVersion || semver.lt(semver.coerce(softwareVersion), '15.0.4')) { - throw new Error(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for reboot operation`); + throw new errors.UnsupportedFirmwareVersionError(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for reboot operation`); } return this.doPostRequest({ @@ -1292,7 +1292,7 @@ export class RokuDeploy { // Check if device version is at least 15.0.4 if (!softwareVersion || semver.lt(semver.coerce(softwareVersion), '15.0.4')) { - throw new Error(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for check update operation`); + throw new errors.UnsupportedFirmwareVersionError(`Device software version ${softwareVersion} is below the minimum required version 15.0.4 for check update operation`); } return this.doPostRequest({ From 20a6be88ecd50b481dea3edf82ae551668862574 Mon Sep 17 00:00:00 2001 From: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:41:35 +0000 Subject: [PATCH 14/25] Increment version to 3.14.3 --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a4c473..3ffa01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.14.3](https://github.com/rokucommunity/roku-deploy/compare/3.14.2...v3.14.3) - 2025-10-30 +### Added + - Add specific error classes for reboot and check for updates functions ([#216](https://github.com/rokucommunity/roku-deploy/pull/216)) + + + ## [3.14.2](https://github.com/rokucommunity/roku-deploy/compare/3.14.1...v3.14.2) - 2025-10-30 ### Changed - chore: add pull-request write permissions ([#214](https://github.com/rokucommunity/roku-deploy/pull/214)) diff --git a/package-lock.json b/package-lock.json index f71645d..fe03b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.14.2", + "version": "3.14.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.14.2", + "version": "3.14.3", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 36502a8..de3f692 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.14.2", + "version": "3.14.3", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From 9945ccbd607d8cf178e9e83d3fec24c10ed912aa Mon Sep 17 00:00:00 2001 From: Christian Holbrook Date: Thu, 30 Oct 2025 12:59:12 -0600 Subject: [PATCH 15/25] Remove .git suffix and lowercase rokucommunity --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de3f692..b0fea8f 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/RokuCommunity/roku-deploy.git" + "url": "https://github.com/rokucommunity/roku-deploy" }, "author": "RokuCommunity", "license": "MIT", From 01c7b5a0f2b8b2618958707bd50e4f3755a0f896 Mon Sep 17 00:00:00 2001 From: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:03:27 +0000 Subject: [PATCH 16/25] Increment version to 3.14.4 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffa01e..e8838f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.14.4](https://github.com/rokucommunity/roku-deploy/compare/3.14.3...v3.14.4) - 2025-10-30 + + + ## [3.14.3](https://github.com/rokucommunity/roku-deploy/compare/3.14.2...v3.14.3) - 2025-10-30 ### Added - Add specific error classes for reboot and check for updates functions ([#216](https://github.com/rokucommunity/roku-deploy/pull/216)) diff --git a/package-lock.json b/package-lock.json index fe03b24..ed42eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.14.3", + "version": "3.14.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.14.3", + "version": "3.14.4", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index b0fea8f..8c4c7c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.14.3", + "version": "3.14.4", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From b57b3f96d751ad1b7ea56184190397a896d5fcf1 Mon Sep 17 00:00:00 2001 From: Christian-Holbrook <118202694+Christian-Holbrook@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:07:50 -0600 Subject: [PATCH 17/25] Update print statement from 'Hello' to 'Goodbye' --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8838f5..f8d39c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.14.4](https://github.com/rokucommunity/roku-deploy/compare/3.14.3...v3.14.4) - 2025-10-30 +### Changed +chore: Update package.json repository to support provenance (#218) @@ -514,3 +516,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - support for negated globs + From 3e8d5f5b7dd93b3d2481ae195e9d602660625a58 Mon Sep 17 00:00:00 2001 From: RokuJyoti Date: Tue, 18 Nov 2025 02:32:22 +0530 Subject: [PATCH 18/25] Support installing and deleting component libraries (#220) Co-authored-by: Bronley Plumb --- package-lock.json | 15 +- src/RokuDeploy.spec.ts | 297 +++++++++++++++++++++++++++++++++++---- src/RokuDeploy.ts | 87 +++++++++++- src/RokuDeployOptions.ts | 6 + 4 files changed, 370 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed42eeb..e93ec6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1435,9 +1435,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001579", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", - "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true, "funding": [ { @@ -1452,7 +1452,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/caseless": { "version": "0.12.0", @@ -6276,9 +6277,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001579", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", - "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true }, "caseless": { diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 6eeb08f..e7931e7 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -21,7 +21,7 @@ const request = r as typeof requestType; const sinon = createSandbox(); -describe('index', () => { +describe('RokuDeploy', () => { let rokuDeploy: RokuDeploy; let options: RokuDeployOptions; @@ -61,15 +61,20 @@ describe('index', () => { }); afterEach(() => { - if (createWriteStreamStub.called && !writeStreamDeferred.isComplete) { - writeStreamDeferred.reject('Deferred was never resolved...so rejecting in the afterEach'); - } + try { + if (createWriteStreamStub.called && !writeStreamDeferred.isComplete) { + writeStreamDeferred.reject('Deferred was never resolved...so rejecting in the afterEach'); + } - sinon.restore(); - //restore the original working directory - process.chdir(cwd); - //delete all temp files - fsExtra.emptyDirSync(tempDir); + sinon.restore(); + //restore the original working directory + process.chdir(cwd); + //delete all temp files + fsExtra.emptyDirSync(tempDir); + } catch (e) { + //not sure why this test fails sometimes in github actions, but hopefully this will mitigate the issue. + console.error('Error in afterEach:', e); + } }); after(() => { @@ -721,7 +726,7 @@ describe('index', () => { options.files = files; await rokuDeploy.createPackage(options); const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath(options)); - const zip = await JSZip.loadAsync(data); + const zip = await JSZip.loadAsync(data as any); for (const file of files) { const zipFileContents = await zip.file(file.toString()).async('string'); @@ -745,7 +750,7 @@ describe('index', () => { }); const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath(options)); - const zip = await JSZip.loadAsync(data); + const zip = await JSZip.loadAsync(data as any); for (const file of filePaths) { const zipFileContents = await zip.file(file.toString())?.async('string'); @@ -1203,6 +1208,42 @@ describe('index', () => { }); }); + it('does not set appType if not explicitly defined', async () => { + delete options.appType; + const stub = mockDoPostRequest(); + + const result = await rokuDeploy.publish(options); + expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.app_type).to.be.undefined; + }); + + it('does not set appType if not appType is set to null or undefined', async () => { + options.appType = null; + const stub = mockDoPostRequest(); + + const result = await rokuDeploy.publish(options); + expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.app_type).to.be.undefined; + }); + + it('sets appType="channel" when defined', async () => { + options.appType = 'channel'; + const stub = mockDoPostRequest(); + + const result = await rokuDeploy.publish(options); + expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.app_type).to.eql('channel'); + }); + + it('sets appType="channel" when defined', async () => { + options.appType = 'dcl'; + const stub = mockDoPostRequest(); + + const result = await rokuDeploy.publish(options); + expect(result.message).to.equal('Successful deploy'); + expect(stub.getCall(0).args[0].formData.app_type).to.eql('dcl'); + }); + it('Does not reject when response contains compile error wording but config is set to ignore compile warnings', () => { options.failOnCompileError = false; @@ -1447,12 +1488,10 @@ describe('index', () => { `; mockDoPostRequest(body); options.rekeySignedPackage = s`../notReal.pkg`; - try { - fsExtra.writeFileSync(s`${tempDir}/notReal.pkg`, ''); - await rokuDeploy.rekeyDevice(options); - } finally { - fsExtra.removeSync(s`${tempDir}/notReal.pkg`); - } + fsExtra.outputFileSync(s`${tempDir}/notReal.pkg`, ''); + //small sleep to ensure the file exists (hack for testing!) + await util.sleep(10); + await rokuDeploy.rekeyDevice(options); }); it('should work with absolute path', async () => { @@ -1912,11 +1951,11 @@ describe('index', () => { symlinkIt('copies files from subdirs of symlinked folders', async () => { fsExtra.ensureDirSync(s`${tempDir}/baseProject/source/lib/promise`); - fsExtra.writeFileSync(s`${tempDir}/baseProject/source/lib/lib.brs`, `'lib.brs`); - fsExtra.writeFileSync(s`${tempDir}/baseProject/source/lib/promise/promise.brs`, `'q.brs`); + fsExtra.outputFileSync(s`${tempDir}/baseProject/source/lib/lib.brs`, `'lib.brs`); + fsExtra.outputFileSync(s`${tempDir}/baseProject/source/lib/promise/promise.brs`, `'q.brs`); fsExtra.ensureDirSync(s`${tempDir}/mainProject/source`); - fsExtra.writeFileSync(s`${tempDir}/mainProject/source/main.brs`, `'main.brs`); + fsExtra.outputFileSync(s`${tempDir}/mainProject/source/main.brs`, `'main.brs`); //symlink the baseProject lib folder into the mainProject fsExtra.symlinkSync(s`${tempDir}/baseProject/source/lib`, s`${tempDir}/mainProject/source/lib`); @@ -2482,7 +2521,7 @@ describe('index', () => { }); const data = fsExtra.readFileSync(outputZipPath); - const zip = await JSZip.loadAsync(data); + const zip = await JSZip.loadAsync(data as any); for (const file of files) { const zipFileContents = await zip.file(file.toString()).async('string'); const sourcePath = path.join(options.rootDir, file); @@ -2510,7 +2549,7 @@ describe('index', () => { await rokuDeploy.zipFolder(stagingDir, outputZipPath, null, ['**/*', '!**/*.map']); const data = fsExtra.readFileSync(outputZipPath); - const zip = await JSZip.loadAsync(data); + const zip = await JSZip.loadAsync(data as any); //the .map files should be missing expect( Object.keys(zip.files).sort() @@ -3166,10 +3205,10 @@ describe('index', () => { fsExtra.ensureDirSync(s`${thisRootDir}/components`); fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); - fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); + fsExtra.outputFileSync(s`${thisRootDir}/source/main.brs`, ''); + fsExtra.outputFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); + fsExtra.outputFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); + fsExtra.outputFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); let files = [ '**/*.xml', @@ -3927,9 +3966,215 @@ describe('index', () => { rokuDeploy['isUpdateRequiredError']({ results: { response: { statusCode: 500 }, body: false } }) ).to.be.false; }); + }); + + describe('getInstalledPackages', () => { + it('sends the dcl_enabled qs flag', async () => { + const stub = mockDoGetRequest(); + sinon.stub(rokuDeploy as any, 'getPackagesFromResponseBody').returns([]); + const result = await rokuDeploy['getInstalledPackages']({} as any); + expect(stub.getCall(0).args[0].qs.dcl_enabled).to.eql('1'); + expect(result).to.eql([]); + }); + + it('augments if qs is already defined', async () => { + sinon.stub(rokuDeploy as any, 'generateBaseRequestOptions').returns({ + qs: { + existing: 'value' + } + } as any); + const stub = mockDoGetRequest(); + sinon.stub(rokuDeploy as any, 'getPackagesFromResponseBody').returns([]); + const result = await rokuDeploy['getInstalledPackages']({} as any); + expect(stub.getCall(0).args[0].qs).to.eql({ + existing: 'value', + dcl_enabled: '1' + }); + expect(result).to.eql([]); + }); + + it('properly parses the response', async () => { + const stub = mockDoGetRequest(` + var params = JSON.parse('{"messages":null,"metadata":{"dev_id":"12345","dev_key":true,"voice_sdk":false},"packages":[{"appType":"channel","archiveFileName":"roku-deploy.zip","fileType":"zip","id":"0","location":"nvram","md5":"a8d2f9974e2736174c1033b8a7183288","pkgPath":"","size":"2267547"}]}'); + `); + const result = await rokuDeploy['getInstalledPackages']({} as any); + expect(stub.getCall(0).args[0].qs.dcl_enabled).to.eql('1'); + expect(result).to.eql([{ + appType: 'channel', + archiveFileName: 'roku-deploy.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: 'a8d2f9974e2736174c1033b8a7183288', + pkgPath: '', + size: '2267547' + }]); + }); + it('handles when packages is not an array', async () => { + mockDoGetRequest(` + var params = JSON.parse('{"messages":null,"metadata":{"dev_id":"12345","dev_key":true,"voice_sdk":false},"packages": 123}'); + `); + const result = await rokuDeploy['getInstalledPackages']({} as any); + expect(result).to.eql([]); + }); + + it('handles when the item is not an object', async () => { + mockDoGetRequest(` + var params = JSON.parse('123'); + `); + const result = await rokuDeploy['getInstalledPackages']({} as any); + expect(result).to.eql([]); + }); }); + describe('deleteComponentLibrary', () => { + it('does not crash if qs is undefined', async () => { + const stub = mockDoPostRequest(); + + sinon.stub(rokuDeploy as any, 'generateBaseRequestOptions').returns({} as any); + await rokuDeploy.deleteComponentLibrary({} as any); + + expect(stub.getCall(0).args[0].qs.dcl_enabled).to.eql('1'); + }); + + it('augments if qs is already defined', async () => { + sinon.stub(rokuDeploy as any, 'generateBaseRequestOptions').returns({ + qs: { + existing: 'value' + } + } as any); + const stub = mockDoPostRequest(); + + await rokuDeploy.deleteComponentLibrary({} as any); + + expect(stub.getCall(0).args[0].qs).to.eql({ + existing: 'value', + dcl_enabled: '1' + }); + }); + + it('deletes the component library', async () => { + options.failOnCompileError = true; + options.remoteDebug = true; + options.remoteDebugConnectEarly = true; + const stub = mockDoPostRequest(); + + await rokuDeploy.deleteComponentLibrary({ + host: '0.0.0.0', + password: 'aaaa', + fileName: 'fakeFile.zip' + }); + + //ensure we're sending the correct form inputs + expect(stub.getCall(0).args[0].formData).to.eql({ + mysubmit: 'Delete', + app_type: 'dcl', + fileName: 'fakeFile.zip' + }); + //also set the query string parameter that enables DCL behaviors (this seems to be important as well for some reason...) + expect(stub.getCall(0).args[0].qs.dcl_enabled).to.eql('1'); + }); + }); + + describe('deleteAllComponentLibraries', () => { + it('sends no requests if there are no DCLs to delete', async () => { + //return 0 packages + sinon.stub(rokuDeploy as any, 'getInstalledPackages').returns(Promise.resolve([])); + const stub = sinon.stub(rokuDeploy, 'deleteComponentLibrary').returns(Promise.resolve()); + await rokuDeploy.deleteAllComponentLibraries({} as any); + expect(stub.called).to.be.false; + }); + + it('sends no requests if there are no DCLs to delete', async () => { + //return 1 channel package + sinon.stub(rokuDeploy as any, 'getInstalledPackages').returns(Promise.resolve([{ + appType: 'channel', + archiveFileName: 'roku-deploy.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: 'a8d2f9974e2736174c1033b8a7183288', + pkgPath: '', + size: '2267547' + }])); + const stub = sinon.stub(rokuDeploy, 'deleteComponentLibrary').returns(Promise.resolve()); + await rokuDeploy.deleteAllComponentLibraries({} as any); + expect(stub.called).to.be.false; + }); + + it('sends single request if only have one DCL to delete', async () => { + //return 1 channel package + sinon.stub(rokuDeploy as any, 'getInstalledPackages').returns(Promise.resolve([{ + appType: 'channel', + archiveFileName: 'roku-deploy.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: 'a8d2f9974e2736174c1033b8a7183288', + pkgPath: '', + size: '2267547' + }, { + appType: 'dcl', + archiveFileName: 'lib.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: '7221a9bfb63be42f4fc6b0de22584af6', + pkgPath: '', + size: '1231' + }])); + const stub = sinon.stub(rokuDeploy, 'deleteComponentLibrary').returns(Promise.resolve()); + await rokuDeploy.deleteAllComponentLibraries({} as any); + expect(stub.getCall(0).args[0]).to.eql({ + fileName: 'lib.zip' + }); + }); + + it('sends one request for each DCL', async () => { + //return 1 channel package + sinon.stub(rokuDeploy as any, 'getInstalledPackages').returns(Promise.resolve([{ + appType: 'dcl', + archiveFileName: 'lib1.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: '7221a9bfb63be42f4fc6b0de22584af6', + pkgPath: '', + size: '1231' + }, { + appType: 'channel', + archiveFileName: 'roku-deploy.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: 'a8d2f9974e2736174c1033b8a7183288', + pkgPath: '', + size: '226754' + }, { + appType: 'dcl', + archiveFileName: 'lib2.zip', + fileType: 'zip', + id: '0', + location: 'nvram', + md5: '7221a9bfb63be42f4fc6b0de22584af6', + pkgPath: '', + size: '1231' + }])); + const stub = sinon.stub(rokuDeploy, 'deleteComponentLibrary').returns(Promise.resolve()); + await rokuDeploy.deleteAllComponentLibraries({} as any); + expect(stub.getCalls().map(x => x.args)).to.eql([ + [{ + fileName: 'lib1.zip' + }], + [{ + fileName: 'lib2.zip' + }] + ]); + }); + }); + + function mockDoGetRequest(body = '', statusCode = 200) { return sinon.stub(rokuDeploy as any, 'doGetRequest').callsFake((params) => { let results = { response: { statusCode: statusCode }, body: body }; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 54766bb..f6033ef 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -436,7 +436,8 @@ export class RokuDeploy { let requestOptions = this.generateBaseRequestOptions(route, options, { mysubmit: 'Replace', - archive: readStream + archive: readStream, + ...(options.appType ? { 'app_type': options.appType } : {}) }); //attach the remotedebug flag if configured @@ -868,6 +869,30 @@ export class RokuDeploy { return result; } + /** + * Parse out the list of packages that are currently installed on the device by looking for the JSON in the response body + * @param body + * @returns + */ + private getPackagesFromResponseBody(body: string): RokuPackage[] { + let jsonParseRegex = /JSON\.parse\(('.+')\);/igm; + let jsonMatch: RegExpExecArray; + + while ((jsonMatch = jsonParseRegex.exec(body))) { + let [, jsonString] = jsonMatch; + let jsonObject = parseJsonc(jsonString); + if (typeof jsonObject === 'object' && !Array.isArray(jsonObject) && jsonObject !== null) { + let packages = jsonObject.packages; + + if (!Array.isArray(packages)) { + continue; + } + return packages; + } + } + return []; + } + /** * Create a zip of the project, and then publish to the target Roku device * @param options @@ -901,6 +926,53 @@ export class RokuDeploy { return this.doPostRequest(deleteOptions); } + /** + * Delete the component library with the specified filename from the device + */ + public async deleteComponentLibrary(options?: { host: string; password: string; fileName: string; username?: string }) { + options = this.getOptions(options) as any; + + let deleteOptions = this.generateBaseRequestOptions('plugin_install', options); + deleteOptions.formData = { + mysubmit: 'Delete', + 'app_type': 'dcl', + fileName: options.fileName + }; + deleteOptions.qs ??= {}; + // eslint-disable-next-line camelcase + deleteOptions.qs.dcl_enabled = '1'; + await this.doPostRequest(deleteOptions); + } + + /** + * Delete all component libraries from the device + */ + public async deleteAllComponentLibraries(options: { host: string; password: string; username?: string }) { + const packages = await this.getInstalledPackages(options); + for (const pkg of packages) { + if (pkg.appType === 'dcl') { + await this.deleteComponentLibrary({ + ...options, + fileName: pkg.archiveFileName + }); + } + } + } + + /** + * Fetch the full list of installed packages from the device. Useful for finding the file names of installed component libraries or the dev channel. + */ + private async getInstalledPackages(options: { host: string; password: string; username?: string }): Promise { + options = this.getOptions(options) as any; + let deleteOptions = this.generateBaseRequestOptions('plugin_install', options); + deleteOptions.qs ??= {}; + // eslint-disable-next-line camelcase + deleteOptions.qs.dcl_enabled = '1'; + const result = await this.doGetRequest(deleteOptions); + const packages = this.getPackagesFromResponseBody(result.body); + return packages; + } + /** * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ @@ -1250,7 +1322,7 @@ export class RokuDeploy { if (ext === '.jpg' || ext === '.png' || ext === '.jpeg') { compression = 'STORE'; } - zip.file(file.dest.replace(/[\\/]/g, '/'), data, { + zip.file(file.dest.replace(/[\\/]/g, '/'), data as any, { compression: compression }); }); @@ -1343,6 +1415,17 @@ export interface RokuMessages { successes: string[]; } +export interface RokuPackage { + appType: 'channel' | 'dcl'; + archiveFileName: string; + fileType: string; + id: number; + location: string; + md5: string; + pkgPath: string; + size: string; +} + enum RokuMessageType { success = 'success', info = 'info', diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index 8b44f99..a6fc07f 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -24,6 +24,12 @@ export interface RokuDeployOptions { */ rootDir?: string; + /** + * What type of app is being sideloaded? `'channel'` is for applications, and `'dcl'` is for installing component libraries on device for use in channels. + * @default 'channel' + */ + appType?: 'channel' | 'dcl'; + /** * An array of source file paths, source file globs, or {src,dest} objects indicating * where the source files are and where they should be placed From def4fc65cb454db149b5a6d011f471eeef9e065b Mon Sep 17 00:00:00 2001 From: "rokucommunity-bot[bot]" <93661887+rokucommunity-bot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:07:03 +0000 Subject: [PATCH 19/25] 3.15.0 (#221) Co-authored-by: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> Co-authored-by: Bronley Plumb --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d39c5..ffc1bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.15.0](https://github.com/rokucommunity/roku-deploy/compare/3.14.4...v3.15.0) - 2025-11-17 +### Added + - Support installing and deleting component libraries ([#220](https://github.com/rokucommunity/roku-deploy/pull/220)) + + + ## [3.14.4](https://github.com/rokucommunity/roku-deploy/compare/3.14.3...v3.14.4) - 2025-10-30 ### Changed chore: Update package.json repository to support provenance (#218) @@ -517,3 +523,4 @@ chore: Update package.json repository to support provenance (#218) + diff --git a/package-lock.json b/package-lock.json index e93ec6b..51d1a7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.14.4", + "version": "3.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.14.4", + "version": "3.15.0", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 8c4c7c7..957fc8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.14.4", + "version": "3.15.0", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From afd389f2bbf4705bd33c6d3e9110c7a025b4372b Mon Sep 17 00:00:00 2001 From: Christian-Holbrook <118202694+Christian-Holbrook@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:10:10 -0700 Subject: [PATCH 20/25] Add support for detecting ecpNetworkAccessMode (#223) Co-authored-by: Bronley Plumb --- .eslintrc.js | 3 +- package-lock.json | 12 ++-- src/Errors.ts | 7 +++ src/RokuDeploy.spec.ts | 124 +++++++++++++++++++++++++++++++++++++++++ src/RokuDeploy.ts | 42 ++++++++++++-- 5 files changed, 177 insertions(+), 11 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 98fed75..bd06f0f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -173,7 +173,8 @@ module.exports = { 'camelcase': 'off', 'dot-notation': 'off', 'new-cap': 'off', - 'no-shadow': 'off' + 'no-shadow': 'off', + 'prefer-promise-reject-errors': 'off' } } ], diff --git a/package-lock.json b/package-lock.json index 51d1a7c..9db35b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1435,9 +1435,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -6277,9 +6277,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true }, "caseless": { diff --git a/src/Errors.ts b/src/Errors.ts index 4218e5d..2d2d835 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -15,6 +15,13 @@ export class UnauthorizedDeviceResponseError extends Error { } } +export class EcpNetworkAccessModeDisabledError extends Error { + constructor(message: string, public results?: any) { + super(message); + Object.setPrototypeOf(this, EcpNetworkAccessModeDisabledError.prototype); + } +} + export class UnparsableDeviceResponseError extends Error { constructor(message: string, public results?: any) { super(message); diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index e7931e7..6a755fd 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -563,6 +563,130 @@ describe('RokuDeploy', () => { } assert.fail('Exception should have been thrown'); }); + + it('handles all error scenarios in catch block', async () => { + const doGetRequestStub = sinon.stub(rokuDeploy as any, 'doGetRequest'); + + doGetRequestStub.rejects({ results: { response: { headers: { server: 'Roku' } } } }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect(e).to.be.instanceof(errors.EcpNetworkAccessModeDisabledError); + } + + doGetRequestStub.rejects({ results: { response: { headers: { server: 'Apache' } } } }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results.response.headers.server).to.equal('Apache'); + } + + doGetRequestStub.rejects({ results: { response: { headers: {} } } }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results.response.headers.server).to.be.undefined; + } + + doGetRequestStub.rejects({ results: { response: { headers: { server: null } } } }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results.response.headers.server).to.be.null; + } + + doGetRequestStub.rejects({ results: { response: {} } }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results.response.headers).to.be.undefined; + } + + doGetRequestStub.rejects({ results: {} }); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results.response).to.be.undefined; + } + + doGetRequestStub.rejects({}); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect((e as any).results).to.be.undefined; + } + + const err = new Error('Network error'); + doGetRequestStub.rejects(err); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect(e).to.equal(err); + } + + // eslint-disable-next-line prefer-promise-reject-errors + doGetRequestStub.callsFake(() => Promise.reject(null)); + try { + await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect(e).to.be.null; + } + }); + }); + + describe('getEcpNetworkAccessMode', () => { + it('returns ecpSettingMode from device info', async () => { + sinon.stub(rokuDeploy, 'getDeviceInfo').resolves({ ecpSettingMode: 'enabled' }); + const result = await rokuDeploy.getEcpNetworkAccessMode({ host: '1.1.1.1' }); + expect(result).to.equal('enabled'); + }); + + it(`returns 'disabled' when response header had Roku in it`, async () => { + const getDeviceInfoStub = sinon.stub(rokuDeploy, 'getDeviceInfo'); + getDeviceInfoStub.rejects({ results: { response: { headers: { server: 'Roku' } } } }); + expect(await rokuDeploy.getEcpNetworkAccessMode({ host: '1.1.1.1' })).to.equal('disabled'); + }); + + it('handles all error scenarios in catch block', async () => { + const getDeviceInfoStub = sinon.stub(rokuDeploy, 'getDeviceInfo'); + async function doTest(rejectionValue: any) { + getDeviceInfoStub.rejects(rejectionValue); + try { + await rokuDeploy.getEcpNetworkAccessMode({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect(e).to.be.instanceof(errors.UnknownDeviceResponseError); + } + } + + await doTest({ results: { response: { headers: { server: 'Apache' } } } }); + await doTest({ results: { response: { headers: {} } } }); + await doTest({ results: { response: { headers: { server: null } } } }); + await doTest({ results: { response: {} } }); + await doTest({ results: {} }); + await doTest({}); + await doTest(new Error('Network error')); + }); + + it('handles null error from rejected promise', async () => { + const getDeviceInfoStub = sinon.stub(rokuDeploy, 'getDeviceInfo'); + getDeviceInfoStub.callsFake(() => Promise.reject(null)); + try { + await rokuDeploy.getEcpNetworkAccessMode({ host: '1.1.1.1' }); + assert.fail('Exception should have been thrown'); + } catch (e) { + expect(e).to.be.instanceof(errors.UnknownDeviceResponseError); + } + }); }); describe('normalizeDeviceInfoFieldValue', () => { diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index f6033ef..cdc7106 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1196,10 +1196,18 @@ export class RokuDeploy { const url = `http://${options.host}:${options.remotePort}/query/device-info`; - let response = await this.doGetRequest({ - url: url, - timeout: options.timeout - }); + let response; + try { + response = await this.doGetRequest({ + url: url, + timeout: options.timeout + }); + } catch (e) { + if ((e as any)?.results?.response?.headers?.server?.includes('Roku')) { + throw new errors.EcpNetworkAccessModeDisabledError(`Unable to access device-info because ecp-setting-mode is 'disabled'`, response); + } + throw e; + } try { const parsedContent = await xml2js.parseStringPromise(response.body, { explicitArray: false @@ -1223,6 +1231,30 @@ export class RokuDeploy { } } + /** + * Get the External Control Protocol (ECP) setting mode of the device. This determines whether + * the device accepts remote control commands via the ECP API. + * + * @param options - Configuration options including host, remotePort, timeout, etc. + * @returns The ECP setting mode: + * - 'enabled': fully enabled and accepting commands + * - 'disabled': ECP is disabled (device may still be reachable but ECP commands won't work) + * - 'limited': Restricted functionality, text and movement commands only + * - 'permissive': Full access for internal networks + */ + public async getEcpNetworkAccessMode(options: GetDeviceInfoOptions): Promise { + try { + const deviceInfo = await this.getDeviceInfo(options); + return deviceInfo.ecpSettingMode; + } catch (e) { + if ((e as any)?.results?.response?.headers?.server?.includes('Roku')) { + return 'disabled'; + } + throw new errors.UnknownDeviceResponseError('Could not retrieve device ECP setting'); + } + + } + /** * Normalize a deviceInfo field value. This includes things like converting boolean strings to booleans, number strings to numbers, * decoding HtmlEntities, etc. @@ -1488,3 +1520,5 @@ export interface GetDeviceInfoOptions { */ enhance?: boolean; } + +export type EcpNetworkAccessMode = 'enabled' | 'disabled' | 'limited' | 'permissive'; From 4a326840422ac161c6af4abdec35d9bcd9592de0 Mon Sep 17 00:00:00 2001 From: "rokucommunity-bot[bot]" <93661887+rokucommunity-bot[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:19:53 +0000 Subject: [PATCH 21/25] 3.16.0 (#224) Co-authored-by: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc1bdc..4a8417a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.16.0](https://github.com/rokucommunity/roku-deploy/compare/3.15.0...v3.16.0) - 2025-12-05 +### Added + - Add support for detecting ecpNetworkAccessMode ([#223](https://github.com/rokucommunity/roku-deploy/pull/223)) + + + ## [3.15.0](https://github.com/rokucommunity/roku-deploy/compare/3.14.4...v3.15.0) - 2025-11-17 ### Added - Support installing and deleting component libraries ([#220](https://github.com/rokucommunity/roku-deploy/pull/220)) diff --git a/package-lock.json b/package-lock.json index 9db35b9..cc10cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.15.0", + "version": "3.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.15.0", + "version": "3.16.0", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 957fc8c..4feb9d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.15.0", + "version": "3.16.0", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From 6b9217f2d1ba9d6745e38398ebb055e9e332481b Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 5 Dec 2025 14:06:43 -0500 Subject: [PATCH 22/25] Add ecpSettingMode to device-info interface (#225) --- src/DeviceInfo.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/DeviceInfo.ts b/src/DeviceInfo.ts index cc18f8e..c78cf8d 100644 --- a/src/DeviceInfo.ts +++ b/src/DeviceInfo.ts @@ -1,5 +1,7 @@ //there are 2 copies of this interface in here. If you add a new field, be sure to add it to both +import type { EcpNetworkAccessMode } from './RokuDeploy'; + export interface DeviceInfo { udn?: string; serialNumber?: string; @@ -31,6 +33,7 @@ export interface DeviceInfo { softwareVersion?: string; softwareBuild?: number; secureDevice?: boolean; + ecpSettingMode?: EcpNetworkAccessMode; language?: string; country?: string; locale?: string; @@ -104,6 +107,7 @@ export interface DeviceInfoRaw { 'software-version'?: string; 'software-build'?: string; 'secure-device'?: string; + 'ecp-setting-mode'?: EcpNetworkAccessMode; 'language'?: string; 'country'?: string; 'locale'?: string; From 871afe9337545f2c0c7496ebc488a4f096824160 Mon Sep 17 00:00:00 2001 From: "rokucommunity-bot[bot]" <93661887+rokucommunity-bot[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:09:10 +0000 Subject: [PATCH 23/25] 3.16.1 (#226) Co-authored-by: rokucommunity-bot <93661887+rokucommunity-bot@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8417a..f5b1fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [3.16.1](https://github.com/rokucommunity/roku-deploy/compare/3.16.0...v3.16.1) - 2025-12-05 +### Added + - Add ecpSettingMode to device-info interface ([#225](https://github.com/rokucommunity/roku-deploy/pull/225)) + + + ## [3.16.0](https://github.com/rokucommunity/roku-deploy/compare/3.15.0...v3.16.0) - 2025-12-05 ### Added - Add support for detecting ecpNetworkAccessMode ([#223](https://github.com/rokucommunity/roku-deploy/pull/223)) diff --git a/package-lock.json b/package-lock.json index cc10cbf..e52785e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roku-deploy", - "version": "3.16.0", + "version": "3.16.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roku-deploy", - "version": "3.16.0", + "version": "3.16.1", "license": "MIT", "dependencies": { "@types/request": "^2.47.0", diff --git a/package.json b/package.json index 4feb9d0..cb8ffe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roku-deploy", - "version": "3.16.0", + "version": "3.16.1", "description": "Package and publish a Roku application using Node.js", "main": "dist/index.js", "scripts": { From e6e5068afcf18ce98702b0151fdfddda9dc930e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:23:32 -0500 Subject: [PATCH 24/25] Bump lodash from 4.17.21 to 4.17.23 (#227) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e52785e..11f92e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3139,9 +3139,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -7552,9 +7552,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash.flattendeep": { "version": "4.4.0", From dd379e7e99cc7e72ef8cc2130b187e7f054f565c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:22:46 +0000 Subject: [PATCH 25/25] Initial plan