diff --git a/platforms/platform-alexa/docs/README.md b/platforms/platform-alexa/docs/README.md index f079090fd..2b66c037e 100644 --- a/platforms/platform-alexa/docs/README.md +++ b/platforms/platform-alexa/docs/README.md @@ -572,7 +572,41 @@ You can build Alexa Skills with Jovo that make use of the Alexa Conversations di You can use Jovo with [Alexa Skill Connections](https://developer.amazon.com/docs/alexa/custom-skills/understand-skill-connections.html) by sending a `Connections.StartConnection` directive as shown in the [official Alexa docs](https://developer.amazon.com/docs/alexa/custom-skills/use-skill-connections-to-request-tasks.html#implement-a-handler-to-return-a-connectionsstartconnection-directive-to-use-skill-connection). -For this, the Jovo Alexa integration offers convenience [output classes](https://www.jovo.tech/docs/output-classes). Below is an overview of all classes: +For example, if you want to connect to a [custom task in a specific skill](https://developer.amazon.com/en-US/docs/alexa/custom-skills/use-skill-connections-to-request-tasks.html#for-custom-task-direct-skill-connection-with-send_errors_only), you can use the `StartConnectionOutput` class provided by the Jovo Alexa integration. + +```typescript +import { StartConnectionOutput, OnCompletion } from '@jovotech/platform-alexa'; +// ... + +someHandler() { + // ... + + return this.$send(StartConnectionOutput, { + taskName: { + amazonPredefinedTask: false, + name: 'CountDown', + }, + // Input for the task + input: { + upperLimit: 10, + lowerLimit: 1, + }, + // Decides whether session is picked up after task + onCompletion: OnCompletion.SendErrorsOnly, + token: '', + // defaults to 1 + taskVersion: 1, + // This is mandatory in case taskName.amazonPredefinedTask is false + providerSkillId: '', + }); +} +``` + +The Output Options can be changed to create a [managed skill connection](https://developer.amazon.com/en-US/docs/alexa/custom-skills/use-skill-connections-to-request-tasks.html#for-managed-skill-connection), by setting `taskName.amazonPredefinedTask` to true and omitting `providerSkillId`. + +In case the session is supposed to be [resumed](https://developer.amazon.com/en-US/docs/alexa/custom-skills/use-skill-connections-to-request-tasks.html#return-connectionsstartconnection-directive-with-resume_session-set-explicitly-or-by-default) after the task is handled, `onCompletion` can be changed to `OnCompletion.ResumeSession`. + +For some specific connections, the Jovo Alexa integration offers convenience [output classes](https://www.jovo.tech/docs/output-classes). Below is an overview of all classes: - [`ConnectionAskForPermissionConsentOutput`](https://github.com/jovotech/jovo-framework/tree/v4/latest/platforms/platform-alexa/src/output/templates/ConnectionAskForPermissionConsentOutput.ts) - [`ConnectionLinkAppOutput`](https://github.com/jovotech/jovo-framework/tree/v4/latest/platforms/platform-alexa/src/output/templates/ConnectionLinkAppOutput.ts) diff --git a/platforms/platform-alexa/docs/cli-commands.md b/platforms/platform-alexa/docs/cli-commands.md index 7867e6ba2..7600e205b 100644 --- a/platforms/platform-alexa/docs/cli-commands.md +++ b/platforms/platform-alexa/docs/cli-commands.md @@ -36,6 +36,11 @@ The Alexa CLI plugin hooks into the following commands: - [`deploy`](#deploy): Deploy project files to the Alexa Developer Console - [`get`](#get): Synchronize your local project files with the Alexa Developer Console +Also it provides the following platform specific commands: + +- [`validate:alexa`](#validate): Trigger the Alexa Skill Validation +- [`certify:alexa`](#certify): Trigger the Alexa Skill Certification + ## build The Alexa CLI plugin hooks into the `build` command and creates a `platform.alexa` folder inside the `build` directory in the root of your Jovo project. [Learn more about the `build` command here](https://www.jovo.tech/docs/build-command). @@ -132,4 +137,37 @@ $ jovo get:platform alexa # Turn Alexa Interaction Model into Jovo Model $ jovo build:platform alexa --reverse -``` \ No newline at end of file +``` + +## validate + +The Alexa CLI plugin provides a command to easily trigger a Skill Validation. + +```sh +$ jovo verify:alexa +``` + +After successfully triggering the validation, you can check the status in the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask#/). + +| Flag | Description | Examples | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `--skill-stage` | Either `development`, `live` or `certification` depending on what skill stage is to be validated. Default is `development` | `jovo validate:alexa --skill-stage development` | | | +| `--skill-id` | The skillId of the to be validated skill. If not provided, will be taken from the stage in the project configuration. [Learn more about ASK skillId configuration here](./project-config.md#skillid) | `jovo validate:alexa --skill-id amzn1.ask.skill.123example` | +| `--ask-profile` | Deploy to using the specified ASK profile. If not provided as flag, will be taken from project configuration. [Learn more about ASK profile configuration here](./project-config.md#askprofile) | `jovo validate:alexa --ask-profile default` | +| `--locales` | List of locales in which to validate the skill. If not provided, will do for all locales in project configuration. [Learn more about locales configuration here](./project-config.md#locales) | `jovo validate:alexa --locales en-US de-DE` | + +## certify + +The Alexa CLI plugin provides a command to easily trigger a Skill Certification. + +```sh +$ jovo certify:alexa +``` + +After successfully triggering the certification, you can check the status of the certification in the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask#/). + +| Flag | Description | Examples | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `--skill-id` | The skillId of the to be validated skill. If not provided, will be taken from the stage in the project configuration. [Learn more about ASK skillId configuration here](./project-config.md#skillid) | `jovo certify:alexa --skill-id amzn1.ask.skill.123example` | +| `--ask-profile` | Deploy to using the specified ASK profile. If not provided as flag, will be taken from project configuration. [Learn more about ASK profile configuration here](./project-config.md#askprofile) | `jovo certify:alexa --ask-profile default` | +| `--publication-method` | Either `MANUAL_PUBLISHING` or `AUTO_PUBLISHING`. Default is `MANUAL_PUBLISHING`. [Learn more about publication methods](https://developer.amazon.com/en-US/docs/alexa/smapi/ask-cli-command-reference.html#submit-skill-for-certification-subcommand) | `jovo validate:alexa --publication-method AUTO_PUBLISHING` | diff --git a/platforms/platform-alexa/src/cli/commands/CertifyCommand.ts b/platforms/platform-alexa/src/cli/commands/CertifyCommand.ts new file mode 100644 index 000000000..03aa155c6 --- /dev/null +++ b/platforms/platform-alexa/src/cli/commands/CertifyCommand.ts @@ -0,0 +1,44 @@ +import { JovoCliError, MAGNIFYING_GLASS, PluginCommand, Task, flags } from '@jovotech/cli-core'; +import * as smapi from '../smapi'; +import { AlexaCli } from '..'; +import { PublicationMethodLike, PublicationMethod } from '../interfaces'; + +export class CertifyCommand extends PluginCommand { + $plugin!: AlexaCli; + static id = 'certify:alexa'; + static description = 'This submits an alexa skill to certification'; + static examples: string[] = ['jovo certify:alexa']; + static flags = { + ...PluginCommand.flags, + 'skill-id': flags.string({ char: 's', description: 'Alexa Skill ID' }), + 'ask-profile': flags.string({ + description: 'Name of used ASK profile', + }), + 'publication-method': flags.string({ + options: Object.values(PublicationMethod), + default: PublicationMethod.MANUAL_PUBLISHING, + }), + }; + static args = []; + + async run(): Promise { + const { flags } = this.parse(CertifyCommand); + const skillId = flags['skill-id'] || this.$plugin.config.skillId; + const askProfile = flags['ask-profile'] || this.$plugin.config.askProfile || 'default'; + const publicationMethod: PublicationMethodLike = flags['publication-method']; + + const certifyTask: Task = new Task( + `${MAGNIFYING_GLASS} Submitting Alexa Skill ${skillId} to Certification`, + async () => { + if (!skillId) + throw new JovoCliError({ + message: 'Cannot submit Skill to Certification without skillId', + hint: 'Either add a skillId to the stage in the project configuration or add the --skill-id flag', + }); + + return smapi.submitSkillForCertification(skillId, publicationMethod, askProfile); + }, + ); + await certifyTask.run(); + } +} diff --git a/platforms/platform-alexa/src/cli/commands/ValidateCommand.ts b/platforms/platform-alexa/src/cli/commands/ValidateCommand.ts new file mode 100644 index 000000000..0d588cd87 --- /dev/null +++ b/platforms/platform-alexa/src/cli/commands/ValidateCommand.ts @@ -0,0 +1,63 @@ +import { + JovoCliError, + Log, + MAGNIFYING_GLASS, + PluginCommand, + Task, + flags, +} from '@jovotech/cli-core'; +import * as smapi from '../smapi'; +import { AlexaCli } from '..'; + +export class ValidateCommand extends PluginCommand { + $plugin!: AlexaCli; + static id = 'validate:alexa'; + static description = 'This submits a skill validation'; + static examples: string[] = ['jovo validate:alexa']; + static flags = { + 'skill-stage': flags.string({ + description: 'Alexa Skill Stage', + options: ['development', 'live', 'certification'], + default: 'development', + }), + 'skill-id': flags.string({ char: 's', description: 'Alexa Skill ID' }), + 'ask-profile': flags.string({ + description: 'Name of used ASK profile', + }), + 'locales': flags.string({ multiple: true, char: 'l' }), + ...PluginCommand.flags, + }; + static args = []; + + async run(): Promise { + const { flags } = this.parse(ValidateCommand); + const skillId = flags['skill-id'] || this.$plugin.config.skillId; + const askProfile = flags['ask-profile'] || this.$plugin.config.askProfile || 'default'; + const locales = + flags.locales || + Object.values(this.$plugin.config.locales || {}).reduce( + (prev, curr) => [...prev, ...curr], + [], + ); + + const validateTask: Task = new Task( + `${MAGNIFYING_GLASS} Submitting Alexa Skill ${skillId} to Validation`, + async () => { + if (!skillId) + throw new JovoCliError({ + message: 'Cannot submit Skill Validation without skillId', + hint: 'Either add a skillId to the stage in the project configuration or add the --skill-id flag', + }); + + const validationResponse = await smapi.submitSkillValidation( + skillId, + locales, + flags['skill-stage'], + askProfile, + ); + return `Started Validation with id ${validationResponse.id}`; + }, + ); + await validateTask.run(); + } +} diff --git a/platforms/platform-alexa/src/cli/index.ts b/platforms/platform-alexa/src/cli/index.ts index ef522e54d..baaedacad 100644 --- a/platforms/platform-alexa/src/cli/index.ts +++ b/platforms/platform-alexa/src/cli/index.ts @@ -2,6 +2,7 @@ import type { NewContext } from '@jovotech/cli-command-new'; import { JovoCliPlugin, Log, + PluginCommand, PluginHook, PluginType, promptSupportedLocales, @@ -15,6 +16,8 @@ import { DeployHook } from './hooks/DeployHook'; import { GetHook } from './hooks/GetHook'; import { NewHook } from './hooks/NewHook'; import { AlexaCliConfig, AlexaConversationsConfig, SupportedLocalesType } from './interfaces'; +import { CertifyCommand } from './commands/CertifyCommand'; +import { ValidateCommand } from './commands/ValidateCommand'; export type AlexaCliInitConfig = | RequiredOnlyWhere @@ -105,6 +108,10 @@ export class AlexaCli extends JovoCliPlugin { return [BuildHook, GetHook, DeployHook, NewHook]; } + getCommands(): (typeof PluginCommand)[] { + return [CertifyCommand, ValidateCommand]; + } + /** * The base path to platform's build folder */ diff --git a/platforms/platform-alexa/src/cli/interfaces.ts b/platforms/platform-alexa/src/cli/interfaces.ts index 8c9ee7a6b..727c30401 100644 --- a/platforms/platform-alexa/src/cli/interfaces.ts +++ b/platforms/platform-alexa/src/cli/interfaces.ts @@ -1,5 +1,5 @@ import { PluginConfig, PluginContext } from '@jovotech/cli-core'; -import { UnknownObject } from '@jovotech/framework'; +import { EnumLike, UnknownObject } from '@jovotech/framework'; import { ConversationsTarget } from '../interfaces'; import { SupportedLocales } from './constants'; @@ -116,3 +116,17 @@ export interface SkillStatusResponse { }; interactionModel?: UnknownObject; } + +export enum PublicationMethod { + MANUAL_PUBLISHING = 'MANUAL_PUBLISHING', + AUTO_PUBLISHING = 'AUTO_PUBLISHING', +} + +export type PublicationMethodLike = EnumLike | string; + +export type ValidationStatus = 'SUCCEEDED' | 'FAILED' | 'IN_PROGRESS'; + +export interface SkillValidationResponse { + id: string; + status: ValidationStatus; +} diff --git a/platforms/platform-alexa/src/cli/smapi/SkillManagement.ts b/platforms/platform-alexa/src/cli/smapi/SkillManagement.ts index 0b8a91c82..f6300ecab 100644 --- a/platforms/platform-alexa/src/cli/smapi/SkillManagement.ts +++ b/platforms/platform-alexa/src/cli/smapi/SkillManagement.ts @@ -1,6 +1,12 @@ import { JovoCliError, wait } from '@jovotech/cli-core'; -import { AskSkillList, SkillStatusError, SkillStatusResponse } from '../interfaces'; +import { + AskSkillList, + SkillStatusError, + SkillStatusResponse, + SkillValidationResponse, +} from '../interfaces'; import { execAskCommand } from '../utilities'; +import { PublicationMethodLike } from '../interfaces'; export async function listSkills(askProfile?: string): Promise { const { stdout } = await execAskCommand( @@ -45,3 +51,41 @@ export async function getSkillStatus(skillId: string, askProfile?: string): Prom } } } + +export async function submitSkillForCertification( + skillId: string, + publicationMethod: PublicationMethodLike, + askProfile?: string, +): Promise { + const { stdout } = await execAskCommand( + 'smapiSubmitSkillForCertification', + [ + 'ask smapi submit-skill-for-certification', + `-s ${skillId}`, + `--publication-method ${publicationMethod}`, + ], + askProfile, + ); + + return stdout; +} + +export async function submitSkillValidation( + skillId: string, + locales: string[], + stage: string, + askProfile?: string, +): Promise { + const { stdout } = await execAskCommand( + 'smapiSubmitSkillValidation', + [ + 'ask smapi submit-skill-validation', + `-s ${skillId}`, + `--stage ${stage}`, + `--locales ${locales.join(' ')}`, + ], + askProfile, + ); + + return JSON.parse(stdout!); +} diff --git a/platforms/platform-alexa/src/output/models/index.ts b/platforms/platform-alexa/src/output/models/index.ts index 15537a585..b40d43021 100644 --- a/platforms/platform-alexa/src/output/models/index.ts +++ b/platforms/platform-alexa/src/output/models/index.ts @@ -57,6 +57,7 @@ export * from './common/AsinProduct'; export * from './common/ConnectionPostalAddress'; export * from './common/ConsentLevel'; export * from './common/PermissionScope'; +export * from './common/OnCompletion'; export * from './dialog/DialogConfirmIntentDirective'; export * from './dialog/DialogConfirmSlotDirective'; export * from './dialog/DialogDelegateDirective'; diff --git a/platforms/platform-alexa/src/output/templates/ConnectionAddToShoppingCartOutput.ts b/platforms/platform-alexa/src/output/templates/ConnectionAddToShoppingCartOutput.ts index f5a1602f4..db0b1e31c 100644 --- a/platforms/platform-alexa/src/output/templates/ConnectionAddToShoppingCartOutput.ts +++ b/platforms/platform-alexa/src/output/templates/ConnectionAddToShoppingCartOutput.ts @@ -1,6 +1,7 @@ import { BaseOutput, Output, OutputOptions, OutputTemplate } from '@jovotech/framework'; import { AsinProduct } from '../models'; import { OnCompletion } from '../models/common/OnCompletion'; +import { StartConnectionOutput } from './StartConnectionOutput'; export interface ConnectionAddToShoppingCartOutputOptions extends OutputOptions { shouldEndSession?: boolean; @@ -19,33 +20,15 @@ export class ConnectionAddToShoppingCartOutput extends BaseOutput { + constructor(jovo: Jovo, options: DeepPartial | undefined) { + super(jovo, options); + } + + getDefaultOptions(): DirectSkillConnectionOutputOptions { + return { + taskVersion: 1, + taskName: { amazonPredefinedTask: true, name: '' }, + input: {}, + onCompletion: OnCompletion.ResumeSession, + }; + } + + build(): OutputTemplate { + if (!this.options.taskName.amazonPredefinedTask && !this.options.providerSkillId) { + throw new JovoError({ + message: 'for direct skill connection, providerSkillId must be provided', + hint: 'to link to a task in a specific skill, provide providerSkillId, to use managed skill connection, set taskName.amazonPredefinedTask to true', + learnMore: + 'https://developer.amazon.com/en-US/docs/alexa/custom-skills/use-skill-connections-to-request-tasks.html#for-direct-skill-connection-1', + }); + } + + const shouldEndSession = + this.options.onCompletion === OnCompletion.SendErrorsOnly + ? true + : this.options.shouldEndSession; + + const taskPrefix = this.options.taskName.amazonPredefinedTask + ? 'AMAZON' + : this.options.providerSkillId; + + return { + platforms: { + alexa: { + nativeResponse: { + response: { + shouldEndSession, + directives: [ + { + type: 'Connections.StartConnection', + uri: `connection://${taskPrefix}.${this.options.taskName.name}/${ + this.options.taskVersion + }${ + this.options.providerSkillId ? `?provider=${this.options.providerSkillId}` : '' + }`, + input: this.options.input, + onCompletion: this.options.onCompletion, + token: this.options.token, + }, + ], + }, + }, + }, + }, + }; + } +} diff --git a/platforms/platform-alexa/src/output/templates/index.ts b/platforms/platform-alexa/src/output/templates/index.ts index 16a9b2158..7bf2c6d73 100644 --- a/platforms/platform-alexa/src/output/templates/index.ts +++ b/platforms/platform-alexa/src/output/templates/index.ts @@ -28,5 +28,6 @@ export * from './ConnectionVerifyPersonOutput'; export * from './ConnectionScheduleFoodEstablishmentReservationOutput'; export * from './ProgressiveResponseOutput'; export * from './CanFulfillIntentOutput'; +export * from './StartConnectionOutput'; export * from './AplRenderDocumentOutput';