Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0



## [4.0.0-alpha.3] - unreleased
### Changed (Breaking)
- Renamed `RekeyDeviceOptions.rekeySignedPackage` → `pkg` to match CLI `--pkg` option
- Renamed `ZipOptions.stagingDir` → `dir` to match CLI `--dir` option
- Renamed `remotePort` → `ecpPort` throughout all options interfaces and `getOptions()` defaults
- CLI `sideload` command: replaced `--noclose` flag with `--close` boolean (use `--no-close` to skip closing the channel)
### Added
- `sideload()` now accepts `zip` (explicit zip path) and `rootDir` (auto-zip a directory) options directly
- `sideload()` now automatically calls `closeChannel()` before sideloading (controlled by new `close` option, defaults to `true`)
- CLI `sideload --no-close` flag to skip closing the channel before sideloading



## [4.0.0-alpha.2](https://github.com/rokucommunity/roku-deploy/compare/4.0.0-alpha.1...v4.0.0-alpha.2) - 2025-06-02
### Added
- Add interactive remote mode ([#169](https://github.com/rokucommunity/roku-deploy/pull/169))
Expand Down
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,16 @@ Lastly, the default files array has changed. node modules and static analysis fi
## CLI Usage

### Sideload a project to your Roku device
Sideload a .zip package or directory to a roku device:
Sideload a .zip package or directory to a roku device. By default, the channel is closed before sideloading. Use `--no-close` to skip.
```shell
# Sideload a zip file
npx roku-deploy sideload --host 'ip.of.roku' --password 'password' --zip './path/to/your/app.zip'

# Sideload from a directory (will be zipped first automatically)
npx roku-deploy sideload --host 'ip.of.roku' --password 'password' --rootDir './path/to/your/project'

# Sideload without closing the channel first
npx roku-deploy sideload --host 'ip.of.roku' --password 'password' --zip './path/to/your/app.zip' --no-close
```

### Create a signed package from an existing dev channel
Expand Down Expand Up @@ -212,7 +215,7 @@ Use this logic if you'd like to create a zip from your application folder.
//create a signed package of your project
rokuDeploy.zip({
outDir: 'folder/to/put/zip',
stagingDir: 'path/to/files/to/zip',
dir: 'path/to/files/to/zip',
outFile: 'filename-of-your-app.zip'
//...other options if necessary
Comment on lines 215 to 220
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeScript example is not valid syntax: missing commas between object properties (e.g. after outFile: 'filename-of-your-app.zip'). Also the comment says "create a signed package" but the example calls rokuDeploy.zip(). Fix the example so it compiles and matches the API being demonstrated.

Copilot uses AI. Check for mistakes.
}).then(function(){
Expand All @@ -232,14 +235,26 @@ rokuDeploy.keyPress({
```

### Sideloading a project
If you've already created a zip using some other tool, you can use roku-deploy to sideload the zip.
Sideload a zip file, a directory, or a pre-built zip at the default `outDir`/`outFile` location. The current dev channel is closed before sideloading by default; pass `close: false` to skip.
```typescript
//sideload a package onto a specified Roku
// Sideload a zip file
rokuDeploy.sideload({
host: 'ip-of-roku',
password: 'password for roku dev admin portal',
outDir: 'folder/where/your/zip/resides/',
outFile: 'filename-of-your-app.zip'
zip: './path/to/your/app.zip'
//...other options if necessary
}).then(function(){
//the app has been sideloaded
}, function(error) {
//it failed
console.error(error);
});

// Sideload from a source directory (will be zipped automatically)
rokuDeploy.sideload({
host: 'ip-of-roku',
password: 'password for roku dev admin portal',
rootDir: './path/to/your/project'
//...other options if necessary
}).then(function(){
Comment on lines +240 to 259
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These TypeScript examples are missing commas between properties (e.g. after zip: './path/to/your/app.zip' / rootDir: './path/to/your/project'), making them invalid code. Add the missing commas so the examples compile.

Copilot uses AI. Check for mistakes.
//the app has been sideloaded
Expand All @@ -263,8 +278,7 @@ rokuDeploy.convertToSquashfs({
rokuDeploy.createSignedPackage({
host: '1.2.3.4',
password: 'password',
signingPassword: 'signing password',
stagingDir: './path/to/staging/directory'
signingPassword: 'signing password'
//...other options if necessary
})
Comment on lines 278 to 283
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeScript example is missing commas between properties (e.g. after signingPassword: 'signing password'), so it won’t compile as-written. Add the missing commas to keep the README examples copy/pasteable.

Copilot uses AI. Check for mistakes.
```
Expand Down Expand Up @@ -294,7 +308,7 @@ rokuDeploy.captureScreenshot({
rokuDeploy.rekeyDevice({
host: 'ip-of-roku',
password: 'password',
rekeySignedPackage: './path/to/signed.pkg'
pkg: './path/to/signed.pkg'
//...other options if necessary
Comment on lines 308 to 312
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeScript example is missing a comma after pkg: './path/to/signed.pkg', making the snippet invalid. Add the trailing comma so the example compiles.

Copilot uses AI. Check for mistakes.
})
```
Expand Down Expand Up @@ -482,7 +496,7 @@ Here are the available options for customizing to your developer-specific workfl
- **signingPassword:** string (*required for signing*)
The password used for creating signed packages.

- **rekeySignedPackage:** string (*required for rekeying*)
- **pkg:** string (*required for rekeying*)
Path to a copy of the signed package you want to use for rekeying.

- **devId:** string
Expand Down Expand Up @@ -555,10 +569,10 @@ Here are the available options for customizing to your developer-specific workfl
just in case roku adds support for custom usernames in the future.

- **packagePort?:** number = `80`
The port used for package-related requests. This is mainly used for things like emulators, or when your roku is behind a firewall with a port-forward.
The port used for package-related requests. This is mainly used when your roku is behind a firewall with a port-forward.

- **remotePort?:** number = `8060`
The port used for sending remote control commands (like home press or back press). This is mainly used for things like emulators, or when your roku is behind a firewall with a port-forward.
- **ecpPort?:** number = `8060`
The port used for sending ECP/remote control commands (like key presses). This is mainly used when your roku is behind a firewall with a port-forward.

- **screenshotDir?:** string = `"./tmp/roku-deploy/screenshots/"`
The directory where screenshots should be saved. Will use the OS temp directory by default.
Expand Down
48 changes: 24 additions & 24 deletions src/RokuDeploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('RokuDeploy', () => {
stagingDir: stagingDir,
signingPassword: '12345',
host: 'localhost',
rekeySignedPackage: `${tempDir}/testSignedPackage.pkg`
pkg: `${tempDir}/testSignedPackage.pkg`
});
options.rootDir = rootDir;
fsExtra.emptyDirSync(tempDir);
Expand Down Expand Up @@ -416,7 +416,7 @@ describe('RokuDeploy', () => {

it('should use given port if provided', async () => {
const stub = mockDoGetRequest(body);
await rokuDeploy.getDeviceInfo({ host: '1.1.1.1', remotePort: 9999 });
await rokuDeploy.getDeviceInfo({ host: '1.1.1.1', ecpPort: 9999 });
expect(stub.getCall(0).args[0].url).to.eql('http://1.1.1.1:9999/query/device-info');
});

Expand All @@ -427,7 +427,7 @@ describe('RokuDeploy', () => {
<udn>29380007-0800-1025-80a4-d83154332d7e</udn>
</device-info>
`);
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true });
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', ecpPort: 8060, enhance: true });
expect(result.isStick).not.to.exist;
});

Expand All @@ -443,7 +443,7 @@ describe('RokuDeploy', () => {

it('should sanitize additional data when the host+param+format signature is triggered', async () => {
mockDoGetRequest(body);
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true });
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', ecpPort: 8060, enhance: true });
expect(result).to.include({
// make sure the number fields are turned into numbers
softwareBuild: 4170,
Expand Down Expand Up @@ -482,7 +482,7 @@ describe('RokuDeploy', () => {

it('converts keys to camel case when enabled', async () => {
mockDoGetRequest(body);
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true });
const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', ecpPort: 8060, enhance: true });
const props = [
'udn',
'serialNumber',
Expand Down Expand Up @@ -742,7 +742,7 @@ describe('RokuDeploy', () => {
try {
fsExtra.ensureDirSync(options.stagingDir);
await rokuDeploy.zip({
stagingDir: s`${tempDir}/path/to/nowhere`,
dir: s`${tempDir}/path/to/nowhere`,
outDir: outDir
});
} catch (e) {
Expand All @@ -755,7 +755,7 @@ describe('RokuDeploy', () => {
let err;
try {
await rokuDeploy.zip({
stagingDir: s`${tempDir}/path/to/nowhere`,
dir: s`${tempDir}/path/to/nowhere`,
outDir: outDir
});
} catch (e) {
Expand Down Expand Up @@ -816,7 +816,7 @@ describe('RokuDeploy', () => {
resolve();
});
});
await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', remotePort: 987, key: 'home' });
await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', ecpPort: 987, key: 'home' });
await promise;
});

Expand All @@ -841,7 +841,7 @@ describe('RokuDeploy', () => {
resolve();
});
});
await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', remotePort: 987, key: 'home', timeout: 1000 });
await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', ecpPort: 987, key: 'home', timeout: 1000 });
await promise;
});
});
Expand Down Expand Up @@ -1646,7 +1646,7 @@ describe('RokuDeploy', () => {
<keyed-developer-id>${options.devId}</keyed-developer-id>
</device-info>`;
mockDoGetRequest(body);
fsExtra.outputFileSync(path.resolve(rootDir, options.rekeySignedPackage), '');
fsExtra.outputFileSync(path.resolve(rootDir, options.pkg), '');
});

it('does not crash when archive is undefined', async () => {
Expand All @@ -1657,7 +1657,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1678,7 +1678,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: s`notReal.pkg`,
pkg: s`notReal.pkg`,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1695,7 +1695,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: s`${tempDir}/testSignedPackage.pkg`,
pkg: s`${tempDir}/testSignedPackage.pkg`,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1709,7 +1709,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1723,7 +1723,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: undefined
});
Expand All @@ -1735,7 +1735,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1755,7 +1755,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: options.devId
});
Expand All @@ -1775,7 +1775,7 @@ describe('RokuDeploy', () => {
await rokuDeploy.rekeyDevice({
host: '1.2.3.4',
password: 'password',
rekeySignedPackage: options.rekeySignedPackage,
pkg: options.pkg,
signingPassword: options.signingPassword,
devId: '45fdc2019903ac333ff624b0b2cddd2c733c3e74'
});
Expand Down Expand Up @@ -2969,7 +2969,7 @@ describe('RokuDeploy', () => {
});

await rokuDeploy.zip({
stagingDir: stagingDir,
dir: stagingDir,
outDir: outDir
});
const data = fsExtra.readFileSync(rokuDeploy['getOutputZipFilePath']({ outDir: outDir }));
Expand Down Expand Up @@ -3762,13 +3762,13 @@ describe('RokuDeploy', () => {

});

describe('remotePort', () => {
describe('ecpPort', () => {
it('defaults to 8060', () => {
expect(rokuDeploy.getOptions({}).remotePort).to.equal(8060);
expect(rokuDeploy.getOptions({}).ecpPort).to.equal(8060);
});

it('can be overridden', () => {
expect(rokuDeploy.getOptions({ remotePort: 1234 }).remotePort).to.equal(1234);
expect(rokuDeploy.getOptions({ ecpPort: 1234 }).ecpPort).to.equal(1234);
});
});

Expand Down Expand Up @@ -3857,10 +3857,10 @@ describe('RokuDeploy', () => {
});

it('throws error when rekeyDevice is missing required options', async () => {
const requiredOptions: Partial<RekeyDeviceOptions> = { host: '1.2.3.4', password: 'abcd', rekeySignedPackage: 'abcd', signingPassword: 'abcd' };
const requiredOptions: Partial<RekeyDeviceOptions> = { host: '1.2.3.4', password: 'abcd', pkg: 'abcd', signingPassword: 'abcd' };
await testRequiredOptions('rekeyDevice', requiredOptions, 'host');
await testRequiredOptions('rekeyDevice', requiredOptions, 'password');
await testRequiredOptions('rekeyDevice', requiredOptions, 'rekeySignedPackage');
await testRequiredOptions('rekeyDevice', requiredOptions, 'pkg');
await testRequiredOptions('rekeyDevice', requiredOptions, 'signingPassword');
});

Expand Down
Loading
Loading