• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / vscode-brightscript-language / 28104973919

24 Jun 2026 02:14PM UTC coverage: 56.503% (+0.3%) from 56.169%
28104973919

Pull #829

github

web-flow
Merge 5b60889eb into d06fe08e5
Pull Request #829: Add restart device and software update commands to Devices panel

2339 of 4558 branches covered (51.32%)

Branch coverage included in aggregate %.

78 of 103 new or added lines in 3 files covered. (75.73%)

1 existing line in 1 file now uncovered.

3730 of 6183 relevant lines covered (60.33%)

40.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.73
/src/BrightScriptCommands.ts
1
import * as request from 'postman-request';
1✔
2
import * as vscode from 'vscode';
1✔
3
import BrightScriptFileUtils from './BrightScriptFileUtils';
1✔
4
import { GlobalStateManager } from './GlobalStateManager';
1✔
5
import { brighterScriptPreviewCommand } from './commands/BrighterScriptPreviewCommand';
1✔
6
import { captureScreenshotCommand } from './commands/CaptureScreenshotCommand';
1✔
7
import { rekeyAndPackageCommand } from './commands/RekeyAndPackageCommand';
1✔
8
import { languageServerInfoCommand } from './commands/LanguageServerInfoCommand';
1✔
9
import { util } from './util';
1✔
10
import { util as rokuDebugUtil } from 'roku-debug/dist/util';
1✔
11
import type { RemoteControlManager, RemoteControlModeInitiator } from './managers/RemoteControlManager';
12
import type { WhatsNewManager } from './managers/WhatsNewManager';
13
import type { ConfiguredDevice, DeviceManager, RokuDevice } from './deviceDiscovery/DeviceManager';
14
import * as xml2js from 'xml2js';
1✔
15
import { firstBy } from 'thenby';
1✔
16
import type { UserInputManager } from './managers/UserInputManager';
17
import { clearNpmPackageCacheCommand } from './commands/ClearNpmPackageCacheCommand';
1✔
18
import type { LocalPackageManager } from './managers/LocalPackageManager';
19
import { profilingCommands } from './commands/ProfilingCommands';
1✔
20
import { vscodeContextManager } from './managers/VscodeContextManager';
1✔
21
import type { CredentialStore } from './managers/CredentialStore';
22
import type { DevicesViewProvider } from './viewProviders/DevicesViewProvider';
23
import { DEVICE_FILTER_KEYS } from './deviceFilters';
1✔
24
import { rokuDeploy } from 'roku-deploy';
1✔
25

