├── test ├── fixture │ ├── testersList.txt │ └── testersList.csv ├── temp │ └── abstract-build-flow │ │ ├── some-file.txt │ │ └── build.zip ├── unit │ ├── fixture │ │ ├── build-flow │ │ │ └── some-file.txt │ │ ├── model │ │ │ ├── cli_config │ │ │ ├── app-config-no-profiles.json │ │ │ ├── unsupported-file-type.notsupport │ │ │ ├── dialog │ │ │ │ ├── invalid-dialog-replay-file.json │ │ │ │ └── dialog-replay-file.json │ │ │ ├── invalid-yaml.yaml │ │ │ ├── json-config-yaml.yaml │ │ │ ├── hosted-proj │ │ │ │ ├── .ask │ │ │ │ │ └── ask-states.json │ │ │ │ └── ask-resources.json │ │ │ ├── invalid-json.json │ │ │ ├── regular-proj │ │ │ │ ├── random-json-config.json │ │ │ │ ├── ask-resources-min.json │ │ │ │ ├── ask-resources.json │ │ │ │ └── .ask │ │ │ │ │ └── ask-states.json │ │ │ ├── json-config.json │ │ │ ├── hosted-skill-resources-config.json │ │ │ ├── app-config.yaml │ │ │ ├── yaml-config.yaml │ │ │ ├── yaml-config.yml │ │ │ ├── yaml-from-json-result.yaml │ │ │ ├── app-config.json │ │ │ ├── yaml-json-result.json │ │ │ ├── resources-config.yaml │ │ │ ├── regional-stack-file.yaml │ │ │ ├── manifest.json │ │ │ ├── regional-stack-file.json │ │ │ └── resources-config.json │ │ └── controller │ │ │ ├── invalid-deploy-delegate-instance.js │ │ │ └── valid-deploy-delegate-instance.js │ ├── run-test.js │ ├── model │ │ ├── dialog │ │ │ └── input.test.ts │ │ ├── dialog-save-skill-io-file-test.js │ │ ├── yaml-parser-test.js │ │ ├── import-status-test.ts │ │ └── import-status.test.ts │ ├── builtins │ │ └── build-flows │ │ │ ├── zip-only-test.js │ │ │ └── nodejs-npm-test.js │ ├── commands │ │ ├── skill │ │ │ └── skill-commander-test.ts │ │ ├── util │ │ │ └── util-commander-test.ts │ │ ├── dialog │ │ │ └── interactive-mode.spec.ts │ │ ├── run │ │ │ └── run-flow │ │ │ │ └── java-run-test.js │ │ ├── smapi │ │ │ ├── smapi-docs-test.js │ │ │ └── smapi-commander-test.js │ │ └── configure │ │ │ └── questions-test.js │ ├── view │ │ └── json-view-test.js │ ├── utils │ │ ├── local-host-server-test.js │ │ └── hash-utils-test.js │ └── clients │ │ └── smapi-client-test │ │ └── resources │ │ └── vendor.js ├── integration │ ├── fixtures │ │ ├── code-builder │ │ │ ├── zip-only │ │ │ │ └── lambda │ │ │ │ │ └── some-file.txt │ │ │ ├── custom │ │ │ │ ├── lambda │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ └── hooks │ │ │ │ │ └── build.sh │ │ │ ├── python-pip │ │ │ │ └── lambda │ │ │ │ │ ├── requirements.txt │ │ │ │ │ └── hello_world.py │ │ │ ├── nodejs-npm │ │ │ │ └── lambda │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ └── java-mvn │ │ │ │ └── lambda │ │ │ │ ├── src │ │ │ │ └── com │ │ │ │ │ └── amazon │ │ │ │ │ └── ask │ │ │ │ │ └── helloworld │ │ │ │ │ └── HelloWorldStreamHandler.java │ │ │ │ └── pom.xml │ │ ├── catalog-upload.json │ │ ├── experiment-metric-configuration.json │ │ ├── job-definition.json │ │ ├── skill-manifest.json │ │ ├── account-linking-request.json │ │ ├── create-in-skill-product-request.json │ │ ├── interaction-model.json │ │ ├── lwa-swagger.json │ │ └── annotation-set.json │ └── run-test.js ├── tsconfig.json └── functional │ └── run-test.js ├── jsconfig.json ├── .prettierignore ├── .github ├── auto-assign-config.yml ├── workflows │ ├── auto-assign.yml │ ├── release.yml │ ├── unit-test.yml │ ├── functional-test-clean-up.yml │ ├── functional-test.yml │ └── integration-test.yml └── ISSUE_TEMPLATE │ └── issue-report.md ├── lib ├── model │ ├── callback.ts │ ├── dialog-save-skill-io-file.js │ ├── yaml-parser.js │ ├── regional-stack-file.js │ ├── sample-template.ts │ └── import-status.ts ├── exceptions │ ├── cli-error.js │ ├── cli-warn.js │ ├── cli-cfn-deployer-error.js │ ├── cli-file-not-found-error.js │ ├── cli-data-format-error.ts │ └── cli-retriable-error.ts ├── utils │ ├── metrics.js │ ├── local-host-server.js │ ├── unflatten.js │ ├── hash-utils.js │ ├── ac-util.js │ ├── url-utils.js │ ├── dynamic-config.js │ ├── profile-helper.js │ └── zip-utils.js ├── commands │ ├── smapi │ │ ├── customizations │ │ │ ├── aliases.json │ │ │ ├── parameters-map.js │ │ │ └── parameters.json │ │ ├── docs-templates │ │ │ ├── doc-site.mustache │ │ │ └── github.mustache │ │ ├── appended-commands │ │ │ ├── export-package │ │ │ │ └── helper.js │ │ │ ├── get-task │ │ │ │ └── index.ts │ │ │ └── search-task │ │ │ │ └── index.ts │ │ ├── before-send-processor.js │ │ └── smapi-docs.js │ ├── skill │ │ ├── skill-commander.ts │ │ └── add-locales │ │ │ ├── index.ts │ │ │ └── ui.js │ ├── run │ │ └── run-flow │ │ │ ├── abstract-run-flow.js │ │ │ ├── java-run.js │ │ │ ├── nodejs-run.js │ │ │ └── python-run.js │ ├── util │ │ ├── util-commander.ts │ │ └── upgrade-project │ │ │ └── ui.js │ ├── deploy │ │ └── ui.js │ ├── dialog │ │ ├── interactive-mode.ts │ │ ├── helper.ts │ │ └── replay-mode.ts │ ├── new │ │ └── template-helper.ts │ └── configure │ │ ├── helper.js │ │ └── messages.js ├── postinstall.js ├── controllers │ ├── authorization-controller │ │ ├── questions.js │ │ ├── ui.js │ │ └── messages.js │ └── skill-code-controller │ │ └── code-builder.js ├── clients │ ├── smapi-client │ │ └── resources │ │ │ ├── vendor.js │ │ │ ├── history.js │ │ │ ├── evaluations.js │ │ │ ├── account-linking.js │ │ │ ├── task.js │ │ │ ├── manifest.ts │ │ │ ├── private-skill.js │ │ │ └── skill-package.js │ └── aws-client │ │ ├── abstract-aws-client.ts │ │ ├── aws-util.ts │ │ └── iam-client.ts ├── view │ ├── json-view.js │ ├── import-status │ │ └── import-status-view-events.ts │ ├── prompt-view.ts │ ├── dialog-repl-view.ts │ └── spinner-view.js └── builtins │ ├── build-flows │ ├── zip-only.js │ ├── nodejs-npm.js │ ├── custom.js │ ├── java-mvn.js │ └── python-pip.js │ └── deploy-delegates │ └── cfn-deployer │ └── assets │ └── basic-lambda.yaml ├── .prettierrc.json ├── .prettierrc ├── .npmignore ├── .gitignore ├── bin ├── ask-util.ts ├── ask-skill.ts ├── ask-smapi.ts └── ask.ts ├── .commitlintrc.json ├── docs ├── contributer │ └── Generate-Smapi-Docs.md ├── concepts │ ├── CI-CD.md │ └── New-Command.md ├── Upgrade-Project-From-V1.md └── FAQ.md ├── Vagrantfile └── scripts ├── smapi-docs.js └── ask-clean-up.ts /test/fixture/testersList.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/temp/abstract-build-flow/some-file.txt: -------------------------------------------------------------------------------- 1 | some-file -------------------------------------------------------------------------------- /test/unit/fixture/build-flow/some-file.txt: -------------------------------------------------------------------------------- 1 | some-file -------------------------------------------------------------------------------- /test/unit/fixture/model/cli_config: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ask-cli" 3 | } -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/zip-only/lambda/some-file.txt: -------------------------------------------------------------------------------- 1 | some content -------------------------------------------------------------------------------- /test/unit/fixture/model/app-config-no-profiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixture/controller/invalid-deploy-delegate-instance.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixture/testersList.csv: -------------------------------------------------------------------------------- 1 | testerId, name 2 | a@a.com, Jack 3 | b@b.com, Mike 4 | c@c.com, David -------------------------------------------------------------------------------- /test/integration/fixtures/catalog-upload.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": "http://example.com" 3 | } 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/custom/lambda/index.js: -------------------------------------------------------------------------------- 1 | console.log("node lambda code"); 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/python-pip/lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | ask-sdk-core>=1.10.2 2 | -------------------------------------------------------------------------------- /test/unit/fixture/model/unsupported-file-type.notsupport: -------------------------------------------------------------------------------- 1 | File type not supported by the model 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/nodejs-npm/lambda/index.js: -------------------------------------------------------------------------------- 1 | console.log("node lambda code"); 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/python-pip/lambda/hello_world.py: -------------------------------------------------------------------------------- 1 | print("python lambda code") 2 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "." 4 | }, 5 | "include": ["lib", "test"] 6 | } 7 | -------------------------------------------------------------------------------- /test/temp/abstract-build-flow/build.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa/ask-cli/HEAD/test/temp/abstract-build-flow/build.zip -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github 2 | dist 3 | build 4 | node_modules 5 | docs 6 | scripts 7 | *.md 8 | coverage 9 | .nyc_output/ 10 | -------------------------------------------------------------------------------- /.github/auto-assign-config.yml: -------------------------------------------------------------------------------- 1 | addReviewers: true 2 | 3 | addAssignees: author 4 | 5 | reviewers: 6 | - tydonelson 7 | - CamdenFoucht 8 | -------------------------------------------------------------------------------- /lib/model/callback.ts: -------------------------------------------------------------------------------- 1 | export type callbackError = Error | null; 2 | export type uiCallback = (err: callbackError, result?: any) => void; 3 | -------------------------------------------------------------------------------- /test/unit/fixture/controller/valid-deploy-delegate-instance.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bootstrap: () => "bootstrap", 3 | invoke: () => "invoke", 4 | }; 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixture/model/dialog/invalid-dialog-replay-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "skillId": "", 3 | "locale": "", 4 | "type": "text", 5 | "userInput": [" ", "world"] 6 | } 7 | -------------------------------------------------------------------------------- /lib/exceptions/cli-error.js: -------------------------------------------------------------------------------- 1 | module.exports = class CliError extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = "CliError"; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha"], 4 | "noEmit": true 5 | }, 6 | "extends": "../tsconfig.json", 7 | "include": ["./**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixture/model/dialog/dialog-replay-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "skillId": "amzn1.ask.skill.1234567890", 3 | "locale": "en-US", 4 | "type": "text", 5 | "userInput": ["hello", "world"] 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixture/model/invalid-yaml.yaml: -------------------------------------------------------------------------------- 1 | simpleKey: simpleValue 2 | objectField: 3 | internalKey: internalValue 4 | arrayField: 5 | nested: 6 | innerArray: 7 | - innerKey: treasure 8 | . 9 | -------------------------------------------------------------------------------- /lib/utils/metrics.js: -------------------------------------------------------------------------------- 1 | const {MetricClient} = require("../clients/metric-client"); 2 | 3 | // metric client singleton 4 | const metricClient = new MetricClient(); 5 | 6 | module.exports = metricClient; 7 | -------------------------------------------------------------------------------- /test/functional/run-test.js: -------------------------------------------------------------------------------- 1 | process.env.ASK_SHARE_USAGE = false; 2 | 3 | ["./commands/high-level-commands-test.js"].forEach((testFile) => { 4 | // eslint-disable-next-line global-require 5 | require(testFile); 6 | }); 7 | -------------------------------------------------------------------------------- /lib/exceptions/cli-warn.js: -------------------------------------------------------------------------------- 1 | const CliError = require("./cli-error"); 2 | 3 | module.exports = class CliWarn extends CliError { 4 | constructor(message) { 5 | super(message); 6 | this.name = "CliWarn"; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/unit/fixture/model/json-config-yaml.yaml: -------------------------------------------------------------------------------- 1 | simpleKey: simpleValue 2 | objectField: 3 | internalKey: internalValue 4 | arrayField: 5 | - 1 6 | - 2 7 | - 3 8 | nested: 9 | innerArray: 10 | - innerKey: treasure 11 | -------------------------------------------------------------------------------- /test/unit/fixture/model/hosted-proj/.ask/ask-states.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillId": "amzn1.ask.skill.5555555-4444-3333-2222-1111111111" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/unit/fixture/model/invalid-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | "objectField": { 4 | "internalKey": "internalValue" 5 | }, 6 | "arrayField": [ 7 | "1" 8 | "2" 9 | "3" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lib/exceptions/cli-cfn-deployer-error.js: -------------------------------------------------------------------------------- 1 | const CliError = require("./cli-error"); 2 | 3 | module.exports = class CliCFNDeployerError extends CliError { 4 | constructor(message) { 5 | super(message); 6 | this.name = "CliCFNDeployerError"; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/exceptions/cli-file-not-found-error.js: -------------------------------------------------------------------------------- 1 | const CliError = require("./cli-error"); 2 | 3 | module.exports = class CliFileNotFoundError extends CliError { 4 | constructor(message) { 5 | super(message); 6 | this.name = "CliFileNotFoundError"; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/unit/fixture/model/hosted-proj/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillInfrastructure": { 6 | "type": "@ask-cli/hosted-skill-deployer" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: pull_request 3 | 4 | jobs: 5 | add-reviews: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: kentaro-m/auto-assign-action@v1.1.2 9 | with: 10 | configuration-path: ".github/auto-assign-config.yml" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": false, 4 | "embeddedLanguageFormatting": "auto", 5 | "printWidth": 140, 6 | "proseWrap": "never", 7 | "semi": true, 8 | "singleQuote": false, 9 | "tabWidth": 2, 10 | "trailingComma": "all", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc.js 2 | .commitlintrc.json 3 | .eslintrc.json 4 | .github 5 | .nyc_output 6 | .vscode 7 | coverage 8 | docs 9 | jsconfig.json 10 | scripts 11 | test 12 | package-lock.json 13 | build 14 | *.tgz 15 | lib 16 | bin 17 | !dist/** 18 | dist/test 19 | .husky 20 | .prettierrc 21 | .prettierignore 22 | -------------------------------------------------------------------------------- /test/unit/fixture/model/regular-proj/random-json-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "simpleKey": "simpleValue", 3 | "objectField": { 4 | "internalKey": "internalValue" 5 | }, 6 | "arrayField": [1, 2, 3], 7 | "nested": { 8 | "innerArray": [ 9 | { 10 | "innerKey": "treasure" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/run-test.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const chai = require("chai"); 3 | const fs = require("fs-extra"); 4 | const sinonChai = require("sinon-chai"); 5 | const chaiAsPromised = require("chai-as-promised"); 6 | 7 | chai.use(chaiAsPromised); 8 | chai.use(sinonChai); 9 | 10 | process.env.ASK_SHARE_USAGE = false; 11 | -------------------------------------------------------------------------------- /test/integration/fixtures/experiment-metric-configuration.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "CPDR", 4 | "metricTypes": ["KEY"], 5 | "expectedChange": "DECREASE" 6 | }, 7 | { 8 | "name": "ISP_SALES_QUANTITY", 9 | "metricTypes": ["KEY"], 10 | "expectedChange": "INCREASE" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/unit/fixture/model/json-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "simpleKey": "simpleValue", 3 | "objectField": { 4 | "internalKey": "internalValue" 5 | }, 6 | "arrayField": [ 7 | 1, 8 | 2, 9 | 3 10 | ], 11 | "nested": { 12 | "innerArray": [ 13 | { 14 | "innerKey": "treasure" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /lib/exceptions/cli-data-format-error.ts: -------------------------------------------------------------------------------- 1 | import CliError from "./cli-error"; 2 | 3 | export class CliDataFormatError extends CliError { 4 | constructor(message: string) { 5 | super(message); 6 | this.name = "CliDataFormatError"; 7 | 8 | if (Object.setPrototypeOf) { 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/integration/fixtures/job-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobDefinition": { 3 | "type": "CatalogAutoRefresh", 4 | "resource": { 5 | "type": "Catalog", 6 | "id": "amzn1.ask.interactionModel.catalog.661c3570-99d8-4b3f-95b7-db8c403a6125" 7 | }, 8 | "trigger": { 9 | "type": "Scheduled", 10 | "hour": 4 11 | }, 12 | "status": "ENABLED" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/custom/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.1.0", 4 | "description": "test", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/fixtures/skill-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "name": "integration test" 7 | } 8 | } 9 | }, 10 | "apis": { 11 | "custom": {} 12 | } 13 | }, 14 | "hosting": { 15 | "alexaHosted": { 16 | "runtime": "NODE_16_X" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/nodejs-npm/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.1.0", 4 | "description": "test", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | build 4 | dist/ 5 | package/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Coverage directory used by Istanbul 13 | coverage 14 | .nyc_output/ 15 | 16 | # IDE related 17 | .idea/ 18 | .vscode/ 19 | 20 | # MAC OS 21 | .DS_Store 22 | 23 | # test temp 24 | test/temp 25 | 26 | .vagrant 27 | 28 | # distribution files 29 | *.tgz 30 | -------------------------------------------------------------------------------- /test/integration/run-test.js: -------------------------------------------------------------------------------- 1 | process.env.ASK_SHARE_USAGE = false; 2 | 3 | const tests = ["./code-builder/code-builder-test.js"]; 4 | 5 | if (process.platform !== 'win32') { 6 | // smapi tests are not supported on windows 7 | tests.push('./commands/smapi-commands-test.js'); 8 | } 9 | tests.forEach((testFile) => { 10 | // eslint-disable-next-line global-require 11 | require(testFile); 12 | }); 13 | -------------------------------------------------------------------------------- /bin/ask-util.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {commander, UTIL_COMMAND_MAP} from "../lib/commands/util/util-commander"; 3 | 4 | commander.parse(process.argv); 5 | 6 | if (!process.argv.slice(2).length) { 7 | commander.outputHelp(); 8 | } else if (UTIL_COMMAND_MAP[process.argv[2]] === undefined) { 9 | console.error('Command not recognized. Please run "ask util" to check the user instructions.'); 10 | process.exit(1); 11 | } 12 | -------------------------------------------------------------------------------- /bin/ask-skill.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {commander, SKILL_COMMAND_MAP} from "../lib/commands/skill/skill-commander"; 4 | 5 | commander.parse(process.argv); 6 | 7 | if (!process.argv.slice(2).length) { 8 | commander.outputHelp(); 9 | } else if (SKILL_COMMAND_MAP[process.argv[2]] === undefined) { 10 | console.error('Command not recognized. Please run "ask skill" to check the user instructions.'); 11 | process.exit(1); 12 | } 13 | -------------------------------------------------------------------------------- /lib/commands/smapi/customizations/aliases.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile": "p", 3 | "skill-id": "s", 4 | "isp-id": "i", 5 | "stage": "g", 6 | "locale": "l", 7 | "locales": "l", 8 | "file": "f", 9 | "catalog-id": "c", 10 | "validation-id": "i", 11 | "utterance": "u", 12 | "simulation-id": "i", 13 | "json": "j", 14 | "text": "t", 15 | "certification-id": "c", 16 | "task-name": "t", 17 | "task-version": "v", 18 | "keywords": "k" 19 | } 20 | -------------------------------------------------------------------------------- /lib/postinstall.js: -------------------------------------------------------------------------------- 1 | console.log(` 2 | ================================================================================ 3 | ASK CLI collects telemetry to better understand customer needs. You can 4 | OPT OUT and disable telemetry by setting the 'share_usage' key to 'false' 5 | in '~/.ask/cli_config'. 6 | 7 | Learn more: https://developer.amazon.com/docs/alexa/smapi/ask-cli-telemetry.html 8 | ================================================================================ 9 | `); 10 | -------------------------------------------------------------------------------- /lib/commands/smapi/customizations/parameters-map.js: -------------------------------------------------------------------------------- 1 | const parameterRename = require("./parameters.json"); 2 | 3 | const apiToCommanderMap = new Map(); 4 | const customizationMap = new Map(); 5 | 6 | Object.keys(parameterRename).forEach((key) => { 7 | const value = parameterRename[key]; 8 | customizationMap.set(key, value); 9 | if (value.name) { 10 | apiToCommanderMap.set(key, value.name); 11 | } 12 | }); 13 | 14 | module.exports = {apiToCommanderMap, customizationMap}; 15 | -------------------------------------------------------------------------------- /lib/controllers/authorization-controller/questions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CONFIRM_ALLOW_BROWSER_SIGN_IN: [ 3 | { 4 | message: "Do you confirm that you used the browser to sign in to Alexa Skills Kit Tools?", 5 | type: "confirm", 6 | name: "choice", 7 | default: true, 8 | }, 9 | ], 10 | INFORM_ERROR: { 11 | // message is filled out in code 12 | type: "list", 13 | choices: ["Ok"], 14 | name: "choice", 15 | default: "Ok", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/commands/skill/skill-commander.ts: -------------------------------------------------------------------------------- 1 | import {Command} from "commander"; 2 | 3 | import {createCommand as localesCreateCommand} from "./add-locales"; 4 | 5 | const commander = new Command(); 6 | const SKILL_COMMAND_MAP: Record void> = { 7 | "add-locales": localesCreateCommand, 8 | }; 9 | 10 | Object.values(SKILL_COMMAND_MAP).forEach((create) => create(commander)); 11 | 12 | commander.name("ask skill"); 13 | commander.description("increase the productivity when managing skill metadata"); 14 | 15 | export {commander, SKILL_COMMAND_MAP}; 16 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "body-leading-blank": [1, "always"], 4 | "footer-leading-blank": [1, "always"], 5 | "subject-empty": [2, "never"], 6 | "subject-full-stop": [2, "never", "."], 7 | "subject-case": [2, "never", ["sentence-case", "pascal-case", "start-case", "upper-case"]], 8 | "scope-case": [2, "always", "lower-case"], 9 | "type-case": [2, "always", "lower-case"], 10 | "type-empty": [2, "never"], 11 | "type-enum": [2, "always", ["chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"]] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/unit/fixture/model/hosted-skill-resources-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillId": "amzn1.ask.skill.5555555-4444-3333-2222-1111111111", 6 | "skillInfrastructure": { 7 | "type": "@ask-cli/hosted-skill-deployer", 8 | "deployState": { 9 | "repository": { 10 | "type": "GIT", 11 | "url": "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/5555555-4444-3333-2222-1111111111" 12 | } 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/local-host-server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | 3 | module.exports = class LocalHostServer { 4 | constructor(PORT) { 5 | this.port = PORT; 6 | this.server = null; 7 | } 8 | 9 | create(requestHandler) { 10 | this.server = http.createServer(requestHandler); 11 | } 12 | 13 | listen(callback) { 14 | this.server.listen(this.port, callback); 15 | } 16 | 17 | registerEvent(eventName, callback) { 18 | this.server.on(eventName, callback); 19 | } 20 | 21 | destroy() { 22 | this.server.close(); 23 | this.server.unref(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/integration/fixtures/account-linking-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountLinkingRequest": { 3 | "skipOnEnablement": true, 4 | "type": "AUTH_CODE", 5 | "authorizationUrl": "https://www.example.com/auth_url", 6 | "domains": ["example.com"], 7 | "clientId": "string", 8 | "scopes": ["www.example.com"], 9 | "accessTokenUrl": "https://www.example.com/accessToken_url", 10 | "clientSecret": "abcdaslkaa", 11 | "accessTokenScheme": "HTTP_BASIC", 12 | "defaultTokenExpirationInSeconds": 20, 13 | "redirectUrls": ["https://www.example.com/redirect"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bin/ask-smapi.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import Messenger from "../lib/view/messenger"; 4 | import jsonView from "../lib/view/json-view"; 5 | import {makeSmapiCommander} from "../lib/commands/smapi/smapi-commander"; 6 | 7 | const commander = makeSmapiCommander(); 8 | 9 | if (!process.argv.slice(2).length) { 10 | commander.outputHelp(); 11 | } else { 12 | commander 13 | .parseAsync(process.argv) 14 | .then((result) => Messenger.getInstance().info(result)) 15 | .catch((err) => { 16 | Messenger.getInstance().error(jsonView.toString(err)); 17 | process.exit(1); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /docs/contributer/Generate-Smapi-Docs.md: -------------------------------------------------------------------------------- 1 | # Generate Smapi Docs 2 | 3 | Smapi documentation is auto generated. 4 | 5 | Generate docs and save to a file with the following command: 6 | 7 | ``` 8 | node scripts/smapi-docs.js [template-name] > filepath 9 | ``` 10 | template-name - optional parameter. If not specified 'github' template is used. 11 | All templates are located in the 'lib/commands/smapi/docs-templates'. 12 | 13 | Examples: 14 | 15 | ``` 16 | node scripts/smapi-docs.js > docs/concepts/Smapi-Command.md 17 | ``` 18 | 19 | ``` 20 | node scripts/smapi-docs.js doc-site > Smapi-Command-Doc-Site.md 21 | ``` -------------------------------------------------------------------------------- /test/unit/fixture/model/app-config.yaml: -------------------------------------------------------------------------------- 1 | machine_id: machineId 2 | share_usage: false 3 | profiles: 4 | testProfile: 5 | aws_profile: awsProfile 6 | token: 7 | access_token: accessToken 8 | refresh_token: refreshToken 9 | token_type: bearer 10 | expires_in: 3600 11 | expires_at: expiresAt 12 | vendor_id: vendorId 13 | default: 14 | aws_profile: default 15 | token: 16 | access_token: Atza|w 17 | refresh_token: Atzr|n 18 | token_type: bearer 19 | expires_in: 3600 20 | expires_at: "2018-09-11T18:12:18.843Z" 21 | vendor_id: wn 22 | -------------------------------------------------------------------------------- /lib/commands/run/run-flow/abstract-run-flow.js: -------------------------------------------------------------------------------- 1 | import nodemon from "nodemon"; 2 | import Messenger from "../../../view/messenger"; 3 | 4 | export class AbstractRunFlow { 5 | constructor(execConfig) { 6 | this.execConfig = execConfig; 7 | } 8 | 9 | execCommand() { 10 | return new Promise(() => { 11 | nodemon({verbose: true, ...this.execConfig}) 12 | .on('error', (error) => { 13 | reject(`Debugging session returned error code: ${(error) ? error.code : "undefined"}`); 14 | }); 15 | }); 16 | } 17 | 18 | getExecConfig() { 19 | return this.execConfig; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | askFolder = 'ask-cli' 5 | vagrantHome = '/home/vagrant' 6 | 7 | Vagrant.configure("2") do |config| 8 | config.vm.box = "hashicorp/bionic64" 9 | config.vm.provision "shell", inline: <<-SCRIPT 10 | curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - 11 | apt-get install -y nodejs 12 | cd #{askFolder}; npm link 13 | SCRIPT 14 | config.vm.synced_folder "#{Dir.home}/.ask", "#{vagrantHome}/.ask" 15 | config.vm.synced_folder "#{Dir.home}/.aws", "#{vagrantHome}/.aws" 16 | config.vm.synced_folder ".", "#{vagrantHome}/#{askFolder}" 17 | end 18 | -------------------------------------------------------------------------------- /test/unit/fixture/model/regular-proj/ask-resources-min.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesobjectVersion": "1.0", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skillPackage" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./awsStack/lambda-NA/src" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "type": "@ask-cli/cfn-deployer", 15 | "userConfig": { 16 | "runtime": "nodejs12.x", 17 | "handler": "index.handler", 18 | "template": "./awsStacks/skill-infra.yaml" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/clients/smapi-client/resources/vendor.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../../../utils/constants"); 2 | 3 | const EMPTY_HEADERS = {}; 4 | const EMPTY_QUERY_PARAMS = {}; 5 | const NULL_PAYLOAD = null; 6 | 7 | module.exports = (smapiHandle) => { 8 | function listVendors(callback) { 9 | const url = "vendors"; 10 | smapiHandle( 11 | CONSTANTS.SMAPI.API_NAME.LIST_VENDORS, 12 | CONSTANTS.HTTP_REQUEST.VERB.GET, 13 | CONSTANTS.SMAPI.VERSION.V1, 14 | url, 15 | EMPTY_QUERY_PARAMS, 16 | EMPTY_HEADERS, 17 | NULL_PAYLOAD, 18 | callback, 19 | ); 20 | } 21 | 22 | return { 23 | listVendors, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Publish the ask-cli package to the npmjs registry" 2 | 3 | # this action will only run when a new GitHub release is published. 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: '16.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm ci 18 | - run: npm run build 19 | - run: npm test 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.ASK_TOOLS_NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /test/unit/fixture/model/yaml-config.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | DeployEngineArn: 3 | Type: String 4 | Description: Deployment Lambda ARN 5 | SkillId: 6 | Type: String 7 | Description: Skill ID 8 | Mappings: 9 | AlexaAwsRegionMap: 10 | NA: 11 | AwsRegion: us-east-1 12 | EU: 13 | AwsRegion: eu-west-1 14 | FE: 15 | AwsRegion: us-west-2 16 | Resources: 17 | HelloWorldSkillInfrastructure: 18 | Type: Custom::AlexaSkillInfrastructure 19 | Properties: 20 | InfrastructureIAMRole: !GetAtt SkillIAMRole.Arn 21 | SkillId: !Ref SkillId 22 | ServiceToken: !Ref DeployEngineArn 23 | AWSTemplateFormatVersion: 2010-09-09 24 | -------------------------------------------------------------------------------- /test/unit/fixture/model/yaml-config.yml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | DeployEngineArn: 3 | Type: String 4 | Description: Deployment Lambda ARN 5 | SkillId: 6 | Type: String 7 | Description: Skill ID 8 | Mappings: 9 | AlexaAwsRegionMap: 10 | NA: 11 | AwsRegion: us-east-1 12 | EU: 13 | AwsRegion: eu-west-1 14 | FE: 15 | AwsRegion: us-west-2 16 | Resources: 17 | HelloWorldSkillInfrastructure: 18 | Type: Custom::AlexaSkillInfrastructure 19 | Properties: 20 | InfrastructureIAMRole: !GetAtt SkillIAMRole.Arn 21 | SkillId: !Ref SkillId 22 | ServiceToken: !Ref DeployEngineArn 23 | AWSTemplateFormatVersion: 2010-09-09 24 | -------------------------------------------------------------------------------- /test/unit/model/dialog/input.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import {inputToCommand, SpecialCommand} from "../../../../lib/model/dialog/inputs"; 3 | 4 | describe("Dialog Model - Input Helpers", () => { 5 | describe("input to command", () => { 6 | it("resolves commands from inputs", () => { 7 | expect(inputToCommand(".quit")).equals(SpecialCommand.QUIT); 8 | }); 9 | 10 | it("is case insensitive", () => { 11 | expect(inputToCommand(".Quit")).equals(SpecialCommand.QUIT); 12 | }); 13 | 14 | it("unknown commands return unknown", () => { 15 | expect(inputToCommand(".hi")).equals(SpecialCommand.UNKNOWN); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/fixture/model/yaml-from-json-result.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | DeployEngineArn: 3 | Type: String 4 | Description: Deployment Lambda ARN 5 | SkillId: 6 | Type: String 7 | Description: Skill ID 8 | Mappings: 9 | AlexaAwsRegionMap: 10 | NA: 11 | AwsRegion: us-east-1 12 | EU: 13 | AwsRegion: eu-west-1 14 | FE: 15 | AwsRegion: us-west-2 16 | Resources: 17 | HelloWorldSkillInfrastructure: 18 | Type: Custom::AlexaSkillInfrastructure 19 | Properties: 20 | InfrastructureIAMRole: !GetAtt SkillIAMRole.Arn 21 | SkillId: !Ref SkillId 22 | ServiceToken: !Ref DeployEngineArn 23 | AWSTemplateFormatVersion: 2010-09-09 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | paths-ignore: 9 | - '**.md' 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | node: [16, 18] 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node }} 27 | - run: npm install 28 | - run: npm run test:report 29 | -------------------------------------------------------------------------------- /lib/commands/smapi/docs-templates/doc-site.mustache: -------------------------------------------------------------------------------- 1 | ## smapi {#smapi-command} 2 | 3 | {{{smapiCommandDescription}}} 4 | 5 | **Syntax** 6 | 7 | `$ {{baseCommand}} ` 8 | 9 | smapi subcommands 10 | 11 | | | | 12 | |---|---| 13 | {{#commands}} 14 | | {{{description}}} | [{{name}}](#{{name}}-subcommand) | 15 | {{/commands}} 16 | {{#commands}} 17 | 18 | #### {{name}} {#{{name}}-subcommand} 19 | 20 | {{description}} 21 | 22 | **Syntax** 23 | 24 | `$ {{baseCommand}} {{name}} {{{optionsString}}}` 25 | 26 | **Options** 27 |
28 | {{#options}} 29 |
{{{name}}}
30 |
{{{description}}}
31 | {{/options}} 32 |
33 | {{/commands}} 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/commands/smapi/docs-templates/github.mustache: -------------------------------------------------------------------------------- 1 | # SMAPI COMMAND 2 | 3 | {{{smapiCommandDescription}}} 4 | 5 | `smapi` command format: 6 | 7 | `$ {{baseCommand}} ` 8 | 9 | ## Subcommands 10 | 11 | | Task | Subcommand | 12 | |---|---| 13 | {{#commands}} 14 | | {{{description}}} | [{{name}}](#{{name}}) | 15 | {{/commands}} 16 | {{#commands}} 17 | 18 | ### {{name}} 19 | 20 | {{description}} 21 | 22 | `{{name}}` command format: 23 | 24 | `$ {{baseCommand}} {{name}} {{{optionsString}}}` 25 | 26 | **Options** 27 | 28 |
29 | {{#options}} 30 |
{{{name}}}
31 |
{{{description}}}
32 | {{/options}} 33 |
34 | {{/commands}} 35 | 36 | 37 | -------------------------------------------------------------------------------- /lib/exceptions/cli-retriable-error.ts: -------------------------------------------------------------------------------- 1 | import CliError from "./cli-error"; 2 | 3 | export class CliRetriableError extends CliError { 4 | constructor(message: string) { 5 | super(message); 6 | this.name = "CliRetriableError"; 7 | 8 | if (Object.setPrototypeOf) { 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | } 12 | } 13 | 14 | export class RetriableServiceError extends CliRetriableError { 15 | protected readonly payload: any; 16 | 17 | constructor(message: string, body?: any) { 18 | super(message); 19 | this.name = "RetriableServiceError"; 20 | this.payload = body; 21 | 22 | if (Object.setPrototypeOf) { 23 | Object.setPrototypeOf(this, new.target.prototype); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/utils/unflatten.js: -------------------------------------------------------------------------------- 1 | const explodeProperty = (currUnflattened, key, flattenedObj, delimiter) => { 2 | const keys = key.split(delimiter); 3 | const value = flattenedObj[key]; 4 | const lastKeyIndex = keys.length - 1; 5 | 6 | for (let idx = 0; idx < lastKeyIndex; idx++) { 7 | const currKey = keys[idx]; 8 | if (!currUnflattened[currKey]) { 9 | currUnflattened[currKey] = {}; 10 | } 11 | currUnflattened = currUnflattened[currKey]; 12 | } 13 | 14 | currUnflattened[keys[lastKeyIndex]] = value; 15 | }; 16 | 17 | const unflatten = (flattened, delimiter = ".") => 18 | Object.keys(flattened).reduce((acc, key) => { 19 | explodeProperty(acc, key, flattened, delimiter); 20 | return acc; 21 | }, {}); 22 | 23 | module.exports = unflatten; 24 | -------------------------------------------------------------------------------- /.github/workflows/functional-test-clean-up.yml: -------------------------------------------------------------------------------- 1 | name: Functional Test Clean Up 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '15 7 * * *' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '16.x' 19 | - run: npm install 20 | - run: npm run functional-test:clean-up --unhandled-rejections=strict 21 | env: 22 | ASK_VENDOR_ID: ${{ secrets.ASK_VENDOR_ID }} 23 | ASK_REFRESH_TOKEN: ${{ secrets.ASK_REFRESH_TOKEN }} 24 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 25 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 26 | -------------------------------------------------------------------------------- /lib/view/json-view.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../utils/constants"); 2 | 3 | module.exports = { 4 | toString, 5 | }; 6 | 7 | /** 8 | * Convert JSON object to display string 9 | * @param {Object} jsonObject 10 | * @returns formatted JSON string or serialization error 11 | */ 12 | function toString(jsonObject) { 13 | try { 14 | if (typeof jsonObject === 'string') { 15 | return jsonObject; 16 | } 17 | // handle issue when Error object serialized to {} 18 | if (jsonObject instanceof Error) { 19 | jsonObject = {message: jsonObject.message, stack: jsonObject.stack, detail: jsonObject}; 20 | } 21 | return JSON.stringify(jsonObject, null, CONSTANTS.CONFIGURATION.JSON_DISPLAY_INDENT); 22 | } catch (e) { 23 | return e.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/smapi-docs.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { readFileSync } = require('fs-extra'); 3 | const Mustache = require('mustache'); 4 | 5 | const { makeSmapiCommander } = require('../lib/commands/smapi/smapi-commander'); 6 | const { SmapiDocs } = require('../lib/commands/smapi/smapi-docs'); 7 | 8 | const templateName = process.argv[2] || 'github'; 9 | const templatePath = join('lib', 'commands', 'smapi', 'docs-templates', `${templateName}.mustache`); 10 | const template = readFileSync(templatePath).toString(); 11 | 12 | const commander = makeSmapiCommander(); 13 | 14 | const docs = new SmapiDocs(commander); 15 | 16 | const viewData = docs.generateViewData(); 17 | 18 | const generatedDocs = Mustache.render(template, viewData); 19 | 20 | console.log(generatedDocs); 21 | -------------------------------------------------------------------------------- /test/unit/fixture/model/app-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "machine_id": "machineId", 3 | "share_usage": false, 4 | "profiles": { 5 | "testProfile": { 6 | "aws_profile": "awsProfile", 7 | "token": { 8 | "access_token": "accessToken", 9 | "refresh_token": "refreshToken", 10 | "token_type": "bearer", 11 | "expires_in": 3600, 12 | "expires_at": "expiresAt" 13 | }, 14 | "vendor_id": "vendorId" 15 | }, 16 | "default": { 17 | "aws_profile": "default", 18 | "token": { 19 | "access_token": "Atza|w", 20 | "refresh_token": "Atzr|n", 21 | "token_type": "bearer", 22 | "expires_in": 3600, 23 | "expires_at": "2018-09-11T18:12:18.843Z" 24 | }, 25 | "vendor_id": "wn" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/controllers/authorization-controller/ui.js: -------------------------------------------------------------------------------- 1 | const inquirer = require("inquirer"); 2 | const questions = require("./questions"); 3 | 4 | module.exports = { 5 | confirmAllowSignIn, 6 | informReceivedError 7 | }; 8 | export function confirmAllowSignIn(callback) { 9 | inquirer 10 | .prompt(questions.CONFIRM_ALLOW_BROWSER_SIGN_IN) 11 | .then((answer) => { 12 | callback(null, answer.choice); 13 | }) 14 | .catch((error) => { 15 | callback(error); 16 | }); 17 | } 18 | 19 | export function informReceivedError(callback, error) { 20 | inquirer 21 | .prompt([{...questions.INFORM_ERROR, message: `Sign in error: ${error}.`}]) 22 | .then((answer) => { 23 | callback(null, answer.choice); 24 | }) 25 | .catch((error) => { 26 | callback(error); 27 | }); 28 | } -------------------------------------------------------------------------------- /lib/commands/util/util-commander.ts: -------------------------------------------------------------------------------- 1 | import {createCommand as upgradeCreateCommand} from "./upgrade-project"; 2 | import {createCommand as gitCredsCreateCommand} from "./git-credentials-helper"; 3 | import {createCommand as generateLwaTokenCommand} from "./generate-lwa-tokens"; 4 | import {Command} from "commander"; 5 | 6 | const commander = new Command(); 7 | 8 | const UTIL_COMMAND_MAP: Record void> = { 9 | "upgrade-project": upgradeCreateCommand, 10 | "git-credentials-helper": gitCredsCreateCommand, 11 | "generate-lwa-tokens": generateLwaTokenCommand, 12 | }; 13 | 14 | Object.values(UTIL_COMMAND_MAP).forEach((createCommand) => createCommand(commander)); 15 | 16 | commander.name("ask util"); 17 | commander.description("tooling functions when using ask-cli to manage Alexa Skill"); 18 | 19 | export {commander, UTIL_COMMAND_MAP}; 20 | -------------------------------------------------------------------------------- /lib/builtins/build-flows/zip-only.js: -------------------------------------------------------------------------------- 1 | const AbstractBuildFlow = require("./abstract-build-flow"); 2 | 3 | class ZipOnlyBuildFlow extends AbstractBuildFlow { 4 | /** 5 | * Returns true if the build flow can handle the build 6 | */ 7 | static canHandle() { 8 | return true; 9 | } 10 | 11 | /** 12 | * Constructor 13 | * @param {Object} options 14 | * @param {String} options.cwd working directory for build 15 | * @param {String} options.src source directory 16 | * @param {String} options.buildFile full path for zip file to generate 17 | * @param {Boolean} options.doDebug debug flag 18 | */ 19 | constructor({cwd, src, buildFile, doDebug}) { 20 | super({cwd, src, buildFile, doDebug}); 21 | } 22 | 23 | /** 24 | * Executes build 25 | * @param {Function} callback 26 | */ 27 | execute(callback) { 28 | this.createZip(callback); 29 | } 30 | } 31 | 32 | module.exports = ZipOnlyBuildFlow; 33 | -------------------------------------------------------------------------------- /.github/workflows/functional-test.yml: -------------------------------------------------------------------------------- 1 | name: Functional Test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | 8 | jobs: 9 | build: 10 | if: "!contains(github.event.head_commit.message, 'chore(release):')" 11 | 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '16.x' 19 | - run: git config --global user.email "ask-cli-test@amazon.com" 20 | - run: git config --global user.name "Ask Cli Test" 21 | - run: npm install 22 | - run: npm link 23 | - run: npm run functional-test 24 | env: 25 | ASK_VENDOR_ID: ${{ secrets.ASK_VENDOR_ID }} 26 | ASK_REFRESH_TOKEN: ${{ secrets.ASK_REFRESH_TOKEN }} 27 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 28 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 29 | DEBUG: true 30 | -------------------------------------------------------------------------------- /test/unit/fixture/model/yaml-json-result.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "DeployEngineArn": { 4 | "Type": "String", 5 | "Description": "Deployment Lambda ARN" 6 | }, 7 | "SkillId": { 8 | "Type": "String", 9 | "Description": "Skill ID" 10 | } 11 | }, 12 | "Mappings": { 13 | "AlexaAwsRegionMap": { 14 | "NA": { 15 | "AwsRegion": "us-east-1" 16 | }, 17 | "EU": { 18 | "AwsRegion": "eu-west-1" 19 | }, 20 | "FE": { 21 | "AwsRegion": "us-west-2" 22 | } 23 | } 24 | }, 25 | "Resources": { 26 | "HelloWorldSkillInfrastructure": { 27 | "Type": "Custom::AlexaSkillInfrastructure", 28 | "Properties": { 29 | "InfrastructureIAMRole": "!GetAtt SkillIAMRole.Arn", 30 | "SkillId": "!Ref SkillId", 31 | "ServiceToken": "!Ref DeployEngineArn" 32 | } 33 | } 34 | }, 35 | "AWSTemplateFormatVersion": "2010-09-09" 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | name: Integration Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | paths-ignore: 9 | - '**.md' 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | node: [16, 18] 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node }} 27 | - name: Set up Python 3.x 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.x' 31 | - run: npm install 32 | - run: npm link 33 | - run: npm run integration-test 34 | env: 35 | ASK_VENDOR_ID: test 36 | ASK_REFRESH_TOKEN: test 37 | AWS_ACCESS_KEY_ID: test 38 | AWS_SECRET_ACCESS_KEY: test 39 | -------------------------------------------------------------------------------- /lib/utils/hash-utils.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const folderHash = require("folder-hash"); 3 | const fs = require("fs"); 4 | 5 | module.exports = { 6 | getHash, 7 | getFileHash, 8 | }; 9 | 10 | /** 11 | * Function used to generate hashcode of input folder/file 12 | * @param {*} sourcePath folder/file path 13 | * @param {*} callback { error, hashCode } 14 | */ 15 | function getHash(sourcePath, callback) { 16 | const options = { 17 | algo: "sha1", 18 | encoding: "base64", 19 | folders: { 20 | exclude: [".*", "node_modules", "test_coverage", "dist", "build"], 21 | ignoreRootName: true, 22 | }, 23 | }; 24 | 25 | folderHash.hashElement(sourcePath, options, (error, result) => { 26 | callback(error, error ? null : result.hash); 27 | }); 28 | } 29 | /** 30 | * Returns hash of a file 31 | * @param{String} filePath 32 | */ 33 | function getFileHash(filePath) { 34 | const sum = crypto.createHash("sha256"); 35 | sum.update(fs.readFileSync(filePath)); 36 | return sum.digest("hex"); 37 | } 38 | -------------------------------------------------------------------------------- /docs/concepts/CI-CD.md: -------------------------------------------------------------------------------- 1 | # CI/CD - Continuous integration and continuous delivery 2 | 3 | The following guide provides instructions how to use `ask cli` in CI/CD environment (Jenkins, Travis, Github Actions, etc). 4 | 5 | 1) Set up the following environment variables: 6 | - AWS_ACCESS_KEY_ID 7 | - AWS_SECRET_ACCESS_KEY 8 | - ASK_REFRESH_TOKEN or ASK_ACCESS_TOKEN 9 | - ASK_VENDOR_ID 10 | 11 | Values for ASK_REFRESH_TOKEN and ASK_ACCESS_TOKEN can be created using the following command: 12 | ``` 13 | ask util generate-lwa-tokens 14 | ``` 15 | or ASK_REFRESH_TOKEN, ASK_ACCESS_TOKEN and ASK_VENDOR_ID can be copied from $HOME/.ask/cli_config after running 16 | ``` 17 | ask configure 18 | ``` 19 | 20 | 2) Add profile __ENVIRONMENT_ASK_PROFILE__ to ask-resources.json 21 | ``` 22 | { 23 | "askcliResourcesVersion": "2020-03-31", 24 | "profiles": { 25 | "__ENVIRONMENT_ASK_PROFILE__": { 26 | "skillMetadata": { 27 | "src": "./skill-package" 28 | }, 29 | "code": { 30 | "default": { 31 | "src": "./lambda" 32 | } 33 | }, 34 | ... 35 | } 36 | } 37 | } 38 | 39 | ``` -------------------------------------------------------------------------------- /lib/utils/ac-util.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const {COMPILER} = require("./constants"); 4 | const ResourcesConfig = require("../model/resources-config"); 5 | const Manifest = require("../model/manifest"); 6 | const jsonView = require("../view/json-view"); 7 | 8 | module.exports = { 9 | isAcSkill, 10 | syncManifest, 11 | }; 12 | 13 | /** 14 | * determine whether the skill is AC skill by check if skill-package/conversations exist 15 | * @param {String} skillPackagePath the path of skill-package 16 | * @returns boolean 17 | */ 18 | function isAcSkill(profile) { 19 | const skillPackageSrc = ResourcesConfig.getInstance().getSkillMetaSrc(profile); 20 | const conversationPath = path.join(skillPackageSrc, COMPILER.ACDL_PATH); 21 | if (fs.existsSync(conversationPath)) { 22 | return true; 23 | } 24 | return false; 25 | } 26 | 27 | /** 28 | * update build/skill-package/skill.json based on skill.json in src 29 | */ 30 | function syncManifest(manifestPath) { 31 | const {content} = Manifest.getInstance(); 32 | fs.writeFileSync(manifestPath, jsonView.toString(content), "utf-8"); 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/builtins/build-flows/zip-only-test.js: -------------------------------------------------------------------------------- 1 | const {expect} = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const AbstractBuildFlow = require("../../../../lib/builtins/build-flows/abstract-build-flow"); 5 | const ZipOnlyBuildFlow = require("../../../../lib/builtins/build-flows/zip-only"); 6 | 7 | describe("ZipOnlyBuildFlow test", () => { 8 | let config; 9 | let createZipStub; 10 | beforeEach(() => { 11 | config = { 12 | cwd: "cwd", 13 | src: "src", 14 | buildFile: "buildFile", 15 | doDebug: false, 16 | }; 17 | createZipStub = sinon.stub(AbstractBuildFlow.prototype, "createZip").yields(); 18 | }); 19 | describe("# inspect correctness of execute", () => { 20 | it("| should execute commands", (done) => { 21 | const buildFlow = new ZipOnlyBuildFlow(config); 22 | 23 | buildFlow.execute((err, res) => { 24 | expect(err).eql(undefined); 25 | expect(res).eql(undefined); 26 | expect(createZipStub.callCount).eql(1); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | afterEach(() => { 33 | sinon.restore(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/java-mvn/lambda/src/com/amazon/ask/helloworld/HelloWorldStreamHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 4 | except in compliance with the License. A copy of the License is located at 5 | http://aws.amazon.com/apache2.0/ 6 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 7 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 8 | the specific language governing permissions and limitations under the License. 9 | */ 10 | 11 | package com.amazon.ask.helloworld; 12 | 13 | import com.amazon.ask.Skill; 14 | import com.amazon.ask.Skills; 15 | import com.amazon.ask.SkillStreamHandler; 16 | 17 | public class HelloWorldStreamHandler extends SkillStreamHandler { 18 | 19 | private static Skill getSkill() { 20 | return Skills.standard() 21 | .build(); 22 | } 23 | 24 | public HelloWorldStreamHandler() { 25 | super(getSkill()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /lib/clients/smapi-client/resources/history.js: -------------------------------------------------------------------------------- 1 | const R = require("ramda"); 2 | 3 | const CONSTANTS = require("../../../utils/constants"); 4 | 5 | const EMPTY_HEADERS = {}; 6 | const NULL_PAYLOAD = null; 7 | 8 | module.exports = (smapiHandle) => { 9 | /** 10 | * Get utterance transcripts for a skill 11 | * @param {String} skillId | skill id 12 | * @param {*} queryParameters | maxResults 13 | * sortDirection 14 | * sortField 15 | * nextToken 16 | * @param {function} callback | callback function from command 17 | */ 18 | function getIntentRequestsHistory(skillId, queryParams, callback) { 19 | let queryObject = R.clone(queryParams); 20 | if (R.isEmpty(queryObject)) { 21 | queryObject = {}; 22 | } 23 | const url = `skills/${skillId}/history/intentRequests`; 24 | 25 | smapiHandle( 26 | CONSTANTS.SMAPI.API_NAME.INTENT_REQUEST_HISTORY, 27 | CONSTANTS.HTTP_REQUEST.VERB.GET, 28 | CONSTANTS.SMAPI.VERSION.V1, 29 | url, 30 | queryObject, 31 | EMPTY_HEADERS, 32 | NULL_PAYLOAD, 33 | callback, 34 | ); 35 | } 36 | 37 | return { 38 | getIntentRequestsHistory, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /test/unit/fixture/model/regular-proj/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesobjectVersion": "1.0", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skillPackage" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./awsStack/lambda-NA/src" 11 | }, 12 | "NA": { 13 | "src": "./awsStack/lambda-NA/src" 14 | }, 15 | "EU": { 16 | "src": "./awsStack/lambda-EU/src" 17 | } 18 | }, 19 | "skillInfrastructure": { 20 | "type": "@ask-cli/cfn-deployer", 21 | "userConfig": { 22 | "runtime": "nodejs12.x", 23 | "handler": "index.handler", 24 | "template": "./awsStacks/skill-infra.yaml", 25 | "targetEndpoint": [ 26 | "custom", 27 | "smartHome" 28 | ], 29 | "regionOverrides": { 30 | "NA": { 31 | "awsRegion": "us-east-1", 32 | "template": "./awsStacks/skill-infra.yaml" 33 | }, 34 | "FE": { 35 | "awsRegion": "us-east-2", 36 | "template": "./awsStacks/skill-infra.yaml" 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /test/unit/commands/skill/skill-commander-test.ts: -------------------------------------------------------------------------------- 1 | import {commander} from "../../../../lib/commands/skill/skill-commander"; 2 | import AddlocalesCommand from "../../../../lib/commands/skill/add-locales"; 3 | import sinon from "sinon"; 4 | import Messenger from "../../../../lib/view/messenger"; 5 | 6 | /** 7 | * Simple test which loads the skill commander while running tests. 8 | * This was previously not done and could fail due to changes. 9 | */ 10 | describe("Skill Commander Test", () => { 11 | let errorStub, warnStub, infoStub; 12 | 13 | beforeEach(() => { 14 | errorStub = sinon.stub(); 15 | warnStub = sinon.stub(); 16 | infoStub = sinon.stub(); 17 | sinon.stub(Messenger, "getInstance").returns({ 18 | info: infoStub, 19 | warn: warnStub, 20 | error: errorStub, 21 | dispose: sinon.stub(), 22 | }); 23 | sinon.stub(process, "exit"); 24 | }); 25 | 26 | it("loads and runs a command", async () => { 27 | sinon.stub(AddlocalesCommand.prototype, "handle").resolves(); 28 | sinon.stub(AddlocalesCommand.prototype, "_remindsIfNewVersion").resolves(); 29 | await commander.parseAsync(["something", "something", "add-locales"]); 30 | }); 31 | 32 | afterEach(() => { 33 | sinon.restore(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/commands/util/util-commander-test.ts: -------------------------------------------------------------------------------- 1 | import {commander} from "../../../../lib/commands/util/util-commander"; 2 | import UpdateProjectCommand from "../../../../lib/commands/util/upgrade-project"; 3 | import sinon from "sinon"; 4 | import Messenger from "../../../../lib/view/messenger"; 5 | import * as httpClient from "../../../../lib/clients/http-client"; 6 | 7 | /** 8 | * Simple test which loads the util commander while running tests. 9 | * This was previously not done and could fail due to changes. 10 | */ 11 | describe("Util Commander Test", () => { 12 | let errorStub, warnStub, infoStub; 13 | 14 | beforeEach(() => { 15 | errorStub = sinon.stub(); 16 | warnStub = sinon.stub(); 17 | infoStub = sinon.stub(); 18 | sinon.stub(Messenger, "getInstance").returns({ 19 | info: infoStub, 20 | warn: warnStub, 21 | error: errorStub, 22 | dispose: sinon.stub(), 23 | }); 24 | sinon.stub(process, "exit"); 25 | sinon.stub(httpClient, "request").yields({statusCode: 200}); 26 | }); 27 | 28 | it("loads and runs a command", async () => { 29 | sinon.stub(UpdateProjectCommand.prototype, "handle").resolves(); 30 | await commander.parseAsync(["something", "something", "upgrade-project"]); 31 | }); 32 | 33 | afterEach(() => { 34 | sinon.restore(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/clients/aws-client/abstract-aws-client.ts: -------------------------------------------------------------------------------- 1 | import {fromEnv, fromIni} from "@aws-sdk/credential-providers"; 2 | import {AwsCredentialIdentityProvider} from "@aws-sdk/types"; 3 | 4 | import CONSTANTS from "../../utils/constants"; 5 | import stringUtils from "../../utils/string-utils"; 6 | 7 | /** 8 | * Abstract Class for AWS Client 9 | */ 10 | export default abstract class AbstractAwsClient { 11 | credentials: AwsCredentialIdentityProvider; 12 | profile?: string; 13 | region?: string; 14 | 15 | /** 16 | * Constructor 17 | * @param configuration aws client config 18 | */ 19 | constructor(configuration: AwsClientConfiguration) { 20 | const {awsProfile, awsRegion} = configuration; 21 | if (!stringUtils.isNonBlankString(awsProfile) || !stringUtils.isNonBlankString(awsRegion)) { 22 | throw new Error("Invalid awsProfile or Invalid awsRegion"); 23 | } 24 | this.credentials = 25 | awsProfile === CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS 26 | ? fromEnv() 27 | : fromIni({ 28 | profile: awsProfile, 29 | }); 30 | this.profile = awsProfile; 31 | this.region = awsRegion; 32 | } 33 | } 34 | 35 | /** 36 | * Interface for AWS CLient Configuration 37 | */ 38 | export interface AwsClientConfiguration { 39 | awsProfile?: string; 40 | awsRegion?: string; 41 | } 42 | -------------------------------------------------------------------------------- /lib/model/dialog-save-skill-io-file.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const jsonView = require("../view/json-view"); 3 | const R = require("ramda"); 4 | 5 | class DialogSaveSkillIoFile { 6 | /** 7 | * Constructor for DialogSaveSkillIoFile class 8 | * @param {string} filePath 9 | */ 10 | constructor(filePath) { 11 | this.filePath = filePath; 12 | this.content = {invocations: []}; 13 | } 14 | 15 | /** 16 | * Adds invocation start request to in memory invocation list 17 | * @param {*} request invocation request 18 | */ 19 | startInvocation(request) { 20 | this.currentInvocation = {request: {}, response: {}}; 21 | this.currentInvocation.request = request; 22 | } 23 | 24 | /** 25 | * Adds invocation end response to in memory invocation list 26 | * @param {*} response invocation response 27 | */ 28 | endInvocation(response) { 29 | if (!this.currentInvocation) { 30 | this.currentInvocation = {response: {}}; 31 | } 32 | this.currentInvocation.response = response; 33 | this.content.invocations.push(R.clone(this.currentInvocation)); 34 | } 35 | 36 | /** 37 | * Saves invocation list to a file 38 | */ 39 | save() { 40 | if (this.filePath) { 41 | fs.writeFileSync(this.filePath, jsonView.toString(this.content), "utf-8"); 42 | } 43 | } 44 | } 45 | 46 | module.exports = DialogSaveSkillIoFile; 47 | -------------------------------------------------------------------------------- /lib/view/import-status/import-status-view-events.ts: -------------------------------------------------------------------------------- 1 | export type ImportStatusViewEvents = 2 | | "buildingACLight" 3 | | "buildingACFull" 4 | | "buildACSuccess" 5 | | "buildACFailed" 6 | | "buildIMSuccess" 7 | | "buildIMFailed" 8 | | "fetchingSkillIdSuccess" 9 | | "fetchingNewSkillIdSuccess" 10 | | "fetchingImportIdSuccess" 11 | | "fetchingIdFailed" 12 | | "cancelTask"; 13 | export const IMPORT_STATUS_BUILDING_AC_LIGHT_EVENT: ImportStatusViewEvents = "buildingACLight"; 14 | export const IMPORT_STATUS_BUILDING_AC_FULL_EVENT: ImportStatusViewEvents = "buildingACFull"; 15 | export const IMPORT_STATUS_AC_BUILD_SUCCESS_EVENT: ImportStatusViewEvents = "buildACSuccess"; 16 | export const IMPORT_STATUS_AC_BUILD_FAILED_EVENT: ImportStatusViewEvents = "buildACFailed"; 17 | export const IMPORT_STATUS_IM_BUILD_SUCCESS_EVENT: ImportStatusViewEvents = "buildIMSuccess"; 18 | export const IMPORT_STATUS_IM_BUILD_FAILED_EVENT: ImportStatusViewEvents = "buildIMFailed"; 19 | export const IMPORT_STATUS_FETCHING_SKILL_ID_SUCESS_EVENT: ImportStatusViewEvents = "fetchingSkillIdSuccess"; 20 | export const IMPORT_STATUS_FETCHING_NEW_SKILL_ID_SUCESS_EVENT: ImportStatusViewEvents = "fetchingNewSkillIdSuccess"; 21 | export const IMPORT_STATUS_FETCHING_IMPORT_ID_SUCESS_EVENT: ImportStatusViewEvents = "fetchingImportIdSuccess"; 22 | export const IMPORT_STATUS_CANCEL_TASK_EVENT: ImportStatusViewEvents = "cancelTask"; 23 | -------------------------------------------------------------------------------- /lib/clients/smapi-client/resources/evaluations.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../../../utils/constants"); 2 | 3 | const EMPTY_HEADERS = {}; 4 | const EMPTY_QUERY_PARAMS = {}; 5 | 6 | module.exports = (smapiHandle) => { 7 | /** 8 | * Profile utterance with NLU 9 | * @param {string} skillId | skill id 10 | * @param {string} stage | skill stage, default is development 11 | * @param {string} locale | skill locale 12 | * @param {string} utterance | utterance to be profiled 13 | * @param {string} multiTurnToken | multiTurn token for dialog 14 | * @param {function} callback | callback function from command 15 | */ 16 | function callProfileNlu(skillId, stage, locale, utterance, multiTurnToken, callback) { 17 | const skillStage = stage || CONSTANTS.SKILL.STAGE.DEVELOPMENT; 18 | const url = `skills/${skillId}/stages/${skillStage}/interactionModel/locales/${locale}/profileNlu`; 19 | const payload = { 20 | utterance, 21 | }; 22 | 23 | if (multiTurnToken) { 24 | payload.multiTurnToken = multiTurnToken; 25 | } 26 | 27 | smapiHandle( 28 | CONSTANTS.SMAPI.API_NAME.NLU_PROFILE, 29 | CONSTANTS.HTTP_REQUEST.VERB.POST, 30 | CONSTANTS.SMAPI.VERSION.V1, 31 | url, 32 | EMPTY_QUERY_PARAMS, 33 | EMPTY_HEADERS, 34 | payload, 35 | callback, 36 | ); 37 | } 38 | 39 | return { 40 | callProfileNlu, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /lib/view/prompt-view.ts: -------------------------------------------------------------------------------- 1 | import {accessSync, constants} from "fs"; 2 | import inquirer from "inquirer"; 3 | import {filterNonAlphanumeric} from "../utils/string-utils"; 4 | import {uiCallback} from "../model/callback"; 5 | 6 | /** 7 | * To get user's input project folder name 8 | * @param {string} defaultName a default project name 9 | * @param {uiCallback} callback { error, response } 10 | */ 11 | export function getProjectFolderName(defaultName: string, callback: uiCallback) { 12 | inquirer 13 | .prompt([ 14 | { 15 | message: "Please type in your folder name for the skill project (alphanumeric): ", 16 | type: "input", 17 | default: defaultName, 18 | name: "projectFolderName", 19 | validate: (input) => { 20 | if (!input || filterNonAlphanumeric(input) === "") { 21 | return 'Project folder name should consist of alphanumeric character(s) plus "-" only.'; 22 | } 23 | try { 24 | accessSync(process.cwd(), constants.W_OK); 25 | } catch (error) { 26 | return `No write access inside of the folder: ${process.cwd()}.`; 27 | } 28 | return true; 29 | }, 30 | }, 31 | ]) 32 | .then((answer) => { 33 | callback(null, filterNonAlphanumeric(answer.projectFolderName)); 34 | }) 35 | .catch((error) => { 36 | callback(error); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/commands/run/run-flow/java-run.js: -------------------------------------------------------------------------------- 1 | import { RUNTIME } from "../../../utils/constants"; 2 | import { AbstractRunFlow } from "./abstract-run-flow"; 3 | 4 | export class JavaRunFlow extends AbstractRunFlow { 5 | static canHandle(runtime) { 6 | return runtime === RUNTIME.JAVA; 7 | } 8 | 9 | constructor({ skillInvocationInfo, waitForAttach, debugPort, token, skillId, runRegion, watch }) { 10 | let execCmd = 11 | `cd ${skillInvocationInfo.skillCodeFolderName}; mvn exec:exec -Dexec.executable=java -Dexec.args=` + 12 | '"-classpath %classpath com.amazon.ask.localdebug.LocalDebuggerInvoker ' + 13 | `--accessToken ${token} --skillId ${skillId} --skillStreamHandlerClass ${skillInvocationInfo.handlerName} --region ${runRegion}"`; 14 | if (waitForAttach) { 15 | execCmd = 16 | `cd ${skillInvocationInfo.skillCodeFolderName}; mvn exec:exec -Dexec.executable=java -Dexec.args=` + 17 | `"-classpath %classpath -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=${debugPort} ` + 18 | "com.amazon.ask.localdebug.LocalDebuggerInvoker " + 19 | `--accessToken ${token} --skillId ${skillId} --region ${runRegion} --skillStreamHandlerClass ${skillInvocationInfo.handlerName}"`; 20 | } 21 | super({ 22 | exec: execCmd, 23 | ext: "java, xml", 24 | watch: watch ? `${skillInvocationInfo.skillCodeFolderName}` : watch, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/view/dialog-repl-view.ts: -------------------------------------------------------------------------------- 1 | import {CliReplView, CliReplViewConfig} from "./cli-repl-view"; 2 | 3 | export class DialogReplView extends CliReplView { 4 | /** 5 | * Constructor for DialogReplView. 6 | * @param {Object} configuration config object. 7 | */ 8 | constructor(configuration: CliReplViewConfig) { 9 | const conf = configuration || {}; 10 | conf.prettifyHeaderFooter = (arg) => { 11 | const lines = arg.split("\n"); 12 | const terminalWidth = process.stdout.columns; 13 | const halfWidth = Math.floor(terminalWidth / 2); 14 | const bar = "=".repeat(terminalWidth); 15 | const formattedLines = lines.map((line) => { 16 | const paddedLine = ` ${line.trim()} `; 17 | const offset = halfWidth - Math.floor(paddedLine.length / 2); 18 | if (offset < 0) { 19 | return `===${paddedLine}===`; 20 | } 21 | return bar.slice(0, offset) + paddedLine + bar.slice(offset + paddedLine.length); 22 | }); 23 | return `\n${formattedLines.join("\n")}\n`; 24 | }; 25 | super(conf); 26 | } 27 | 28 | /** 29 | * Specify the record special command. 30 | * @param {Function} func What function to execute when .record command is inputted. 31 | */ 32 | registerRecordCommand(func: (args: string) => void) { 33 | this.registerSpecialCommand("record", "Record input utterances to a replay file of a specified name.", func); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/commands/smapi/appended-commands/export-package/helper.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../../../../utils/constants"); 2 | const jsonView = require("../../../../view/json-view"); 3 | const Retry = require("../../../../utils/retry-utility"); 4 | 5 | module.exports = { 6 | pollExportStatus, 7 | }; 8 | 9 | /** 10 | * Wrapper for polling smapi skill package export status. 11 | * @param {String} exportId 12 | * @param {Function} callback (err, lastExportStatus) 13 | */ 14 | function pollExportStatus(smapiClient, exportId, callback) { 15 | const retryConfig = { 16 | base: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.MIN_TIME_OUT, 17 | factor: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.FACTOR, 18 | maxRetry: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.MAX_RETRY, 19 | }; 20 | const retryCall = (loopCallback) => { 21 | smapiClient.skillPackage.getExportStatus(exportId, (pollErr, pollResponse) => { 22 | if (pollErr) { 23 | return loopCallback(pollErr); 24 | } 25 | if (pollResponse.statusCode >= 300) { 26 | return loopCallback(jsonView.toString(pollResponse.body)); 27 | } 28 | loopCallback(null, pollResponse); 29 | }); 30 | }; 31 | const shouldRetryCondition = (retryResponse) => retryResponse.body.status === CONSTANTS.SKILL.PACKAGE_STATUS.IN_PROGRESS; 32 | Retry.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => callback(err, err ? null : res)); 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/view/json-view-test.js: -------------------------------------------------------------------------------- 1 | const {expect} = require("chai"); 2 | const jsonView = require("../../../lib/view/json-view"); 3 | 4 | describe("View test - JSON view test", () => { 5 | describe("# Test function toString", () => { 6 | it("| convert object to JSON string", () => { 7 | // setup 8 | const TEST_OBJ = { 9 | key: "value", 10 | }; 11 | const TEST_OBJ_JSON_STRING = '{\n "key": "value"\n}'; 12 | // call 13 | const jsonDisplay = jsonView.toString(TEST_OBJ); 14 | // verify 15 | expect(jsonDisplay).equal(TEST_OBJ_JSON_STRING); 16 | }); 17 | 18 | it("| convert error to JSON string", () => { 19 | // setup 20 | const error = new Error(); 21 | error.extraInfo = "some info"; 22 | 23 | // call 24 | const jsonDisplay = jsonView.toString(error); 25 | // verify 26 | expect(jsonDisplay).includes('"stack"'); 27 | expect(jsonDisplay).includes('"message"'); 28 | expect(jsonDisplay).includes('"extraInfo"'); 29 | }); 30 | 31 | it("| display error when JSON stringify throws error", () => { 32 | // setup 33 | const TEST_CIRCULAR_OBJ = {}; 34 | TEST_CIRCULAR_OBJ.key = {key2: TEST_CIRCULAR_OBJ}; 35 | // call 36 | const jsonDisplay = jsonView.toString(TEST_CIRCULAR_OBJ); 37 | // verify 38 | expect(jsonDisplay.startsWith("TypeError: Converting circular structure to JSON")).equal(true); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/commands/smapi/customizations/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "stageV2": { 3 | "name": "stage" 4 | }, 5 | "skill stage": { 6 | "name": "stage" 7 | }, 8 | "skill locale": { 9 | "name": "locale" 10 | }, 11 | "vendorId": { 12 | "skip": true 13 | }, 14 | "version": { 15 | "name": "vers" 16 | }, 17 | "createSkillRequest": { 18 | "skipUnwrap": true, 19 | "name": "manifest" 20 | }, 21 | "updateSkillRequest": { 22 | "skipUnwrap": true, 23 | "name": "manifest" 24 | }, 25 | "TestersRequest": { 26 | "customParameters": [ 27 | { 28 | "name": "testersEmails", 29 | "description": "List of email address of beta testers.", 30 | "required": true, 31 | "isArray": true 32 | } 33 | ] 34 | }, 35 | "createInSkillProductRequest": { 36 | "skipUnwrap": true 37 | }, 38 | "updateInSkillProductRequest": { 39 | "skipUnwrap": true, 40 | "name": "inSkillProduct" 41 | }, 42 | "CreateSubscriberRequest": { 43 | "skipUnwrap": true 44 | }, 45 | "UpdateSubscriberRequest": { 46 | "skipUnwrap": true 47 | }, 48 | "accountLinkingRequest": { 49 | "skipUnwrap": true 50 | }, 51 | "interactionModel": { 52 | "skipUnwrap": true 53 | }, 54 | "slotType": { 55 | "skipUnwrap": true 56 | }, 57 | "sslCertificatePayload": { 58 | "skipUnwrap": true 59 | }, 60 | "createJobDefinitionRequest": { 61 | "skipUnwrap": true, 62 | "name": "jobDefinition" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/unit/utils/local-host-server-test.js: -------------------------------------------------------------------------------- 1 | const {expect} = require("chai"); 2 | const http = require("http"); 3 | const sinon = require("sinon"); 4 | 5 | const CONSTANTS = require("../../../lib/utils/constants"); 6 | const LocalServer = require("../../../lib/utils/local-host-server"); 7 | 8 | describe("# Server test - Local host server test", () => { 9 | const TEST_PORT = CONSTANTS.LWA.LOCAL_PORT; 10 | const TEST_EVENT = "testEvent"; 11 | const listenStub = sinon.stub(); 12 | const onStub = sinon.stub(); 13 | const closeStub = sinon.stub(); 14 | const unrefStub = sinon.stub(); 15 | 16 | const TEST_SERVER = { 17 | listen: listenStub, 18 | on: onStub, 19 | close: closeStub, 20 | unref: unrefStub, 21 | }; 22 | 23 | afterEach(() => { 24 | sinon.restore(); 25 | }); 26 | 27 | it("| test server methods", () => { 28 | // setup 29 | const httpStub = sinon.stub(http, "createServer").returns(TEST_SERVER); 30 | const localServer = new LocalServer(TEST_PORT); 31 | 32 | // call 33 | localServer.create(() => {}); 34 | localServer.listen(() => {}); 35 | localServer.registerEvent(TEST_EVENT, () => {}); 36 | localServer.destroy(); 37 | 38 | // verify 39 | expect(httpStub.callCount).eq(1); 40 | expect(listenStub.callCount).eq(1); 41 | expect(onStub.callCount).eq(1); 42 | expect(closeStub.callCount).eq(1); 43 | expect(unrefStub.callCount).eq(1); 44 | expect(localServer.server).to.deep.eq(TEST_SERVER); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/unit/fixture/model/resources-config.yaml: -------------------------------------------------------------------------------- 1 | askcliResourcesobjectVersion: "1.0" 2 | profiles: 3 | default: 4 | skillId: amzn1.ask.skill.5555555-4444-3333-2222-1111111111 5 | skillMetadata: 6 | src: "./skillPackage" 7 | lastDeployHash: "" 8 | code: 9 | default: 10 | src: "./awsStack/lambda-NA/src" 11 | lastDeployHash: "" 12 | NA: 13 | src: "./awsStack/lambda-NA/src" 14 | lastDeployHash: "" 15 | EU: 16 | src: "./awsStack/lambda-EU/src" 17 | lastDeployHash: "" 18 | skillInfrastructure: 19 | type: "@ask-cli/cfn-deployer" 20 | userConfig: 21 | runtime: nodejs12.x 22 | handler: index.handler 23 | template: "./awsStacks/skill-infra.yaml" 24 | regionOverrides: 25 | NA: 26 | awsRegion: us-east-1 27 | template: "./awsStacks/skill-infra.yaml" 28 | EU: 29 | awsRegion: eu-west-1 30 | template: "./awsStacks/skill-infra.yaml" 31 | deployState: 32 | default: 33 | stackId: "" 34 | s3: 35 | bucket: "" 36 | key: endpoint/code 37 | objectVersion: "" 38 | NA: 39 | stackId: "" 40 | s3: 41 | bucket: "" 42 | key: endpoint/code 43 | objectVersion: "" 44 | EU: 45 | stackId: "" 46 | s3: 47 | bucket: "" 48 | key: endpoint/code 49 | objectVersion: "" 50 | -------------------------------------------------------------------------------- /lib/utils/url-utils.js: -------------------------------------------------------------------------------- 1 | const validUrl = require("valid-url"); 2 | const path = require("path"); 3 | const url = require("url"); 4 | 5 | module.exports = { 6 | isValidUrl, 7 | isLambdaArn, 8 | isHttpsUrl, 9 | isUrlOfficialTemplate, 10 | isUrlWithJsonExtension, 11 | }; 12 | 13 | function isValidUrl(urlString) { 14 | return typeof urlString === "string" && !!validUrl.isUri(urlString); 15 | } 16 | 17 | function isLambdaArn(urlString) { 18 | if (!isValidUrl(urlString)) { 19 | return false; 20 | } 21 | const lambdaRegex = new RegExp( 22 | [ 23 | "arn:aws:lambda:[a-z]+-[a-z]+-[0-9]:[0-9]{12}:", 24 | "function:[a-zA-Z0-9-_]+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?", 25 | "(:[a-zA-Z0-9-_]+)?", 26 | ].join(""), 27 | ); 28 | return lambdaRegex.test(urlString); 29 | } 30 | 31 | function isHttpsUrl(urlString) { 32 | if (!isValidUrl(urlString)) { 33 | return false; 34 | } 35 | return url.parse(urlString).protocol === "https:"; 36 | } 37 | 38 | function isUrlOfficialTemplate(inputUrl) { 39 | if (!isValidUrl(inputUrl)) { 40 | return false; 41 | } 42 | const hostname = url.parse(inputUrl).hostname; 43 | const urlSource = url.parse(inputUrl).pathname.split("/")[1]; 44 | 45 | return hostname === "github.com" && urlSource === "alexa"; 46 | } 47 | 48 | function isUrlWithJsonExtension(inputUrl) { 49 | if (!isValidUrl(inputUrl)) { 50 | return false; 51 | } 52 | const urlType = path.extname(inputUrl); 53 | return urlType === ".json"; 54 | } 55 | -------------------------------------------------------------------------------- /lib/view/spinner-view.js: -------------------------------------------------------------------------------- 1 | const ora = require("ora"); 2 | 3 | const TERMINATE_STYLE = { 4 | SUCCEED: "succeed", 5 | FAIL: "fail", 6 | WARN: "warn", 7 | INFO: "info", 8 | PERSIST: "stopAndPersist", 9 | CLEAR: "stop", 10 | }; 11 | 12 | class SpinnerView { 13 | constructor(oraConfig) { 14 | if (!oraConfig) { 15 | oraConfig = {}; 16 | } 17 | if (!oraConfig.color) { 18 | oraConfig.color = "yellow"; 19 | } 20 | if (!oraConfig.spinner) { 21 | oraConfig.spinner = process.platform === "darwin" ? "dots" : "balloon"; 22 | } 23 | this.oraSpinner = ora(oraConfig); 24 | } 25 | 26 | start(text) { 27 | this.oraSpinner.start(text); 28 | } 29 | 30 | update(text) { 31 | this.oraSpinner.text = text; 32 | } 33 | 34 | terminate(style, optionalMessage) { 35 | if (!style) { 36 | style = TERMINATE_STYLE.CLEAR; 37 | } 38 | this.oraSpinner[style](optionalMessage); 39 | } 40 | 41 | /** 42 | * Terminates and prints the newMessage, 43 | * then restarts the spinner with the previous message 44 | * @param {String} [style=succeed] - one of the TERMINATE_STYLE constants values 45 | * @param {String} newMessage - the text to print before starting the spinner again 46 | **/ 47 | restart(style, newMessage) { 48 | const originalMessage = this.oraSpinner.text; 49 | this.terminate(style, newMessage); 50 | this.start(originalMessage); 51 | } 52 | } 53 | 54 | module.exports = SpinnerView; 55 | module.exports.TERMINATE_STYLE = TERMINATE_STYLE; 56 | -------------------------------------------------------------------------------- /test/unit/fixture/model/regional-stack-file.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Resources: 3 | AlexaSkillIAMRole: 4 | Type: AWS::IAM::Role 5 | Properties: 6 | AssumeRolePolicyDocument: 7 | Version: 2012-10-17 8 | Statement: 9 | - Effect: Allow 10 | Principal: 11 | Service: 12 | - lambda.amazonaws.com 13 | Action: 14 | - sts:AssumeRole 15 | Path: / 16 | Policies: 17 | - PolicyName: alexaSkillExectionPolicy 18 | PolicyDocument: 19 | Version: 2012-10-17 20 | Statement: 21 | - Effect: Allow 22 | Action: 23 | - logs:* 24 | Resource: arn:aws:logs:*:*:* 25 | AlexaSkillFunction: 26 | Type: AWS::Lambda::Function 27 | Properties: 28 | Handler: index.handler 29 | Role: !Ref AlexaSkillIAMRoleARN 30 | Code: 31 | S3Bucket: endpoint.s3.bucket 32 | S3Key: endpoint.s3.key 33 | S3ObjectVersion: endpoint.s3.version 34 | Runtime: nodejs12.x 35 | MemorySize: 512 36 | Timeout: 60 37 | AlexaSkillFunctionEventPermission: 38 | Type: AWS::Lambda::Permission 39 | Properties: 40 | Action: lambda:invokeFunction 41 | FunctionName: !GetAtt AlexaSkillFunction.Arn 42 | Principal: alexa-appkit.amazon.com 43 | Outputs: 44 | SkillLambdaARN: 45 | Description: LambdaARN for the regional endpoint 46 | Value: !GetAtt AlexaSkillFunction.Arn 47 | -------------------------------------------------------------------------------- /test/integration/fixtures/create-in-skill-product-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "inSkillProductDefinition": { 3 | "version": "string", 4 | "type": "SUBSCRIPTION", 5 | "referenceName": "string", 6 | "purchasableState": "PURCHASABLE", 7 | "subscriptionInformation": { 8 | "subscriptionPaymentFrequency": "MONTHLY", 9 | "subscriptionTrialPeriodDays": 0 10 | }, 11 | "publishingInformation": { 12 | "locales": { 13 | "en-US": { 14 | "name": "string", 15 | "smallIconUri": "https://dummyimage.com/108/108/fff", 16 | "largeIconUri": "https://dummyimage.com/512/512/fff", 17 | "summary": "string", 18 | "description": "string", 19 | "examplePhrases": ["string"], 20 | "keywords": ["string"], 21 | "customProductPrompts": { 22 | "purchasePromptDescription": "test", 23 | "boughtCardDescription": "test" 24 | } 25 | } 26 | }, 27 | "distributionCountries": ["US"], 28 | "pricing": { 29 | "amazon.com": { 30 | "releaseDate": "2099-04-13T00:00:00Z", 31 | "defaultPriceListing": { 32 | "price": 0.99, 33 | "currency": "USD" 34 | } 35 | } 36 | }, 37 | "taxInformation": { 38 | "category": "SOFTWARE" 39 | } 40 | }, 41 | "privacyAndCompliance": { 42 | "locales": { 43 | "en-US": { 44 | "privacyPolicyUrl": "http://www.example.com" 45 | } 46 | } 47 | }, 48 | "testingInstructions": "string" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/unit/commands/dialog/interactive-mode.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import sinon from "sinon"; 3 | 4 | import {InteractiveMode} from "../../../../lib/commands/dialog/interactive-mode"; 5 | import {DialogController} from "../../../../lib/controllers/dialog-controller"; 6 | import {DialogReplView} from "../../../../lib/view/dialog-repl-view"; 7 | 8 | describe("# Command: Dialog - Interactive Mode test ", () => { 9 | const TEST_ERROR = "error"; 10 | const dialogReplViewPrototype = Object.getPrototypeOf(DialogReplView); 11 | 12 | afterEach(() => { 13 | Object.setPrototypeOf(DialogReplView, dialogReplViewPrototype); 14 | sinon.restore(); 15 | }); 16 | 17 | it("| test interactive mode start, dialog repl view creation throws error", async () => { 18 | // setup 19 | const dialogReplViewStub = sinon.stub().throws(new Error(TEST_ERROR)); 20 | Object.setPrototypeOf(DialogReplView, dialogReplViewStub); 21 | const interactiveMode = new InteractiveMode({} as any); 22 | // call 23 | await expect(interactiveMode.start()).rejectedWith(TEST_ERROR); 24 | }); 25 | 26 | it("| test interactive mode start, setupSpecialCommands throws error", async () => { 27 | // setup 28 | const dialogReplViewStub = sinon.stub(); 29 | Object.setPrototypeOf(DialogReplView, dialogReplViewStub); 30 | sinon.stub(DialogController.prototype, "setupSpecialCommands").rejects(Error(TEST_ERROR)); 31 | const interactiveMode = new InteractiveMode({} as any); 32 | // call 33 | await expect(interactiveMode.start()).rejectedWith(TEST_ERROR); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/commands/deploy/ui.js: -------------------------------------------------------------------------------- 1 | const inquirer = require("inquirer"); 2 | const acUtils = require("../../utils/ac-util"); 3 | const ResourcesConfig = require("../../model/resources-config"); 4 | const CONSTANTS = require("../../utils/constants"); 5 | 6 | module.exports = { 7 | confirmDeploymentIfNeeded, 8 | }; 9 | 10 | /** 11 | * Confirms the deployment before deploying the skill. 12 | * 13 | * Confirmation is rendered when the skill project is Alexa Conversations, and the 14 | * last deployment type in ask-states is undefined or not Alexa Conversations. 15 | */ 16 | function confirmDeploymentIfNeeded(profile, callback) { 17 | const lastDeployType = ResourcesConfig.getInstance().getSkillMetaLastDeployType(profile); 18 | const isAcSkill = acUtils.isAcSkill(profile); 19 | 20 | // If last deployment was Alexa Conversations, then this prompt has already been answered 21 | const isPromptNeeded = isAcSkill && lastDeployType !== CONSTANTS.DEPLOYMENT_TYPE.ALEXA_CONVERSATIONS; 22 | 23 | if (isPromptNeeded) { 24 | inquirer 25 | .prompt([ 26 | { 27 | message: 28 | "Skills with ACDL are not yet compatible with the https://developer.amazon.com, " + 29 | "hence this skill will be disabled on the Developer Console. Would you like to proceed?", 30 | type: "confirm", 31 | name: "confirmation", 32 | default: false, 33 | }, 34 | ]) 35 | .then((answer) => { 36 | callback(null, answer.confirmation); 37 | }) 38 | .catch((error) => { 39 | callback(error); 40 | }); 41 | } else { 42 | callback(null, true); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/commands/dialog/interactive-mode.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | import {DialogReplView} from "../../view/dialog-repl-view"; 4 | import {DialogController, DialogControllerProps} from "../../controllers/dialog-controller"; 5 | 6 | const SPECIAL_COMMANDS_HEADER = 7 | 'Use ".record " or ".record --append-quit" to save list of utterances to a file.\n' + 8 | 'You can exit the interactive mode by entering ".quit" or "ctrl + c".'; 9 | 10 | export interface InteractiveModeProps extends DialogControllerProps { 11 | header?: string; 12 | } 13 | 14 | export class InteractiveMode extends DialogController { 15 | header: string; 16 | 17 | constructor(config: InteractiveModeProps) { 18 | super(config); 19 | this.header = 20 | config.header || 21 | "Welcome to ASK Dialog\n" + 22 | "In interactive mode, type your utterance text onto the console and hit enter\n" + 23 | "Alexa will then evaluate your input and give a response!\n"; 24 | } 25 | 26 | async start(): Promise { 27 | try { 28 | const interactiveReplView = new DialogReplView({ 29 | prompt: chalk.yellow.bold("User > "), 30 | header: this.header + SPECIAL_COMMANDS_HEADER, 31 | footer: "Goodbye!", 32 | evalFunc: (input, replCallback) => { 33 | this.evaluateUtterance(input, interactiveReplView) 34 | .then((x) => replCallback(null, x)) 35 | .catch((x) => replCallback(x, null)); 36 | }, 37 | }); 38 | await this.setupSpecialCommands(interactiveReplView); 39 | } catch (error) { 40 | throw error; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/commands/run/run-flow/nodejs-run.js: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { existsSync } from "fs"; 3 | import { RUNTIME, RUN } from "../../../utils/constants"; 4 | import CliError from "../../../exceptions/cli-error"; 5 | import { AbstractRunFlow } from "./abstract-run-flow"; 6 | 7 | export class NodejsRunFlow extends AbstractRunFlow { 8 | static canHandle(runtime) { 9 | return runtime === RUNTIME.NODE; 10 | } 11 | 12 | constructor({ skillInvocationInfo, waitForAttach, debugPort, token, skillId, runRegion, watch }) { 13 | const skillFolderPath = join(process.cwd(), skillInvocationInfo.skillCodeFolderName); 14 | const script = join(skillFolderPath, RUN.NODE.SCRIPT_LOCATION); 15 | if (!existsSync(script)) { 16 | throw new CliError( 17 | "ask-sdk-local-debug cannot be found. Please install ask-sdk-local-debug to your skill code project. " + 18 | "Refer https://www.npmjs.com/package/ask-sdk-local-debug for more info.", 19 | ); 20 | } 21 | const execMap = waitForAttach 22 | ? { 23 | js: `node --inspect-brk=${debugPort}`, 24 | } 25 | : undefined; 26 | super({ 27 | execMap, 28 | script, 29 | args: [ 30 | "--accessToken", 31 | `"${token}"`, 32 | "--skillId", 33 | skillId, 34 | "--handlerName", 35 | skillInvocationInfo.handlerName, 36 | "--skillEntryFile", 37 | join(skillFolderPath, `${skillInvocationInfo.skillFileName}.js`), 38 | "--region", 39 | runRegion, 40 | ], 41 | watch: watch ? `${skillInvocationInfo.skillCodeFolderName}` : watch, 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/unit/utils/hash-utils-test.js: -------------------------------------------------------------------------------- 1 | const folderHash = require("folder-hash"); 2 | const sinon = require("sinon"); 3 | const {expect} = require("chai"); 4 | 5 | const hashUtils = require("../../../lib/utils/hash-utils"); 6 | 7 | describe("Utils test - hash utility", () => { 8 | describe("# getHash function tests", () => { 9 | const TEST_DIR_PATH = "DIR_PATH"; 10 | const expectedOptions = { 11 | algo: "sha1", 12 | encoding: "base64", 13 | folders: { 14 | exclude: [".*", "node_modules", "test_coverage", "dist", "build"], 15 | ignoreRootName: true, 16 | }, 17 | }; 18 | 19 | it("| can correctly callback hash code", (done) => { 20 | const TEST_HASH_CODE = "HASH_CODE"; 21 | const TEST_RESULT = { 22 | hash: TEST_HASH_CODE, 23 | }; 24 | // setup 25 | sinon.stub(folderHash, "hashElement").withArgs(TEST_DIR_PATH, expectedOptions).callsArgWith(2, null, TEST_RESULT); 26 | 27 | // call 28 | hashUtils.getHash(TEST_DIR_PATH, (error, response) => { 29 | // verify 30 | expect(error).equal(null); 31 | expect(response).equal(TEST_HASH_CODE); 32 | done(); 33 | }); 34 | }); 35 | 36 | it("| can correctly callback error", (done) => { 37 | const TEST_ERROR = "ERROR"; 38 | // setup 39 | sinon.stub(folderHash, "hashElement").withArgs(TEST_DIR_PATH, expectedOptions).callsArgWith(2, TEST_ERROR); 40 | 41 | // call 42 | hashUtils.getHash(TEST_DIR_PATH, (error) => { 43 | // verify 44 | expect(error).equal(TEST_ERROR); 45 | done(); 46 | }); 47 | }); 48 | 49 | afterEach(() => { 50 | sinon.restore(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/model/yaml-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * YAML load and dump from/to JSON object. 3 | * Support the loading of AWS Cloudformation Intrinsic function: 4 | * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html 5 | */ 6 | 7 | const fs = require("fs"); 8 | const yaml = require("js-yaml"); 9 | 10 | // Custom type for "!Ref" 11 | const refYamlType = new yaml.Type("!Ref", { 12 | kind: "scalar", 13 | resolve: (data) => !!data && typeof data === "string" && data.trim(), 14 | construct: (data) => `!Ref ${data}`, 15 | }); 16 | 17 | // Custom type for "!GetAtt" 18 | const getattYamlType = new yaml.Type("!GetAtt", { 19 | kind: "scalar", 20 | resolve: (data) => !!data && typeof data === "string" && data.trim(), 21 | construct: (data) => `!GetAtt ${data}`, 22 | }); 23 | 24 | // Create AWS schema for YAML parser 25 | // https://github.com/nodeca/js-yaml/blob/master/migrate_v3_to_v4.md 26 | const awsSchema = yaml.JSON_SCHEMA.extend([refYamlType, getattYamlType]); 27 | 28 | /** 29 | * Load the yaml file with aws's schema 30 | * @param {String} filePath File path for the yaml file 31 | */ 32 | function load(filePath) { 33 | const fileData = fs.readFileSync(filePath, "utf-8"); 34 | return yaml.load(fileData, {schema: awsSchema}); 35 | } 36 | 37 | /** 38 | * Dump the yaml file by merge the new content to the original file 39 | * @param {String} filePath File path for the yaml file 40 | * @param {Object} content Content to be written into filePath 41 | */ 42 | function dump(filePath, content) { 43 | fs.writeFileSync(filePath, yaml.dump(content).replace(/: *'(.+)'/g, ": $1")); // remove the single quotes for the value 44 | } 45 | 46 | module.exports = { 47 | load, 48 | dump, 49 | }; 50 | -------------------------------------------------------------------------------- /lib/clients/aws-client/aws-util.ts: -------------------------------------------------------------------------------- 1 | import {DEFAULT_PROFILE, parseKnownFiles} from "@smithy/shared-ini-file-loader"; 2 | import fs from "fs-extra"; 3 | import os from "os"; 4 | import path from "path"; 5 | import R from "ramda"; 6 | 7 | import CONSTANTS from "../../utils/constants"; 8 | 9 | /** 10 | * Returns the associated aws profile name to a ask profile 11 | * @param {string} askProfile cli profile name 12 | */ 13 | export function getAWSProfile(askProfile: string): string | undefined { 14 | if (askProfile === CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.PROFILE_NAME) { 15 | if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) { 16 | throw new Error("Environmental variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY not defined."); 17 | } 18 | return CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS; 19 | } 20 | const askConfig = fs.readJSONSync(path.join(os.homedir(), ".ask", "cli_config")); 21 | return R.view(R.lensPath(["profiles", askProfile, "aws_profile"]), askConfig); 22 | } 23 | 24 | /** 25 | * Returns the default aws region or global default aws region if available. 26 | * @param {string} awsProfile aws profile name 27 | */ 28 | export async function getCLICompatibleDefaultRegion(awsProfile: string): Promise { 29 | const profile = awsProfile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || DEFAULT_PROFILE; 30 | let region = process.env.AWS_REGION || process.env.AMAZON_REGION || process.env.AWS_DEFAULT_REGION || process.env.AMAZON_DEFAULT_REGION; 31 | if (!region) { 32 | const config = await parseKnownFiles({}).catch(() => undefined); 33 | region = config && config[profile] && config[profile].region; 34 | } 35 | return region || CONSTANTS.AWS_SKILL_INFRASTRUCTURE_DEFAULT_REGION; 36 | } 37 | -------------------------------------------------------------------------------- /lib/builtins/build-flows/nodejs-npm.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | const AbstractBuildFlow = require("./abstract-build-flow"); 5 | 6 | class NodeJsNpmBuildFlow extends AbstractBuildFlow { 7 | /** 8 | * If this file exists than the build flow can handle build 9 | */ 10 | static get manifest() { 11 | return "package.json"; 12 | } 13 | 14 | static get _lockFiles() { return ["package-lock.json", "npm-shrinkwrap.json"]; } 15 | 16 | /** 17 | * Returns true if the build flow can handle the build 18 | */ 19 | static canHandle({ src }) { 20 | return fs.existsSync(path.join(src, NodeJsNpmBuildFlow.manifest)); 21 | } 22 | 23 | /** 24 | * Constructor 25 | * @param {Object} options 26 | * @param {String} options.cwd working directory for build 27 | * @param {String} options.src source directory 28 | * @param {String} options.buildFile full path for zip file to generate 29 | * @param {Boolean} options.doDebug debug flag 30 | */ 31 | constructor({ cwd, src, buildFile, doDebug }) { 32 | super({ cwd, src, buildFile, doDebug }); 33 | } 34 | 35 | /** 36 | * Executes build 37 | * @param {Function} callback 38 | */ 39 | execute(callback) { 40 | const installCmd = this._hasLockFile() ? "ci" : "install"; 41 | const quietFlag = this.doDebug ? "" : " --quiet"; 42 | this.env.NODE_ENV = "production"; 43 | this.debug(`Installing NodeJS dependencies based on the ${NodeJsNpmBuildFlow.manifest}.`); 44 | this.execCommand(`npm ${installCmd}${quietFlag}`); 45 | this.createZip(callback); 46 | } 47 | 48 | _hasLockFile() { 49 | return NodeJsNpmBuildFlow._lockFiles.some(file => fs.existsSync(path.join(this.src, file))); 50 | } 51 | } 52 | 53 | module.exports = NodeJsNpmBuildFlow; 54 | -------------------------------------------------------------------------------- /test/unit/fixture/model/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "de-DE": { 6 | "summary": "one sentence description", 7 | "examplePhrases": [ 8 | "This is good", 9 | "no icon" 10 | ], 11 | "name": "skillName de", 12 | "description": "skill description" 13 | }, 14 | "en-US": { 15 | "summary": "Sample Short Description", 16 | "examplePhrases": [ 17 | "Alexa open hello world", 18 | "Alexa tell hello world hello", 19 | "Alexa ask hello world say hello" 20 | ], 21 | "name": "skillName us", 22 | "description": "Sample Full Description" 23 | }, 24 | "ja-JP": { 25 | "summary": "coniqiwa", 26 | "examplePhrases": [ 27 | "More to come", 28 | "coniqiwa" 29 | ], 30 | "name": "skillName jp", 31 | "description": "coniqiwa" 32 | } 33 | }, 34 | "isAvailableWorldwide": true, 35 | "testingInstructions": "Sample Testing Instructions.", 36 | "category": "WINE_AND_BEVERAGE", 37 | "distributionCountries": [] 38 | }, 39 | "apis": { 40 | "custom": { 41 | "endpoint": { 42 | "url": "TEST_URL1" 43 | }, 44 | "interfaces": [ 45 | { 46 | "type": "VIDEO_APP" 47 | } 48 | ], 49 | "regions": { 50 | "EU": { 51 | "endpoint": { 52 | "url": "TEST_URL2" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "events": { 59 | "endpoint": { 60 | "uri": "TEST_EVENT_URL" 61 | } 62 | }, 63 | "manifestVersion": "1.0" 64 | } 65 | } -------------------------------------------------------------------------------- /bin/ask.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import semver from "semver"; 3 | 4 | if (!semver.gte(process.version, "8.3.0")) { 5 | console.log("Version of node.js doesn't meet minimum requirement."); 6 | console.log("Please ensure system has node.js version 8.3.0 or higher."); 7 | process.exit(1); 8 | } 9 | 10 | import { Command } from 'commander'; 11 | const commander = new Command(); 12 | 13 | import {createCommand as configureCommand} from "../lib/commands/configure"; 14 | import {createCommand as deployCommand} from "../lib/commands/deploy"; 15 | import {createCommand as newCommand} from "../lib/commands/new"; 16 | import {createCommand as initCommand} from "../lib/commands/init"; 17 | import {createCommand as dialogCommand} from "../lib/commands/dialog"; 18 | import {createCommand as runCommand} from "../lib/commands/run"; 19 | 20 | [configureCommand, deployCommand, newCommand, initCommand, dialogCommand, runCommand].forEach( 21 | (command) => command(commander), 22 | ); 23 | 24 | commander 25 | .description("Command Line Interface for Alexa Skill Kit") 26 | .command("smapi", "list of Alexa Skill Management API commands") 27 | .command("skill", "increase the productivity when managing skill metadata") 28 | .command("util", "tooling functions when using ask-cli to manage Alexa Skill") 29 | .version(require("../package.json").version) 30 | .parse(process.argv); 31 | 32 | const ALLOWED_ASK_ARGV_2 = [ 33 | "configure", 34 | "deploy", 35 | "new", 36 | "init", 37 | "dialog", 38 | "smapi", 39 | "skill", 40 | "util", 41 | "help", 42 | "-v", 43 | "--version", 44 | "-h", 45 | "--help", 46 | "run", 47 | ]; 48 | if (process.argv[2] && ALLOWED_ASK_ARGV_2.indexOf(process.argv[2]) === -1) { 49 | console.log('Command not recognized. Please run "ask" to check the user instructions.'); 50 | process.exit(1); 51 | } 52 | -------------------------------------------------------------------------------- /lib/commands/smapi/appended-commands/get-task/index.ts: -------------------------------------------------------------------------------- 1 | import {AbstractCommand} from "../../../abstract-command"; 2 | import jsonView from "../../../../view/json-view"; 3 | import Messenger from "../../../../view/messenger"; 4 | import optionModel from "../../../option-model.json"; 5 | import profileHelper from "../../../../utils/profile-helper"; 6 | import SmapiClient from "../../../../clients/smapi-client"; 7 | import {OptionModel} from "../../../option-validator"; 8 | 9 | export default class GetTaskCommand extends AbstractCommand { 10 | name() { 11 | return "get-task"; 12 | } 13 | 14 | description() { 15 | return "Get the task definition details specified by the taskName and version."; 16 | } 17 | 18 | requiredOptions() { 19 | return ["skill-id", "task-name", "task-version"]; 20 | } 21 | 22 | optionalOptions() { 23 | return ["profile", "debug"]; 24 | } 25 | 26 | async handle(cmd: Record): Promise { 27 | const {skillId, taskName, taskVersion, profile, debug} = cmd; 28 | const smapiClient = new SmapiClient({ 29 | profile: profileHelper.runtimeProfile(profile), 30 | doDebug: debug, 31 | }); 32 | return new Promise((resolve, reject) => { 33 | smapiClient.task.getTask(skillId, taskName, taskVersion, (err: any, result: any) => { 34 | if (err || result.statusCode >= 400) { 35 | const error = err || jsonView.toString(result.body); 36 | Messenger.getInstance().error(error); 37 | reject(error); 38 | } else { 39 | const res = jsonView.toString(JSON.parse(result.body.definition)); 40 | Messenger.getInstance().info(res); 41 | resolve(res); 42 | } 43 | }); 44 | }); 45 | } 46 | } 47 | 48 | export const createCommand = new GetTaskCommand(optionModel as OptionModel).createCommand(); 49 | -------------------------------------------------------------------------------- /lib/builtins/build-flows/custom.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | const AbstractBuildFlow = require("./abstract-build-flow"); 5 | 6 | class CustomBuildFlow extends AbstractBuildFlow { 7 | /** 8 | * If this file exists than the build flow can handle build 9 | */ 10 | static get manifest() { 11 | return process.platform === "win32" ? "build.ps1" : "build.sh"; 12 | } 13 | 14 | static get _customScriptPath() { 15 | return path.join(process.cwd(), "hooks", CustomBuildFlow.manifest); 16 | } 17 | 18 | /** 19 | * Returns true if the build flow can handle the build 20 | */ 21 | static canHandle() { 22 | return fs.existsSync(CustomBuildFlow._customScriptPath); 23 | } 24 | 25 | /** 26 | * Constructor 27 | * @param {Object} options 28 | * @param {String} options.cwd working directory for build 29 | * @param {String} options.src source directory 30 | * @param {String} options.buildFile full path for zip file to generate 31 | * @param {Boolean} options.doDebug debug flag 32 | */ 33 | constructor({cwd, src, buildFile, doDebug}) { 34 | super({cwd, src, buildFile, doDebug}); 35 | } 36 | 37 | /** 38 | * Executes build 39 | * @param {Function} callback 40 | */ 41 | execute(callback) { 42 | this.debug(`Executing custom hook script ${CustomBuildFlow._customScriptPath}.`); 43 | let command = `${CustomBuildFlow._customScriptPath} "${this.buildFile}" ${this.doDebug}`; 44 | if (this.isWindows) { 45 | const powerShellPrefix = "PowerShell.exe -Command"; 46 | const doDebug = this.doDebug ? "$True" : "$False"; 47 | command = `${powerShellPrefix} "& {& '${CustomBuildFlow._customScriptPath}' '${this.buildFile}' ${doDebug} }"`; 48 | } 49 | this.execCommand(command); 50 | callback(); 51 | } 52 | } 53 | 54 | module.exports = CustomBuildFlow; 55 | -------------------------------------------------------------------------------- /lib/commands/smapi/before-send-processor.js: -------------------------------------------------------------------------------- 1 | const AppConfig = require("../../model/app-config"); 2 | 3 | class BeforeSendProcessor { 4 | constructor(commandName, paramsObject, modelIntrospector, profile) { 5 | this.params = modelIntrospector.operations.get(commandName).params; 6 | this.definitions = modelIntrospector.definitions; 7 | this.paramsObject = paramsObject; 8 | this.profile = profile; 9 | } 10 | 11 | processAll() { 12 | this.appendVendorId(); 13 | this.mapTestersEmails(); 14 | } 15 | 16 | appendVendorId() { 17 | const vendorId = AppConfig.getInstance().getVendorId(this.profile); 18 | const nonBodyParam = this.params.find((p) => p.name === "vendorId"); 19 | if (nonBodyParam) { 20 | this.paramsObject.vendorId = vendorId; 21 | return; 22 | } 23 | 24 | const bodyParam = this.params.find((p) => p.in === "body"); 25 | if (bodyParam && bodyParam.required && bodyParam.schema && bodyParam.schema.$ref) { 26 | const key = bodyParam.schema.$ref.split("/").pop(); 27 | const definition = this.definitions.get(key); 28 | if (!definition.properties) return; 29 | if (Object.keys(definition.properties).includes("vendorId")) { 30 | this.paramsObject[bodyParam.name] = this.paramsObject[bodyParam.name] || {}; 31 | this.paramsObject[bodyParam.name].vendorId = vendorId; 32 | } 33 | } 34 | } 35 | 36 | mapTestersEmails() { 37 | const hasTestersParam = this.params.find((p) => p.in === "body" && p.name === "TestersRequest"); 38 | if (hasTestersParam) { 39 | const {testersEmails} = this.paramsObject; 40 | this.paramsObject.testersRequest = { 41 | testers: testersEmails.map((email) => ({emailId: email})), 42 | }; 43 | 44 | delete this.paramsObject.testersEmails; 45 | } 46 | } 47 | } 48 | 49 | module.exports = BeforeSendProcessor; 50 | -------------------------------------------------------------------------------- /lib/clients/smapi-client/resources/account-linking.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../../../utils/constants"); 2 | 3 | const ACCOUNT_LINKING_URL_BASE = "accountLinkingClient"; 4 | 5 | const EMPTY_HEADERS = {}; 6 | const EMPTY_QUERY_PARAMS = {}; 7 | const NULL_PAYLOAD = null; 8 | 9 | module.exports = (smapiHandle) => { 10 | function setAccountLinking(skillId, stage, accountLinkingInfo, callback) { 11 | const url = `skills/${skillId}/stages/${stage}/${ACCOUNT_LINKING_URL_BASE}`; 12 | const payload = { 13 | accountLinkingRequest: accountLinkingInfo, 14 | }; 15 | smapiHandle( 16 | CONSTANTS.SMAPI.API_NAME.SET_ACCOUNT_LINKING, 17 | CONSTANTS.HTTP_REQUEST.VERB.PUT, 18 | CONSTANTS.SMAPI.VERSION.V1, 19 | url, 20 | EMPTY_QUERY_PARAMS, 21 | EMPTY_HEADERS, 22 | payload, 23 | callback, 24 | ); 25 | } 26 | 27 | function getAccountLinking(skillId, stage, callback) { 28 | const url = `skills/${skillId}/stages/${stage}/${ACCOUNT_LINKING_URL_BASE}`; 29 | smapiHandle( 30 | CONSTANTS.SMAPI.API_NAME.GET_ACCOUNT_LINKING, 31 | CONSTANTS.HTTP_REQUEST.VERB.GET, 32 | CONSTANTS.SMAPI.VERSION.V1, 33 | url, 34 | EMPTY_QUERY_PARAMS, 35 | EMPTY_HEADERS, 36 | NULL_PAYLOAD, 37 | callback, 38 | ); 39 | } 40 | 41 | function deleteAccountLinking(skillId, stage, callback) { 42 | const url = `skills/${skillId}/stages/${stage}/${ACCOUNT_LINKING_URL_BASE}`; 43 | smapiHandle( 44 | CONSTANTS.SMAPI.API_NAME.DELETE_ACCOUNT_LINKING, 45 | CONSTANTS.HTTP_REQUEST.VERB.DELETE, 46 | CONSTANTS.SMAPI.VERSION.V1, 47 | url, 48 | EMPTY_QUERY_PARAMS, 49 | EMPTY_HEADERS, 50 | NULL_PAYLOAD, 51 | callback, 52 | ); 53 | } 54 | 55 | return { 56 | setAccountLinking, 57 | getAccountLinking, 58 | deleteAccountLinking, 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /test/integration/fixtures/code-builder/custom/hooks/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Shell script for ask-cli code build for nodejs-npm flow. 4 | # 5 | # Script Usage: build.sh 6 | # OUT_FILE is the file name for the output (required) 7 | # DO_DEBUG is boolean value for debug logging 8 | # 9 | # Run this script whenever a package.json is defined 10 | 11 | readonly OUT_FILE=${1:-"upload.zip"} 12 | readonly DO_DEBUG=${2:-false} 13 | 14 | main() { 15 | if [[ $DO_DEBUG = true ]]; then 16 | echo "###########################" 17 | echo "####### Build Code ########" 18 | echo "###########################" 19 | fi 20 | 21 | if ! install_dependencies; then 22 | display_stderr "Failed to install the dependencies in the project." 23 | exit 1 24 | else 25 | if ! zip_node_modules; then 26 | display_stderr "Failed to zip the artifacts to ${OUT_FILE}." 27 | exit 1 28 | else 29 | if [[ $DO_DEBUG = true ]]; then 30 | echo "###########################" 31 | echo "Codebase built successfully" 32 | echo "###########################" 33 | fi 34 | exit 0 35 | fi 36 | fi 37 | } 38 | 39 | display_stderr() { 40 | echo "[Error] $1" >&2 41 | } 42 | 43 | display_debug() { 44 | echo "[Debug] $1" >&2 45 | } 46 | 47 | install_dependencies() { 48 | [[ $DO_DEBUG == true ]] && display_debug "Installing NodeJS dependencies based on the package.json." 49 | [[ $DO_DEBUG == false ]] && QQ=true # decide if quiet flag will be appended 50 | 51 | npm install --production ${QQ:+--quiet} 52 | return $? 53 | } 54 | 55 | zip_node_modules() { 56 | if [[ $DO_DEBUG = true ]]; then 57 | display_debug "Zipping source files and dependencies to $OUT_FILE." 58 | zip -vr "$OUT_FILE" ./* 59 | else 60 | zip -qr "$OUT_FILE" ./* 61 | fi 62 | return $? 63 | } 64 | 65 | # Execute main function 66 | main "$@" 67 | -------------------------------------------------------------------------------- /lib/commands/smapi/smapi-docs.js: -------------------------------------------------------------------------------- 1 | const smapiCommandDescription = 2 | "Provides a number of subcommands that " + 3 | "map 1:1 to the underlying API operations in Alexa Skill Management API (SMAPI)." + 4 | "The commands allow detailed control of API inputs and expose raw outputs. " + 5 | "There are subcommands for creating and updating the skill, interaction model, " + 6 | "and account linking information as well as starting the skill certification process."; 7 | 8 | class SmapiDocs { 9 | constructor(commander) { 10 | this.commander = commander; 11 | this.commands = commander.commands; 12 | } 13 | 14 | _makeOptionsString(options) { 15 | return options 16 | .map((option) => { 17 | const {mandatory, flags} = option; 18 | const prefix = mandatory ? "<" : "["; 19 | const suffix = mandatory ? ">" : "]"; 20 | return `${prefix}${flags.split(",").join("|")}${suffix}`; 21 | }) 22 | .join(" "); 23 | } 24 | 25 | _cleanDescription(description = "") { 26 | let cleanedDescription = description.trim(); 27 | if (cleanedDescription.length > 1 && !cleanedDescription.endsWith(".")) { 28 | cleanedDescription = `${cleanedDescription}.`; 29 | } 30 | // adding new line before * to correctly render list in markdown 31 | return cleanedDescription.replace("*", "\n*"); 32 | } 33 | 34 | generateViewData() { 35 | const commands = this.commands.map((command) => { 36 | const parsedCommand = { 37 | name: command._name, 38 | description: this._cleanDescription(command._description), 39 | optionsString: this._makeOptionsString(command.options), 40 | options: command.options.map((option) => ({name: option.flags, description: this._cleanDescription(option.description)})), 41 | }; 42 | return parsedCommand; 43 | }); 44 | return {smapiCommandDescription, baseCommand: "ask smapi", commands}; 45 | } 46 | } 47 | 48 | module.exports = {SmapiDocs}; 49 | -------------------------------------------------------------------------------- /lib/commands/skill/add-locales/index.ts: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import {AbstractCommand} from "../../abstract-command"; 3 | import optionModel from "../../option-model.json"; 4 | import CONSTANTS from "../../../utils/constants"; 5 | import profileHelper from "../../../utils/profile-helper"; 6 | import Messenger from "../../../view/messenger"; 7 | import helper from "./helper"; 8 | import ui from "./ui"; 9 | import {OptionModel} from "../../option-validator"; 10 | 11 | export default class AddLocalesCommand extends AbstractCommand { 12 | name() { 13 | return "add-locales"; 14 | } 15 | 16 | description() { 17 | return "add new locale(s) from existing locale or from the template"; 18 | } 19 | 20 | requiredOptions() { 21 | return []; 22 | } 23 | 24 | optionalOptions() { 25 | return ["profile", "debug"]; 26 | } 27 | 28 | async handle(cmd: Record): Promise { 29 | let profile: string; 30 | try { 31 | profile = profileHelper.runtimeProfile(cmd.profile); 32 | helper.initiateModels(profile); 33 | } catch (err) { 34 | Messenger.getInstance().error(err); 35 | throw err; 36 | } 37 | return new Promise((resolve, reject) => { 38 | ui.selectLocales(R.keys(CONSTANTS.ALEXA.LANGUAGES), (selectErr: any, selectedLocales: string[]) => { 39 | if (selectErr) { 40 | Messenger.getInstance().error(selectErr); 41 | return reject(selectErr); 42 | } 43 | helper.addLocales(selectedLocales, profile, cmd.debug, (addErr: any, iModelSourceByLocales: any) => { 44 | if (addErr) { 45 | Messenger.getInstance().error(addErr); 46 | return reject(addErr); 47 | } 48 | ui.displayAddLocalesResult(selectedLocales, iModelSourceByLocales, profile); 49 | resolve(); 50 | }); 51 | }); 52 | }); 53 | } 54 | } 55 | 56 | export const createCommand = new AddLocalesCommand(optionModel as OptionModel).createCommand(); 57 | -------------------------------------------------------------------------------- /lib/controllers/skill-code-controller/code-builder.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | const CustomBuildFlow = require("../../builtins/build-flows/custom"); 5 | const JavaMvnBuildFlow = require("../../builtins/build-flows/java-mvn"); 6 | const NodeJsNpmBuildFlow = require("../../builtins/build-flows/nodejs-npm"); 7 | const PythonPipBuildFlow = require("../../builtins/build-flows/python-pip"); 8 | const ZipOnlyBuildFlow = require("../../builtins/build-flows/zip-only"); 9 | 10 | // order is important 11 | const BUILD_FLOWS = [CustomBuildFlow, NodeJsNpmBuildFlow, JavaMvnBuildFlow, PythonPipBuildFlow, ZipOnlyBuildFlow]; 12 | 13 | class CodeBuilder { 14 | /** 15 | * Constructor for CodeBuilder 16 | * @param {Object} config { src, build, doDebug }, where build = { folder, file }. 17 | */ 18 | constructor(config) { 19 | const {src, build, doDebug} = config; 20 | this.src = src; 21 | this.build = build; 22 | this.doDebug = doDebug; 23 | this.BuildFlowClass = {}; 24 | this._selectBuildFlowClass(); 25 | } 26 | 27 | /** 28 | * Executes build flow 29 | * @param {Function} callback (error) 30 | */ 31 | execute(callback) { 32 | try { 33 | this._setUpBuildFolder(); 34 | } catch (fsErr) { 35 | return process.nextTick(callback(fsErr)); 36 | } 37 | const options = {cwd: this.build.folder, src: this.src, buildFile: this.build.file, doDebug: this.doDebug}; 38 | const builder = new this.BuildFlowClass(options); 39 | builder.execute((error) => callback(error)); 40 | } 41 | 42 | _setUpBuildFolder() { 43 | fs.ensureDirSync(this.build.folder); 44 | fs.emptyDirSync(this.build.folder); 45 | fs.copySync(path.resolve(this.src), this.build.folder, {filter: (src) => !src.includes(this.build.folder)}); 46 | } 47 | 48 | _selectBuildFlowClass() { 49 | this.BuildFlowClass = BUILD_FLOWS.find((flow) => flow.canHandle({src: this.src})); 50 | } 51 | } 52 | 53 | module.exports = CodeBuilder; 54 | -------------------------------------------------------------------------------- /test/unit/clients/smapi-client-test/resources/vendor.js: -------------------------------------------------------------------------------- 1 | const {expect} = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const httpClient = require("../../../../../lib/clients/http-client"); 5 | const AuthorizationController = require("../../../../../lib/controllers/authorization-controller"); 6 | const CONSTANTS = require("../../../../../lib/utils/constants"); 7 | 8 | const noop = () => {}; 9 | 10 | module.exports = (smapiClient) => { 11 | describe("# smapi client vendor APIs", () => { 12 | let httpClientStub; 13 | const TEST_PROFILE = "testProfile"; 14 | const TEST_ACCESS_TOKEN = "access_token"; 15 | 16 | beforeEach(() => { 17 | httpClientStub = sinon.stub(httpClient, "request").callsFake(noop); 18 | sinon.stub(AuthorizationController.prototype, "tokenRefreshAndRead"); 19 | }); 20 | 21 | [ 22 | { 23 | testCase: "list-vendors", 24 | apiFunc: smapiClient.vendor.listVendors, 25 | parameters: [noop], 26 | expectedOptions: { 27 | url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/vendors`, 28 | method: CONSTANTS.HTTP_REQUEST.VERB.GET, 29 | headers: { 30 | authorization: TEST_ACCESS_TOKEN, 31 | }, 32 | body: null, 33 | json: false, 34 | }, 35 | }, 36 | ].forEach(({testCase, apiFunc, parameters, expectedOptions}) => { 37 | it(`| call ${testCase} successfully`, (done) => { 38 | // setup 39 | AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); 40 | // call 41 | apiFunc(...parameters); 42 | // verify 43 | expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); 44 | expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); 45 | expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); 46 | done(); 47 | }); 48 | }); 49 | 50 | afterEach(() => { 51 | sinon.restore(); 52 | }); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/commands/dialog/helper.ts: -------------------------------------------------------------------------------- 1 | import {ISmapiClient, isSmapiError, SmapiResponseError} from "../../clients/smapi-client"; 2 | 3 | /** 4 | * Validates if a skill is enabled for simulation. Calls Skill Management apis (SMAPI) to achieve this. 5 | * @param {*} dialogMode encapsulates configuration required validate skill information 6 | * @param {*} callback 7 | */ 8 | export async function validateDialogArgs(dialogMode: {smapiClient: ISmapiClient; skillId: string; stage: string; locale: string}) { 9 | const {smapiClient, skillId, stage, locale} = dialogMode; 10 | 11 | const response = await smapiClient.skill.manifest.getManifest(skillId, stage); 12 | 13 | if (isSmapiError(response)) { 14 | throw smapiErrorMsg("get-manifest", response); 15 | } 16 | 17 | const apis = response.body.manifest?.apis; 18 | if (!apis) { 19 | throw 'Ensure "manifest.apis" object exists in the skill manifest.'; 20 | } 21 | 22 | const apisKeys = Object.keys(apis); 23 | if (!apisKeys || apisKeys.length !== 1) { 24 | throw "Dialog command only supports custom skill type."; 25 | } 26 | 27 | if (apisKeys[0] !== "custom") { 28 | throw `Dialog command only supports custom skill type, but current skill is a "${apisKeys[0]}" type.`; 29 | } 30 | 31 | const locales = response.body.manifest?.publishingInformation?.locales; 32 | if (!locales) { 33 | throw 'Ensure the "manifest.publishingInformation.locales" exists in the skill manifest before simulating your skill.'; 34 | } 35 | 36 | if (!locales[locale]) { 37 | throw `Locale ${locale} was not found for your skill. Ensure the locale you want to simulate exists in your publishingInformation.`; 38 | } 39 | 40 | const enableResponse = await smapiClient.skill.getSkillEnablement(skillId, stage); 41 | 42 | if (isSmapiError(enableResponse)) { 43 | throw smapiErrorMsg("get-skill-enablement", enableResponse); 44 | } 45 | } 46 | 47 | function smapiErrorMsg(operation: string, res: SmapiResponseError) { 48 | return `SMAPI ${operation} request error: ${res.statusCode} - ${res.body.message}`; 49 | } 50 | -------------------------------------------------------------------------------- /docs/concepts/New-Command.md: -------------------------------------------------------------------------------- 1 | # NEW COMMAND 2 | 3 | `ask new` allows developers to create a new skill from Alexa-provided templates or custom templates and support several deployment methods/deployers. 4 | 5 | 6 | **STRUCTURE OF NEW COMMAND:** 7 | 8 | `ask new [--template-url