From 7d2db3f3ecc6e57fce15d2607deb91745de6b739 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Mon, 19 May 2025 10:59:03 -0500 Subject: [PATCH] refactor: convert deviceprofiles:create command to yargs --- .../lib/commands/capabilities-util.test.ts | 80 ------------------- .../cli/src/commands/deviceprofiles/create.ts | 44 ---------- .../cli/src/lib/commands/capabilities-util.ts | 49 ------------ .../src/lib/commands/deviceprofiles-util.ts | 44 ---------- .../command/util/capabilities-choose.test.ts | 38 +++++++++ .../command/util/capabilities-util.test.ts | 53 +++++++++--- .../command/util/deviceprofiles-util.test.ts | 60 +++++++++----- src/commands/capabilities/create.ts | 6 +- src/commands/deviceprofiles/create.ts | 71 ++++++++++++++++ src/commands/index.ts | 2 + src/lib/command/util/capabilities-choose.ts | 22 ++++- src/lib/command/util/capabilities-util.ts | 13 +++ .../lib/command/util/deviceprofiles-create.ts | 28 +++---- src/lib/command/util/deviceprofiles-util.ts | 41 +++++++++- 14 files changed, 282 insertions(+), 269 deletions(-) delete mode 100644 packages/cli/src/__tests__/lib/commands/capabilities-util.test.ts delete mode 100644 packages/cli/src/commands/deviceprofiles/create.ts delete mode 100644 packages/cli/src/lib/commands/capabilities-util.ts delete mode 100644 packages/cli/src/lib/commands/deviceprofiles-util.ts create mode 100644 src/commands/deviceprofiles/create.ts rename packages/cli/src/lib/commands/deviceprofiles/create-util.ts => src/lib/command/util/deviceprofiles-create.ts (91%) diff --git a/packages/cli/src/__tests__/lib/commands/capabilities-util.test.ts b/packages/cli/src/__tests__/lib/commands/capabilities-util.test.ts deleted file mode 100644 index 191dc5f1..00000000 --- a/packages/cli/src/__tests__/lib/commands/capabilities-util.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import inquirer from 'inquirer' - -import { SmartThingsClient } from '@smartthings/core-sdk' - -import { APIOrganizationCommand, selectFromList, Sorting, Table, TableGenerator } from '@smartthings/cli-lib' - -import { - CapabilitySummaryWithNamespace, - chooseCapability, - chooseCapabilityFiltered, - convertToId, - getAllFiltered, - getCustomByNamespace, - getIdFromUser, - getStandard, -} from '../../../lib/commands/capabilities-util.js' -import * as capabilitiesUtil from '../../../lib/commands/capabilities-util.js' - - -jest.mock('inquirer') - -describe('getAllFiltered', () => { - const getStandardSpy = jest.spyOn(capabilitiesUtil, 'getStandard') - const getCustomByNamespaceSpy = jest.spyOn(capabilitiesUtil, 'getCustomByNamespace') - - it('skips filter when empty', async () => { - getStandardSpy.mockResolvedValueOnce(standardCapabilitiesWithNamespaces) - getCustomByNamespaceSpy.mockResolvedValueOnce(customCapabilitiesWithNamespaces) - - expect(await getAllFiltered({} as SmartThingsClient, '')) - .toStrictEqual(allCapabilitiesWithNamespaces) - }) - - it('filters out items by name', async () => { - getStandardSpy.mockResolvedValueOnce(standardCapabilitiesWithNamespaces) - getCustomByNamespaceSpy.mockResolvedValueOnce(customCapabilitiesWithNamespaces) - - expect(await getAllFiltered({} as SmartThingsClient, 'switch')) - .toStrictEqual([switchCapability]) - }) - - it('filters out deprecated items', async () => { - getStandardSpy.mockResolvedValueOnce(standardCapabilitiesWithNamespaces) - getCustomByNamespaceSpy.mockResolvedValueOnce(customCapabilitiesWithNamespaces) - - expect(await getAllFiltered({} as SmartThingsClient, 'b')) - .toStrictEqual([buttonCapability, ...customCapabilitiesWithNamespaces]) - }) -}) - -describe('chooseCapabilityFiltered', () => { - it('uses selectFromList', async () => { - expect(await chooseCapabilityFiltered(command, 'user prompt', 'filter')).toBe(selectedCapabilityId) - - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith( - command, - expect.objectContaining({ itemName: 'capability' }), - expect.objectContaining({ - getIdFromUser, - promptMessage: 'user prompt', - }), - ) - }) - - it('uses list function that uses getAllFiltered', async () => { - expect(await chooseCapabilityFiltered(command, 'user prompt', 'filter')).toBe(selectedCapabilityId) - - expect(selectFromListMock).toHaveBeenCalledTimes(1) - - const listItems = selectFromListMock.mock.calls[0][2].listItems - const getAllFilteredSpy = jest.spyOn(capabilitiesUtil, 'getAllFiltered') - .mockResolvedValueOnce(customCapabilitiesWithNamespaces) - - expect(await listItems()).toBe(customCapabilitiesWithNamespaces) - - expect(getAllFilteredSpy).toHaveBeenCalledTimes(1) - expect(getAllFilteredSpy).toHaveBeenCalledWith(client, 'filter') - }) -}) diff --git a/packages/cli/src/commands/deviceprofiles/create.ts b/packages/cli/src/commands/deviceprofiles/create.ts deleted file mode 100644 index 3a930939..00000000 --- a/packages/cli/src/commands/deviceprofiles/create.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DeviceProfile } from '@smartthings/core-sdk' - -import { APIOrganizationCommand, inputAndOutputItem, inputProcessor } from '@smartthings/cli-lib' - -import { buildTableOutput, cleanupForCreate, DeviceDefinitionRequest } from '../../lib/commands/deviceprofiles-util.js' -import { createWithDefaultConfig, getInputFromUser } from '../../lib/commands/deviceprofiles/create-util.js' - - -export default class DeviceProfileCreateCommand extends APIOrganizationCommand { - static description = 'create a new device profile\n' + - 'Creates a new device profile. If a vid field is not present in the meta ' + - 'then a default device presentation will be created for this profile and the ' + - 'vid set to reference it.' + - this.apiDocsURL('createDeviceProfile') - - static flags = { - ...APIOrganizationCommand.flags, - ...inputAndOutputItem.flags, - } - - static examples = [ - '$ smartthings deviceprofiles:create -i myprofile.json # create a device profile from the JSON file definition', - '$ smartthings deviceprofiles:create -i myprofile.yaml # create a device profile from the YAML file definition', - '$ smartthings deviceprofiles:create # create a device profile with interactive dialog', - ] - - async run(): Promise { - const createDeviceProfile = async (_: void, data: DeviceDefinitionRequest): Promise => { - if (data.view) { - throw new Error('Input contains "view" property. Use deviceprofiles:view:create instead.') - } - - if (!data.metadata?.vid) { - const profileAndConfig = await createWithDefaultConfig(this.client, data) - return profileAndConfig.deviceProfile - } - - return await this.client.deviceProfiles.create(cleanupForCreate(data)) - } - await inputAndOutputItem(this, - { buildTableOutput: data => buildTableOutput(this.tableGenerator, data) }, - createDeviceProfile, inputProcessor(() => true, () => getInputFromUser(this))) - } -} diff --git a/packages/cli/src/lib/commands/capabilities-util.ts b/packages/cli/src/lib/commands/capabilities-util.ts deleted file mode 100644 index 9f5f2d70..00000000 --- a/packages/cli/src/lib/commands/capabilities-util.ts +++ /dev/null @@ -1,49 +0,0 @@ -import inquirer from 'inquirer' - -import { - type Capability, - type CapabilityArgument, - type CapabilitySummary, - type CapabilityJSONSchema, - type CapabilityNamespace, - type SmartThingsClient, -} from '@smartthings/core-sdk' - -import { type TableGenerator } from '../../table-generator.js' -import { type APICommand } from '../api-command.js' -import { type ListDataFunction, type Sorting } from '../io-defs.js' -import { sort } from '../output.js' -import { selectFromList, type SelectFromListConfig, type SelectFromListFlags } from '../select.js' - - -export const getAllFiltered = async ( - client: SmartThingsClient, - filter: string, -): Promise => { - const list = (await Promise.all([getStandard(client), getCustomByNamespace(client)])).flat() - if (filter) { - filter = filter.toLowerCase() - return list.filter(capability => - capability.id.toLowerCase().includes(filter) && capability.status !== 'deprecated') - } - return list -} - -export const chooseCapabilityFiltered = async ( - command: APICommand, - promptMessage: string, - filter: string, -): Promise => { - const config: SelectFromListConfig = { - itemName: 'capability', - pluralItemName: 'capabilities', - primaryKeyName: 'id', - sortKeyName: 'id', - listTableFieldDefinitions: ['id', 'version', 'status'], - } - return selectFromList(command, config, { - listItems: () => getAllFiltered(command.client, filter), - getIdFromUser, - promptMessage, - }) -} diff --git a/packages/cli/src/lib/commands/deviceprofiles-util.ts b/packages/cli/src/lib/commands/deviceprofiles-util.ts deleted file mode 100644 index 6c3bfa53..00000000 --- a/packages/cli/src/lib/commands/deviceprofiles-util.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - DeviceProfile, - DeviceProfileCreateRequest, - DeviceProfileUpdateRequest, - LocaleReference, -} from '@smartthings/core-sdk' - -import { - APIOrganizationCommand, - ChooseOptions, - chooseOptionsWithDefaults, - selectFromList, - SelectFromListConfig, - stringTranslateToId, - WithLocales, -} from '@smartthings/cli-lib' - - -/** - * Convert the `DeviceProfile` to a `DeviceProfileCreateRequest` by removing fields which can't - * be included in an create request. - */ -export const cleanupForCreate = (deviceProfile: Partial): DeviceProfileCreateRequest => { - const components = deviceProfile.components?.map(component => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { label, ...withoutLabel } = component - return withoutLabel - }) - const createRequest = { ...deviceProfile, components } - delete createRequest.id - delete createRequest.status - delete createRequest.restrictions - return createRequest -} - -/** - * Convert the `DeviceProfile` to a `DeviceProfileUpdateRequest` by removing fields which can't - * be included in an update request. - */ -export const cleanupForUpdate = (deviceProfile: Partial): DeviceProfileUpdateRequest => { - const updateRequest = cleanupForCreate(deviceProfile) - delete updateRequest.name - return updateRequest -} diff --git a/src/__tests__/lib/command/util/capabilities-choose.test.ts b/src/__tests__/lib/command/util/capabilities-choose.test.ts index b9b82114..62249201 100644 --- a/src/__tests__/lib/command/util/capabilities-choose.test.ts +++ b/src/__tests__/lib/command/util/capabilities-choose.test.ts @@ -9,6 +9,7 @@ import type { selectFromList, SelectFromListFlags } from '../../../../lib/comman import type { CapabilitySummaryWithNamespace, convertToId, + getAllFiltered, getCustomByNamespace, translateToId, } from '../../../../lib/command/util/capabilities-util.js' @@ -28,10 +29,12 @@ jest.unstable_mockModule('../../../../lib/command/select.js', () => ({ })) const convertToIdMock = jest.fn() +const getAllFilteredMock = jest.fn() const getCustomByNamespaceMock = jest.fn() const translateToIdMock = jest.fn() jest.unstable_mockModule('../../../../lib/command/util/capabilities-util.js', () => ({ convertToId: convertToIdMock, + getAllFiltered: getAllFilteredMock, getCustomByNamespace: getCustomByNamespaceMock, translateToId: translateToIdMock, })) @@ -39,6 +42,7 @@ jest.unstable_mockModule('../../../../lib/command/util/capabilities-util.js', () const { chooseCapability, + chooseCapabilityFiltered, getIdFromUser, } = await import('../../../../lib/command/util/capabilities-choose.js') @@ -283,3 +287,37 @@ describe('chooseCapability', () => { expect(apiCapabilitiesListLocalesMock).toHaveBeenCalledWith('capability-2', 1) }) }) + +describe('chooseCapabilityFiltered', () => { + it('uses selectFromList', async () => { + expect(await chooseCapabilityFiltered(command, 'user prompt', 'filter')).toBe(selectedCapabilityId) + + expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ itemName: 'capability' }), + expect.objectContaining({ + getIdFromUser, + promptMessage: 'user prompt', + }), + ) + }) + + it('uses list function that uses getAllFiltered', async () => { + const customCapabilitiesWithNamespaces = [ + { id: 'capability-1', version: 1, namespace: 'namespace-1' }, + { id: 'capability-2', version: 1, namespace: 'namespace-1' }, + { id: 'capability-3', version: 1, namespace: 'namespace-2' }, + ] + + expect(await chooseCapabilityFiltered(command, 'user prompt', 'filter')).toBe(selectedCapabilityId) + + expect(selectFromListMock).toHaveBeenCalledTimes(1) + + const listItems = selectFromListMock.mock.calls[0][2].listItems + getAllFilteredMock.mockResolvedValueOnce(customCapabilitiesWithNamespaces) + + expect(await listItems()).toBe(customCapabilitiesWithNamespaces) + + expect(getAllFilteredMock).toHaveBeenCalledExactlyOnceWith(client, 'filter') + }) +}) diff --git a/src/__tests__/lib/command/util/capabilities-util.test.ts b/src/__tests__/lib/command/util/capabilities-util.test.ts index cf141a16..4e9ccbd8 100644 --- a/src/__tests__/lib/command/util/capabilities-util.test.ts +++ b/src/__tests__/lib/command/util/capabilities-util.test.ts @@ -29,6 +29,7 @@ jest.unstable_mockModule('../../../../lib/command/output.js', () => ({ const { attributeTypeDisplayString, convertToId, + getAllFiltered, getCustomByNamespace, getStandard, translateToId, @@ -108,15 +109,23 @@ const apiCapabilitiesListNamespacesMock = const apiCapabilitiesListMock = jest.fn() const apiCapabilitiesListStandardMock = jest.fn() -const client = { capabilities: { - listNamespaces: apiCapabilitiesListNamespacesMock, - list: apiCapabilitiesListMock, - listStandard: apiCapabilitiesListStandardMock, -} } as unknown as SmartThingsClient +const client = { + capabilities: { + listNamespaces: apiCapabilitiesListNamespacesMock, + list: apiCapabilitiesListMock, + listStandard: apiCapabilitiesListStandardMock, + }, +} as unknown as SmartThingsClient const ns1Capabilities = [{ id: 'capability-1', version: 1 }, { id: 'capability-2', version: 1 }] const ns2Capabilities = [{ id: 'capability-3', version: 1 }] +const standardCapabilities = [ + { id: 'switch', version: 1 }, + { id: 'button', version: 1 }, + { id: 'bridge', version: 1, status: 'deprecated' }, +] +apiCapabilitiesListStandardMock.mockResolvedValue(standardCapabilities) const customCapabilitiesWithNamespaces = [ { id: 'capability-1', version: 1, namespace: 'namespace-1' }, { id: 'capability-2', version: 1, namespace: 'namespace-1' }, @@ -128,6 +137,8 @@ const bridgeCapability = { id: 'bridge', version: 1, status: 'deprecated', names const standardCapabilitiesWithNamespaces = [switchCapability, buttonCapability, bridgeCapability] const allCapabilitiesWithNamespaces = [...standardCapabilitiesWithNamespaces, ...customCapabilitiesWithNamespaces] const sortedCapabilitiesWithNamespaces = [...customCapabilitiesWithNamespaces, ...standardCapabilitiesWithNamespaces] +apiCapabilitiesListNamespacesMock.mockResolvedValue([{ name: 'namespace-1' } as CapabilityNamespace]) +apiCapabilitiesListMock.mockResolvedValue(ns1Capabilities) describe('getCustomByNamespace', () => { @@ -160,15 +171,10 @@ describe('getCustomByNamespace', () => { }) describe('getStandard', () => { - const standardCapabilities = [ - { id: 'switch', version: 1 }, - { id: 'button', version: 1 }, - { id: 'bridge', version: 1, status: 'deprecated' }, - ] - it('returns standard capabilities with the "st" namespace', async () => { - apiCapabilitiesListStandardMock.mockResolvedValueOnce(standardCapabilities) expect(await getStandard(client)).toStrictEqual(standardCapabilitiesWithNamespaces) + + expect(apiCapabilitiesListStandardMock).toHaveBeenCalledExactlyOnceWith() }) }) @@ -250,3 +256,26 @@ describe('convertToId', () => { expect(() => convertToId('1', [{ id: 1993 } as unknown as CapabilitySummaryWithNamespace])).toThrow() }) }) + +describe('getAllFiltered', () => { + it('skips filter when empty', async () => { + const allCapabilities = [ + ...standardCapabilitiesWithNamespaces, + customCapabilitiesWithNamespaces[0], + customCapabilitiesWithNamespaces[1], + ] + expect(await getAllFiltered(client, '')).toStrictEqual(allCapabilities) + }) + + it('filters out items by name', async () => { + expect(await getAllFiltered(client, 'switch')).toStrictEqual([switchCapability]) + }) + + it('filters out deprecated items', async () => { + expect(await getAllFiltered(client, 'b')).toStrictEqual([ + buttonCapability, + customCapabilitiesWithNamespaces[0], + customCapabilitiesWithNamespaces[1], + ]) + }) +}) diff --git a/src/__tests__/lib/command/util/deviceprofiles-util.test.ts b/src/__tests__/lib/command/util/deviceprofiles-util.test.ts index 7fa1eb1e..c3225747 100644 --- a/src/__tests__/lib/command/util/deviceprofiles-util.test.ts +++ b/src/__tests__/lib/command/util/deviceprofiles-util.test.ts @@ -1,4 +1,5 @@ import { + type DeviceComponent, type DeviceProfile, type DeviceProfilePreferenceRequest, DeviceProfileStatus, @@ -16,10 +17,37 @@ import { const { buildTableOutput, + cleanupForCreate, + cleanupForUpdate, entryValues, } = await import('../../../../lib/command/util/deviceprofiles-util.js') +const baseDeviceProfileCreate = { + name:'Device Profile', + components: [], +} +const baseDeviceProfile: DeviceProfile & { restrictions: unknown } = { + ...baseDeviceProfileCreate, + id: 'device-profile-id', + status: DeviceProfileStatus.PUBLISHED, + restrictions: 'some restrictions', +} +const componentsWithoutLabels: DeviceComponent[] = [ + { id: 'main', capabilities: [{ id: 'switch', version: 1 }] }, + { + id: 'second', + capabilities: [{ id: 'switch', version: 1 }, { id: 'cap-2', version: 1 }], + }, + { id: 'third' }, +] +const components: DeviceComponent[] = [ + componentsWithoutLabels[0], + { ...componentsWithoutLabels[1], label: 'Second Component' }, + { ...componentsWithoutLabels[2], label: 'Third Component' }, +] + + describe('entryValues', () => { it('returns empty string for empty list', () => { expect(entryValues([])).toBe('') @@ -42,13 +70,6 @@ describe('entryValues', () => { }) describe('buildTableOutput', () => { - const baseDeviceProfile: DeviceProfile = { - id: 'device-profile-id', - name:'Device Profile', - components: [], - status: DeviceProfileStatus.PUBLISHED, - } - it('includes basic info', () => { expect(buildTableOutput(tableGeneratorMock, baseDeviceProfile)).toBe(mockedTableOutput) @@ -87,17 +108,7 @@ describe('buildTableOutput', () => { }) it('includes components with capabilities', () => { - const deviceProfile = { - ...baseDeviceProfile, - components: [ - { id: 'main', capabilities: [{ id: 'switch', version: 1 }] }, - { - id: 'second', - capabilities: [{ id: 'switch', version: 1 }, { id: 'cap-2', version: 1 }], - }, - { id: 'third' }, - ], - } + const deviceProfile = { ...baseDeviceProfile, components } expect(buildTableOutput(tableGeneratorMock, deviceProfile)).toBe(mockedTableOutput) @@ -186,3 +197,16 @@ describe('buildTableOutput', () => { ) }) }) + +test('cleanupForCreate', () => { + expect(cleanupForCreate({ ...baseDeviceProfile, components })).toStrictEqual({ + ...baseDeviceProfileCreate, + components: componentsWithoutLabels, + }) +}) + +test('cleanupForUpdate', () => { + expect(cleanupForUpdate({ ...baseDeviceProfile, components })).toStrictEqual({ + components: componentsWithoutLabels, + }) +}) diff --git a/src/commands/capabilities/create.ts b/src/commands/capabilities/create.ts index f52dc533..12776f9d 100644 --- a/src/commands/capabilities/create.ts +++ b/src/commands/capabilities/create.ts @@ -14,8 +14,8 @@ import { } from '@smartthings/core-sdk' import { fatalError } from '../../lib/util.js' -import { apiCommandBuilder, apiDocsURL } from '../../lib/command/api-command.js' -import { apiOrganizationCommand, type APIOrganizationCommandFlags } from '../../lib/command/api-organization-command.js' +import { apiDocsURL } from '../../lib/command/api-command.js' +import { apiOrganizationCommand, apiOrganizationCommandBuilder, type APIOrganizationCommandFlags } from '../../lib/command/api-organization-command.js' import { inputAndOutputItem, inputAndOutputItemBuilder, @@ -37,7 +37,7 @@ const command = 'capabilities:create' const describe = 'create a capability' const builder = (yargs: Argv): Argv => - inputAndOutputItemBuilder(apiCommandBuilder(yargs)) + inputAndOutputItemBuilder(apiOrganizationCommandBuilder(yargs)) .option('namespace', { alias: 'n', description: 'the namespace to create the capability under', diff --git a/src/commands/deviceprofiles/create.ts b/src/commands/deviceprofiles/create.ts new file mode 100644 index 00000000..a5ad2cee --- /dev/null +++ b/src/commands/deviceprofiles/create.ts @@ -0,0 +1,71 @@ +import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' + +import { type DeviceProfile } from '@smartthings/core-sdk' + +import { apiDocsURL } from '../../lib/command/api-command.js' +import { + apiOrganizationCommand, + apiOrganizationCommandBuilder, + type APIOrganizationCommandFlags, +} from '../../lib/command/api-organization-command.js' +import { + inputAndOutputItem, + inputAndOutputItemBuilder, + type InputAndOutputItemFlags, +} from '../../lib/command/input-and-output-item.js' +import { userInputProcessor } from '../../lib/command/input-processor.js' +import { createWithDefaultConfig, getInputFromUser } from '../../lib/command/util/deviceprofiles-create.js' +import { + buildTableOutput, + cleanupForCreate, + type DeviceDefinitionRequest, +} from '../../lib/command/util/deviceprofiles-util.js' + + +export type CommandArgs = + & APIOrganizationCommandFlags + & InputAndOutputItemFlags + & { + namespace?: string + } + +const command = 'deviceprofiles:create' + +const describe = 'create a new device profile' + +const builder = (yargs: Argv): Argv => + inputAndOutputItemBuilder(apiOrganizationCommandBuilder(yargs)) + .example([ + ['$0 deviceprofiles:create', 'create a device profile from prompted input'], + ['$0 deviceprofiles:create --dry-run', 'build JSON for a device profile from prompted input'], + ['$0 deviceprofiles:create -i my-profile.yaml', 'create a device profile defined in "my-profile.yaml'], + ]) + .epilog('Creates a new device profile. If a vid field is not present in the metadata ' + + 'then a default device presentation will be created for this profile and the ' + + 'vid set to reference it.\n\n' + + apiDocsURL('createDeviceProfile')) + +const handler = async (argv: ArgumentsCamelCase): Promise => { + const command = await apiOrganizationCommand(argv) + + const createDeviceProfile = async (_: void, data: DeviceDefinitionRequest): Promise => { + if (data.view) { + throw new Error('Input contains "view" property. Use deviceprofiles:view:create instead.') + } + + if (!data.metadata?.vid) { + const profileAndConfig = await createWithDefaultConfig(command.client, data) + return profileAndConfig.deviceProfile + } + + return await command.client.deviceProfiles.create(cleanupForCreate(data)) + } + await inputAndOutputItem( + command, + { buildTableOutput: data => buildTableOutput(command.tableGenerator, data) }, + createDeviceProfile, userInputProcessor(() => getInputFromUser(command)), + ) +} + +const cmd: CommandModule = { command, describe, builder, handler } +export default cmd diff --git a/src/commands/index.ts b/src/commands/index.ts index 29949485..6579a475 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -29,6 +29,7 @@ import devicepreferencesTranslationsCommand from './devicepreferences/translatio import devicepreferencesTranslationsCreateCommand from './devicepreferences/translations/create.js' import devicepreferencesTranslationsUpdateCommand from './devicepreferences/translations/update.js' import deviceprofilesCommand from './deviceprofiles.js' +import deviceprofilesCreateCommand from './deviceprofiles/create.js' import deviceprofilesViewCommand from './deviceprofiles/view.js' import devicesCommand from './devices.js' import devicesCapabilityStatusCommand from './devices/capability-status.js' @@ -118,6 +119,7 @@ export const commands: CommandModule[] = [ devicepreferencesTranslationsCreateCommand, devicepreferencesTranslationsUpdateCommand, deviceprofilesCommand, + deviceprofilesCreateCommand, deviceprofilesViewCommand, devicesCommand, devicesCapabilityStatusCommand, diff --git a/src/lib/command/util/capabilities-choose.ts b/src/lib/command/util/capabilities-choose.ts index 4407ade6..11e2462e 100644 --- a/src/lib/command/util/capabilities-choose.ts +++ b/src/lib/command/util/capabilities-choose.ts @@ -7,8 +7,9 @@ import { selectFromList, type SelectFromListConfig, type SelectFromListFlags } f import { type CapabilityId, convertToId, - getCustomByNamespace, type CapabilitySummaryWithNamespace, + getAllFiltered, + getCustomByNamespace, translateToId, } from './capabilities-util.js' @@ -96,3 +97,22 @@ export const chooseCapability = async ( }, ) } + +export const chooseCapabilityFiltered = async ( + command: APICommand, + promptMessage: string, + filter: string, +): Promise => { + const config: SelectFromListConfig = { + itemName: 'capability', + pluralItemName: 'capabilities', + primaryKeyName: 'id', + sortKeyName: 'id', + listTableFieldDefinitions: ['id', 'version', 'status'], + } + return selectFromList(command, config, { + listItems: () => getAllFiltered(command.client, filter), + getIdFromUser, + promptMessage, + }) +} diff --git a/src/lib/command/util/capabilities-util.ts b/src/lib/command/util/capabilities-util.ts index 69164854..63bc397b 100644 --- a/src/lib/command/util/capabilities-util.ts +++ b/src/lib/command/util/capabilities-util.ts @@ -142,3 +142,16 @@ export const convertToId = ( return false } } + +export const getAllFiltered = async ( + client: SmartThingsClient, + filter: string, +): Promise => { + const list = (await Promise.all([getStandard(client), getCustomByNamespace(client)])).flat() + if (filter) { + filter = filter.toLowerCase() + return list.filter(capability => + capability.id.toLowerCase().includes(filter) && capability.status !== 'deprecated') + } + return list +} diff --git a/packages/cli/src/lib/commands/deviceprofiles/create-util.ts b/src/lib/command/util/deviceprofiles-create.ts similarity index 91% rename from packages/cli/src/lib/commands/deviceprofiles/create-util.ts rename to src/lib/command/util/deviceprofiles-create.ts index 72168d5d..f7c3fb8c 100644 --- a/packages/cli/src/lib/commands/deviceprofiles/create-util.ts +++ b/src/lib/command/util/deviceprofiles-create.ts @@ -1,18 +1,18 @@ -import { Errors } from '@oclif/core' import inquirer from 'inquirer' import { - DeviceProfile, - DeviceProfileRequest, - PresentationDeviceConfig, - PresentationDeviceConfigCreate, - SmartThingsClient, + type DeviceProfile, + type DeviceProfileRequest, + type PresentationDeviceConfig, + type PresentationDeviceConfigCreate, + type SmartThingsClient, } from '@smartthings/core-sdk' -import { APICommand } from '@smartthings/cli-lib' - -import { CapabilityId, chooseCapabilityFiltered } from '../capabilities-util.js' -import { cleanupForCreate, cleanupForUpdate, DeviceDefinitionRequest } from '../deviceprofiles-util.js' +import { chooseCapabilityFiltered } from './capabilities-choose.js' +import { type CapabilityId } from './capabilities-util.js' +import { cleanupForCreate, cleanupForUpdate, DeviceDefinitionRequest } from './deviceprofiles-util.js' +import { APICommand } from '../api-command.js' +import { fatalError } from '../../util.js' const capabilitiesWithoutPresentations = ['healthCheck', 'execute'] @@ -123,12 +123,12 @@ export const capabilityDefined = async (client: SmartThingsClient, idStr: string try { const capability = await client.capabilities.get(idStr, 1) return !!capability - } catch (e) { + } catch { return false } } -export const promptAndAddCapability = async (command: APICommand, deviceProfile: DeviceProfileRequest, componentId: string, prompt = 'Capability ID'): Promise => { +export const promptAndAddCapability = async (command: APICommand, deviceProfile: DeviceProfileRequest, componentId: string, prompt = 'Capability ID'): Promise => { let capabilityId: CapabilityId = { id: '', version: 0 } const idStr = (await inquirer.prompt({ type: 'input', @@ -155,7 +155,7 @@ export const promptAndAddCapability = async (command: APICommand): Promise => { +export const getInputFromUser = async (command: APICommand): Promise => { const name = (await inquirer.prompt({ type: 'input', name: 'deviceProfileName', diff --git a/src/lib/command/util/deviceprofiles-util.ts b/src/lib/command/util/deviceprofiles-util.ts index ea3fbba1..60eb62e6 100644 --- a/src/lib/command/util/deviceprofiles-util.ts +++ b/src/lib/command/util/deviceprofiles-util.ts @@ -1,10 +1,12 @@ import { - DeviceProfile, - DeviceProfileRequest, - PresentationDeviceConfigEntry, + type DeviceProfile, + type DeviceProfileCreateRequest, + type DeviceProfileRequest, + type DeviceProfileUpdateRequest, + type PresentationDeviceConfigEntry, } from '@smartthings/core-sdk' -import { TableGenerator } from '../../table-generator.js' +import { type TableGenerator } from '../../table-generator.js' export type ViewPresentationDeviceConfigEntry = @@ -84,3 +86,34 @@ export const buildTableOutput = (tableGenerator: TableGenerator, data: DevicePro } return table.toString() } + +/** + * Convert the `DeviceProfile` to a `DeviceProfileCreateRequest` by removing fields which can't + * be included in a create request. + */ +export const cleanupForCreate = ( + deviceProfile: Partial, +): DeviceProfileCreateRequest => { + const components = deviceProfile.components?.map(component => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { label, ...withoutLabel } = component + return withoutLabel + }) + const createRequest = { ...deviceProfile, components } + delete createRequest.id + delete createRequest.status + delete createRequest.restrictions + return createRequest +} + +/** + * Convert the `DeviceProfile` to a `DeviceProfileUpdateRequest` by removing fields which can't + * be included in an update request. + */ +export const cleanupForUpdate = ( + deviceProfile: Partial, +): DeviceProfileUpdateRequest => { + const updateRequest = cleanupForCreate(deviceProfile) + delete updateRequest.name + return updateRequest +}