26
export class BrightScriptCommands {
1✔
27

28
    constructor(
29
        private remoteControlManager: RemoteControlManager,
72✔
30
        private whatsNewManager: WhatsNewManager,
72✔
31
        private context: vscode.ExtensionContext,
72✔
32
        private deviceManager: DeviceManager,
72✔
33
        private userInputManager: UserInputManager,
72✔
34
        private localPackageManager: LocalPackageManager,
72✔
35
        private credentialStore: CredentialStore
72✔
36
    ) {
37
        this.fileUtils = new BrightScriptFileUtils();
72✔
38
    }
39

40
    private fileUtils: BrightScriptFileUtils;
41
    public host: string;
42
    public password: string;
43
    public workspacePath: string;
44
    private keypressNotifiers = [] as ((key: string, literalCharacter: boolean) => void)[];
72✔
45

46
    public registerCommands() {
47

48
        brighterScriptPreviewCommand.register(this.context);
16✔
49
        languageServerInfoCommand.register(this.context, this.localPackageManager);
16✔
50
        captureScreenshotCommand.register(this.context, this);
16✔
51
        rekeyAndPackageCommand.register(this.context, this, this.userInputManager);
16✔
52
        clearNpmPackageCacheCommand.register(this.context, this.localPackageManager);
16✔
53
        profilingCommands.register(this.context);
16✔
54

55
        this.registerGeneralCommands();
16✔
56

57
        this.registerCommand('sendRemoteCommand', async (key: string) => {
16✔
58
            await this.sendRemoteCommand(key);
×
59
        });
60

61
        //the "Refresh" button in the Devices list
62
        this.registerCommand('refreshDeviceList', (key: string) => {
16✔
63
            this.deviceManager.refresh(true);
×
64
        });
65

66
        this.registerCommand('rescanDevices', () => {
16✔
67
            this.deviceManager.refresh(true);
×
68
        });
69

70
        // Refresh a single device (inline button on hover in devices panel)
71
        this.registerCommand('refreshDevice', async (item: { key: string }) => {
16✔
72
            await this.deviceManager.healthCheckDevice({ serialNumber: item.key }, true);
×
73
        });
74

75
        this.registerCommand('sendRemoteText', async () => {
16✔
76
            let items: vscode.QuickPickItem[] = [];
×
77
            for (const item of new GlobalStateManager(this.context).sendRemoteTextHistory) {
×
78
                items.push({ label: item });
×
79
            }
80

81
            const stuffUserTyped = await util.showQuickPickInputBox({
×
82
                placeholder: 'Press enter to send all typed characters to the Roku',
83
                items: items
84
            });
85
            console.log('userInput', stuffUserTyped);
×
86

87
            if (stuffUserTyped) {
×
88
                new GlobalStateManager(this.context).addTextHistory(stuffUserTyped);
×
89
                let fallbackToHttp = true;
×
90
                await this.getRemoteHost();
×
91
                //TODO fix SceneGraphDebugCommandController to not timeout so quickly
92
                // try {
93
                //     let commandController = new SceneGraphDebugCommandController(this.host);
94
                //     let response = await commandController.type(stuffUserTyped);
95
                //     if (!response.error) {
96
                //         fallbackToHttp = false;
97
                //     }
98
                // } catch (error) {
99
                //     console.error(error);
100
                //     // Let this fallback to the old HTTP based logic
101
                // }
102

103
                if (fallbackToHttp) {
×
104
                    for (let character of stuffUserTyped) {
×
105
                        await this.sendAsciiToDevice(character);
×
106
                    }
107
                }
108
            }
109
            await vscode.commands.executeCommand('workbench.action.focusPanel');
×
110
        });
111

112
        this.registerCommand('toggleRemoteControlMode', (initiator: RemoteControlModeInitiator) => {
16✔
113
            return this.remoteControlManager.toggleRemoteControlMode(initiator);
×
114
        });
115

116
        this.registerCommand('enableRemoteControlMode', () => {
16✔
117
            return this.remoteControlManager.setRemoteControlMode(true, 'command');
×
118
        });
119

120
        this.registerCommand('disableRemoteControlMode', () => {
16✔
121
            return this.remoteControlManager.setRemoteControlMode(false, 'command');
×
122
        });
123

124
        this.registerCommand('pressBackButton', async () => {
16✔
125
            await this.sendRemoteCommand('Back');
×
126
        });
127

128
        this.registerCommand('pressBackspaceButton', async () => {
16✔
129
            await this.sendRemoteCommand('Backspace');
×
130
        });
131

132
        this.registerCommand('pressHomeButton', async () => {
16✔
133
            await this.sendRemoteCommand('Home');
×
134
        });
135

136
        this.registerCommand('restartDevApplication', async () => {
16✔
137
            await this.restartDevApplication();
×
138
        });
139

140
        this.registerCommand('pressUpButton', async () => {
16✔
141
            await this.sendRemoteCommand('Up');
×
142
        });
143

144
        this.registerCommand('pressDownButton', async () => {
16✔
145
            await this.sendRemoteCommand('Down');
×
146
        });
147

148
        this.registerCommand('pressRightButton', async () => {
16✔
149
            await this.sendRemoteCommand('Right');
×
150
        });
151

152
        this.registerCommand('pressLeftButton', async () => {
16✔
153
            await this.sendRemoteCommand('Left');
×
154
        });
155

156
        this.registerCommand('pressSelectButton', async () => {
16✔
157
            await this.sendRemoteCommand('Select');
×
158
        });
159

160
        this.registerCommand('pressPlayButton', async () => {
16✔
161
            await this.sendRemoteCommand('Play');
×
162
        });
163

164
        this.registerCommand('pressRevButton', async () => {
16✔
165
            await this.sendRemoteCommand('Rev');
×
166
        });
167

168
        this.registerCommand('pressFwdButton', async () => {
16✔
169
            await this.sendRemoteCommand('Fwd');
×
170
        });
171

172
        this.registerCommand('pressStarButton', async () => {
16✔
173
            await this.sendRemoteCommand('Info');
×
174
        });
175

176
        this.registerCommand('pressInstantReplayButton', async () => {
16✔
177
            await this.sendRemoteCommand('InstantReplay');
×
178
        });
179

180
        this.registerCommand('pressSearchButton', async () => {
16✔
181
            await this.sendRemoteCommand('Search');
×
182
        });
183

184
        this.registerCommand('pressEnterButton', async () => {
16✔
185
            await this.sendRemoteCommand('Enter');
×
186
        });
187

188
        this.registerCommand('pressFindRemote', async () => {
16✔
189
            await this.sendRemoteCommand('FindRemote');
×
190
        });
191

192
        this.registerCommand('pressVolumeDown', async () => {
16✔
193
            await this.sendRemoteCommand('VolumeDown');
×
194
        });
195

196
        this.registerCommand('pressVolumeMute', async () => {
16✔
197
            await this.sendRemoteCommand('VolumeMute');
×
198
        });
199

200
        this.registerCommand('pressVolumeUp', async () => {
16✔
201
            await this.sendRemoteCommand('VolumeUp');
×
202
        });
203

204
        this.registerCommand('setVolume', async () => {
16✔
205
            let result = await vscode.window.showInputBox({
×
206
                placeHolder: 'The target volume level (0-100)',
207
                value: '',
208
                validateInput: (text: string) => {
209
                    const num = Number(text);
×
210
                    if (isNaN(num)) {
×
211
                        return 'Value must be a number';
×
212
                    } else if (num < 0 || num > 100) {
×
213
                        return 'Please enter a number between 0 and 100';
×
214
                    }
215
                    return null;
×
216
                }
217
            });
218
            const targetVolume = Number(result);
×
219

220
            if (!isNaN(targetVolume)) {
×
221
                await vscode.window.withProgress({
×
222
                    location: vscode.ProgressLocation.Notification,
223
                    title: 'Setting volume'
224
                }, async (progress) => {
225
                    const totalCommands = 100 + targetVolume;
×
226
                    const incrementValue = 100 / totalCommands;
×
227
                    let executedCommands = 0;
×
228

229
                    for (let i = 0; i < 100; i++) {
×
230
                        await this.sendRemoteCommand('VolumeDown');
×
231
                        executedCommands++;
×
232
                        progress.report({ increment: incrementValue, message: `decreasing volume - ${Math.round((executedCommands / totalCommands) * 100)}%` });
×
233
                    }
234

235
                    for (let i = 0; i < targetVolume; i++) {
×
236
                        await this.sendRemoteCommand('VolumeUp');
×
237
                        executedCommands++;
×
238
                        progress.report({ increment: incrementValue, message: `increasing volume - ${Math.round((executedCommands / totalCommands) * 100)}%` });
×
239
                    }
240
                });
241
            }
242
        });
243

244
        this.registerCommand('pressPowerOff', async () => {
16✔
245
            await this.sendRemoteCommand('PowerOff');
×
246
        });
247

248
        this.registerCommand('pressPowerOn', async () => {
16✔
249
            await this.sendRemoteCommand('PowerOn');
×
250
        });
251

252
        this.registerCommand('pressChannelUp', async () => {
16✔
253
            await this.sendRemoteCommand('ChannelUp');
×
254
        });
255

256
        this.registerCommand('pressChannelDown', async () => {
16✔
257
            await this.sendRemoteCommand('ChannelDown');
×
258
        });
259

260
        this.registerCommand('pressBlue', async () => {
16✔
261
            await this.sendRemoteCommand('Blue');
×
262
        });
263

264
        this.registerCommand('pressGreen', async () => {
16✔
265
            await this.sendRemoteCommand('Green');
×
266
        });
267

268
        this.registerCommand('pressRed', async () => {
16✔
269
            await this.sendRemoteCommand('Red');
×
270
        });
271

272
        this.registerCommand('pressYellow', async () => {
16✔
273
            await this.sendRemoteCommand('Yellow');
×
274
        });
275

276
        this.registerCommand('pressExit', async () => {
16✔
277
            await this.sendRemoteCommand('Exit');
×
278
        });
279

280
        this.registerCommand('changeTvInput', async (host?: string) => {
16✔
281
            const selectedInput = await vscode.window.showQuickPick([
×
282
                'InputHDMI1',
283
                'InputHDMI2',
284
                'InputHDMI3',
285
                'InputHDMI4',
286
                'InputAV1',
287
                'InputTuner'
288
            ]);
289

290
            if (selectedInput) {
×
291
                await this.sendRemoteCommand(selectedInput, host);
×
292
            }
293
        });
294

295
        this.registerKeyboardInputs();
16✔
296
    }
297

298
    /**
299
     * Registers all the commands for a-z, A-Z, 0-9, and all the primary character such as !, @, #, ', ", etc...
300
     */
301
    private registerKeyboardInputs() {
302
        // Get all the keybindings from our package.json
303
        const extension = vscode.extensions.getExtension('RokuCommunity.brightscript');
16✔
304
        const keybindings = (extension.packageJSON.contributes.keybindings as Array<{
16✔
305
            key: string;
306
            command: string;
307
            when: string;
308
            args: any;
309
        }>);
310

311
        for (let keybinding of keybindings) {
16✔
312
            // Find every keybinding that is related to sending text characters to the device
313
            if (keybinding.command.includes('.sendAscii+')) {
2,320✔
314

315
                if (!keybinding.args) {
1,536!
316
                    throw new Error(`Can not register command: ${keybinding.command}. Missing Arguments.`);
×
317
                }
318

319
                // Dynamically register the the command defined in the keybinding
320
                this.registerCommand(keybinding.command, async (character: string) => {
1,536✔
321
                    await this.sendAsciiToDevice(character);
×
322
                });
323
            }
324
        }
325
    }
326

327
    private registerGeneralCommands() {
328
        //a command that does absolutely nothing. It's here to allow us to absorb unsupported keypresses when in **remote control mode**.
329
        this.registerCommand('doNothing', () => { });
16✔
330

331
        this.registerCommand('toggleXML', async () => {
16✔
332
            await this.onToggleXml();
×
333
        });
334

335
        this.registerCommand('goToParentComponent', async () => {
16✔
336
            await this.onGoToParentComponent();
×
337
        });
338

339
        this.registerCommand('clearGlobalState', async () => {
16✔
340
            new GlobalStateManager(this.context).clear();
×
341
            await vscode.window.showInformationMessage('BrightScript Language extension global state cleared');
×
342
        });
343

344
        this.registerCommand('clearCurrentDeviceList', async () => {
16✔
345
            const toatsPromise = util.showTimedNotification('Clearing device list');
×
346
            await this.deviceManager.clearCurrentDeviceList();
×
347
            await toatsPromise;
×
348
        });
349

350
        this.registerCommand('enableDeviceDiscovery', async () => {
16✔
351
            await util.setConfigurationValueAtUserOrClosestScope('brightscript.deviceDiscovery.enabled', true);
×
352
        });
353

354
        this.registerCommand('disableDeviceDiscovery', async () => {
16✔
355
            await util.setConfigurationValueAtUserOrClosestScope('brightscript.deviceDiscovery.enabled', false);
×
356
        });
357

358
        this.registerCommand('clearDeviceCache', async () => {
16✔
359
            this.deviceManager.clearAllCache();
×
360
            await util.showTimedNotification('Clearing device cache');
×
361
        });
362

363
        this.registerCommand('clearLastSeenDevices', async () => {
16✔
364
            new GlobalStateManager(this.context).clearLastSeenDevices();
×
365
            await vscode.window.showInformationMessage('Last seen devices cleared');
×
366
        });
367

368
        this.registerCommand('copyToClipboard', async (value: string) => {
16✔
369
            try {
×
370
                if (util.isNullish(value)) {
×
371
                    throw new Error('Cannot copy ${value} to clipboard');
×
372
                }
373
                await vscode.env.clipboard.writeText(value?.toString());
×
374
                await vscode.window.showInformationMessage(`Copied to clipboard: ${value}`);
×
375
            } catch (error) {
376
                await vscode.window.showErrorMessage(`Could not copy value to clipboard`);
×
377
            }
378
        });
379

380
        this.registerCommand('openUrl', async (url: string) => {
16✔
381
            try {
×
382
                await vscode.env.openExternal(vscode.Uri.parse(url));
×
383
            } catch (error) {
384
                await vscode.window.showErrorMessage(`Tried to open url but failed: ${url}`);
×
385
            }
386
        });
387

388
        this.registerCommand('openRegistryInBrowser', async (host: string) => {
16✔
389
            if (!host) {
×
390
                host = await this.userInputManager.promptForHost();
×
391
            }
392

393
            let responseText = await util.spinAsync('Fetching app list', async () => {
×
394
                return (await util.httpGet(`http://${host}:8060/query/apps`, { timeout: 4_000 })).body as string;
×
395
            });
396

397
            const parsed = await xml2js.parseStringPromise(responseText);
×
398

399
            //convert the items to QuickPick items
400
            const items: Array<vscode.QuickPickItem & { appId?: string }> = parsed.apps.app.map((appData: any) => {
×
401
                return {
×
402
                    label: appData._,
403
                    detail: `ID: ${appData.$.id}`,
404
                    description: `${appData.$.version}`,
405
                    appId: `${appData.$.id}`
406
                } as vscode.QuickPickItem;
407
                //sort the items alphabetically
408
            }).sort(firstBy('label'));
409

410
            //move the dev app to the top (and add a label/section to differentiate it)
411
            const devApp = items.find(x => x.appId === 'dev');
×
412
            if (devApp) {
×
413
                items.splice(items.indexOf(devApp), 1);
×
414
                items.unshift(
×
415
                    { kind: vscode.QuickPickItemKind.Separator, label: 'dev' },
416
                    devApp,
417
                    { kind: vscode.QuickPickItemKind.Separator, label: ' ' }
418
                );
419
            }
420

421
            const selectedApp: typeof items[0] = await vscode.window.showQuickPick(items, { placeHolder: 'Which app would you like to see the registry for?' });
×
422

423
            if (selectedApp) {
×
424
                const appId = selectedApp.appId;
×
425
                let url = `http://${host}:8060/query/registry/${appId}`;
×
426
                try {
×
427
                    await vscode.env.openExternal(vscode.Uri.parse(url));
×
428
                } catch (error) {
429
                    await vscode.window.showErrorMessage(`Tried to open url but failed: ${url}`);
×
430
                }
431
            }
432
        });
433

434
        this.registerCommand('setActiveDevice', async (deviceOrItem: string | { key: string }) => {
16✔
435
            let ip: string;
436
            if (typeof deviceOrItem === 'object' && deviceOrItem?.key) {
×
437
                ip = this.deviceManager.getDevice(deviceOrItem.key)?.ip;
×
438
            } else if (typeof deviceOrItem === 'string') {
×
439
                ip = deviceOrItem;
×
440
            }
441
            if (!ip) {
×
442
                ip = await this.userInputManager.promptForHost();
×
443
            }
444
            if (!ip) {
×
445
                throw new Error('Tried to set active device but failed.');
×
446
            } else {
447
                await this.context.workspaceState.update('remoteHost', ip);
×
448
                await vscodeContextManager.set('activeHost', ip);
×
449
                await util.showTimedNotification(`'${ip}' set as active device`);
×
450
            }
451
        });
452

453
        this.registerCommand('editDeviceInUserSettings', async (deviceOrItem: { key: string }) => {
16✔
454
            const device = this.deviceManager.getDevice(deviceOrItem?.key);
×
455
            await this.openSettingsJsonAtDevice(device, 'user');
×
456
        });
457

458
        this.registerCommand('editDeviceInWorkspaceSettings', async (deviceOrItem: { key: string }) => {
16✔
459
            const device = this.deviceManager.getDevice(deviceOrItem?.key);
×
460
            await this.openSettingsJsonAtDevice(device, 'workspace');
×
461
        });
462

463
        this.registerCommand('addDeviceToUserSettings', async (deviceOrItem: { key: string }) => {
16✔
464
            const device = this.deviceManager.getDevice(deviceOrItem?.key);
×
465
            if (!device) {
×
466
                void vscode.window.showErrorMessage('Could not find device to add to settings.');
×
467
                return;
×
468
            }
469

470
            const config = vscode.workspace.getConfiguration('brightscript');
×
471
            const inspection = config.inspect<ConfiguredDevice[]>('devices');
×
472
            const userDevices = inspection?.globalValue || [];
×
473

474
            if (userDevices.some(d => d.host === device.ip || (device.serialNumber && d.serialNumber === device.serialNumber))) {
×
475
                void vscode.window.showInformationMessage('Device is already in your user settings.');
×
476
                return;
×
477
            }
478

479
            // Copy any cred-store-cached password into the settings entry so the device
480
            // is portable across machines via Settings Sync. The cred store keeps its own
481
            // copy — it's a running cache of validated passwords that gets refreshed on
482
            // each successful password validation.
483
            const storedPassword = device.serialNumber
×
484
                ? await this.credentialStore.getPassword(device.serialNumber)
485
                : undefined;
486

487
            const newDevice = {
×
488
                host: device.ip,
489
                ...(device.serialNumber && { serialNumber: device.serialNumber }),
×
490
                ...(storedPassword && { password: storedPassword })
×
491
            };
492
            userDevices.push(newDevice);
×
493

494
            await config.update('devices', userDevices, vscode.ConfigurationTarget.Global);
×
495
            const displayName = device.deviceInfo['user-device-name'] || device.deviceInfo['default-device-name'] || device.ip;
×
496
            void vscode.window.showInformationMessage(`Added "${displayName}" to user settings.`);
×
497
        });
498

499
        this.registerCommand('addDeviceToWorkspaceSettings', async (deviceOrItem: { key: string }) => {
16✔
500
            const device = this.deviceManager.getDevice(deviceOrItem?.key);
×
501
            if (!device) {
×
502
                void vscode.window.showErrorMessage('Could not find device to add to settings.');
×
503
                return;
×
504
            }
505

506
            const config = vscode.workspace.getConfiguration('brightscript');
×
507
            const inspection = config.inspect<ConfiguredDevice[]>('devices');
×
508
            const workspaceDevices = inspection?.workspaceValue || [];
×
509

510
            if (workspaceDevices.some(d => (device.serialNumber && d.serialNumber === device.serialNumber))) {
×
511
                void vscode.window.showInformationMessage('Device is already in your workspace settings.');
×
512
                return;
×
513
            }
514

515
            const storedPassword = device.serialNumber
×
516
                ? await this.credentialStore.getPassword(device.serialNumber)
517
                : undefined;
518

519
            const newDevice = {
×
520
                host: device.ip,
521
                ...(device.serialNumber && { serialNumber: device.serialNumber }),
×
522
                ...(storedPassword && { password: storedPassword })
×
523
            };
524
            workspaceDevices.push(newDevice);
×
525

526
            await config.update('devices', workspaceDevices, vscode.ConfigurationTarget.Workspace);
×
527
            const displayName = device.deviceInfo['user-device-name'] || device.deviceInfo['default-device-name'] || device.ip;
×
528
            void vscode.window.showInformationMessage(`Added "${displayName}" to workspace settings.`);
×
529
        });
530

531
        this.registerCommand('clearDefaultDevicePassword', async () => {
16✔
532
            await vscode.workspace.getConfiguration('brightscript').update('defaultDevicePassword', undefined, vscode.ConfigurationTarget.Global);
2✔
533
            await util.showTimedNotification('Default device password cleared.');
2✔
534
        });
535

536
        this.registerCommand('setDefaultDevicePassword', async () => {
16✔
537
            const currentValue = vscode.workspace.getConfiguration('brightscript').get<string>('defaultDevicePassword') ?? '';
3!
538

539
            const password = await vscode.window.showInputBox({
3✔
540
                placeHolder: 'Enter the default developer password (applied to devices without their own password)',
541
                password: true,
542
                value: currentValue,
543
                prompt: 'Set default device password'
544
            });
545

546
            if (password === undefined) {
3✔
547
                return;
1✔
548
            }
549

550
            //this value is only supported at the global level, so just always write it there
551
            await vscode.workspace.getConfiguration('brightscript').update('defaultDevicePassword', password, vscode.ConfigurationTarget.Global);
2✔
552
        });
553

554
        this.registerCommand('setDevicePassword', async (serialNumber: string) => {
16✔
555
            if (!serialNumber) {
×
556
                throw new Error('Device serial number is required to set password.');
×
557
            }
558

559
            const device = this.deviceManager.getDevice({ serialNumber: serialNumber });
×
560
            const displayName = device?.deviceInfo?.['user-device-name'] || device?.deviceInfo?.['default-device-name'] || device?.ip || serialNumber;
×
561

562
            const password = await vscode.window.showInputBox({
×
563
                placeHolder: 'Enter the developer account password for this device',
564
                password: true,
565
                prompt: `Set password for device: ${displayName}`,
566
                // Roku's own webserver UI enforces the same 4-character minimum.
567
                validateInput: (value) => {
568
                    return value.length < 4 ? 'Password must be at least 4 characters' : undefined;
×
569
                }
570
            });
571

572
            if (password !== undefined) {
×
573
                await this.setDevicePassword(serialNumber, password);
×
574
                await vscode.window.showInformationMessage(`Password set for device: ${displayName}`);
×
575
            }
576
        });
577

578
        this.registerCommand('clearDevicePassword', async (serialNumber: string) => {
16✔
579
            if (!serialNumber) {
×
580
                throw new Error('Device serial number is required to clear password.');
×
581
            }
582

583
            const device = this.deviceManager.getDevice({ serialNumber: serialNumber });
×
584
            const displayName = device?.deviceInfo?.['user-device-name'] || device?.deviceInfo?.['default-device-name'] || device?.ip || serialNumber;
×
585

586
            await this.setDevicePassword(serialNumber, '');
×
587
            await vscode.window.showInformationMessage(`Password cleared for device: ${displayName}`);
×
588
        });
589

590
        this.registerCommand('clearActiveDevice', async () => {
16✔
591
            await this.context.workspaceState.update('remoteHost', '');
×
592
            await vscodeContextManager.set('activeHost', '');
×
593
            await util.showTimedNotification('Active device cleared');
×
594
        });
595

596
        this.registerCommand('showReleaseNotes', () => {
16✔
597
            this.whatsNewManager.showReleaseNotes();
×
598
        });
599
    }
600

601
    public async openFile(filename: string, range: vscode.Range = null, preview = false): Promise<boolean> {
×
602
        let uri = vscode.Uri.file(filename);
×
603
        try {
×
604
            let doc = await vscode.workspace.openTextDocument(uri); // calls back into the provider
×
605
            await vscode.window.showTextDocument(doc, { preview: preview });
×
606
            if (range) {
×
607
                await this.gotoRange(range);
×
608
            }
609
        } catch (e) {
610
            return false;
×
611
        }
612
        return true;
×
613
    }
614

615
    private async gotoRange(range: vscode.Range) {
616
        let editor = vscode.window.activeTextEditor;
×
617
        editor.selection = new vscode.Selection(
×
618
            range.start.line,
619
            range.start.character,
620
            range.start.line,
621
            range.start.character
622
        );
623
        await vscode.commands.executeCommand('revealLine', {
×
624
            lineNumber: range.start.line,
625
            at: 'center'
626
        });
627
    }
628

629
    public async onToggleXml() {
630
        if (vscode.window.activeTextEditor) {
3✔
631
            const currentDocument = vscode.window.activeTextEditor.document;
2✔
632
            let alternateFileName = this.fileUtils.getAlternateFileName(currentDocument.fileName);
2✔
633
            if (alternateFileName) {
2✔
634
                if (
1!
635
                    !await this.openFile(alternateFileName) &&
2✔
636
                    alternateFileName.toLowerCase().endsWith('.brs')
637
                ) {
638
                    await this.openFile(this.fileUtils.getBsFileName(alternateFileName));
×
639
                }
640
            }
641
        }
642
    }
643

644
    public async onGoToParentComponent() {
645
        const editor = vscode.window.activeTextEditor;
4✔
646
        if (!editor) {
4✔
647
            return;
1✔
648
        }
649
        const currentDocument = editor.document;
3✔
650
        const fileName = currentDocument.fileName;
3✔
651
        const lowerFileName = fileName.toLowerCase();
3✔
652
        const isXml = lowerFileName.endsWith('.xml');
3✔
653
        const isBrs = lowerFileName.endsWith('.brs') || lowerFileName.endsWith('.bs');
3✔
654

655
        if (!isXml && !isBrs) {
3✔
656
            return;
1✔
657
        }
658

659
        // Get or open the XML document
660
        let xmlDoc: vscode.TextDocument;
661
        if (isXml) {
2!
662
            xmlDoc = currentDocument;
2✔
663
        } else {
664
            const xmlFileName = this.fileUtils.getAlternateFileName(fileName);
×
665
            if (!xmlFileName) {
×
666
                return;
×
667
            }
668
            try {
×
669
                xmlDoc = await vscode.workspace.openTextDocument(vscode.Uri.file(xmlFileName));
×
670
            } catch (e) {
671
                return;
×
672
            }
673
        }
674

675
        const xmlContent = xmlDoc.getText();
2✔
676
        const parentName = this.fileUtils.getParentComponentName(xmlContent);
2✔
677
        if (!parentName) {
2✔
678
            await vscode.window.showInformationMessage('No parent component found');
1✔
679
            return;
1✔
680
        }
681

682
        const extendsPosition = this.getExtendsValuePosition(xmlContent, xmlDoc);
1✔
683
        if (!extendsPosition) {
1!
684
            return;
×
685
        }
686

687
        // Delegate to the definition provider via the LSP
688
        const locations = await vscode.commands.executeCommand<vscode.Location[]>(
1✔
689
            'vscode.executeDefinitionProvider',
690
            xmlDoc.uri,
691
            extendsPosition
692
        );
693

694
        if (!locations || locations.length === 0) {
1!
695
            await vscode.window.showInformationMessage(`Could not find parent component: ${parentName}`);
×
696
            return;
×
697
        }
698

699
        const parentXmlPath = locations[0].uri.fsPath;
1✔
700

701
        if (isBrs) {
1!
702
            const parentBrsPath = this.fileUtils.getAlternateFileName(parentXmlPath);
×
703
            if (parentBrsPath && !await this.openFile(parentBrsPath)) {
×
704
                await this.openFile(this.fileUtils.getBsFileName(parentBrsPath));
×
705
            }
706
        } else {
707
            await this.openFile(parentXmlPath);
1✔
708
        }
709
    }
710

711
    private getExtendsValuePosition(xmlContent: string, xmlDoc: vscode.TextDocument): vscode.Position | undefined {
712
        // Match extends="VALUE" capturing the VALUE portion; [^>]+ spans across lines since [^>] matches \n
713
        const match = /<component[^>]+extends\s*=\s*["']([^"']+)/i.exec(xmlContent);
1✔
714
        if (!match) {
1!
715
            return undefined;
×
716
        }
717
        // Offset to first character of the value (after the opening quote)
718
        const valueOffset = match.index + match[0].length - match[1].length;
1✔
719
        return xmlDoc.positionAt(valueOffset);
1✔
720
    }
721

722
    public async restartDevApplication() {
723
        await this.getRemoteHost();
3✔
724
        const host = this.host;
3✔
725
        if (!host) {
3!
726
            return;
×
727
        }
728

729
        await util.spinAsync('Restarting dev app', async () => {
3✔
730
            const appsResponse = await util.httpGet(`http://${host}:8060/query/apps`, { timeout: 5_000 });
3✔
731
            const appsParsed = await xml2js.parseStringPromise(appsResponse.body as string);
3✔
732
            const appList: Array<{ $?: { id?: string } }> = appsParsed?.apps?.app ?? [];
3!
733
            const hasDev = appList.some(entry => entry.$?.id === 'dev');
3!
734
            if (!hasDev) {
3✔
735
                await vscode.window.showErrorMessage(`No dev channel sideloaded on ${host}. Sideload your project before restarting.`);
1✔
736
                return;
1✔
737
            }
738

739
            // `/true` forces a full terminate even if the channel is suspended in the background via Instant Resume.
740
            // Harmless if dev isn't running — the device just returns FAILED in the body.
741
            await this.ecpPost(host, 'exit-app/dev/true');
2✔
742

743
            const launchResponse = await this.ecpPost(host, 'launch/dev');
2✔
744
            if (launchResponse.statusCode !== 200) {
2!
745
                await vscode.window.showErrorMessage(`Failed to launch dev channel on ${host} (HTTP ${launchResponse.statusCode}).`);
×
746
                return;
×
747
            }
748

749
            // give a little bit of time to let the app boot up before checking its status
750
            await util.sleep(1000);
2✔
751
            const verifyResponse = await util.httpGet(`http://${host}:8060/query/active-app`, { timeout: 5_000 });
2✔
752
            const verifyParsed = await xml2js.parseStringPromise(verifyResponse.body as string);
2✔
753
            const verifyAppId: string | undefined = verifyParsed?.['active-app']?.app?.[0]?.$?.id;
2!
754
            if (verifyAppId === 'dev') {
2✔
755
                void util.showTimedNotification('Dev app restarted', 2000);
1✔
756
            } else {
757
                await vscode.window.showWarningMessage(`Sent the dev launch command, but the foreground app is "${verifyAppId ?? 'unknown'}". The dev app may still be loading.`);
1!
758
            }
759
        });
760
    }
761

762
    private ecpPost(host: string, path: string) {
763
        return new Promise<request.Response>((resolve, reject) => {
×
764
            request.post(`http://${host}:8060/${path}`, (err: Error | null, response: request.Response) => {
×
765
                return err ? reject(err) : resolve(response);
×
766
            });
767
        });
768
    }
769

770
    /**
771
     * Restart a Roku device. When `host` is omitted (e.g. invoked from the command palette)
772
     * a device picker is shown. Resolves and validates the developer password the same way a
773
     * debug launch does before issuing the reboot.
774
     */
775
    public async restartDevice(host?: string): Promise<void> {
776
        const target = await this.resolveDeviceHost(host);
11✔
777
        if (!target) {
11✔
778
            return;
1✔
779
        }
780

781
        const confirm = await vscode.window.showWarningMessage(
10✔
782
            `Are you sure you want to restart ${target.label}? This will close all running channels.`,
783
            { modal: true },
784
            'Restart'
785
        );
786
        if (confirm !== 'Restart') {
10✔
787
            return;
1✔
788
        }
789

790
        const password = await this.resolveValidatedPassword(target.host, target.serialNumber);
9✔
791
        if (password === undefined) {
9✔
792
            return;
2✔
793
        }
794

795
        try {
7✔
796
            await rokuDeploy.rebootDevice({ host: target.host, password: password, timeout: 10000 });
7✔
797
            void vscode.window.showInformationMessage(`Restart initiated on ${target.label}`);
6✔
798
        } catch (e) {
799
            void vscode.window.showErrorMessage(`Failed to restart device: ${e.message}`);
1✔
800
        }
801
    }
802

803
    /**
804
     * Ask a Roku device to check for and install any available software updates. Host and
805
     * password are resolved the same way as `restartDevice`.
806
     */
807
    public async checkForUpdates(host?: string): Promise<void> {
808
        const target = await this.resolveDeviceHost(host);
2✔
809
        if (!target) {
2!
NEW
810
            return;
×
811
        }
812

813
        const confirm = await vscode.window.showInformationMessage(
2✔
814
            `Check for software updates on ${target.label}? The device will check for and install any available updates.`,
815
            { modal: true },
816
            'Check for Updates'
817
        );
818
        if (confirm !== 'Check for Updates') {
2✔
819
            return;
1✔
820
        }
821

822
        const password = await this.resolveValidatedPassword(target.host, target.serialNumber);
1✔
823
        if (password === undefined) {
1!
NEW
824
            return;
×
825
        }
826

827
        try {
1✔
828
            await rokuDeploy.checkForUpdate({ host: target.host, password: password, timeout: 10000 });
1✔
829
            void vscode.window.showInformationMessage(`Software update check initiated on ${target.label}`);
1✔
830
        } catch (e) {
NEW
831
            void vscode.window.showErrorMessage(`Failed to check for updates: ${e.message}`);
×
832
        }
833
    }
834

835
    /**
836
     * Resolve the device a device-targeted command should act on.
837
     *
838
     * When `host` is provided (e.g. from a Devices view tree item) it is used directly.
839
     * Otherwise the device picker is always shown; these are device-specific actions, so we
840
     * never silently fall back to the active device. The resolved host is probed so the caller
841
     * has a fresh serial number for password lookup and a friendly display label for prompts.
842
     *
843
     * Returns undefined when the user cancels device selection.
844
     */
845
    private async resolveDeviceHost(host?: string): Promise<{ host: string; serialNumber: string | undefined; label: string } | undefined> {
846
        if (!host) {
13✔
847
            try {
2✔
848
                host = await this.userInputManager.promptForHost();
2✔
849
            } catch {
850
                // promptForHost rejects when the user dismisses the picker; treat as a cancel.
851
                return undefined;
1✔
852
            }
853
        }
854
        if (!host) {
12!
NEW
855
            return undefined;
×
856
        }
857

858
        const device = await this.deviceManager.validateAndAddDevice(host);
12✔
859
        const label = device ? this.deviceManager.getDeviceDisplayName(device, true) : host;
12!
860
        return { host: host, serialNumber: device?.serialNumber, label: label };
12!
861
    }
862

863
    /**
864
     * Resolve a developer password that the device actually accepts, mirroring the debug
865
     * launch flow: try every known credential source in order, validate each against the
866
     * device, and prompt (re-prompting after a rejection) when none are accepted.
867
     *
868
     * The accepted password is persisted following the same rules as a debug launch, so
869
     * subsequent commands resolve without re-prompting.
870
     *
871
     * Returns the validated password, or undefined when the device is unreachable or the
872
     * user cancels the prompt.
873
     */
874
    private async resolveValidatedPassword(host: string, serialNumber: string | undefined): Promise<string | undefined> {
875
        const candidates = await this.collectPasswordCandidates(serialNumber);
10✔
876
        for (const candidate of candidates) {
10✔
877
            const validation = await this.deviceManager.validateDevicePassword(host, candidate);
8✔
878
            if (validation === 'ok') {
8✔
879
                await this.persistAcceptedPassword(serialNumber, candidate);
7✔
880
                return candidate;
7✔
881
            }
882
            if (validation === 'unreachable') {
1!
883
                void vscode.window.showErrorMessage(`Device at ${host} is unreachable.`);
1✔
884
                return undefined;
1✔
885
            }
886
            // 'bad-password' — fall through to the next candidate
887
        }
888

889
        // No stored / configured candidate was accepted. Prompt, re-prompting after each
890
        // bad-password attempt until the user enters a working one or cancels (empty / Esc).
891
        let placeholder = candidates.length > 0
2!
892
            ? 'The password was rejected by the device. Try again, or press Esc to cancel.'
893
            : 'The Roku development webserver password.';
894
        while (true) {
2✔
895
            const value = await this.promptForDevicePassword(placeholder);
3✔
896
            if (!value) {
3✔
897
                return undefined;
1✔
898
            }
899
            const validation = await this.deviceManager.validateDevicePassword(host, value);
2✔
900
            if (validation === 'ok') {
2✔
901
                await this.persistAcceptedPassword(serialNumber, value);
1✔
902
                return value;
1✔
903
            }
904
            if (validation === 'unreachable') {
1!
NEW
905
                void vscode.window.showErrorMessage(`Device at ${host} is unreachable.`);
×
NEW
906
                return undefined;
×
907
            }
908
            placeholder = 'The password was rejected by the device. Try again, or press Esc to cancel.';
1✔
909
        }
910
    }
911

912
    /**
913
     * Build the ordered, de-duplicated list of candidate passwords to try when resolving
914
     * credentials for a device-targeted command. Variable placeholders and empty values are
915
     * filtered out so the validation loop only sees real passwords.
916
     */
917
    private async collectPasswordCandidates(serialNumber: string | undefined): Promise<string[]> {
918
        const candidates: string[] = [];
10✔
919
        const addCandidate = (value: string | undefined | null) => {
10✔
920
            const trimmed = value?.trim();
30✔
921
            // eslint-disable-next-line no-template-curly-in-string
922
            if (!trimmed || trimmed === '${promptForPassword}' || trimmed === '${activeHostPassword}') {
30✔
923
                return;
22✔
924
            }
925
            candidates.push(trimmed);
8✔
926
        };
927

928
        if (serialNumber) {
10!
929
            addCandidate(await this.credentialStore.getPassword(serialNumber));
10✔
930

931
            const scanScope = (devices: ConfiguredDevice[] | undefined) => {
10✔
932
                for (const entry of devices ?? []) {
20!
NEW
933
                    if (entry.serialNumber === serialNumber) {
×
NEW
934
                        addCandidate(entry.password);
×
935
                    }
936
                }
937
            };
938
            const rootInspection = vscode.workspace.getConfiguration('brightscript').inspect<ConfiguredDevice[]>('devices');
10✔
939
            scanScope(rootInspection?.globalValue);
10!
940
            scanScope(rootInspection?.workspaceValue);
10!
941
            for (const folder of vscode.workspace.workspaceFolders ?? []) {
10!
NEW
942
                const folderInspection = vscode.workspace.getConfiguration('brightscript', folder.uri).inspect<ConfiguredDevice[]>('devices');
×
NEW
943
                scanScope(folderInspection?.workspaceFolderValue);
×
944
            }
945
        }
946

947
        addCandidate(this.deviceManager.getDefaultPassword());
10✔
948
        addCandidate(await this.context.workspaceState.get('remotePassword'));
10✔
949

950
        // Dedupe while preserving insertion order so a password referenced by multiple
951
        // sources is only validated once.
952
        return Array.from(new Set(candidates));
10✔
953
    }
954

955
    /**
956
     * Persist an accepted password the same way a debug launch does: refresh the credential
957
     * store only when an entry already exists for this serial (persisting is an explicit
958
     * opt-in via `setDevicePassword`/settings), and update the global `remotePassword` fallback.
959
     */
960
    private async persistAcceptedPassword(serialNumber: string | undefined, password: string): Promise<void> {
961
        if (serialNumber && (await this.credentialStore.getPassword(serialNumber)) !== undefined) {
8✔
962
            await this.credentialStore.setPassword(serialNumber, password);
5✔
963
        }
964
        await this.context.workspaceState.update('remotePassword', password);
8✔
965
    }
966

967
    /**
968
     * Password input dialog. Returns the typed value, or undefined on Esc / hide.
969
     */
970
    private async promptForDevicePassword(placeholder: string): Promise<string | undefined> {
NEW
971
        const input = vscode.window.createInputBox();
×
NEW
972
        input.placeholder = placeholder;
×
NEW
973
        input.password = true;
×
NEW
974
        try {
×
NEW
975
            return await new Promise<string | undefined>(resolve => {
×
NEW
976
                input.onDidAccept(() => {
×
NEW
977
                    resolve(input.value);
×
NEW
978
                    input.hide();
×
979
                });
NEW
980
                input.onDidHide(() => {
×
NEW
981
                    resolve(undefined);
×
982
                });
NEW
983
                input.show();
×
984
            });
985
        } finally {
NEW
986
            input.dispose();
×
987
        }
988
    }
989

990
    public async sendRemoteCommand(key: string, host?: string, literalCharacter = false) {
×
991
        for (const notifier of this.keypressNotifiers) {
×
992
            notifier(key, literalCharacter);
×
993
        }
994

995
        if (literalCharacter) {
×
996
            key = 'Lit_' + encodeURIComponent(key);
×
997
        }
998

999
        // do we have a temporary override?
1000
        if (!host) {
×
1001
            // Get the long lived host ip
1002
            await this.getRemoteHost();
×
1003
            host = this.host;
×
1004
        }
1005

1006
        if (host) {
×
1007
            let clickUrl = `http://${host}:8060/keypress/${key}`;
×
1008
            console.log(`send ${clickUrl}`);
×
1009
            return new Promise((resolve, reject) => {
×
1010
                request.post(clickUrl, (err, response) => {
×
1011
                    if (err) {
×
1012
                        return reject(err);
×
1013
                    }
1014
                    return resolve(response);
×
1015
                });
1016
            });
1017
        }
1018
    }
1019

1020
    public async getRemoteHost(showPrompt = true) {
×
1021
        this.host = await this.context.workspaceState.get('remoteHost');
×
1022
        if (!this.host) {
×
1023
            let config = util.getConfiguration('brightscript.remoteControl');
×
1024
            this.host = config.get('host');
×
1025
            // eslint-disable-next-line no-template-curly-in-string
1026
            if ((!this.host || this.host === '${promptForHost}') && showPrompt) {
×
1027
                this.host = await this.userInputManager.promptForHost();
×
1028
            }
1029
        }
1030
        if (!this.host) {
×
1031
            throw new Error('Can\'t send command: host is required.');
×
1032
        } else {
1033
            await this.context.workspaceState.update('remoteHost', this.host);
×
1034
        }
1035
        if (this.host) {
×
1036
            //try resolving the hostname. (sometimes it fails for no reason, so just ignore the crash if it does)
1037
            try {
×
1038
                this.host = await rokuDebugUtil.dnsLookup(this.host);
×
1039
            } catch (e) {
1040
                console.error('Error doing dns lookup for host ', this.host, e);
×
1041
            }
1042
        }
1043
        return this.host;
×
1044
    }
1045

1046
    public async getRemotePassword(showPrompt = true) {
×
1047
        this.password = await this.context.workspaceState.get('remotePassword');
×
1048
        if (!this.password) {
×
1049
            let config = util.getConfiguration('brightscript.remoteControl');
×
1050
            this.password = config.get('password');
×
1051
            // eslint-disable-next-line no-template-curly-in-string
1052
            if ((!this.password || this.password === '${promptForPassword}') && showPrompt) {
×
1053
                this.password = await vscode.window.showInputBox({
×
1054
                    placeHolder: 'The developer account password for your Roku device',
1055
                    value: ''
1056
                });
1057
            }
1058
        }
1059
        if (!this.password) {
×
1060
            throw new Error(`Can't send command: password is required.`);
×
1061
        } else {
1062
            await this.context.workspaceState.update('remotePassword', this.password);
×
1063
        }
1064
        return this.password;
×
1065
    }
1066

1067
    public async getWorkspacePath() {
1068
        this.workspacePath = await this.context.workspaceState.get('workspacePath');
×
1069
        //let folderUri: vscode.Uri;
1070
        if (!this.workspacePath) {
×
1071
            if (vscode.workspace.workspaceFolders?.length === 1) {
×
1072
                this.workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
×
1073
            } else {
1074
                //there are multiple workspaces, ask the user to specify which one they want to use
1075
                let workspaceFolder = await vscode.window.showWorkspaceFolderPick();
×
1076
                if (workspaceFolder) {
×
1077
                    this.workspacePath = workspaceFolder.uri.fsPath;
×
1078
                }
1079
            }
1080
        }
1081
        return this.workspacePath;
×
1082
    }
1083

1084
    /**
1085
     * Store a password for a specific device, keyed by serial number.
1086
     * An empty password clears the stored entry.
1087
     *
1088
     * Writes the password to every `brightscript.devices[]` settings entry that
1089
     * matches the SN (user/workspace/workspace-folder scopes) and also keeps the
1090
     * cred store updated. The cred store acts as a running cache of passwords —
1091
     * kept fresh alongside settings here, and refreshed on each successful
1092
     * password validation elsewhere.
1093
     */
1094
    public async setDevicePassword(serialNumber: string, password: string) {
1095
        await this.writeDevicePasswordToSettings(serialNumber, password);
×
1096
        if (password) {
×
1097
            await this.credentialStore.setPassword(serialNumber, password);
×
1098
        } else {
1099
            await this.credentialStore.clearPassword(serialNumber);
×
1100
        }
1101
    }
1102

1103
    /**
1104
     * Update/clear the `password` field on any `brightscript.devices[]` entries
1105
     * whose `serialNumber` matches. Writes to every writable scope that contains
1106
     * a matching entry — user (Global), workspace, and each workspace folder.
1107
     * The default (package.json) scope is read-only and is never written.
1108
     * Empty password removes the field rather than writing an empty string.
1109
     * Returns true when at least one scope contained a matching entry.
1110
     */
1111
    private async writeDevicePasswordToSettings(serialNumber: string, password: string): Promise<boolean> {
1112
        if (!serialNumber) {
×
1113
            return false;
×
1114
        }
1115

1116
        const scopeHasMatch = (devices: ConfiguredDevice[] | undefined): boolean => !!devices?.some(entry => entry.serialNumber === serialNumber);
×
1117

1118
        const rewriteEntries = (devices: ConfiguredDevice[]): ConfiguredDevice[] => devices.map(entry => {
×
1119
            if (entry.serialNumber !== serialNumber) {
×
1120
                return entry;
×
1121
            }
1122
            if (password) {
×
1123
                return { ...entry, password: password };
×
1124
            }
1125
            const { password: _existingPassword, ...entryWithoutPassword } = entry;
×
1126
            return entryWithoutPassword;
×
1127
        });
1128

1129
        let found = false;
×
1130

1131
        // User (Global) + workspace scopes — resource-agnostic
1132
        const rootConfig = vscode.workspace.getConfiguration('brightscript');
×
1133
        const rootInspection = rootConfig.inspect<ConfiguredDevice[]>('devices');
×
1134

1135
        if (scopeHasMatch(rootInspection?.globalValue)) {
×
1136
            found = true;
×
1137
            await rootConfig.update('devices', rewriteEntries(rootInspection.globalValue), vscode.ConfigurationTarget.Global);
×
1138
        }
1139
        if (scopeHasMatch(rootInspection?.workspaceValue)) {
×
1140
            found = true;
×
1141
            await rootConfig.update('devices', rewriteEntries(rootInspection.workspaceValue), vscode.ConfigurationTarget.Workspace);
×
1142
        }
1143

1144
        // Workspace-folder scope — one setting per folder in multi-root workspaces
1145
        const workspaceFolders = vscode.workspace.workspaceFolders ?? [];
×
1146
        for (const folder of workspaceFolders) {
×
1147
            const folderConfig = vscode.workspace.getConfiguration('brightscript', folder.uri);
×
1148
            const folderInspection = folderConfig.inspect<ConfiguredDevice[]>('devices');
×
1149
            if (scopeHasMatch(folderInspection?.workspaceFolderValue)) {
×
1150
                found = true;
×
1151
                await folderConfig.update('devices', rewriteEntries(folderInspection.workspaceFolderValue), vscode.ConfigurationTarget.WorkspaceFolder);
×
1152
            }
1153
        }
1154

1155
        return found;
×
1156
    }
1157

1158
    /**
1159
     * Get the stored password for a specific device by serial number.
1160
     */
1161
    public async getDevicePassword(serialNumber: string | undefined): Promise<string | undefined> {
1162
        return this.credentialStore.getPassword(serialNumber);
×
1163
    }
1164

1165
    /**
1166
     * Get the password for the currently active device.
1167
     * Resolves the active host's IP to a serial number via DeviceManager,
1168
     * then reads from the SN-keyed credential store. Falls back to the
1169
     * workspace-global password when no per-device entry exists.
1170
     */
1171
    public async getActiveHostPassword(): Promise<string | undefined> {
1172
        const activeHost = this.context.workspaceState.get<string>('remoteHost');
×
1173
        if (activeHost && typeof activeHost === 'string') {
×
1174
            const serialNumber = this.deviceManager.getDevice({ ip: activeHost })?.serialNumber;
×
1175
            const devicePassword = await this.credentialStore.getPassword(serialNumber);
×
1176
            if (devicePassword) {
×
1177
                return devicePassword;
×
1178
            }
1179
        }
1180
        return this.getRemotePassword(false);
×
1181
    }
1182

1183
    /**
1184
     * Return the active host IP if one is set and passes a health check; otherwise undefined.
1185
     */
1186
    public async getHealthyActiveHost(): Promise<string | undefined> {
1187
        const activeHost = vscodeContextManager.get<string>('activeHost');
×
1188
        if (!activeHost) {
×
1189
            return undefined;
×
1190
        }
1191
        const isHealthy = await this.deviceManager.healthCheckDevice({ ip: activeHost }, true, false);
×
1192
        return isHealthy ? activeHost : undefined;
×
1193
    }
1194

1195
    /**
1196
     * Open the settings JSON file and position cursor at the specified device entry
1197
     */
1198
    private async openSettingsJsonAtDevice(device: RokuDevice | undefined, scope: 'user' | 'workspace'): Promise<void> {
1199
        // Open the appropriate settings JSON file
1200
        const command = scope === 'user'
×
1201
            ? 'workbench.action.openSettingsJson'
1202
            : 'workbench.action.openWorkspaceSettingsFile';
1203
        await vscode.commands.executeCommand(command);
×
1204

1205
        // Get the active editor (should be the settings file we just opened)
1206
        const editor = vscode.window.activeTextEditor;
×
1207
        if (!editor || !device) {
×
1208
            return;
×
1209
        }
1210

1211
        const text = editor.document.getText();
×
1212

1213
        // Search for the device by IP or serial number
1214
        const searchTerms = [device.ip];
×
1215
        if (device.serialNumber) {
×
1216
            searchTerms.push(device.serialNumber);
×
1217
        }
1218

1219
        let matchIndex = -1;
×
1220
        for (const term of searchTerms) {
×
1221
            const index = text.indexOf(`"${term}"`);
×
1222
            if (index !== -1) {
×
1223
                matchIndex = index;
×
1224
                break;
×
1225
            }
1226
        }
1227

1228
        if (matchIndex !== -1) {
×
1229
            const position = editor.document.positionAt(matchIndex + 1); // +1 to skip opening quote
×
1230
            const selection = new vscode.Selection(position, position);
×
1231
            editor.selection = selection;
×
1232
            editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter);
×
1233
        }
1234
    }
1235

1236
    public registerKeypressNotifier(notifier: (key: string, literalCharacter: boolean) => void) {
1237
        this.keypressNotifiers.push(notifier);
14✔
1238
    }
1239

1240
    private registerCommand(name: string, callback: (...args: any[]) => any, thisArg?: any) {
1241
        const prefix = 'extension.brightscript.';
2,822✔
1242
        const commandName = name.startsWith(prefix) ? name : prefix + name;
2,822✔
1243
        this.context.subscriptions.push(vscode.commands.registerCommand(commandName, callback, thisArg));
2,822✔
1244
    }
1245

1246
    /**
1247
     * Register the per-facet toggle commands plus the reset command backing the Devices
1248
     * view filter submenu. Each facet has two command variants (unchecked + ".active");
1249
     * the submenu picks which to render via a `when` clause on the per-facet context key.
1250
     * Both call the same toggle handler.
1251
     */
1252
    public registerDevicesViewCommands(devicesViewProvider: DevicesViewProvider) {
1253
        for (const key of DEVICE_FILTER_KEYS) {
14✔
1254
            const handler = () => devicesViewProvider.toggleFilter(key);
126✔
1255
            this.registerCommand(`devicesView.toggleFilter.${key}`, handler);
126✔
1256
            this.registerCommand(`devicesView.toggleFilter.${key}.active`, handler);
126✔
1257
        }
1258
        this.registerCommand('devicesView.resetFilters', () => devicesViewProvider.resetFilters());
14✔
1259
        // These also appear in the command palette, where there's no tree element; resolving
1260
        // a missing/unknown key to undefined lets `restartDevice`/`checkForUpdates` fall back
1261
        // to the active device or device picker.
1262
        this.registerCommand('devicesView.restartDevice', (element?: { key?: string }) => {
14✔
1263
            return this.restartDevice(element?.key ? this.deviceManager.getDevice(element.key)?.ip : undefined);
2!
1264
        });
1265
        this.registerCommand('devicesView.checkAndInstallUpdates', (element?: { key?: string }) => {
14✔
1266
            return this.checkForUpdates(element?.key ? this.deviceManager.getDevice(element.key)?.ip : undefined);
1!
1267
        });
1268
    }
1269

1270
    private async sendAsciiToDevice(character: string) {
1271
        await this.sendRemoteCommand(character, undefined, true);
×
1272
    }
1273
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc