From 9088df07ed0fff0b292816cfff04ed70d0afdcf2 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Mon, 16 Jun 2025 09:38:07 -0500 Subject: [PATCH 1/2] refactor: convert edge:channels:unassign command to yargs --- .../src/commands/edge/channels/unassign.ts | 73 ------ .../commands/edge/channels/delete.test.ts | 6 +- .../commands/edge/channels/drivers.test.ts | 17 +- .../command/util/edge/channels-choose.test.ts | 234 +++++------------- src/commands/edge/channels/delete.ts | 3 +- src/commands/edge/channels/drivers.ts | 7 +- src/commands/edge/channels/invites/create.ts | 2 +- src/commands/edge/channels/invites/delete.ts | 7 +- src/commands/edge/channels/unassign.ts | 62 +++++ src/commands/index.ts | 2 + src/lib/command/util/edge/channels-choose.ts | 82 +++--- 11 files changed, 182 insertions(+), 313 deletions(-) delete mode 100644 packages/edge/src/commands/edge/channels/unassign.ts create mode 100644 src/commands/edge/channels/unassign.ts diff --git a/packages/edge/src/commands/edge/channels/unassign.ts b/packages/edge/src/commands/edge/channels/unassign.ts deleted file mode 100644 index 41ef8a8a..00000000 --- a/packages/edge/src/commands/edge/channels/unassign.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Flags } from '@oclif/core' - -import { DriverChannelDetails } from '@smartthings/core-sdk' - -import { APICommand, ChooseOptions, chooseOptionsWithDefaults, selectFromList, SelectFromListConfig, stringTranslateToId } from '@smartthings/cli-lib' - -import { chooseChannel } from '../../../lib/commands/channels-util.js' -import { EdgeCommand } from '../../../lib/edge-command.js' - - -export type NamedDriverChannelDetails = DriverChannelDetails & { - name: string -} - -export async function chooseAssignedDriver(command: APICommand, promptMessage: string, - channelId: string, commandLineDriverId?: string, options?: Partial>): Promise { - const opts = chooseOptionsWithDefaults(options) - const config: SelectFromListConfig = { - itemName: 'driver', - primaryKeyName: 'driverId', - sortKeyName: 'name', - } - const listItems = async (): Promise => { - const driverDetails = await command.client.channels.listAssignedDrivers(channelId) - return (await Promise.all(driverDetails.map(async details => { - try { - const driver = await command.client.drivers.get(details.driverId) - return { ...details, name: driver.name } - } catch (error) { - if (error.response?.status === 404) { - return { ...details, name: '' } - } - throw error - } - }))) - } - const preselectedId = opts.allowIndex - ? await stringTranslateToId(config, commandLineDriverId, listItems) - : commandLineDriverId - return selectFromList(command, config, { preselectedId, listItems, promptMessage }) -} - -export class ChannelsUnassignCommand extends EdgeCommand { - static description = 'remove a driver from a channel' + - this.apiDocsURL('deleteDriverChannel') - - static flags = { - ...EdgeCommand.flags, - channel: Flags.string({ - char: 'C', - description: 'channel id', - helpValue: '', - }), - } - - static args = [ - { - name: 'driverId', - description: 'driver id', - }, - ] - - async run(): Promise { - const channelId = await chooseChannel(this, 'Select a channel:', - this.flags.channel, { useConfigDefault: true }) - const driverId = await chooseAssignedDriver(this, 'Select a driver to remove from the selected channel:', - channelId, this.args.driverId) - - await this.client.channels.unassignDriver(channelId, driverId) - - this.log(`${driverId} removed from channel ${channelId}`) - } -} diff --git a/src/__tests__/commands/edge/channels/delete.test.ts b/src/__tests__/commands/edge/channels/delete.test.ts index 8d43ebab..521b28b1 100644 --- a/src/__tests__/commands/edge/channels/delete.test.ts +++ b/src/__tests__/commands/edge/channels/delete.test.ts @@ -74,7 +74,11 @@ test('handler', async () => { await expect(cmd.handler(inputArgv)).resolves.not.toThrow() expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(inputArgv) - expect(chooseChannelMock).toHaveBeenCalledExactlyOnceWith(command, 'Choose a channel to delete.', 'cmd-line-id') + expect(chooseChannelMock).toHaveBeenCalledExactlyOnceWith( + command, + 'cmd-line-id', + { promptMessage: 'Choose a channel to delete.' }, + ) expect(apiChannelsDeleteMock).toHaveBeenCalledExactlyOnceWith('chosen-channel-id') expect(resetManagedConfigKeyMock).toHaveBeenCalledExactlyOnceWith(cliConfig, 'defaultChannel', expect.any(Function)) expect(consoleLogSpy).toHaveBeenCalledWith('Channel chosen-channel-id deleted.') diff --git a/src/__tests__/commands/edge/channels/drivers.test.ts b/src/__tests__/commands/edge/channels/drivers.test.ts index ec414f80..a5f73d8d 100644 --- a/src/__tests__/commands/edge/channels/drivers.test.ts +++ b/src/__tests__/commands/edge/channels/drivers.test.ts @@ -5,8 +5,12 @@ import type { ArgumentsCamelCase, Argv } from 'yargs' import type { CommandArgs } from '../../../../commands/edge/channels/drivers.js' import type { APICommand, APICommandFlags } from '../../../../lib/command/api-command.js' import type { outputList, outputListBuilder } from '../../../../lib/command/output-list.js' -import type { DriverChannelDetailsWithName, listAssignedDriversWithNames } from '../../../../lib/command/util/edge-drivers.js' -import type { chooseChannel } from '../../../../lib/command/util/edge/channels-choose.js' +import type { + DriverChannelDetailsWithName, + listAssignedDriversWithNames, +} from '../../../../lib/command/util/edge-drivers.js' +import type { ChooseFunction } from '../../../../lib/command/util/util-util.js' +import type { ChannelChoice, chooseChannelFn } from '../../../../lib/command/util/edge/channels-choose.js' import { apiCommandMocks } from '../../../test-lib/api-command-mock.js' import { buildArgvMock, buildArgvMockStub } from '../../../test-lib/builder-mock.js' @@ -25,9 +29,10 @@ jest.unstable_mockModule('../../../../lib/command/util/edge-drivers.js', () => ( listAssignedDriversWithNames: listAssignedDriversWithNamesMock, })) -const chooseChannelMock = jest.fn() +const chooseChannelMock = jest.fn>() +const chooseChannelFnMock = jest.fn().mockReturnValue(chooseChannelMock) jest.unstable_mockModule('../../../../lib/command/util/edge/channels-choose.js', () => ({ - chooseChannel: chooseChannelMock, + chooseChannelFn: chooseChannelFnMock, })) @@ -74,11 +79,11 @@ test('handler', async () => { await expect(cmd.handler(inputArgv)).resolves.not.toThrow() expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(inputArgv) + expect(chooseChannelFnMock).toHaveBeenCalledExactlyOnceWith({ includeReadOnly: true }) expect(chooseChannelMock).toHaveBeenCalledExactlyOnceWith( command, - 'Select a channel.', 'cmd-line-id', - { allowIndex: true, includeReadOnly: true, useConfigDefault: true }, + { allowIndex: true, useConfigDefault: true }, ) expect(outputListMock).toHaveBeenCalledExactlyOnceWith( command, diff --git a/src/__tests__/lib/command/util/edge/channels-choose.test.ts b/src/__tests__/lib/command/util/edge/channels-choose.test.ts index f8d0cc66..2fe1b0d8 100644 --- a/src/__tests__/lib/command/util/edge/channels-choose.test.ts +++ b/src/__tests__/lib/command/util/edge/channels-choose.test.ts @@ -1,29 +1,16 @@ import { jest } from '@jest/globals' -import { ChannelsEndpoint, type Channel } from '@smartthings/core-sdk' +import type { ChannelsEndpoint, Channel, DriverChannelDetails } from '@smartthings/core-sdk' import type { APICommand } from '../../../../../lib/command/api-command.js' -import type { stringTranslateToId } from '../../../../../lib/command/command-util.js' -import type { ListDataFunction } from '../../../../../lib/command/io-defs.js' -import type { selectFromList } from '../../../../../lib/command/select.js' -import type { chooseOptionsWithDefaults } from '../../../../../lib/command/util/util-util.js' +import type { ChooseFunction, createChooseFn } from '../../../../../lib/command/util/util-util.js' import type { listChannels } from '../../../../../lib/command/util/edge/channels.js' -import type { ChannelChoice, ChooseChannelOptions } from '../../../../../lib/command/util/edge/channels-choose.js' +import type { ChannelChoice } from '../../../../../lib/command/util/edge/channels-choose.js' -const stringTranslateToIdMock = jest.fn() -jest.unstable_mockModule('../../../../../lib/command/command-util.js', () => ({ - stringTranslateToId: stringTranslateToIdMock, -})) - -const selectFromListMock = jest.fn().mockResolvedValue('chosen-channel-id') -jest.unstable_mockModule('../../../../../lib/command/select.js', () => ({ - selectFromList: selectFromListMock, -})) - -const chooseOptionsWithDefaultsMock = jest.fn>() +const createChooseFnMock = jest.fn>() jest.unstable_mockModule('../../../../../lib/command/util/util-util.js', () => ({ - chooseOptionsWithDefaults: chooseOptionsWithDefaultsMock, + createChooseFn: createChooseFnMock, })) const listChannelsMock = jest.fn() @@ -33,196 +20,91 @@ jest.unstable_mockModule('../../../../../lib/command/util/edge/channels.js', () const { - chooseChannelOptionsWithDefaults, - chooseChannel, + chooseChannelFn, } = await import('../../../../../lib/command/util/edge/channels-choose.js') -describe('chooseChannelOptionsWithDefaults', () => { - it('has a reasonable default', () => { - chooseOptionsWithDefaultsMock.mockReturnValue({} as ChooseChannelOptions) - - expect(chooseChannelOptionsWithDefaults()) - .toEqual(expect.objectContaining({ includeReadOnly: false })) - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith(undefined) - }) - - it('accepts true value', () => { - chooseOptionsWithDefaultsMock.mockReturnValue({ includeReadOnly: true } as ChooseChannelOptions) - - expect(chooseChannelOptionsWithDefaults({ includeReadOnly: true })) - .toEqual(expect.objectContaining({ includeReadOnly: true })) - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith({ includeReadOnly: true }) - }) -}) +describe('chooseChannelFn', () => { + const chooseChannelMock = jest.fn>() + createChooseFnMock.mockReturnValue(chooseChannelMock) -describe('chooseChannel', () => { - const channel = { channelId: 'channel-id', name: 'channel name' } as Channel + const channel1 = { channelId: 'channel-id-1', name: 'Channel Uno' } as Channel + const channel2 = { channelId: 'channel-id-2', name: 'Channel Dos' } as Channel + const channels = [channel1, channel2] + listChannelsMock.mockResolvedValue(channels) - const apiChannelsGetMock = jest.fn() + const apiChannelsGetMock = jest.fn().mockResolvedValue(channel1) + const apiChannelsListAssignedDriversMock = jest.fn() const client = { channels: { get: apiChannelsGetMock, + listAssignedDrivers: apiChannelsListAssignedDriversMock, }, } const command = { client } as unknown as APICommand - it('uses default channel if specified', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: false, useConfigDefault: true } as ChooseChannelOptions) - - expect(await chooseChannel(command, 'prompt message', undefined, { useConfigDefault: true })) - .toBe('chosen-channel-id') - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith({ useConfigDefault: true }) - expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith(command, - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ - defaultValue: { - configKey: 'defaultChannel', - getItem: expect.any(Function), - userMessage: expect.any(Function), - }, - promptMessage: 'prompt message', - })) - - expect(stringTranslateToIdMock).not.toHaveBeenCalled() - }) - - it('translates id from index if allowed', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: true } as ChooseChannelOptions) - stringTranslateToIdMock.mockResolvedValueOnce('translated-id') - - expect(await chooseChannel(command, 'prompt message', 'command-line-channel-id', - { allowIndex: true })).toBe('chosen-channel-id') - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith({ allowIndex: true }) - expect(stringTranslateToIdMock).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - 'command-line-channel-id', expect.any(Function)) - expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith(command, - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'translated-id' })) - }) - - it('uses listItems from options', async () => { - const listItemsMock = jest.fn>() - - chooseOptionsWithDefaultsMock.mockReturnValueOnce({} as ChooseChannelOptions) - - expect(await chooseChannel( - command, - 'prompt message', - 'command-line-channel-id', - { listItems: listItemsMock }, - )).toBe('chosen-channel-id') + it('uses listChannels to get channels', async () => { + expect(chooseChannelFn()).toBe(chooseChannelMock) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith({ listItems: listItemsMock }) - expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( - command, - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ listItems: expect.any(Function) }), + expect(createChooseFnMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ itemName: 'channel' }), + expect.any(Function), + { defaultValue: expect.objectContaining({ configKey: 'defaultChannel' }) }, ) - const listItems = selectFromListMock.mock.calls[0][2].listItems - const channels = [channel] - listItemsMock.mockResolvedValueOnce(channels) + const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems()).toBe(channels) + expect(await listItems(command)).toBe(channels) - expect(listItemsMock).toHaveBeenCalledExactlyOnceWith(command) + expect(listChannelsMock).toHaveBeenCalledExactlyOnceWith(command.client, { includeReadOnly: false }) }) - it('defaults to listChannels for listing channels', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: false, includeReadOnly: false } as ChooseChannelOptions) - - expect(await chooseChannel(command, 'prompt message', 'command-line-channel-id')) - .toBe('chosen-channel-id') - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith(undefined) - expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith(command, - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-channel-id' })) - - expect(stringTranslateToIdMock).not.toHaveBeenCalled() + it('includes read-only drivers when requested', async () => { + expect(chooseChannelFn({ includeReadOnly: true })).toBe(chooseChannelMock) - const listItems = selectFromListMock.mock.calls[0][2].listItems + const listItems = createChooseFnMock.mock.calls[0][1] - const list = [{ name: 'Channel' }] as Channel[] - listChannelsMock.mockResolvedValueOnce(list) + expect(await listItems(command)).toBe(channels) - expect(await listItems()).toBe(list) - - expect(listChannelsMock).toHaveBeenCalledExactlyOnceWith(client, { includeReadOnly: false }) + expect(listChannelsMock).toHaveBeenCalledExactlyOnceWith(command.client, { includeReadOnly: true }) }) - it('requests read-only channels when needed', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: false, includeReadOnly: true } as ChooseChannelOptions) - - expect(await chooseChannel(command, 'prompt message', 'command-line-channel-id')) - .toBe('chosen-channel-id') - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith(undefined) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-channel-id' })) + it('filters out channels not assigned to specified driver id', async () => { + expect(chooseChannelFn({ withDriverId: 'pre-chosen-driver-id' })).toBe(chooseChannelMock) - expect(stringTranslateToIdMock).not.toHaveBeenCalled() + const listItems = createChooseFnMock.mock.calls[0][1] - const listItems = selectFromListMock.mock.calls[0][2].listItems + apiChannelsListAssignedDriversMock.mockResolvedValueOnce([{ driverId: 'not-the-chosen-one' } as DriverChannelDetails]) + apiChannelsListAssignedDriversMock.mockResolvedValueOnce([ + { driverId: 'also-not-chosen' }, + { driverId: 'pre-chosen-driver-id' }, + ] as DriverChannelDetails[]) + expect(await listItems(command)).toStrictEqual([channel2]) - const list = [{ name: 'Channel' }] as Channel[] - listChannelsMock.mockResolvedValueOnce(list) - - expect(await listItems()).toBe(list) - - expect(listChannelsMock).toHaveBeenCalledTimes(1) - expect(listChannelsMock).toHaveBeenCalledWith(client, { includeReadOnly: true }) + expect(listChannelsMock).toHaveBeenCalledExactlyOnceWith(command.client, { includeReadOnly: false }) }) - describe('defaultConfig', () => { - test('getItem uses channels.get', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: false, useConfigDefault: true } as ChooseChannelOptions) - - expect(await chooseChannel(command, 'prompt message', undefined, - { useConfigDefault: true })).toBe('chosen-channel-id') + it('uses default channel', async () => { + expect(chooseChannelFn()).toBe(chooseChannelMock) - const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue - - expect(defaultValue).toBeDefined() - const getItem = defaultValue?.getItem as (id: string) => Promise - expect(getItem).toBeDefined() - apiChannelsGetMock.mockResolvedValueOnce(channel) - - expect(await getItem('id-to-check')).toBe(channel) - - expect(apiChannelsGetMock).toHaveBeenCalledTimes(1) - expect(apiChannelsGetMock).toHaveBeenCalledWith('id-to-check') - }) - - test('userMessage returns expected message', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce( - { allowIndex: false, useConfigDefault: true } as ChooseChannelOptions) - - expect(await chooseChannel(command, 'prompt message', undefined, - { useConfigDefault: true })).toBe('chosen-channel-id') + expect(createChooseFnMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ itemName: 'channel' }), + expect.any(Function), + { + defaultValue: { + configKey: 'defaultChannel', + getItem: expect.any(Function), + userMessage: expect.any(Function), + }, + }, + ) - const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue + const defaultValue = createChooseFnMock.mock.calls[0][2]?.defaultValue - expect(defaultValue).toBeDefined() - const userMessage = defaultValue?.userMessage as (channel: Channel) => string - expect(userMessage).toBeDefined() + expect(await defaultValue?.getItem(command, 'channel-id')).toBe(channel1) + expect(apiChannelsGetMock).toHaveBeenCalledExactlyOnceWith('channel-id') - expect(userMessage(channel)) - .toBe('using previously specified default channel named "channel name" (channel-id)') - }) + expect(defaultValue?.userMessage(channel1)) + .toBe('using previously specified default channel named "Channel Uno" (channel-id-1)') }) }) diff --git a/src/commands/edge/channels/delete.ts b/src/commands/edge/channels/delete.ts index 6b5c48b3..52041fa2 100644 --- a/src/commands/edge/channels/delete.ts +++ b/src/commands/edge/channels/delete.ts @@ -31,9 +31,8 @@ const builder = (yargs: Argv): Argv => const handler = async (argv: ArgumentsCamelCase): Promise => { const command = await apiCommand(argv) - const id = await chooseChannel(command, 'Choose a channel to delete.', argv.id) + const id = await chooseChannel(command, argv.id, { promptMessage: 'Choose a channel to delete.' }) await command.client.channels.delete(id) - console.log(`command.cliConfig = ${JSON.stringify(command.cliConfig)}`) await resetManagedConfigKey(command.cliConfig, 'defaultChannel', value => value === id) console.log(`Channel ${id} deleted.`) } diff --git a/src/commands/edge/channels/drivers.ts b/src/commands/edge/channels/drivers.ts index 6acbb838..4a9dc410 100644 --- a/src/commands/edge/channels/drivers.ts +++ b/src/commands/edge/channels/drivers.ts @@ -11,7 +11,7 @@ import { type DriverChannelDetailsWithName, listAssignedDriversWithNames, } from '../../../lib/command/util/edge-drivers.js' -import { chooseChannel } from '../../../lib/command/util/edge/channels-choose.js' +import { chooseChannelFn } from '../../../lib/command/util/edge/channels-choose.js' export type CommandArgs = @@ -50,11 +50,10 @@ const handler = async (argv: ArgumentsCamelCase): Promise => listTableFieldDefinitions: ['name', 'driverId', 'version', 'createdDate', 'lastModifiedDate'], } - const channelId = await chooseChannel( + const channelId = await chooseChannelFn({ includeReadOnly: true })( command, - 'Select a channel.', argv.idOrIndex, - { allowIndex: true, includeReadOnly: true, useConfigDefault: true }, + { allowIndex: true, useConfigDefault: true }, ) await outputList(command, config, () => listAssignedDriversWithNames(command.client, channelId)) diff --git a/src/commands/edge/channels/invites/create.ts b/src/commands/edge/channels/invites/create.ts index 2b9db502..f2cf493d 100644 --- a/src/commands/edge/channels/invites/create.ts +++ b/src/commands/edge/channels/invites/create.ts @@ -45,7 +45,7 @@ const handler = async (argv: ArgumentsCamelCase): Promise => const command = edgeCommand(await apiCommand(argv)) const getInputFromUser = async (): Promise => { - const channelId = await chooseChannel(command, 'Choose a channel:', argv.channel, + const channelId = await chooseChannel(command, argv.channel, { useConfigDefault: true }) const name = (await inquirer.prompt({ diff --git a/src/commands/edge/channels/invites/delete.ts b/src/commands/edge/channels/invites/delete.ts index 72cdf730..6c669a46 100644 --- a/src/commands/edge/channels/invites/delete.ts +++ b/src/commands/edge/channels/invites/delete.ts @@ -46,12 +46,7 @@ const handler = async (argv: ArgumentsCamelCase): Promise => return argv.id } - const channelId = await chooseChannel( - command, - 'Which channel is the invite you want to delete for?', - argv.channel, - { useConfigDefault: true }, - ) + const channelId = await chooseChannel(command, argv.channel, { useConfigDefault: true } ) return chooseInviteFn(command, { channelId })( command, diff --git a/src/commands/edge/channels/unassign.ts b/src/commands/edge/channels/unassign.ts new file mode 100644 index 00000000..f6fc402b --- /dev/null +++ b/src/commands/edge/channels/unassign.ts @@ -0,0 +1,62 @@ +import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' + +import { apiCommand, apiCommandBuilder, apiDocsURL, type APICommandFlags } from '../../../lib/command/api-command.js' +import { chooseChannelFn } from '../../../lib/command/util/edge/channels-choose.js' +import { chooseDriverFromChannelFn } from '../../../lib/command/util/drivers-choose.js' + + +export type CommandArgs = + & APICommandFlags + & { + driverId?: string + channel?: string + } + +const command = 'edge:channels:unassign [driver-id]' + +const describe = 'remove a driver from a channel' + +const builder = (yargs: Argv): Argv => + apiCommandBuilder(yargs) + .positional('driver-id', { describe: 'driver id', type: 'string' }) + .option('channel', { alias: 'C', describe: 'channel to unassigned from', type: 'string' }) + .example([ + ['$0 edge:channels:unassign', 'prompt for a channel and driver'], + [ + '$0 edge:channels:unassign 636169e4-8b9f-4438-a941-953b0d617231', + 'prompt for a channel and remove the specified driver from it', + ], + [ + '$0 edge:channels:unassign --channel 6ea857c6-23d4-4aae-aaf7-7a36daf42f92', + 'prompt for a driver and remove it from the specified channel', + ], + [ + '$0 edge:channels:unassign 636169e4-8b9f-4438-a941-953b0d617231' + + ' --channel 6ea857c6-23d4-4aae-aaf7-7a36daf42f92', + 'remove the specified driver from the specified channel', + ], + ]) + .epilog(apiDocsURL('deleteDriverChannel')) + + +const handler = async (argv: ArgumentsCamelCase): Promise => { + const command = await apiCommand(argv) + + const channelId = await chooseChannelFn({ withDriverId: argv.driverId })( + command, + argv.channel, + { useConfigDefault: true }, + ) + const driverId = await chooseDriverFromChannelFn(channelId)( + command, + argv.driverId, + { promptMessage: 'Select a driver to remove from the selected channel:' }, + ) + + await command.client.channels.unassignDriver(channelId, driverId) + + console.log(`${driverId} removed from channel ${channelId}`) +} + +const cmd: CommandModule = { command, describe, builder, handler } +export default cmd diff --git a/src/commands/index.ts b/src/commands/index.ts index d3e7d2c6..a5f67ff1 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -64,6 +64,7 @@ import edgeChannelsInvitesCommand from './edge/channels/invites.js' import edgeChannelsInvitesAcceptCommand from './edge/channels/invites/accept.js' import edgeChannelsInvitesCreateCommand from './edge/channels/invites/create.js' import edgeChannelsInvitesDeleteCommand from './edge/channels/invites/delete.js' +import edgeChannelsUnassignCommand from './edge/channels/unassign.js' import edgeDriversCommand from './edge/drivers.js' import edgeDriversDefaultCommand from './edge/drivers/default.js' import edgeDriversDevicesCommand from './edge/drivers/devices.js' @@ -187,6 +188,7 @@ export const commands: CommandModule[] = [ edgeChannelsInvitesAcceptCommand, edgeChannelsInvitesCreateCommand, edgeChannelsInvitesDeleteCommand, + edgeChannelsUnassignCommand, edgeDriversCommand, edgeDriversDefaultCommand, edgeDriversDevicesCommand, diff --git a/src/lib/command/util/edge/channels-choose.ts b/src/lib/command/util/edge/channels-choose.ts index fa0547c0..7dc5966e 100644 --- a/src/lib/command/util/edge/channels-choose.ts +++ b/src/lib/command/util/edge/channels-choose.ts @@ -1,58 +1,52 @@ -import { type Channel, type EnrolledChannel } from '@smartthings/core-sdk' +import { type Channel } from '@smartthings/core-sdk' import { type APICommand } from '../../api-command.js' -import { stringTranslateToId } from '../../command-util.js' -import { selectFromList, type SelectFromListConfig } from '../../select.js' -import { ChooseOptions, chooseOptionsWithDefaults } from '../util-util.js' +import { type ChooseFunction, createChooseFn } from '../util-util.js' import { listChannels } from './channels.js' /** - * Both Channel and Enrolled channel have all the fields necessary for choosing a channel. Using - * this allows callers of `chooseChannel` to supply a `listItems` that returns a list of either. + * Using this allows callers of `chooseChannel` to supply a `listItems` that returns a list of anything + * that has both the channelId and name fields. */ -export type ChannelChoice = Channel | EnrolledChannel +export type ChannelChoice = Pick -export type ChooseChannelOptions = ChooseOptions & { - includeReadOnly: boolean +export type ChooseChannelOptions = { + includeReadOnly?: boolean + + /** + * Include only channels that have the specified driver id assigned to them. + */ + withDriverId?: string } -export const chooseChannelOptionsWithDefaults = (options?: Partial): ChooseChannelOptions => ({ - includeReadOnly: false, - ...chooseOptionsWithDefaults(options), -}) - -export async function chooseChannel(command: APICommand, promptMessage: string, - channelFromArg?: string, - options?: Partial): Promise { - const opts = chooseChannelOptionsWithDefaults(options) - const config: SelectFromListConfig = { - itemName: 'channel', - primaryKeyName: 'channelId', - sortKeyName: 'name', +export const chooseChannelFn = (options?: ChooseChannelOptions): ChooseFunction => { + const listItems = async (command: APICommand): Promise<(ChannelChoice)[]> => { + const allChannels = await listChannels(command.client, { includeReadOnly: !!options?.includeReadOnly }) + if (options?.withDriverId) { + const channelsToKeep: string[] = [] + await Promise.all(allChannels.map(async channel => { + const assignedDrivers = await command.client.channels.listAssignedDrivers(channel.channelId) + if (assignedDrivers.find(assignedDriver => assignedDriver.driverId === options.withDriverId)) { + channelsToKeep.push(channel.channelId) + } + })) + return allChannels.filter(channel => channelsToKeep.includes(channel.channelId)) + } + return allChannels } - const listItems = (): Promise => options?.listItems - ? options.listItems(command) - : listChannels(command.client, { includeReadOnly: opts.includeReadOnly }) - - const preselectedId = channelFromArg - ? (opts.allowIndex - ? await stringTranslateToId(config, channelFromArg, listItems) - : channelFromArg) - : undefined - - const defaultValue = opts.useConfigDefault - ? { - configKey: 'defaultChannel', - getItem: (id: string): Promise => command.client.channels.get(id), - userMessage: (channel: ChannelChoice): string => - `using previously specified default channel named "${channel.name}" (${channel.channelId})`, - } - : undefined - return selectFromList( - command, - config, - { preselectedId, listItems, promptMessage, defaultValue }, + const defaultValue = { + configKey: 'defaultChannel', + getItem: (command: APICommand, id: string): Promise => command.client.channels.get(id), + userMessage: (channel: ChannelChoice): string => + `using previously specified default channel named "${channel.name}" (${channel.channelId})`, + } + return createChooseFn( + { itemName: 'channel', primaryKeyName: 'channelId', sortKeyName: 'name' }, + listItems, + { defaultValue }, ) } + +export const chooseChannel = chooseChannelFn() From 59a3ca212d3006520df07a3c5fecc92dc481cdc4 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Mon, 16 Jun 2025 10:26:28 -0500 Subject: [PATCH 2/2] refactor: convert edge:channels:assign command to yargs --- .../edge/src/commands/edge/channels/assign.ts | 44 ------------- src/commands/edge/channels/assign.ts | 64 +++++++++++++++++++ src/commands/index.ts | 2 + src/lib/command/util/edge/channels-choose.ts | 6 +- 4 files changed, 69 insertions(+), 47 deletions(-) delete mode 100644 packages/edge/src/commands/edge/channels/assign.ts create mode 100644 src/commands/edge/channels/assign.ts diff --git a/packages/edge/src/commands/edge/channels/assign.ts b/packages/edge/src/commands/edge/channels/assign.ts deleted file mode 100644 index 933ce051..00000000 --- a/packages/edge/src/commands/edge/channels/assign.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Flags } from '@oclif/core' - -import { chooseChannel } from '../../../lib/commands/channels-util.js' -import { chooseDriver } from '../../../lib/commands/drivers-util.js' -import { EdgeCommand } from '../../../lib/edge-command.js' - - -export class ChannelsAssignCommand extends EdgeCommand { - static description = 'assign a driver to a channel' + - this.apiDocsURL('createDriverChannel') - - static flags = { - ...EdgeCommand.flags, - channel: Flags.string({ - char: 'C', - description: 'channel id', - helpValue: '', - }), - } - - static args = [ - { - name: 'driverId', - description: 'driver id', - }, - { - name: 'version', - description: 'driver version', - }, - ] - - async run(): Promise { - const channelId = await chooseChannel(this, 'Select a channel for the driver.', - this.flags.channel, { useConfigDefault: true }) - const driverId = await chooseDriver(this, 'Select a driver to assign.', this.args.driverId) - - // If the version wasn't specified, grab it from the driver. - const version = this.args.version ?? (await this.client.drivers.get(driverId)).version - - await this.client.channels.assignDriver(channelId, driverId, version) - - this.log(`${driverId} ${this.args.version ? `(version ${this.args.version})` : ''} assigned to channel ${channelId}`) - } -} diff --git a/src/commands/edge/channels/assign.ts b/src/commands/edge/channels/assign.ts new file mode 100644 index 00000000..35784968 --- /dev/null +++ b/src/commands/edge/channels/assign.ts @@ -0,0 +1,64 @@ +import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' + +import { apiCommand, apiCommandBuilder, apiDocsURL, type APICommandFlags } from '../../../lib/command/api-command.js' + +import { chooseChannel } from '../../../lib/command/util/edge/channels-choose.js' +import { chooseDriver } from '../../../lib/command/util/drivers-choose.js' + + +export type CommandArgs = + & APICommandFlags + & { + driverId?: string + driverVersion?: string + channel?: string + } + +const command = 'edge:channels:assign [driver-id] [driver-version]' + +const describe = 'assign a driver to a channel' + +const builder = (yargs: Argv): Argv => + apiCommandBuilder(yargs) + .positional('driver-id', { describe: 'driver id', type: 'string' }) + .positional('driver-version', { describe: 'driver version', type: 'string' }) + .option('channel', { alias: 'C', describe: 'channel to assigned to', type: 'string' }) + .example([ + ['$0 edge:channels:assign', 'prompt for a channel and driver'], + [ + '$0 edge:channels:assign 636169e4-8b9f-4438-a941-953b0d617231', + 'prompt for a channel and assign the specified driver to it', + ], + [ + '$0 edge:channels:assign --channel 6ea857c6-23d4-4aae-aaf7-7a36daf42f92', + 'prompt for a driver and assign it to the specified channel', + ], + [ + '$0 edge:channels:assign 636169e4-8b9f-4438-a941-953b0d617231' + + ' --channel 6ea857c6-23d4-4aae-aaf7-7a36daf42f92', + 'assign the specified driver to the specified channel', + ], + ]) + .epilog(apiDocsURL('createDriverChannel')) + + +const handler = async (argv: ArgumentsCamelCase): Promise => { + const command = await apiCommand(argv) + + const channelId = await chooseChannel( + command, + argv.channel, + { promptMessage: 'Select a channel for the driver.', useConfigDefault: true }, + ) + const driverId = await chooseDriver(command, argv.driverId, { promptMessage: 'Select a driver to assign.' }) + + // If the version wasn't specified, grab it from the driver. + const driverVersion = argv.driverVersion ?? (await command.client.drivers.get(driverId)).version + + await command.client.channels.assignDriver(channelId, driverId, driverVersion) + + console.log(`${driverId} (version ${driverVersion}) assigned to channel ${channelId}`) +} + +const cmd: CommandModule = { command, describe, builder, handler } +export default cmd diff --git a/src/commands/index.ts b/src/commands/index.ts index a5f67ff1..584e8ffa 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -57,6 +57,7 @@ import devicesRenameCommand from './devices/rename.js' import devicesStatusCommand from './devices/status.js' import devicesUpdateCommand from './devices/update.js' import edgeChannelsCommand from './edge/channels.js' +import edgeChannelsAssignCommand from './edge/channels/assign.js' import edgeChannelsCreateCommand from './edge/channels/create.js' import edgeChannelsDeleteCommand from './edge/channels/delete.js' import edgeChannelsDriversCommand from './edge/channels/drivers.js' @@ -181,6 +182,7 @@ export const commands: CommandModule[] = [ devicesStatusCommand, devicesUpdateCommand, edgeChannelsCommand, + edgeChannelsAssignCommand, edgeChannelsCreateCommand, edgeChannelsDeleteCommand, edgeChannelsDriversCommand, diff --git a/src/lib/command/util/edge/channels-choose.ts b/src/lib/command/util/edge/channels-choose.ts index 7dc5966e..249bbf9c 100644 --- a/src/lib/command/util/edge/channels-choose.ts +++ b/src/lib/command/util/edge/channels-choose.ts @@ -24,14 +24,14 @@ export const chooseChannelFn = (options?: ChooseChannelOptions): ChooseFunction< const listItems = async (command: APICommand): Promise<(ChannelChoice)[]> => { const allChannels = await listChannels(command.client, { includeReadOnly: !!options?.includeReadOnly }) if (options?.withDriverId) { - const channelsToKeep: string[] = [] + const filteredChannels: ChannelChoice[] = [] await Promise.all(allChannels.map(async channel => { const assignedDrivers = await command.client.channels.listAssignedDrivers(channel.channelId) if (assignedDrivers.find(assignedDriver => assignedDriver.driverId === options.withDriverId)) { - channelsToKeep.push(channel.channelId) + filteredChannels.push(channel) } })) - return allChannels.filter(channel => channelsToKeep.includes(channel.channelId)) + return filteredChannels } return allChannels }