diff --git a/.gitignore b/.gitignore index 96a21c1ca19..8c6f0d0872c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,12 @@ spec-examples.zip coverage +# Analytics + +.asyncapi-analytics + # Glee assets/create-glee-app/templates/default/.glee assets/create-glee-app/templates/tutorial/.glee assets/create-glee-app/templates/default/docs -assets/create-glee-app/templates/tutorial/docs \ No newline at end of file +assets/create-glee-app/templates/tutorial/docs diff --git a/docs/usage.md b/docs/usage.md index 93015426e78..fd10ebc5002 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -27,7 +27,7 @@ $ npm install -g @asyncapi/cli $ asyncapi COMMAND running command... $ asyncapi (--version) -@asyncapi/cli/1.8.3 linux-x64 node-v18.20.2 +@asyncapi/cli/1.8.4 linux-x64 node-v18.20.2 $ asyncapi --help [COMMAND] USAGE $ asyncapi COMMAND @@ -53,12 +53,11 @@ USAGE * [`asyncapi convert [SPEC-FILE]`](#asyncapi-convert-spec-file) * [`asyncapi diff OLD NEW`](#asyncapi-diff-old-new) * [`asyncapi generate`](#asyncapi-generate) -* [`asyncapi generate fromTemplate ASYNCAPI TEMPLATE`](#asyncapi-generate-fromtemplate-asyncapi-template) -* [`asyncapi generate models LANGUAGE FILE`](#asyncapi-generate-models-language-file) +* [`asyncapi generate fromTemplate [ASYNCAPI] [TEMPLATE]`](#asyncapi-generate-fromtemplate-asyncapi-template) +* [`asyncapi generate models [LANGUAGE] [FILE]`](#asyncapi-generate-models-language-file) * [`asyncapi new`](#asyncapi-new) * [`asyncapi new file`](#asyncapi-new-file) * [`asyncapi new glee`](#asyncapi-new-glee) -* [`asyncapi new project`](#asyncapi-new-project) * [`asyncapi optimize [SPEC-FILE]`](#asyncapi-optimize-spec-file) * [`asyncapi start`](#asyncapi-start) * [`asyncapi start studio`](#asyncapi-start-studio) @@ -92,7 +91,7 @@ EXAMPLES $ asyncapi bundle ./asyncapi.yaml ./features.yaml --base ./asyncapi.yaml --reference-into-components ``` -_See code: [src/commands/bundle.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/bundle.ts)_ +_See code: [src/commands/bundle.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/bundle.ts)_ ## `asyncapi config` @@ -106,7 +105,7 @@ DESCRIPTION CLI config settings ``` -_See code: [src/commands/config/index.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/index.ts)_ +_See code: [src/commands/config/index.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/index.ts)_ ## `asyncapi config analytics` @@ -125,7 +124,7 @@ DESCRIPTION Enable or disable analytics for metrics collection ``` -_See code: [src/commands/config/analytics.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/analytics.ts)_ +_See code: [src/commands/config/analytics.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/analytics.ts)_ ## `asyncapi config context` @@ -139,7 +138,7 @@ DESCRIPTION Manage short aliases for full paths to AsyncAPI documents ``` -_See code: [src/commands/config/context/index.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/index.ts)_ +_See code: [src/commands/config/context/index.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/index.ts)_ ## `asyncapi config context add CONTEXT-NAME SPEC-FILE-PATH` @@ -161,7 +160,7 @@ DESCRIPTION Add a context to the store ``` -_See code: [src/commands/config/context/add.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/add.ts)_ +_See code: [src/commands/config/context/add.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/add.ts)_ ## `asyncapi config context current` @@ -178,7 +177,7 @@ DESCRIPTION Shows the current context that is being used ``` -_See code: [src/commands/config/context/current.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/current.ts)_ +_See code: [src/commands/config/context/current.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/current.ts)_ ## `asyncapi config context edit CONTEXT-NAME NEW-SPEC-FILE-PATH` @@ -199,7 +198,7 @@ DESCRIPTION Edit a context in the store ``` -_See code: [src/commands/config/context/edit.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/edit.ts)_ +_See code: [src/commands/config/context/edit.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/edit.ts)_ ## `asyncapi config context init [CONTEXT-FILE-PATH]` @@ -222,7 +221,7 @@ DESCRIPTION Initialize context ``` -_See code: [src/commands/config/context/init.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/init.ts)_ +_See code: [src/commands/config/context/init.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/init.ts)_ ## `asyncapi config context list` @@ -239,7 +238,7 @@ DESCRIPTION List all the stored contexts in the store ``` -_See code: [src/commands/config/context/list.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/list.ts)_ +_See code: [src/commands/config/context/list.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/list.ts)_ ## `asyncapi config context remove CONTEXT-NAME` @@ -259,7 +258,7 @@ DESCRIPTION Delete a context from the store ``` -_See code: [src/commands/config/context/remove.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/remove.ts)_ +_See code: [src/commands/config/context/remove.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/remove.ts)_ ## `asyncapi config context use CONTEXT-NAME` @@ -279,7 +278,7 @@ DESCRIPTION Set a context as current ``` -_See code: [src/commands/config/context/use.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/context/use.ts)_ +_See code: [src/commands/config/context/use.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/context/use.ts)_ ## `asyncapi config versions` @@ -296,7 +295,7 @@ DESCRIPTION Show versions of AsyncAPI tools used ``` -_See code: [src/commands/config/versions.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/config/versions.ts)_ +_See code: [src/commands/config/versions.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/config/versions.ts)_ ## `asyncapi convert [SPEC-FILE]` @@ -318,7 +317,7 @@ DESCRIPTION Convert asyncapi documents older to newer versions ``` -_See code: [src/commands/convert.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/convert.ts)_ +_See code: [src/commands/convert.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/convert.ts)_ ## `asyncapi diff OLD NEW` @@ -373,7 +372,7 @@ DESCRIPTION Find diff between two asyncapi files ``` -_See code: [src/commands/diff.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/diff.ts)_ +_See code: [src/commands/diff.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/diff.ts)_ ## `asyncapi generate` @@ -387,16 +386,16 @@ DESCRIPTION Generate typed models or other things like clients, applications or docs using AsyncAPI Generator templates. ``` -_See code: [src/commands/generate/index.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/generate/index.ts)_ +_See code: [src/commands/generate/index.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/generate/index.ts)_ -## `asyncapi generate fromTemplate ASYNCAPI TEMPLATE` +## `asyncapi generate fromTemplate [ASYNCAPI] [TEMPLATE]` Generates whatever you want using templates compatible with AsyncAPI Generator. ``` USAGE - $ asyncapi generate fromTemplate ASYNCAPI TEMPLATE [-h] [-d ] [-i] [--debug] [-n ] [-o ] - [--force-write] [-w] [-p ] [--map-base-url ] + $ asyncapi generate fromTemplate [ASYNCAPI] [TEMPLATE] [-h] [-d ] [--no-interactive] [-i] [--debug] [-n ] + [-o ] [--force-write] [-w] [-p ] [--map-base-url ] ARGUMENTS ASYNCAPI - Local path, url or context-name pointing to AsyncAPI file @@ -416,6 +415,7 @@ FLAGS --force-write Force writing of the generated files to given directory even if it is a git repo with unstaged files or not empty dir (defaults to false) --map-base-url= Maps all schema references from base url to local folder + --no-interactive Disable interactive mode and run with the provided flags. DESCRIPTION Generates whatever you want using templates compatible with AsyncAPI Generator. @@ -424,20 +424,20 @@ EXAMPLES $ asyncapi generate fromTemplate asyncapi.yaml @asyncapi/html-template --param version=1.0.0 singleFile=true --output ./docs --force-write ``` -_See code: [src/commands/generate/fromTemplate.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/generate/fromTemplate.ts)_ +_See code: [src/commands/generate/fromTemplate.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/generate/fromTemplate.ts)_ -## `asyncapi generate models LANGUAGE FILE` +## `asyncapi generate models [LANGUAGE] [FILE]` Generates typed models ``` USAGE - $ asyncapi generate models LANGUAGE FILE [-h] [-o ] [--tsModelType class|interface] [--tsEnumType - enum|union] [--tsModuleSystem ESM|CJS] [--tsIncludeComments] [--tsExportType default|named] [--tsJsonBinPack] - [--tsMarshalling] [--tsExampleInstance] [--packageName ] [--javaIncludeComments] [--javaJackson] - [--javaConstraints] [--namespace ] [--csharpAutoImplement] [--csharpNewtonsoft] [--csharpArrayType - Array|List] [--csharpHashcode] [--csharpEqual] [--csharpSystemJson] [--log-diagnostics] [--diagnostics-format - json|stylish|junit|html|text|teamcity|pretty] [--fail-severity error|warn|info|hint] + $ asyncapi generate models [LANGUAGE] [FILE] [-h] [--no-interactive] [-o ] [--tsModelType class|interface] + [--tsEnumType enum|union] [--tsModuleSystem ESM|CJS] [--tsIncludeComments] [--tsExportType default|named] + [--tsJsonBinPack] [--tsMarshalling] [--tsExampleInstance] [--packageName ] [--javaIncludeComments] + [--javaJackson] [--javaConstraints] [--namespace ] [--csharpAutoImplement] [--csharpNewtonsoft] + [--csharpArrayType Array|List] [--csharpHashcode] [--csharpEqual] [--csharpSystemJson] [--log-diagnostics] + [--diagnostics-format json|stylish|junit|html|text|teamcity|pretty] [--fail-severity error|warn|info|hint] ARGUMENTS LANGUAGE (typescript|csharp|golang|java|javascript|dart|python|rust|kotlin|php|cplusplus) The language you want the @@ -477,6 +477,8 @@ FLAGS namespace to use for the generated models. This is required when language is `csharp`,`c++` or `php`. + --no-interactive Disable interactive mode and run with the + provided flags. --packageName= Go, Java and Kotlin specific, define the package to use for the generated models. This is required when language is `go`, `java` or @@ -508,7 +510,7 @@ DESCRIPTION Generates typed models ``` -_See code: [src/commands/generate/models.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/generate/models.ts)_ +_See code: [src/commands/generate/models.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/generate/models.ts)_ ## `asyncapi new` @@ -565,7 +567,7 @@ EXAMPLES $ asyncapi new --file-name=my-asyncapi.yml --example=default-example.yml --no-tty - create a new file with a specific name, using one of the examples and without interactive mode ``` -_See code: [src/commands/new/index.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/new/index.ts)_ +_See code: [src/commands/new/index.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/new/index.ts)_ ## `asyncapi new file` @@ -622,7 +624,7 @@ EXAMPLES $ asyncapi new --file-name=my-asyncapi.yml --example=default-example.yml --no-tty - create a new file with a specific name, using one of the examples and without interactive mode ``` -_See code: [src/commands/new/file.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/new/file.ts)_ +_See code: [src/commands/new/file.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/new/file.ts)_ ## `asyncapi new glee` @@ -644,29 +646,7 @@ DESCRIPTION Creates a new Glee project ``` -_See code: [src/commands/new/glee.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/new/glee.ts)_ - -## `asyncapi new project` - -Creates a new Glee project - -``` -USAGE - $ asyncapi new project [-h] [-n ] [-t ] [-f ] [--force-write] - -FLAGS - -f, --file= The path to the AsyncAPI file for generating a Glee project. - -h, --help Show CLI help. - -n, --name= [default: project] Name of the Project - -t, --template= [default: default] Name of the Template - --force-write Force writing of the generated files to given directory even if it is a git repo with unstaged - files or not empty dir (defaults to false) - -DESCRIPTION - Creates a new Glee project -``` - -_See code: [src/commands/new/project.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/new/project.ts)_ +_See code: [src/commands/new/glee.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/new/glee.ts)_ ## `asyncapi optimize [SPEC-FILE]` @@ -702,7 +682,7 @@ EXAMPLES $ asyncapi optimize ./asyncapi.yaml --optimization=remove-components --output=terminal --no-tty ``` -_See code: [src/commands/optimize.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/optimize.ts)_ +_See code: [src/commands/optimize.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/optimize.ts)_ ## `asyncapi start` @@ -716,7 +696,7 @@ DESCRIPTION Start asyncapi studio ``` -_See code: [src/commands/start/index.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/start/index.ts)_ +_See code: [src/commands/start/index.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/start/index.ts)_ ## `asyncapi start studio` @@ -735,7 +715,7 @@ DESCRIPTION starts a new local instance of Studio ``` -_See code: [src/commands/start/studio.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/start/studio.ts)_ +_See code: [src/commands/start/studio.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/start/studio.ts)_ ## `asyncapi validate [SPEC-FILE]` @@ -762,5 +742,5 @@ DESCRIPTION validate asyncapi file ``` -_See code: [src/commands/validate.ts](https://github.com/asyncapi/cli/blob/v1.8.3/src/commands/validate.ts)_ +_See code: [src/commands/validate.ts](https://github.com/asyncapi/cli/blob/v1.8.4/src/commands/validate.ts)_ diff --git a/package-lock.json b/package-lock.json index ea573e4ba8d..31e9b22d648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@asyncapi/cli", - "version": "1.8.4", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@asyncapi/cli", - "version": "1.8.4", + "version": "1.9.0", "license": "Apache-2.0", "dependencies": { "@asyncapi/avro-schema-parser": "^3.0.21", @@ -21,6 +21,7 @@ "@asyncapi/protobuf-schema-parser": "^3.2.11", "@asyncapi/raml-dt-schema-parser": "^4.0.14", "@asyncapi/studio": "^0.20.0", + "@clack/prompts": "^0.7.0", "@oclif/core": "^1.26.2", "@oclif/errors": "^1.3.6", "@oclif/plugin-not-found": "^2.3.22", @@ -39,6 +40,7 @@ "node-fetch": "^2.0.0", "oclif": "^4.2.0", "open": "^8.4.0", + "picocolors": "^1.0.0", "reflect-metadata": "^0.1.13", "request": "^2.88.2", "serve-handler": "^6.1.3", @@ -3438,6 +3440,40 @@ "node": ">=6.9.0" } }, + "node_modules/@clack/core": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz", + "integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz", + "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==", + "bundleDependencies": [ + "is-unicode-supported" + ], + "dependencies": { + "@clack/core": "^0.3.3", + "is-unicode-supported": "*", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts/node_modules/is-unicode-supported": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -66669,6 +66705,11 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index b4184b17d80..ccaf51c1a0b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@asyncapi/cli", "description": "All in one CLI for all AsyncAPI tools", - "version": "1.8.4", + "version": "1.9.0", "author": "@asyncapi", "bin": { "asyncapi": "./bin/run_bin" @@ -20,6 +20,7 @@ "@asyncapi/protobuf-schema-parser": "^3.2.11", "@asyncapi/raml-dt-schema-parser": "^4.0.14", "@asyncapi/studio": "^0.20.0", + "@clack/prompts": "^0.7.0", "@oclif/core": "^1.26.2", "@oclif/errors": "^1.3.6", "@oclif/plugin-not-found": "^2.3.22", @@ -38,6 +39,7 @@ "node-fetch": "^2.0.0", "oclif": "^4.2.0", "open": "^8.4.0", + "picocolors": "^1.0.0", "reflect-metadata": "^0.1.13", "request": "^2.88.2", "serve-handler": "^6.1.3", diff --git a/src/base.ts b/src/base.ts index 94aee8ffe15..ccca24b00a1 100644 --- a/src/base.ts +++ b/src/base.ts @@ -6,6 +6,7 @@ import { join, resolve } from 'path'; import { existsSync } from 'fs-extra'; import { promises as fPromises } from 'fs'; import { v4 as uuidv4 } from 'uuid'; +import { homedir } from 'os'; const { readFile, writeFile } = fPromises; @@ -86,7 +87,7 @@ export default abstract class extends Command { async recorderFromEnv(prefix: string): Promise { let sink: Sink = new DiscardSink(); - const analyticsConfigFile = join(process.cwd(), '.asyncapi-analytics'); + const analyticsConfigFile = join(homedir(), '.asyncapi-analytics'); if (!existsSync(analyticsConfigFile)) { await writeFile(analyticsConfigFile, JSON.stringify({ analyticsEnabled: 'true', infoMessageShown: 'false', userID: uuidv4()}), { encoding: 'utf8' }); @@ -109,7 +110,7 @@ export default abstract class extends Command { sink = new NewRelicSink(process.env.ASYNCAPI_METRICS_NEWRELIC_KEY || 'eu01xx73a8521047150dd9414f6aedd2FFFFNRAL'); if (analyticsConfigFileContent.infoMessageShown === 'false') { - this.log('\nAsyncAPI anonymously tracks command executions to improve the specification and tools, ensuring no sensitive data reaches our servers. It aids in comprehending how AsyncAPI tools are used and adopted, facilitating ongoing improvements to our specifications and tools.\n\nTo disable tracking, please run the following command:\n asyncapi config analytics --disable\n\nOnce disabled, if you want to enable tracking back again then run:\n asyncapi config analytics --enable'); + this.log('\nAsyncAPI anonymously tracks command executions to improve the specification and tools, ensuring no sensitive data reaches our servers. It aids in comprehending how AsyncAPI tools are used and adopted, facilitating ongoing improvements to our specifications and tools.\n\nTo disable tracking, please run the following command:\n asyncapi config analytics --disable\n\nOnce disabled, if you want to enable tracking back again then run:\n asyncapi config analytics --enable\n'); analyticsConfigFileContent.infoMessageShown = 'true'; await writeFile(analyticsConfigFile, JSON.stringify(analyticsConfigFileContent), { encoding: 'utf8' }); } diff --git a/src/commands/config/analytics.ts b/src/commands/config/analytics.ts index 0e1970406c3..e9c1fdaf224 100644 --- a/src/commands/config/analytics.ts +++ b/src/commands/config/analytics.ts @@ -2,6 +2,7 @@ import { Flags } from '@oclif/core'; import { join, resolve } from 'path'; import Command from '../../base'; import { promises as fPromises } from 'fs'; +import { homedir } from 'os'; const { readFile, writeFile } = fPromises; @@ -15,7 +16,7 @@ export default class Analytics extends Command { async run() { const { flags } = await this.parse(Analytics); - const analyticsConfigFile = join(process.cwd(), '.asyncapi-analytics'); + const analyticsConfigFile = join(homedir(), '.asyncapi-analytics'); try { const analyticsConfigFileContent = JSON.parse(await readFile(resolve(analyticsConfigFile), { encoding: 'utf8' })); @@ -34,7 +35,7 @@ export default class Analytics extends Command { } catch (e: any) { switch (e.code) { case 'ENOENT': - this.error(`Unable to access the analytics configuration file. We tried to access the ".asyncapi-analytics" file in your current working directory ("${process.cwd()}") but the file could not be found.`); + this.error(`Unable to access the analytics configuration file. We tried to access the ".asyncapi-analytics" file in your user's home directory ("${homedir()}") but the file could not be found.`); break; case 'EEXIST': this.error(`Unable to update the analytics configuration file. We tried to update your ".asyncapi-analytics" file in the path "${analyticsConfigFile}" but the file does not exist.`); diff --git a/src/commands/generate/fromTemplate.ts b/src/commands/generate/fromTemplate.ts index 83a39c3d1bc..3dec4adc200 100644 --- a/src/commands/generate/fromTemplate.ts +++ b/src/commands/generate/fromTemplate.ts @@ -1,4 +1,4 @@ -import { Flags, CliUx } from '@oclif/core'; +import { Flags } from '@oclif/core'; import Command from '../../base'; // eslint-disable-next-line // @ts-ignore @@ -13,11 +13,8 @@ import { ValidationError } from '../../errors/validation-error'; import { GeneratorError } from '../../errors/generator-error'; import { Parser } from '@asyncapi/parser'; import type { Example } from '@oclif/core/lib/interfaces'; - -const red = (text: string) => `\x1b[31m${text}\x1b[0m`; -const magenta = (text: string) => `\x1b[35m${text}\x1b[0m`; -const yellow = (text: string) => `\x1b[33m${text}\x1b[0m`; -const green = (text: string) => `\x1b[32m${text}\x1b[0m`; +import { intro, isCancel, spinner, text } from '@clack/prompts'; +import { inverse, yellow, magenta, green, red } from 'picocolors'; interface IMapBaseUrlToFlag { url: string, @@ -68,6 +65,11 @@ export default class Template extends Command { description: 'Disable a specific hook type or hooks from a given hook type', multiple: true }), + 'no-interactive': Flags.boolean({ + description: 'Disable interactive mode and run with the provided flags.', + required: false, + default: false, + }), install: Flags.boolean({ char: 'i', default: false, @@ -103,18 +105,27 @@ export default class Template extends Command { }; static args = [ - { name: 'asyncapi', description: '- Local path, url or context-name pointing to AsyncAPI file', required: true }, - { name: 'template', description: '- Name of the generator template like for example @asyncapi/html-template or https://github.com/asyncapi/html-template', required: true } + { name: 'asyncapi', description: '- Local path, url or context-name pointing to AsyncAPI file' }, + { name: 'template', description: '- Name of the generator template like for example @asyncapi/html-template or https://github.com/asyncapi/html-template' }, ]; parser = new Parser(); async run() { const { args, flags } = await this.parse(Template); // NOSONAR + const interactive = !flags['no-interactive']; + + let { asyncapi, template } = args; + let output = flags.output as string; + if (interactive) { + intro(inverse('AsyncAPI Generator')); + + const parsedArgs = await this.parseArgs(args, output); + asyncapi = parsedArgs.asyncapi; + template = parsedArgs.template; + output = parsedArgs.output; + } - const asyncapi = args['asyncapi']; - const template = args['template']; - const output = flags.output || process.cwd(); const parsedFlags = this.parseFlags(flags['disable-hook'], flags['param'], flags['map-base-url']); const options = { forceWrite: flags['force-write'], @@ -142,13 +153,66 @@ export default class Template extends Command { this.error(`${template} template does not support AsyncAPI v3 documents, please checkout ${v3IssueLink}`); } } - await this.generate(asyncapi, template, output, options, genOption); + await this.generate(asyncapi, template, output, options, genOption, interactive); if (watchTemplate) { - const watcherHandler = this.watcherHandler(asyncapi, template, output, options, genOption); + const watcherHandler = this.watcherHandler(asyncapi, template, output, options, genOption, interactive); await this.runWatchMode(asyncapi, template, output, watcherHandler); } } + private async parseArgs(args: Record, output?: string): Promise<{ asyncapi: string; template: string; output: string; }> { + let asyncapi = args['asyncapi']; + let template = args['template']; + const cancellationMessage = 'Operation cancelled'; + + if (!asyncapi) { + asyncapi = await text({ + message: 'Please provide the path to the AsyncAPI document', + placeholder: 'asyncapi.yaml', + defaultValue: 'asyncapi.yaml', + validate(value: string) { + if (!value) { + return 'The path to the AsyncAPI document is required'; + } else if (!fs.existsSync(value)) { + return 'The file does not exist'; + } + } + }); + } + + if (isCancel(asyncapi)) { + this.error(cancellationMessage, { exit: 1 }); + } + + if (!template) { + template = await text({ + message: 'Please provide the name of the generator template', + placeholder: '@asyncapi/html-template', + defaultValue: '@asyncapi/html-template', + }); + } + + if (!output) { + output = await text({ + message: 'Please provide the output directory', + placeholder: './docs', + validate(value: string) { + if (!value) { + return 'The output directory is required'; + } else if (typeof value !== 'string') { + return 'The output directory must be a string'; + } + } + }) as string; + } + + if (isCancel(output) || isCancel(template)) { + this.error(cancellationMessage, { exit: 1 }); + } + + return { asyncapi, template, output }; + } + private parseFlags(disableHooks?: string[], params?: string[], mapBaseUrl?: string): ParsedFlags { return { params: this.paramParser(params), @@ -204,7 +268,7 @@ export default class Template extends Command { return mapBaseURLToFolder; } - private async generate(asyncapi: string | undefined, template: string, output: string, options: any, genOption: any) { + private async generate(asyncapi: string | undefined, template: string, output: string, options: any, genOption: any, interactive = true) { let specification: Specification; try { specification = await load(asyncapi); @@ -218,16 +282,15 @@ export default class Template extends Command { ); } const generator = new AsyncAPIGenerator(template, output || path.resolve(os.tmpdir(), 'asyncapi-generator'), options); - - CliUx.ux.action.start('Generation in progress. Keep calm and wait a bit'); + const s = interactive ? spinner() : { start: () => null, stop: (string: string) => console.log(string) }; + s.start('Generation in progress. Keep calm and wait a bit'); try { await generator.generateFromString(specification.text(), genOption); - CliUx.ux.action.stop(); } catch (err: any) { - CliUx.ux.action.stop('done\n'); + s.stop('Generation failed'); throw new GeneratorError(err); } - console.log(`${yellow('Check out your shiny new generated files at ') + magenta(output) + yellow('.')}\n`); + s.stop(`${yellow('Check out your shiny new generated files at ') + magenta(output) + yellow('.')}\n`); } private async runWatchMode(asyncapi: string | undefined, template: string, output: string, watchHandler: ReturnType) { @@ -270,7 +333,7 @@ export default class Template extends Command { }); } - private watcherHandler(asyncapi: string, template: string, output: string, options: Record, genOption: any): (changedFiles: Record) => Promise { + private watcherHandler(asyncapi: string, template: string, output: string, options: Record, genOption: any, interactive: boolean): (changedFiles: Record) => Promise { return async (changedFiles: Record): Promise => { console.clear(); console.log('[WATCHER] Change detected'); @@ -292,7 +355,7 @@ export default class Template extends Command { this.log(`\t${magenta(value.path)} was ${eventText}`); } try { - await this.generate(asyncapi, template, output, options, genOption); + await this.generate(asyncapi, template, output, options, genOption, interactive); } catch (err: any) { throw new GeneratorError(err); } diff --git a/src/commands/generate/models.ts b/src/commands/generate/models.ts index 556e7d9af7b..a852b7b0209 100644 --- a/src/commands/generate/models.ts +++ b/src/commands/generate/models.ts @@ -5,6 +5,9 @@ import Command from '../../base'; import { load } from '../../models/SpecificationFile'; import { formatOutput, parse, validationFlags } from '../../parser'; +import { select, text, spinner, isCancel, cancel, intro } from '@clack/prompts'; +import { green, inverse } from 'picocolors'; + import type { AbstractGenerator, AbstractFileGenerator } from '@asyncapi/modelina'; enum Languages { @@ -29,13 +32,17 @@ export default class Models extends Command { name: 'language', description: 'The language you want the typed models generated for.', options: Object.keys(Languages), - required: true }, - { name: 'file', description: 'Path or URL to the AsyncAPI document, or context-name', required: true }, + { name: 'file', description: 'Path or URL to the AsyncAPI document, or context-name' }, ]; static flags = { help: Flags.help({ char: 'h' }), + 'no-interactive': Flags.boolean({ + description: 'Disable interactive mode and run with the provided flags.', + required: false, + default: false, + }), output: Flags.string({ char: 'o', description: 'The output directory where the models should be written to. Omitting this flag will write the models to `stdout`.', @@ -168,8 +175,21 @@ export default class Models extends Command { /* eslint-disable sonarjs/cognitive-complexity */ async run() { const { args, flags } = await this.parse(Models); - const { tsModelType, tsEnumType, tsIncludeComments, tsModuleSystem, tsExportType, tsJsonBinPack, tsMarshalling, tsExampleInstance, namespace, csharpAutoImplement, csharpArrayType, csharpNewtonsoft, csharpHashcode, csharpEqual, csharpSystemJson, packageName, javaIncludeComments, javaJackson, javaConstraints, output } = flags; - const { language, file } = args; + + const { tsModelType, tsEnumType, tsIncludeComments, tsModuleSystem, tsExportType, tsJsonBinPack, tsMarshalling, tsExampleInstance, namespace, csharpAutoImplement, csharpArrayType, csharpNewtonsoft, csharpHashcode, csharpEqual, csharpSystemJson, packageName, javaIncludeComments, javaJackson, javaConstraints } = flags; + let { language, file } = args; + let output = flags.output || 'stdout'; + const interactive = !flags['no-interactive']; + + if (!interactive) { + intro(inverse('AsyncAPI Generate Models')); + + const parsedArgs = await this.parseArgs(args, output); + language = parsedArgs.language; + file = parsedArgs.file; + output = parsedArgs.output; + } + const inputFile = (await load(file)) || (await load()); if (inputFile.isAsyncAPI3()) { this.error('Generate Models command does not support AsyncAPI v3 yet, please checkout https://github.com/asyncapi/modelina/issues/1376'); @@ -343,13 +363,15 @@ export default class Models extends Command { throw new Error(`Could not determine generator for language ${language}, are you using one of the following values ${possibleLanguageValues}?`); } - if (output) { + const s = spinner(); + s.start('Generating models...'); + if (output !== 'stdout') { const models = await fileGenerator.generateToFiles( convertedDoc as any, output, { ...fileOptions, }); const generatedModels = models.map((model) => { return model.modelName; }); - this.log(`Successfully generated the following models: ${generatedModels.join(', ')}`); + s.stop(green(`Successfully generated the following models: ${generatedModels.join(', ')}`)); return; } @@ -358,10 +380,61 @@ export default class Models extends Command { { ...fileOptions }); const generatedModels = models.map((model) => { return ` -## Model name: ${model.modelName} -${model.result} -`; + ## Model name: ${model.modelName} + ${model.result} + `; }); - this.log(`Successfully generated the following models: ${generatedModels.join('\n')}`); + s.stop(green(`Successfully generated the following models: ${generatedModels.join('\n')}`)); + } + + private async parseArgs(args: Record, output?: string) { + let { language, file } = args; + let askForOutput = false; + const operationCancelled = 'Operation cancelled by the user.'; + if (!language) { + language = await select({ + message: 'Select the language you want to generate models for', + options: Object.keys(Languages).map((key) => + ({ value: key, label: key, hint: Languages[key as keyof typeof Languages] }) + ), + }); + + askForOutput = true; + } + + if (isCancel(language)) { + cancel(operationCancelled); + this.exit(); + } + + if (!file) { + file = await text({ + message: 'Enter the path or URL to the AsyncAPI document', + defaultValue: 'asyncapi.yaml', + placeholder: 'asyncapi.yaml', + }); + + askForOutput = true; + } + + if (isCancel(file)) { + cancel(operationCancelled); + this.exit(); + } + + if (!output && askForOutput) { + output = await text({ + message: 'Enter the output directory or stdout to write the models to', + defaultValue: 'stdout', + placeholder: 'stdout', + }) as string; + } + + if (isCancel(output)) { + cancel(operationCancelled); + this.exit(); + } + + return { language, file, output: output || 'stdout' }; } } diff --git a/src/commands/new/project.ts b/src/commands/new/project.ts deleted file mode 100644 index 5b0013431f0..00000000000 --- a/src/commands/new/project.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Config } from '@oclif/core'; -import NewGlee from './glee'; - -export default class NewProject extends NewGlee { - constructor(argv: string[], config: Config) { - super(argv, config); - this.commandName = 'project'; - } -} diff --git a/src/models/SpecificationFile.ts b/src/models/SpecificationFile.ts index e1c55899266..857ac471cf4 100644 --- a/src/models/SpecificationFile.ts +++ b/src/models/SpecificationFile.ts @@ -122,24 +122,25 @@ interface LoadType { /* eslint-disable sonarjs/cognitive-complexity */ export async function load(filePathOrContextName?: string, loadType?: LoadType): Promise { // NOSONAR - if (filePathOrContextName) { - if (loadType?.file) { return Specification.fromFile(filePathOrContextName); } - if (loadType?.context) { return loadFromContext(filePathOrContextName); } - if (loadType?.url) { return Specification.fromURL(filePathOrContextName); } - - const type = await nameType(filePathOrContextName); - if (type === TYPE_CONTEXT_NAME) { - return loadFromContext(filePathOrContextName); - } + try { + if (filePathOrContextName) { + if (loadType?.file) { return Specification.fromFile(filePathOrContextName); } + if (loadType?.context) { return loadFromContext(filePathOrContextName); } + if (loadType?.url) { return Specification.fromURL(filePathOrContextName); } + + const type = await nameType(filePathOrContextName); + if (type === TYPE_CONTEXT_NAME) { + return loadFromContext(filePathOrContextName); + } - if (type === TYPE_URL) { - return Specification.fromURL(filePathOrContextName); + if (type === TYPE_URL) { + return Specification.fromURL(filePathOrContextName); + } + await fileExists(filePathOrContextName); + + return Specification.fromFile(filePathOrContextName); } - await fileExists(filePathOrContextName); - return Specification.fromFile(filePathOrContextName); - } - try { return await loadFromContext(); } catch (e) { const autoDetectedSpecFile = await detectSpecFile(); diff --git a/test/integration/config/analytics.test.ts b/test/integration/config/analytics.test.ts index 8f5ae75183d..80471fbb78b 100644 --- a/test/integration/config/analytics.test.ts +++ b/test/integration/config/analytics.test.ts @@ -1,11 +1,6 @@ import { expect, test } from '@oclif/test'; -import { fileCleanup } from '../../helpers'; describe('config:analytics', () => { - afterEach(() => { - fileCleanup('.asyncapi-analytics'); - }); - describe('with disable flag', () => { test .stderr() diff --git a/test/integration/generate/fromTemplate.test.ts b/test/integration/generate/fromTemplate.test.ts index fe5e0cd126a..7c7dd6518d8 100644 --- a/test/integration/generate/fromTemplate.test.ts +++ b/test/integration/generate/fromTemplate.test.ts @@ -4,6 +4,8 @@ import { test } from '@oclif/test'; import rimraf from 'rimraf'; import { expect } from '@oclif/test'; +const nonInteractive = '--no-interactive'; + const generalOptions = [ 'generate:fromTemplate', './test/fixtures/specification.yml', @@ -21,8 +23,9 @@ describe('template', () => { }); test .stdout() - .command([...generalOptions, '--output=./test/docs/1', '--force-write']) + .command([...generalOptions, '--output=./test/docs/1', '--force-write', '--no-interactive']) .it('should generate minimal template', (ctx, done) => { + console.log(ctx.stdout); expect(ctx.stdout).to.contain( 'Check out your shiny new generated files at ./test/docs/1.\n\n' ); @@ -37,7 +40,9 @@ describe('template', () => { .command([ 'generate:fromTemplate', asyncapiv3, - '@asyncapi/minimaltemplate']) + '@asyncapi/minimaltemplate', + nonInteractive, + ]) .it('give error on disabled template', (ctx, done) => { expect(ctx.stderr).to.equal('Error: @asyncapi/minimaltemplate template does not support AsyncAPI v3 documents, please checkout some link\n'); expect(ctx.stdout).to.equal(''); @@ -54,7 +59,7 @@ describe('template', () => { }); test .stderr() - .command([...generalOptions, `--output=${pathToOutput}`]) + .command([...generalOptions, `--output=${pathToOutput}`, nonInteractive]) .it( 'should throw error if output folder is in a git repository', (ctx, done) => { @@ -75,6 +80,7 @@ describe('template', () => { '-p=version=1.0.0 mode=development', '--output=./test/docs/3', '--force-write', + nonInteractive ]) .it('should pass custom param in the template', (ctx, done) => { expect(ctx.stdout).to.contain( @@ -93,6 +99,7 @@ describe('template', () => { '--output=./test/docs/4', '--force-write', '-d=generate:after', + nonInteractive ]) .it('should not create asyncapi.yaml file', async (_, done) => { const exits = fs.existsSync(path.resolve('./docs/asyncapi.yaml')); @@ -110,6 +117,7 @@ describe('template', () => { '--output=./test/docs/5', '--force-write', '--debug', + nonInteractive ]) .it('should print debug logs', (ctx, done) => { expect(ctx.stdout).to.contain( @@ -130,6 +138,7 @@ describe('template', () => { '--output=./test/docs/6', '--force-write', '--no-overwrite=./test/docs/asyncapi.md', + nonInteractive ]) .it('should skip the filepath and generate normally', (ctx, done) => { expect(ctx.stdout).to.contain( diff --git a/test/integration/new/project.test.ts b/test/integration/new/project.test.ts deleted file mode 100644 index 072abaaa878..00000000000 --- a/test/integration/new/project.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { test } from '@oclif/test'; -import TestHelper from '../../helpers'; -import { PROJECT_DIRECTORY_PATH } from '../../helpers'; -import { expect } from '@oclif/test'; - -const testHelper = new TestHelper(); - -describe('new project', () => { - before(() => { - try { - testHelper.deleteDummyProjectDirectory(); - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } - }); - - describe('creation of new project is successful', () => { - afterEach(() => { - testHelper.deleteDummyProjectDirectory(); - }); - - test - .stderr() - .stdout() - .command(['new:project', '-n=test-project']) - .it('runs new project command with name flag', async (ctx,done) => { - expect(ctx.stderr).to.equal(''); - expect(ctx.stdout).to.equal('Your project "test-project" has been created successfully!\n\nNext steps:\n\n cd test-project\n npm install\n npm run dev\n\nAlso, you can already open the project in your favorite editor and start tweaking it.\n'); - done(); - }); - }); - - describe('when new project name already exists', () => { - beforeEach(() => { - try { - testHelper.createDummyProjectDirectory(); - } catch (e: any) { - if (e.code !== 'EEXIST') { - throw e; - } - } - }); - - afterEach(() => { - testHelper.deleteDummyProjectDirectory(); - }); - - test - .stderr() - .stdout() - .command(['new:project', '-n=test-project']) - .it('should throw error if name of the new project already exists', async (ctx,done) => { - expect(ctx.stderr).to.equal(`Error: Unable to create the project. We tried to use "test-project" as the directory of your new project but it already exists (${PROJECT_DIRECTORY_PATH}). Please specify a different name for the new project. For example, run the following command instead:\n\n asyncapi new project --name test-project-1\n\n`); - expect(ctx.stdout).to.equal(''); - done(); - }); - }); -}); -