From b05f3a31a8877a8fa0e00ff554a3e98328050c44 Mon Sep 17 00:00:00 2001 From: fumer-fubotv <89787347+fumer-fubotv@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:12:04 -0700 Subject: [PATCH] Add commands to rekey device and create packages (#509) * Add ability to capture device screenshots * Add ability to capture device screenshots 1. Added support to show preview of screenshot 2. Fixed a linter issue * Feature request: add ability to rekey/sign apps Added support for the following commands: 1. Rekey Device: Rekey specified device based on the signing password ("brightscript.remoteControl.signingPassword") and package file ("brightscript.remoteControl.signedPackagePath") provided. 2. Create Package: This will present user a list of available launch configs to choose from. Once selected it will create a .zip file and .pkg file based on the config and will save them to the ${workspacefolder}/out 3. Rekey Device and Create Package: This will first rekey device and then create package for the selected launch config * remove stale code * remove redundent code * TBD-126 Updates based on below reqs: rekey: a) "How do you want to enter your stuff?" - "Pick from json file" - "Enter manually" b) prompt for host (prepopulated from json above if possible) c) Prompt for password (prepopulated from json above if possible) d) Prompt for signed package path (prepopulated from json above if available) package: a) "What should we package?" - "Pick a folder" - "Pick from a launch.json" - show a launch.json option picker - "Pick a rokudeploy.json" - b) prompt for host (prepopulated from json above if possible) c) Prompt for password (prepopulated from json above if possible) d) Prompt for signed package path (prepopulated from json above if available) e) Show a summary of stuff, ask to click "Yes, do it" or "No, something's wrong, cancel". * TBD-126 * Update BrightScriptCommands.ts * Refined the flow a bit * TBD-126: 1. Added support to show existing pkg file for rekey confirmation 2. Updated create package flow to confirm all the entries with user and ti update - if needed * TBD-126 Added support to maintain status of previously selected host and config * TBD-126 updated rekey and create package flow * Update RekeyAndPackageCommand.ts 1. updated outfile name to be the source folder name only 2. Fixed a bug for outdir that only supported mac paths 3. Added title to all inputboxes 4. Added a prompt for rootDir incase it is missing during create package 5. Added outfile path in dialog upon successful packaging * TBD-126 Added support to show package path in successful dialog and an option to open package in file explorer * Use `open` to open folder instead of custom function * Normalize a few file paths * Better rekeySignedPackage handling when empty string * simplify default config * Show .pkg in summary * Use correct file paths in more dialogs * Remove unused funcs in BrightScriptCommands.ts --------- Co-authored-by: Bronley Plumb --- package-lock.json | 55 ++- package.json | 21 +- src/BrightScriptCommands.ts | 10 +- src/commands/RekeyAndPackageCommand.ts | 448 +++++++++++++++++++++++++ 4 files changed, 513 insertions(+), 21 deletions(-) create mode 100644 src/commands/RekeyAndPackageCommand.ts diff --git a/package-lock.json b/package-lock.json index 1316a7c7..fdf97442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "net": "^1.0.2", "node-cache": "^4.2.0", "node-ssdp": "^4.0.0", + "open": "^8.4.2", "postman-request": "^2.88.1-postman.32", "pretty-bytes": "^5.6.0", "roku-debug": "^0.21.6", @@ -692,6 +693,23 @@ "node": ">=0.10.0" } }, + "node_modules/@compodoc/live-server/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3551,7 +3569,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, "engines": { "node": ">=8" } @@ -5994,7 +6011,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, "bin": { "is-docker": "cli.js" }, @@ -6211,7 +6227,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -7721,10 +7736,9 @@ } }, "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -11533,6 +11547,19 @@ "proxy-middleware": "latest", "send": "latest", "serve-index": "^1.9.1" + }, + "dependencies": { + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + } } }, "@cspotcode/source-map-support": { @@ -13779,8 +13806,7 @@ "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, "define-properties": { "version": "1.1.4", @@ -15616,8 +15642,7 @@ "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" }, "is-extglob": { "version": "2.1.1", @@ -15750,7 +15775,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "requires": { "is-docker": "^2.0.0" } @@ -16951,10 +16975,9 @@ } }, "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "requires": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", diff --git a/package.json b/package.json index 9bc7b391..d0e322be 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "net": "^1.0.2", "node-cache": "^4.2.0", "node-ssdp": "^4.0.0", + "open": "^8.4.2", "postman-request": "^2.88.1-postman.32", "pretty-bytes": "^5.6.0", "roku-debug": "^0.21.6", @@ -175,7 +176,10 @@ "onCommand:extension.brightscript.sendRemoteText", "onCommand:brighterscript.showPreview", "onCommand:brighterscript.showPreviewToSide", - "onCommand:extension.brightscript.captureScreenshot" + "onCommand:extension.brightscript.captureScreenshot", + "onCommand:extension.brightscript.rekeyDevice", + "onCommand:extension.brightscript.createPackage", + "onCommand:extension.brightscript.rekeyAndPackage" ], "contributes": { "viewsContainers": { @@ -2986,6 +2990,21 @@ "category": "BrighterScript", "icon": "./images/icons/inspect-active.svg" }, + { + "command": "extension.brightscript.rekeyDevice", + "title": "Rekey Device", + "category": "BrightScript" + }, + { + "command": "extension.brightscript.createPackage", + "title": "Create Package", + "category": "BrightScript" + }, + { + "command": "extension.brightscript.rekeyAndPackage", + "title": "Rekey Device and Create Package", + "category": "BrightScript" + }, { "command": "extension.brightscript.captureScreenshot", "title": "Capture Screenshot", diff --git a/src/BrightScriptCommands.ts b/src/BrightScriptCommands.ts index ae071d69..2a8a67ad 100644 --- a/src/BrightScriptCommands.ts +++ b/src/BrightScriptCommands.ts @@ -4,6 +4,7 @@ import BrightScriptFileUtils from './BrightScriptFileUtils'; import { GlobalStateManager } from './GlobalStateManager'; import { brighterScriptPreviewCommand } from './commands/BrighterScriptPreviewCommand'; import { captureScreenshotCommand } from './commands/CaptureScreenshotCommand'; +import { rekeyAndPackageCommand } from './commands/RekeyAndPackageCommand'; import { languageServerInfoCommand } from './commands/LanguageServerInfoCommand'; import { util } from './util'; import { util as rokuDebugUtil } from 'roku-debug/dist/util'; @@ -37,6 +38,7 @@ export class BrightScriptCommands { brighterScriptPreviewCommand.register(this.context); languageServerInfoCommand.register(this.context); captureScreenshotCommand.register(this.context, this); + rekeyAndPackageCommand.register(this.context, this, this.userInputManager); this.registerGeneralCommands(); @@ -394,13 +396,13 @@ export class BrightScriptCommands { } } - public async getRemoteHost() { + public async getRemoteHost(showPrompt = true) { this.host = await this.context.workspaceState.get('remoteHost'); if (!this.host) { let config = vscode.workspace.getConfiguration('brightscript.remoteControl', null); this.host = config.get('host'); // eslint-disable-next-line no-template-curly-in-string - if (!this.host || this.host === '${promptForHost}') { + if ((!this.host || this.host === '${promptForHost}') && showPrompt) { this.host = await vscode.window.showInputBox({ placeHolder: 'The IP address of your Roku device', value: '' @@ -423,13 +425,13 @@ export class BrightScriptCommands { return this.host; } - public async getRemotePassword() { + public async getRemotePassword(showPrompt = true) { this.password = await this.context.workspaceState.get('remotePassword'); if (!this.password) { let config = vscode.workspace.getConfiguration('brightscript.remoteControl', null); this.password = config.get('password'); // eslint-disable-next-line no-template-curly-in-string - if (!this.password || this.password === '${promptForPassword}') { + if ((!this.password || this.password === '${promptForPassword}') && showPrompt) { this.password = await vscode.window.showInputBox({ placeHolder: 'The developer account password for your Roku device', value: '' diff --git a/src/commands/RekeyAndPackageCommand.ts b/src/commands/RekeyAndPackageCommand.ts new file mode 100644 index 00000000..cd456cd8 --- /dev/null +++ b/src/commands/RekeyAndPackageCommand.ts @@ -0,0 +1,448 @@ +import * as vscode from 'vscode'; +import * as rokuDeploy from 'roku-deploy'; +import type { BrightScriptCommands } from '../BrightScriptCommands'; +import * as path from 'path'; +import { readFileSync } from 'fs-extra'; +import type { UserInputManager } from '../managers/UserInputManager'; +import { standardizePath } from 'brighterscript'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import open = require('open'); + +export const FILE_SCHEME = 'bs-captureScreenshot'; + +export class RekeyAndPackageCommand { + + private brightScriptCommands: BrightScriptCommands; + private userInputManager: UserInputManager; + + public register(context: vscode.ExtensionContext, BrightScriptCommandsInstance: BrightScriptCommands, userInputManager: UserInputManager) { + this.brightScriptCommands = BrightScriptCommandsInstance; + this.userInputManager = userInputManager; + + context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rekeyDevice', async (hostParam?: string) => { + await this.rekeyDevice(); + })); + + context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.createPackage', async (hostParam?: string) => { + await this.createPackage({}); + })); + + context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rekeyAndPackage', async (hostParam?: string) => { + await this.createPackage({}, true); + })); + } + + private async rekeyDevice() { + const PICK_FROM_JSON = 'Pick from Json file'; + const MANUAL_ENTRY = 'Enter manually'; + + let rekeyConfig: RekeyConfig = { + signingPassword: '', + rekeySignedPackage: '', + host: '', + password: '' + }; + + let rekeyOptionList = [PICK_FROM_JSON, MANUAL_ENTRY]; + let rekeyOption = await vscode.window.showQuickPick(rekeyOptionList, { placeHolder: 'How would you like to select your configuration', canPickMany: false }); + if (rekeyOption) { + switch (rekeyOption) { + case PICK_FROM_JSON: + rekeyConfig = await this.getRekeyConfigFromJson(rekeyConfig); + break; + + case MANUAL_ENTRY: + rekeyConfig = await this.getRekeyManualEntries(rekeyConfig, {}); + break; + } + } + + await rokuDeploy.rekeyDevice(rekeyConfig); + void vscode.window.showInformationMessage(`Device successfully rekeyed!`); + } + + private async getRekeyConfigFromJson(rekeyConfig: RekeyConfig) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select', + canSelectFiles: true, + canSelectFolders: false, + filters: { + 'Json files': ['json'] + } + }; + + let fileUri = await vscode.window.showOpenDialog(options); + if (fileUri?.[0]) { + let content = JSON.parse(readFileSync(fileUri[0].fsPath).toString()); + + if (content.signingPassword) { + rekeyConfig.signingPassword = content.signingPassword; + } + + if (content.rekeySignedPackage?.includes('./')) { + await this.brightScriptCommands.getWorkspacePath(); + let workspacePath = this.brightScriptCommands.workspacePath; + rekeyConfig.rekeySignedPackage = workspacePath + content.rekeySignedPackage.replace('./', '/'); + } + + if (content.host) { + rekeyConfig.host = content.host; + } + + if (content.password) { + rekeyConfig.password = content.password; + } + } + return this.getRekeyManualEntries(rekeyConfig, rekeyConfig); + } + + private async getRekeyManualEntries(rekeyConfig: RekeyConfig, defaultValues) { + rekeyConfig.host = await this.userInputManager.promptForHost({ defaultValue: rekeyConfig?.host ?? defaultValues?.host }); + + rekeyConfig.password = await vscode.window.showInputBox({ + title: 'Enter password for the Roku device you want to rekey', + value: defaultValues?.password ?? '' + }); + if (!rekeyConfig.password) { + throw new Error('Cancelled'); + } + + rekeyConfig.signingPassword = await vscode.window.showInputBox({ + title: 'Enter signingPassword to be used to rekey the Roku', + value: defaultValues?.signingPassword ?? '' + }); + if (!rekeyConfig.signingPassword) { + throw new Error('Cancelled'); + } + + rekeyConfig.rekeySignedPackage = await this.getSignedPackage(rekeyConfig.rekeySignedPackage); + + const selection = await vscode.window.showInformationMessage('Rekey info:', { + modal: true, + detail: [ + `host: ${rekeyConfig.host}`, + `password: ${rekeyConfig.password}`, + `signing password: ${rekeyConfig.signingPassword}`, + `package: ${rekeyConfig.rekeySignedPackage}` + ].join('\n') + }, 'Rekey', 'I want to change something'); + if (selection === 'Rekey') { + return rekeyConfig; + } else if (selection === 'I want to change something') { + return this.getRekeyManualEntries(rekeyConfig, rekeyConfig); + } + } + + private async getSignedPackage(rekeySignedPackage: string) { + let response = ''; + rekeySignedPackage = standardizePath(rekeySignedPackage); + if (rekeySignedPackage?.length > 0) { + response = await vscode.window.showInformationMessage( + 'Please choose a signed package (a .pkg file) to rekey your device', + { + modal: true, + detail: `Current file: ${rekeySignedPackage}` + }, + 'Use the current file', 'Pick a different file' + ); + } else { + response = await vscode.window.showInformationMessage( + 'Please choose a signed package (a .pkg file) to rekey your device', + { modal: true }, + 'Open file picker' + ); + } + if ((response === 'Open file picker') || (response === 'Pick a different file')) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select signed package file', + canSelectFiles: true, + canSelectFolders: false, + filters: { + 'Pkg files': ['pkg'] + } + }; + let fileUri = await vscode.window.showOpenDialog(options); + if (fileUri?.[0]) { + return fileUri[0].fsPath; + } + } else if (response === 'Use the current file') { + return rekeySignedPackage; + } else { + throw new Error('Cancelled'); + } + } + + private async promptUserForAFolder(dialogTitle) { + let response = ''; + + response = await vscode.window.showInformationMessage( + dialogTitle, + { modal: true }, + 'Open file picker' + ); + + if (response === 'Open file picker') { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select', + canSelectFiles: false, + canSelectFolders: true + }; + let folderUri = await vscode.window.showOpenDialog(options); + if (folderUri?.[0]) { + return folderUri[0].fsPath; + } + } else { + throw new Error('Cancelled'); + } + } + + private async createPackage(defaultValues: Partial, rekeyFlag = false) { + const workspaceFolder = await this.brightScriptCommands.getWorkspacePath(); + + let rokuDeployOptions = defaultValues as RokuDeployOptions; + + let PACKAGE_FOLDER = 'Pick a folder'; + let PACKAGE_FROM_LAUNCH_JSON = 'Pick from a launch.json'; + let PACKAGE_FROM_ROKU_DEPLOY = 'Pick a rokudeploy.json'; + let packageOptionList = []; + + if (rokuDeployOptions.packageConfig) { + packageOptionList.push({ + label: `Previous Selection:`, + detail: `${rokuDeployOptions.packageConfig}` + }); + } + packageOptionList.push(PACKAGE_FOLDER, PACKAGE_FROM_LAUNCH_JSON, PACKAGE_FROM_ROKU_DEPLOY); + + let packageOption = await vscode.window.showQuickPick(packageOptionList, { placeHolder: 'What would you like to package', canPickMany: false }); + if (packageOption) { + switch (packageOption) { + case PACKAGE_FOLDER: + rokuDeployOptions = await this.packageFromFolder(rokuDeployOptions); + break; + + case PACKAGE_FROM_LAUNCH_JSON: + rokuDeployOptions = await this.packageFromLaunchConfig(rokuDeployOptions); + break; + + case PACKAGE_FROM_ROKU_DEPLOY: + rokuDeployOptions = await this.packageFromRokuDeploy(rokuDeployOptions); + break; + } + + rokuDeployOptions.host = await this.userInputManager.promptForHost({ defaultValue: rokuDeployOptions?.host ?? '' }); + + rokuDeployOptions.password = await vscode.window.showInputBox({ + title: 'Enter password for the Roku device', + value: rokuDeployOptions.password ?? '' + }); + if (!rokuDeployOptions.password) { + throw new Error('Cancelled'); + } + + rokuDeployOptions.signingPassword = await vscode.window.showInputBox({ + title: 'Enter signingPassword for the Roku', + value: rokuDeployOptions.signingPassword ?? '' + }); + + if (!rokuDeployOptions.rootDir) { + rokuDeployOptions.rootDir = await this.promptUserForAFolder('Select rootDir to create package'); + } + if (!rokuDeployOptions.rootDir) { + throw new Error('Cancelled'); + } + + //normalize a few options + rokuDeployOptions.outFile ??= rokuDeploy.getOptions(rokuDeployOptions).outFile; + rokuDeployOptions.outDir = standardizePath(rokuDeployOptions.outDir ?? `${workspaceFolder}/out`); + rokuDeployOptions.rootDir = standardizePath(rokuDeployOptions.rootDir); + rokuDeployOptions.retainStagingDir = true; + if (rokuDeployOptions.rekeySignedPackage?.length > 0) { + rokuDeployOptions.rekeySignedPackage = standardizePath(rokuDeployOptions.rekeySignedPackage); + } + + let details = [ + `host: ${rokuDeployOptions.host}`, + `password: ${rokuDeployOptions.password}`, + `signing password: ${rokuDeployOptions.signingPassword}`, + `outDir: ${rokuDeployOptions.outDir}`, + `outFile: ${rokuDeployOptions.outFile}.pkg`, + `rootDir: ${rokuDeployOptions.rootDir}` + ]; + + if (rekeyFlag) { + rokuDeployOptions.rekeySignedPackage = await this.getSignedPackage(rokuDeployOptions.rekeySignedPackage); + details.push(`rekeySignedPackage: ${rokuDeployOptions.rekeySignedPackage}`); + } + + let confirmText = 'Create Package'; + let changeText = 'I want to change something'; + let response = await vscode.window.showInformationMessage('Create Package info:', { + modal: true, + detail: details.join('\n') + }, confirmText, changeText); + + if (response === confirmText) { + if (rekeyFlag) { + //rekey device + await rokuDeploy.rekeyDevice(rokuDeployOptions); + } + + //create a zip and pkg file of the app based on the selected launch config + await rokuDeploy.createPackage(rokuDeployOptions); + let remotePkgPath = await rokuDeploy.signExistingPackage(rokuDeployOptions); + await rokuDeploy.retrieveSignedPackage(remotePkgPath, rokuDeployOptions); + const outPath = standardizePath(`${rokuDeployOptions.outDir}/${rokuDeployOptions.outFile}`); + let successfulMessage = `Package successfully created at ${outPath}`; + void vscode.window.showInformationMessage(successfulMessage, 'View in folder').then(() => { + return open(rokuDeployOptions.outDir); + }); + + } else if (response === changeText) { + return this.createPackage(rokuDeployOptions, rekeyFlag); + } + } + } + + private async packageFromFolder(rokuDeployOptions) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select Folder to package', + canSelectFiles: false, + canSelectFolders: true + }; + let fileUri = await vscode.window.showOpenDialog(options); + if (fileUri?.[0]) { + let rootDir = fileUri?.[0].fsPath; + + rokuDeployOptions.rootDir = rootDir; + rokuDeployOptions.outFile = path.basename(rootDir); + rokuDeployOptions.packageConfig = 'folder: ' + rootDir; + + return rokuDeployOptions; + } + } + + + private async packageFromRokuDeploy(rokuDeployOptions) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select', + canSelectFiles: true, + canSelectFolders: false, + filters: { + 'Json files': ['json'] + } + }; + + let fileUri = await vscode.window.showOpenDialog(options); + if (fileUri?.[0]) { + return this.parseRokuDeployJson(fileUri[0].fsPath, rokuDeployOptions); + } + return rokuDeployOptions; + } + + private async packageFromLaunchConfig(rokuDeployOptions) { + let config = vscode.workspace.getConfiguration('launch', null); + const configurations = config.get('configurations'); + let configNames = []; + for (let config of configurations) { + configNames.push(config.name); + } + + //show user a list of available launch configs to choose from + let selectedConfig = configurations[0]; + let selectedConfigName = await vscode.window.showQuickPick(configNames, { placeHolder: 'Please select a config', canPickMany: false }); + if (selectedConfigName) { + let selectedIndex = configNames.indexOf(selectedConfigName); + selectedConfig = configurations[selectedIndex]; + } + + if (selectedConfig.rootDir?.includes('${workspaceFolder}')) { + await this.brightScriptCommands.getWorkspacePath(); + let workspacePath = this.brightScriptCommands.workspacePath; + + selectedConfig.rootDir = path.normalize(selectedConfig.rootDir.replace('${workspaceFolder}', workspacePath)); + } + rokuDeployOptions.packageConfig = 'launch.json: ' + selectedConfig.rootDir; + + if (!selectedConfig.host.includes('${')) { + rokuDeployOptions.host = selectedConfig.host; + } + + if (!selectedConfig.password.includes('${')) { + rokuDeployOptions.password = selectedConfig.password; + } + + rokuDeployOptions.rootDir = selectedConfig.rootDir; + rokuDeployOptions.files = selectedConfig.files; + rokuDeployOptions.outFile = 'roku-' + selectedConfig.name.replace(/ /g, '-'); + + return rokuDeployOptions; + } + + private async parseRokuDeployJson(filePath: string, rokuDeployOptions) { + rokuDeployOptions.packageConfig = 'rokudeploy.json: ' + filePath; + let content = JSON.parse(readFileSync(filePath).toString()); + await this.brightScriptCommands.getWorkspacePath(); + let workspacePath = this.brightScriptCommands.workspacePath; + + if (content.signingPassword) { + rokuDeployOptions.signingPassword = content.signingPassword; + } + + if (content.rekeySignedPackage?.includes('./')) { + rokuDeployOptions.rekeySignedPackage = workspacePath + content.rekeySignedPackage.replace('./', '/'); + } + + if (content.host) { + rokuDeployOptions.host = content.host; + } + + if (content.password) { + rokuDeployOptions.password = content.password; + } + + if (content.rootDir?.includes('./')) { + rokuDeployOptions.rootDir = workspacePath + content.rootDir.replace('./', '/'); + } + + if (content.outDir?.includes('./')) { + rokuDeployOptions.outDir = workspacePath + content.outDir.replace('./', '/'); + } + + if (content.outFile) { + rokuDeployOptions.outFile = content.outFile; + } + + if (content.retainStagingDir) { + rokuDeployOptions.retainStagingDir = content.retainStagingDir; + } + + return rokuDeployOptions; + } +} + +interface RekeyConfig { + signingPassword: string; + rekeySignedPackage: string; + host: string; + password: string; +} + +interface RokuDeployOptions { + rootDir: string; + outDir: string; + outFile: string; + retainStagingDir: boolean; + host: string; + password: string; + signingPassword: string; + rekeySignedPackage: string; + packageConfig: string; +} + +export const rekeyAndPackageCommand = new RekeyAndPackageCommand();