From 80b404ee88f2abb64ed5d4b64b897d5291177db6 Mon Sep 17 00:00:00 2001 From: shamsartem Date: Tue, 9 Aug 2022 12:00:01 +0300 Subject: [PATCH] Remove camel case restriction for names, pass JSON service to Aqua CLI, fix mrepl timeout, auto-generate aqua interfaces, fix cargo to be +nightly-x86_64, equire service.yaml to have name --- README.md | 41 +++-- package-lock.json | 51 +++--- package.json | 7 +- src/commands/deploy.ts | 193 ++++++++++++-------- src/commands/module/add.ts | 72 ++++---- src/commands/run.ts | 9 +- src/commands/service/add.ts | 91 +++++---- src/commands/service/new.ts | 50 ++--- src/commands/service/repl.ts | 28 +-- src/lib/configs/project/fluence.ts | 5 +- src/lib/configs/project/module.ts | 4 +- src/lib/configs/project/service.ts | 36 +++- src/lib/const.ts | 2 + src/lib/deployedApp.ts | 30 +-- src/lib/helpers/capitilize.ts | 18 ++ src/lib/helpers/downloadFile.ts | 56 ++++-- src/lib/helpers/generateServiceInterface.ts | 46 +++++ src/lib/marineCli.ts | 19 +- src/lib/paths.ts | 4 + src/lib/rust.ts | 10 +- 20 files changed, 496 insertions(+), 276 deletions(-) create mode 100644 src/lib/helpers/capitilize.ts create mode 100644 src/lib/helpers/generateServiceInterface.ts diff --git a/README.md b/README.md index e00371cab..6f4127a0f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ $ npm install -g @fluencelabs/cli $ fluence COMMAND running command... $ fluence (--version) -@fluencelabs/cli/0.0.0 linux-x64 node-v16.14.0 +@fluencelabs/cli/0.2.2 linux-x64 node-v16.14.0 $ fluence --help [COMMAND] USAGE $ fluence COMMAND @@ -48,7 +48,7 @@ services: deploy: - deployId: default ``` -`deployId` can be any unique string in camelCase. It is used in aqua to access ids of deployed services as you will see in a moment. +`deployId` can be any string. It must start with a lowercase letter and contain only letters, numbers, and underscores. It also must be unique service-wise. It is used in aqua to access ids of deployed services as you will see in a moment. You can edit `fluence.yaml` manually if you want to deploy multiple times, deploy on specific network, deploy on specific peerId or if you want to override `service.yaml` 3. Run `fluence service new ./src/services/newService` to generate new service template. You will be asked if you want to add the service to `fluence.yaml` - say yes. @@ -94,13 +94,14 @@ pre-commit runs each time before you commit. It includes prettier and generates If you want README.md file to be correctly generated please don't forget to run `npm run build` before committing Pull request and release process: -1. Run `npm run check` to make sure everything ok with the code -2. Only after that commit your changes to trigger pre-commit hook that updates `README.md`. Read `README.md` to make sure it is correctly updated -3. Push your changes -4. Create pull request and merge your changes to `main` -5. Switch to `main` locally and pull merged changes -6. Run `git tag -a v0.0.0 -m ""` with version number that you want instead of `0.0.0` -5. Run `git push origin v0.0.0` with version number that you want instead of `0.0.0` to trigger release +1. Update version in package.json +2. Run `npm run check` to make sure everything ok with the code +3. Only after that commit your changes to trigger pre-commit hook that updates `README.md`. Read `README.md` to make sure it is correctly updated +4. Push your changes +5. Create pull request and merge your changes to `main` +6. Switch to `main` locally and pull merged changes +7. Run `git tag -a v0.0.0 -m ""` with version number that you want instead of `0.0.0` +8. Run `git push origin v0.0.0` with version number that you want instead of `0.0.0` to trigger release # Commands @@ -153,7 +154,7 @@ EXAMPLES $ fluence aqua ``` -_See code: [dist/commands/aqua.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/aqua.ts)_ +_See code: [dist/commands/aqua.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/aqua.ts)_ ## `fluence autocomplete [SHELL]` @@ -210,7 +211,7 @@ EXAMPLES $ fluence dependency ``` -_See code: [dist/commands/dependency.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/dependency.ts)_ +_See code: [dist/commands/dependency.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/dependency.ts)_ ## `fluence deploy` @@ -234,7 +235,7 @@ EXAMPLES $ fluence deploy ``` -_See code: [dist/commands/deploy.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/deploy.ts)_ +_See code: [dist/commands/deploy.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/deploy.ts)_ ## `fluence help [COMMAND]` @@ -277,7 +278,7 @@ EXAMPLES $ fluence init ``` -_See code: [dist/commands/init.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/init.ts)_ +_See code: [dist/commands/init.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/init.ts)_ ## `fluence module add [PATH | URL]` @@ -291,7 +292,7 @@ ARGUMENTS PATH | URL Path to a module or url to .tar.gz archive FLAGS - --name= Unique module name + --name= Override module name --no-input Don't interactively ask for any input from the user --service= Service name from fluence.yaml or path to the service directory @@ -365,7 +366,7 @@ EXAMPLES $ fluence remove ``` -_See code: [dist/commands/remove.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/remove.ts)_ +_See code: [dist/commands/remove.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/remove.ts)_ ## `fluence run` @@ -384,7 +385,7 @@ FLAGS --data-path= Path to a JSON file in { [argumentName]: argumentValue } format. You can call a function using these argument names --import=... Path to a directory to import from. May be used several times - --json-service= Path to a file that contains a JSON formatted service + --json-service=... Path to a file that contains a JSON formatted service --no-input Don't interactively ask for any input from the user --on= PeerId of a peer where you want to run the function --relay= Relay node multiaddr @@ -397,7 +398,7 @@ EXAMPLES $ fluence run ``` -_See code: [dist/commands/run.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/run.ts)_ +_See code: [dist/commands/run.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.2.2/dist/commands/run.ts)_ ## `fluence service add [PATH | URL]` @@ -411,7 +412,8 @@ ARGUMENTS PATH | URL Path to a service or url to .tar.gz archive FLAGS - --name= Unique service name + --name= Override service name (must start with a lowercase letter and contain only letters, numbers, and + underscores) --no-input Don't interactively ask for any input from the user DESCRIPTION @@ -433,7 +435,8 @@ ARGUMENTS PATH Path to a service FLAGS - --name= Unique service name + --name= Unique service name (must start with a lowercase letter and contain only letters, numbers, and + underscores) --no-input Don't interactively ask for any input from the user DESCRIPTION diff --git a/package-lock.json b/package-lock.json index 31225ed95..2f4005f74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@fluencelabs/cli", - "version": "0.0.0", + "version": "0.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@fluencelabs/cli", - "version": "0.0.0", + "version": "0.2.2", "license": "Apache-2.0", "dependencies": { "@fluencelabs/fluence": "^0.23.0", @@ -19,10 +19,9 @@ "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.1", "ajv": "^8.11.0", - "camelcase": "^5.2.0", "chokidar": "^3.5.3", "decompress": "^4.2.1", - "filenamify": "^4", + "filenamify": "^4.3.0", "inquirer": "^8.2.4", "multiaddr": "^10.0.1", "node-fetch": "^2.6.7", @@ -37,10 +36,10 @@ "devDependencies": { "@oclif/test": "^2", "@tsconfig/node16-strictest": "^1.0.1", - "@types/camelcase": "^5.2.0", "@types/chai": "^4", "@types/chokidar": "^2.1.3", "@types/decompress": "^4.2.4", + "@types/filenamify": "^2.0.2", "@types/iarna__toml": "^2.0.2", "@types/inquirer": "^8.2.1", "@types/mocha": "^9.1.1", @@ -2122,16 +2121,6 @@ "@babel/types": "^7.3.0" } }, - "node_modules/@types/camelcase": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@types/camelcase/-/camelcase-5.2.0.tgz", - "integrity": "sha512-zhHaryYYUUsJ1h6Rq4hisPkljY7c2bkC5PFYQbom5fyKloGJEDK+wdsw2L4hnBwXr4plGjW6D/UVJBbNbOzVpQ==", - "deprecated": "This is a stub types definition. camelcase provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "camelcase": "*" - } - }, "node_modules/@types/chai": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", @@ -2163,6 +2152,16 @@ "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", "dev": true }, + "node_modules/@types/filenamify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/filenamify/-/filenamify-2.0.2.tgz", + "integrity": "sha512-/sO8rlEFYLZGjoDCIy1BmSdo+xNQbtJIgyrElZrzALolPUhBHvY/vQVGKSw4RSkREtuAv3eb6M7mDXvhpFxYbw==", + "deprecated": "This is a stub types definition. filenamify provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "filenamify": "*" + } + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -3474,6 +3473,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "peer": true, "engines": { "node": ">=6" } @@ -16887,15 +16887,6 @@ "@babel/types": "^7.3.0" } }, - "@types/camelcase": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@types/camelcase/-/camelcase-5.2.0.tgz", - "integrity": "sha512-zhHaryYYUUsJ1h6Rq4hisPkljY7c2bkC5PFYQbom5fyKloGJEDK+wdsw2L4hnBwXr4plGjW6D/UVJBbNbOzVpQ==", - "dev": true, - "requires": { - "camelcase": "*" - } - }, "@types/chai": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", @@ -16926,6 +16917,15 @@ "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", "dev": true }, + "@types/filenamify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/filenamify/-/filenamify-2.0.2.tgz", + "integrity": "sha512-/sO8rlEFYLZGjoDCIy1BmSdo+xNQbtJIgyrElZrzALolPUhBHvY/vQVGKSw4RSkREtuAv3eb6M7mDXvhpFxYbw==", + "dev": true, + "requires": { + "filenamify": "*" + } + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -17950,7 +17950,8 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "peer": true }, "caniuse-lite": { "version": "1.0.30001352", diff --git a/package.json b/package.json index 117fe4068..ba22e7409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fluencelabs/cli", - "version": "0.0.0", + "version": "0.2.2", "description": "CLI for working with Fluence network", "author": "Fluence Labs", "bin": { @@ -43,10 +43,9 @@ "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.1", "ajv": "^8.11.0", - "camelcase": "^5.2.0", "chokidar": "^3.5.3", "decompress": "^4.2.1", - "filenamify": "^4", + "filenamify": "^4.3.0", "inquirer": "^8.2.4", "multiaddr": "^10.0.1", "node-fetch": "^2.6.7", @@ -58,10 +57,10 @@ "devDependencies": { "@oclif/test": "^2", "@tsconfig/node16-strictest": "^1.0.1", - "@types/camelcase": "^5.2.0", "@types/chai": "^4", "@types/chokidar": "^2.1.3", "@types/decompress": "^4.2.4", + "@types/filenamify": "^2.0.2", "@types/iarna__toml": "^2.0.2", "@types/inquirer": "^8.2.1", "@types/mocha": "^9.1.1", diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 37e93b18a..6dc19e96d 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -19,7 +19,6 @@ import path from "node:path"; import color from "@oclif/color"; import { CliUx, Command, Flags } from "@oclif/core"; -import camelcase from "camelcase"; import { AquaCLI, initAquaCli } from "../lib/aquaCli"; import { @@ -67,8 +66,10 @@ import { getModuleUrlOrAbsolutePath, getModuleWasmPath, isUrl, + validateAquaName, } from "../lib/helpers/downloadFile"; import { ensureFluenceProject } from "../lib/helpers/ensureFluenceProject"; +import { generateServiceInterface } from "../lib/helpers/generateServiceInterface"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; import { replaceHomeDir } from "../lib/helpers/replaceHomeDir"; import { getKeyPairFromFlags } from "../lib/keypairs"; @@ -221,13 +222,18 @@ export default class Deploy extends Command { } } +type DeployInfoModule = { + moduleName: string; + moduleConfig: ModuleV0; +}; + type DeployInfo = { serviceName: string; serviceDirPath: string; deployId: string; count: number; peerId: string | undefined; - modules: Array; + modules: Array; }; const overrideModule = ( @@ -275,12 +281,7 @@ const prepareForDeploy = async ({ Object.entries(fluenceConfig.services).map( ([serviceName, { get, deploy }]): ServicePathPromises => (async (): ServicePathPromises => ({ - serviceName: - camelcase(serviceName) === serviceName - ? serviceName - : commandObj.error( - `Service name ${color.yellow(serviceName)} not in camelCase` - ), + serviceName, deploy, get, serviceDirPath: isUrl(get) @@ -331,61 +332,90 @@ const prepareForDeploy = async ({ serviceDirPath, }): Array => deploy.map( - ({ deployId, count = 1, peerId, overrideModules }): DeployInfo => ({ - serviceName, - serviceDirPath, - deployId: - camelcase(deployId) === deployId - ? deployId - : commandObj.error( - `DeployId ${color.yellow(deployId)} not in camelCase` - ), - count, - peerId: fluenceConfig?.peerIds?.[peerId ?? ""] ?? peerId, - modules: ((): Array => { - const modulesNotFoundInServiceYaml = Object.keys( - overrideModules ?? {} - ).filter( - (moduleName): boolean => !(moduleName in serviceConfig.modules) - ); + ({ deployId, count = 1, peerId, overrideModules }): DeployInfo => { + const deployIdValidity = validateAquaName(deployId); - if (modulesNotFoundInServiceYaml.length > 0) { - commandObj.error( - `${color.yellow( - FLUENCE_CONFIG_FILE_NAME - )} has service ${color.yellow( - serviceName - )} with deployId ${color.yellow( - deployId - )} that has moduleOverrides for modules that don't exist in the service ${color.yellow( - serviceDirPath - )}. Please make sure ${color.yellow( - modulesNotFoundInServiceYaml.join(", ") - )} spelled correctly ` + if (deployIdValidity !== true) { + return commandObj.error( + `deployId ${color.yellow(deployId)} ${deployIdValidity}` + ); + } + + return { + serviceName, + serviceDirPath, + deployId, + count, + peerId: fluenceConfig?.peerIds?.[peerId ?? ""] ?? peerId, + modules: ((): Array => { + const modulesNotFoundInServiceYaml = Object.keys( + overrideModules ?? {} + ).filter( + (moduleName): boolean => !(moduleName in serviceConfig.modules) ); - } - const { [FACADE_MODULE_NAME]: facadeModule, ...otherModules } = - serviceConfig.modules; - - return [ - ...Object.entries(otherModules).map( - ([moduleName, mod]): ModuleV0 => - overrideModule(mod, overrideModules, moduleName) - ), - overrideModule(facadeModule, overrideModules, FACADE_MODULE_NAME), - ]; - })(), - }) + if (modulesNotFoundInServiceYaml.length > 0) { + commandObj.error( + `${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} has service ${color.yellow( + serviceName + )} with deployId ${color.yellow( + deployId + )} that has moduleOverrides for modules that don't exist in the service ${color.yellow( + serviceDirPath + )}. Please make sure ${color.yellow( + modulesNotFoundInServiceYaml.join(", ") + )} spelled correctly ` + ); + } + + const { [FACADE_MODULE_NAME]: facadeModule, ...otherModules } = + serviceConfig.modules; + + return [ + ...Object.entries(otherModules).map( + ([moduleName, mod]): DeployInfoModule => ({ + moduleConfig: overrideModule( + mod, + overrideModules, + moduleName + ), + moduleName, + }) + ), + { + moduleConfig: overrideModule( + facadeModule, + overrideModules, + FACADE_MODULE_NAME + ), + moduleName: FACADE_MODULE_NAME, + }, + ]; + })(), + }; + } ) ); const setOfAllGets = [ ...new Set( allDeployInfos.flatMap( - ({ modules, serviceDirPath }): Array => - modules.map(({ get }): string => - getModuleUrlOrAbsolutePath(get, serviceDirPath) + ({ + modules, + serviceDirPath, + serviceName, + }): Array<{ get: string; moduleName: string; serviceName: string }> => + modules.map( + ({ + moduleConfig: { get }, + moduleName, + }): { get: string; moduleName: string; serviceName: string } => ({ + get: getModuleUrlOrAbsolutePath(get, serviceDirPath), + moduleName, + serviceName, + }) ) ) ), @@ -397,21 +427,28 @@ const prepareForDeploy = async ({ const mapOfAllModuleConfigs = new Map( await Promise.all( setOfAllGets.map( - (get): Promise<[string, ModuleConfigReadonly]> => + ({ + get, + moduleName, + serviceName, + }): Promise<[string, ModuleConfigReadonly]> => (async (): Promise<[string, ModuleConfigReadonly]> => { - const moduleConfig = - (isUrl(get) - ? await initReadonlyModuleConfig( - await downloadModule(get), - commandObj - ) - : await initReadonlyModuleConfig(get, commandObj)) ?? - CliUx.ux.action.stop(color.red("error")) ?? - commandObj.error( + const moduleConfig = isUrl(get) + ? await initReadonlyModuleConfig( + await downloadModule(get), + commandObj + ) + : await initReadonlyModuleConfig(get, commandObj); + + if (moduleConfig === null) { + CliUx.ux.action.stop(color.red("error")); + + return commandObj.error( `Module with get: ${color.yellow( get )} doesn't have ${color.yellow(MODULE_CONFIG_FILE_NAME)}` ); + } if (moduleConfig.type === "rust") { await marineCli({ @@ -421,6 +458,14 @@ const prepareForDeploy = async ({ }); } + if (moduleName === FACADE_MODULE_NAME) { + await generateServiceInterface({ + pathToFacade: getModuleWasmPath(moduleConfig), + marineCli, + serviceName, + }); + } + return [get, moduleConfig]; })() ) @@ -433,17 +478,21 @@ const prepareForDeploy = async ({ ({ modules, serviceDirPath, ...rest }): PreparedForDeploy => { const deployJSON = { [DEFAULT_DEPLOY_NAME]: { - modules: modules.map(({ get, ...overrides }): JSONModuleConf => { - const moduleConfig = - mapOfAllModuleConfigs.get( + modules: modules.map( + ({ moduleConfig: { get, ...overrides } }): JSONModuleConf => { + const moduleConfig = mapOfAllModuleConfigs.get( getModuleUrlOrAbsolutePath(get, serviceDirPath) - ) ?? - commandObj.error( - `Unreachable. Wasn't able to find module config for ${get}` ); - return serviceModuleToJSONModuleConfig(moduleConfig, overrides); - }), + if (moduleConfig === undefined) { + return commandObj.error( + `Unreachable. Wasn't able to find module config for ${get}` + ); + } + + return serviceModuleToJSONModuleConfig(moduleConfig, overrides); + } + ), }, }; diff --git a/src/commands/module/add.ts b/src/commands/module/add.ts index e49ab9a2e..1046d9fe6 100644 --- a/src/commands/module/add.ts +++ b/src/commands/module/add.ts @@ -19,16 +19,17 @@ import path from "node:path"; import color from "@oclif/color"; import { Command, Flags } from "@oclif/core"; -import camelcase from "camelcase"; import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { initReadonlyModuleConfig } from "../../lib/configs/project/module"; import { initServiceConfig } from "../../lib/configs/project/service"; import { FLUENCE_CONFIG_FILE_NAME, + MODULE_CONFIG_FILE_NAME, NO_INPUT_FLAG, SERVICE_CONFIG_FILE_NAME, } from "../../lib/const"; -import { isUrl, stringToCamelCaseName } from "../../lib/helpers/downloadFile"; +import { downloadModule, isUrl } from "../../lib/helpers/downloadFile"; import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; import { replaceHomeDir } from "../../lib/helpers/replaceHomeDir"; import { input } from "../../lib/prompt"; @@ -45,7 +46,7 @@ export default class Add extends Command { static override flags = { ...NO_INPUT_FLAG, [NAME_FLAG_NAME]: Flags.string({ - description: "Unique module name", + description: "Override module name", helpValue: "", }), service: Flags.directory({ @@ -65,14 +66,28 @@ export default class Add extends Command { const { args, flags } = await this.parse(Add); const isInteractive = getIsInteractive(flags); - const pathToModule: unknown = + const modulePathOrUrl: unknown = args[PATH_OR_URL] ?? (await input({ isInteractive, message: "Enter path to a module or url to .tar.gz archive", })); - assert(typeof pathToModule === "string"); + assert(typeof modulePathOrUrl === "string"); + + const modulePath = isUrl(modulePathOrUrl) + ? await downloadModule(modulePathOrUrl) + : modulePathOrUrl; + + const moduleConfig = await initReadonlyModuleConfig(modulePath, this); + + if (moduleConfig === null) { + this.error( + `${color.yellow( + MODULE_CONFIG_FILE_NAME + )} not found for ${modulePathOrUrl}` + ); + } const serviceNameOrPath = flags.service ?? @@ -111,40 +126,33 @@ export default class Add extends Command { ); } - const moduleName = - flags[NAME_FLAG_NAME] ?? - stringToCamelCaseName(path.basename(pathToModule)); + const validateModuleName = (name: string): true | string => + !(name in (fluenceConfig?.services ?? {})) || + `You already have ${color.yellow(name)} in ${color.yellow( + serviceConfig.$getPath() + )}`; - if (camelcase(moduleName) !== moduleName) { - this.error( - `Module name ${color.yellow( - moduleName - )} not in camelCase. Please use ${color.yellow( - `--${NAME_FLAG_NAME}` - )} flag to specify service name` - ); - } + let validModuleName = flags[NAME_FLAG_NAME] ?? moduleConfig.name; + const serviceNameValidity = validateModuleName(validModuleName); - if (moduleName in serviceConfig.modules) { - this.error( - `You already have ${color.yellow(moduleName)} in ${color.yellow( - SERVICE_CONFIG_FILE_NAME - )}. Provide a unique name for the new module using ${color.yellow( - `--${NAME_FLAG_NAME}` - )} flag or edit the existing module in ${color.yellow( - SERVICE_CONFIG_FILE_NAME - )} manually` - ); + if (serviceNameValidity !== true) { + this.warn(serviceNameValidity); + + validModuleName = await input({ + isInteractive, + message: `Enter another name for module`, + validate: validateModuleName, + }); } serviceConfig.modules = { ...serviceConfig.modules, - [moduleName]: { - get: isUrl(pathToModule) - ? pathToModule + [validModuleName]: { + get: isUrl(modulePathOrUrl) + ? modulePathOrUrl : path.relative( path.resolve(servicePath), - path.resolve(pathToModule) + path.resolve(modulePathOrUrl) ), }, }; @@ -152,7 +160,7 @@ export default class Add extends Command { await serviceConfig.$commit(); this.log( - `Added ${color.yellow(moduleName)} to ${color.yellow( + `Added ${color.yellow(validModuleName)} to ${color.yellow( replaceHomeDir(path.resolve(servicePath)) )}` ); diff --git a/src/commands/run.ts b/src/commands/run.ts index d74dfe9a9..44db4425f 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -44,6 +44,7 @@ const FUNC_FLAG_NAME = "func"; const INPUT_FLAG_NAME = "input"; const ON_FLAG_NAME = "on"; const DATA_FLAG_NAME = "data"; +const JSON_SERVICE = "json-service"; export default class Run extends Command { static override description = "Run aqua script"; @@ -69,9 +70,10 @@ export default class Run extends Command { helpValue: "", multiple: true, }), - "json-service": Flags.file({ + [JSON_SERVICE]: Flags.string({ description: "Path to a file that contains a JSON formatted service", helpValue: "", + multiple: true, }), [ON_FLAG_NAME]: Flags.string({ description: "PeerId of a peer where you want to run the function", @@ -139,7 +141,10 @@ export default class Run extends Command { input: aqua, timeout: flags.timeout, import: imports, - "json-service": appJsonServicePath, + "json-service": [ + appJsonServicePath, + ...(flags[JSON_SERVICE] ?? []), + ], ...data, }, }, diff --git a/src/commands/service/add.ts b/src/commands/service/add.ts index 58fa0da0a..eedb79c3c 100644 --- a/src/commands/service/add.ts +++ b/src/commands/service/add.ts @@ -18,22 +18,27 @@ import assert from "node:assert"; import color from "@oclif/color"; import { Command, Flags } from "@oclif/core"; -import camelcase from "camelcase"; import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { initReadonlyServiceConfig } from "../../lib/configs/project/service"; import { CommandObj, DEFAULT_DEPLOY_NAME, FLUENCE_CONFIG_FILE_NAME, + NAME_FLAG_NAME, NO_INPUT_FLAG, + SERVICE_CONFIG_FILE_NAME, } from "../../lib/const"; -import { stringToCamelCaseName } from "../../lib/helpers/downloadFile"; +import { + AQUA_NAME_REQUIREMENTS, + downloadService, + isUrl, +} from "../../lib/helpers/downloadFile"; import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; import { input } from "../../lib/prompt"; const PATH_OR_URL = "PATH | URL"; -const NAME_FLAG_NAME = "name"; export default class Add extends Command { static override description = `Add service to ${color.yellow( @@ -43,7 +48,7 @@ export default class Add extends Command { static override flags = { ...NO_INPUT_FLAG, [NAME_FLAG_NAME]: Flags.string({ - description: "Unique service name", + description: `Override service name (${AQUA_NAME_REQUIREMENTS})`, helpValue: "", }), }; @@ -57,33 +62,48 @@ export default class Add extends Command { const { args, flags } = await this.parse(Add); const isInteractive = getIsInteractive(flags); await ensureFluenceProject(this, isInteractive); - const pathOrUrlFromArgs: unknown = args[PATH_OR_URL]; - assert( - typeof pathOrUrlFromArgs === "string" || - typeof pathOrUrlFromArgs === "undefined" - ); + const servicePathOrUrl: unknown = + args[PATH_OR_URL] ?? + (await input({ isInteractive, message: "Enter service path or url" })); + + assert(typeof servicePathOrUrl === "string"); + + const servicePath = isUrl(servicePathOrUrl) + ? await downloadService(servicePathOrUrl) + : servicePathOrUrl; + + const serviceConfig = await initReadonlyServiceConfig(servicePath, this); + + if (serviceConfig === null) { + this.error( + `${color.yellow( + SERVICE_CONFIG_FILE_NAME + )} not found for ${servicePathOrUrl}` + ); + } await addService({ commandObj: this, - nameFromFlags: flags[NAME_FLAG_NAME], - pathOrUrl: - pathOrUrlFromArgs ?? - (await input({ isInteractive, message: "Enter service path or url" })), + serviceName: flags[NAME_FLAG_NAME] ?? serviceConfig.name, + pathOrUrl: servicePathOrUrl, + isInteractive, }); } } type AddServiceArg = { commandObj: CommandObj; - nameFromFlags: string | undefined; + serviceName: string; pathOrUrl: string; + isInteractive: boolean; }; export const addService = async ({ commandObj, - nameFromFlags, - pathOrUrl: pathOrUrlFromArgs, + serviceName, + pathOrUrl, + isInteractive, }: AddServiceArg): Promise => { const fluenceConfig = await initFluenceConfig(commandObj); @@ -97,34 +117,29 @@ export const addService = async ({ fluenceConfig.services = {}; } - const serviceName = nameFromFlags ?? stringToCamelCaseName(pathOrUrlFromArgs); + const validateServiceName = (name: string): true | string => + !(name in (fluenceConfig?.services ?? {})) || + `You already have ${color.yellow(name)} in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}`; - if (camelcase(serviceName) !== serviceName) { - commandObj.error( - `Service name ${color.yellow( - serviceName - )} not in camelCase. Please use ${color.yellow( - `--${NAME_FLAG_NAME}` - )} flag to specify service name` - ); - } + let validServiceName = serviceName; + const serviceNameValidity = validateServiceName(validServiceName); - if (serviceName in fluenceConfig.services) { - commandObj.error( - `You already have ${color.yellow(serviceName)} in ${color.yellow( - FLUENCE_CONFIG_FILE_NAME - )}. Provide a unique name for the new service using ${color.yellow( - `--${NAME_FLAG_NAME}` - )} flag or edit the existing service in ${color.yellow( - FLUENCE_CONFIG_FILE_NAME - )} manually` - ); + if (serviceNameValidity !== true) { + commandObj.warn(serviceNameValidity); + + validServiceName = await input({ + isInteractive, + message: `Enter another name for the service (${AQUA_NAME_REQUIREMENTS})`, + validate: validateServiceName, + }); } fluenceConfig.services = { ...fluenceConfig.services, - [serviceName]: { - get: pathOrUrlFromArgs, + [validServiceName]: { + get: pathOrUrl, deploy: [ { deployId: DEFAULT_DEPLOY_NAME, diff --git a/src/commands/service/new.ts b/src/commands/service/new.ts index 03f226584..d6409b771 100644 --- a/src/commands/service/new.ts +++ b/src/commands/service/new.ts @@ -20,12 +20,16 @@ import path from "node:path"; import color from "@oclif/color"; import { Command, Flags } from "@oclif/core"; +import { initNewReadonlyServiceConfig } from "../../lib/configs/project/service"; import { - FACADE_MODULE_NAME, - initNewReadonlyServiceConfig, -} from "../../lib/configs/project/service"; -import { FLUENCE_CONFIG_FILE_NAME, NO_INPUT_FLAG } from "../../lib/const"; -import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; + FLUENCE_CONFIG_FILE_NAME, + NAME_FLAG_NAME, + NO_INPUT_FLAG, +} from "../../lib/const"; +import { + AQUA_NAME_REQUIREMENTS, + ensureValidAquaName, +} from "../../lib/helpers/downloadFile"; import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; import { confirm, input } from "../../lib/prompt"; import { generateNewModule } from "../module/new"; @@ -33,7 +37,6 @@ import { generateNewModule } from "../module/new"; import { addService } from "./add"; const PATH = "PATH"; -const NAME_FLAG_NAME = "name"; export default class New extends Command { static override description = "Create new marine service template"; @@ -41,7 +44,7 @@ export default class New extends Command { static override flags = { ...NO_INPUT_FLAG, [NAME_FLAG_NAME]: Flags.string({ - description: "Unique service name", + description: `Unique service name (${AQUA_NAME_REQUIREMENTS})`, helpValue: "", }), }; @@ -54,30 +57,28 @@ export default class New extends Command { async run(): Promise { const { args, flags } = await this.parse(New); const isInteractive = getIsInteractive(flags); - await ensureFluenceProject(this, isInteractive); - const servicePathFromArgs: unknown = args[PATH]; - - assert( - typeof servicePathFromArgs === "string" || - typeof servicePathFromArgs === "undefined" - ); - const servicePath = - servicePathFromArgs ?? + const servicePath: unknown = + args[PATH] ?? (await input({ isInteractive, message: "Enter service path" })); - const pathToModuleDir = path.join( - servicePath, - "modules", - FACADE_MODULE_NAME - ); + assert(typeof servicePath === "string"); + + const serviceName = await ensureValidAquaName({ + stringToValidate: flags[NAME_FLAG_NAME], + message: "Enter service name", + flagName: NAME_FLAG_NAME, + isInteractive, + }); + const pathToModuleDir = path.join(servicePath, "modules", serviceName); await generateNewModule(pathToModuleDir, this); await initNewReadonlyServiceConfig( servicePath, this, - path.relative(servicePath, pathToModuleDir) + path.relative(servicePath, pathToModuleDir), + serviceName ); this.log( @@ -91,14 +92,15 @@ export default class New extends Command { (await confirm({ isInteractive, message: `Do you want add ${color.yellow( - servicePath + serviceName )} to ${color.yellow(FLUENCE_CONFIG_FILE_NAME)}?`, })) ) { await addService({ commandObj: this, - nameFromFlags: flags[NAME_FLAG_NAME], + serviceName, pathOrUrl: servicePath, + isInteractive, }); } } diff --git a/src/commands/service/repl.ts b/src/commands/service/repl.ts index b71fe1f2c..0352c6531 100644 --- a/src/commands/service/repl.ts +++ b/src/commands/service/repl.ts @@ -26,6 +26,7 @@ import { CliUx, Command } from "@oclif/core"; import { initReadonlyFluenceConfig } from "../../lib/configs/project/fluence"; import { initReadonlyModuleConfig } from "../../lib/configs/project/module"; import { + FACADE_MODULE_NAME, initReadonlyServiceConfig, ModuleV0 as ServiceModule, } from "../../lib/configs/project/service"; @@ -119,7 +120,7 @@ export default class REPL extends Command { commandObj: this, }), [fluenceTmpConfigTomlPath], - { stdio: "inherit" } + { stdio: "inherit", detached: true } ); } } @@ -142,19 +143,24 @@ const ensureServiceConfig = async ({ ? await downloadService(get) : path.resolve(get); - const { facade, ...otherModules } = - (await initReadonlyServiceConfig(serviceDirPath, commandObj))?.modules ?? - CliUx.ux.action.stop(color.red("error")) ?? - commandObj.error( + const modules = (await initReadonlyServiceConfig(serviceDirPath, commandObj)) + ?.modules; + + if (modules === undefined) { + CliUx.ux.action.stop(color.red("error")); + return commandObj.error( `Service ${color.yellow(nameOrPathOrUrl)} doesn't have ${color.yellow( SERVICE_CONFIG_FILE_NAME )}` ); + } + + const { [FACADE_MODULE_NAME]: facade, ...otherModules } = modules; return [...Object.values(otherModules), facade].map( - (mod): ServiceModule => ({ - ...mod, - get: getModuleUrlOrAbsolutePath(mod.get, serviceDirPath), + (moduleConfig): ServiceModule => ({ + ...moduleConfig, + get: getModuleUrlOrAbsolutePath(moduleConfig.get, serviceDirPath), }) ); }; @@ -200,7 +206,7 @@ const ensureModuleConfigs = ({ )} doesn't have ${color.yellow(MODULE_CONFIG_FILE_NAME)}` ); - const overridenModules = { ...moduleConfig, ...overrides }; + const overriddenModules = { ...moduleConfig, ...overrides }; const { name, @@ -212,7 +218,7 @@ const ensureModuleConfigs = ({ mountedBinaries, maxHeapSize, loggingMask, - } = overridenModules; + } = overriddenModules; if (type === "rust") { await marineCli({ @@ -222,7 +228,7 @@ const ensureModuleConfigs = ({ }); } - const load_from = getModuleWasmPath(overridenModules); + const load_from = getModuleWasmPath(overriddenModules); const tomlModuleConfig: TomlModuleConfig = { name, diff --git a/src/lib/configs/project/fluence.ts b/src/lib/configs/project/fluence.ts index 888d5235f..6d6f6dfdb 100644 --- a/src/lib/configs/project/fluence.ts +++ b/src/lib/configs/project/fluence.ts @@ -222,10 +222,11 @@ export type FluenceConfigReadonly = InitializedReadonlyConfig; const examples = ` services: - someService: # Service name in camelCase + someService: # Service name. It must start with a lowercase letter and contain only letters, numbers, and underscores. get: https://github.com/fluencelabs/services/blob/master/adder.tar.gz?raw=true # URL or path deploy: - - deployId: default # any unique string in camelCase. Used in aqua to access deployed service ids + - deployId: default # must start with a lowercase letter and contain only letters, numbers, and underscores. + # Used in aqua to access deployed service ids # You can access deployment info in aqua like this: # services <- App.services() # on services.someService.default!.peerId: diff --git a/src/lib/configs/project/module.ts b/src/lib/configs/project/module.ts index 3e9711c70..c7d7f7116 100644 --- a/src/lib/configs/project/module.ts +++ b/src/lib/configs/project/module.ts @@ -31,7 +31,7 @@ import { export type ConfigV0 = { version: 0; name: string; - type?: "rust"; + type?: "rust" | "compiled"; maxHeapSize?: string; loggerEnabled?: boolean; loggingMask?: number; @@ -45,7 +45,7 @@ const configSchemaV0: JSONSchemaType = { type: "object", properties: { version: { type: "number", enum: [0] }, - type: { type: "string", enum: ["rust"], nullable: true }, + type: { type: "string", enum: ["rust", "compiled"], nullable: true }, name: { type: "string" }, maxHeapSize: { type: "string", nullable: true }, loggerEnabled: { type: "boolean", nullable: true }, diff --git a/src/lib/configs/project/service.ts b/src/lib/configs/project/service.ts index e897307cd..d8001e735 100644 --- a/src/lib/configs/project/service.ts +++ b/src/lib/configs/project/service.ts @@ -17,6 +17,7 @@ import type { JSONSchemaType } from "ajv"; import { CommandObj, SERVICE_CONFIG_FILE_NAME } from "../../const"; +import { validateAquaName } from "../../helpers/downloadFile"; import { ensureFluenceDir } from "../../paths"; import { getConfigInitFunction, @@ -26,6 +27,7 @@ import { getReadonlyConfigInitFunction, Migrations, GetDefaultConfig, + ConfigValidateFunction, } from "../initConfig"; import type { ConfigV0 as ModuleConfig } from "./module"; @@ -40,7 +42,7 @@ const moduleSchema: JSONSchemaType = { type: "object", properties: { get: { type: "string" }, - type: { type: "string", nullable: true, enum: ["rust"] }, + type: { type: "string", nullable: true, enum: ["rust", "compiled"] }, name: { type: "string", nullable: true }, maxHeapSize: { type: "string", nullable: true }, loggerEnabled: { type: "boolean", nullable: true }, @@ -61,6 +63,7 @@ export const FACADE_MODULE_NAME = "facade"; export type ConfigV0 = { version: 0; + name: string; modules: { [FACADE_MODULE_NAME]: ModuleV0 } & Record; }; @@ -68,6 +71,7 @@ const configSchemaV0: JSONSchemaType = { type: "object", properties: { version: { type: "number", enum: [0] }, + name: { type: "string" }, modules: { type: "object", additionalProperties: moduleSchema, @@ -77,7 +81,7 @@ const configSchemaV0: JSONSchemaType = { required: [FACADE_MODULE_NAME], }, }, - required: ["version", "modules"], + required: ["version", "name", "modules"], }; const migrations: Migrations = []; @@ -113,9 +117,20 @@ type Config = ConfigV0; type LatestConfig = ConfigV0; export type ServiceConfig = InitializedConfig; - export type ServiceConfigReadonly = InitializedReadonlyConfig; +const validate: ConfigValidateFunction = ( + config +): ReturnType> => { + const validity = validateAquaName(config.name); + + if (validity === true) { + return true; + } + + return `Invalid service name: ${validity}`; +}; + const getInitConfigOptions = ( configDirPath: string ): InitConfigOptions => ({ @@ -126,6 +141,7 @@ const getInitConfigOptions = ( getSchemaDirPath: ensureFluenceDir, getConfigDirPath: (): string => configDirPath, examples, + validate, }); export const initServiceConfig = ( @@ -143,11 +159,16 @@ export const initReadonlyServiceConfig = ( ); const getDefault: ( - relativePathToFacade: string + relativePathToFacade: string, + name: string ) => GetDefaultConfig = - (relativePathToFacade: string): GetDefaultConfig => + ( + relativePathToFacade: string, + name: string + ): GetDefaultConfig => (): LatestConfig => ({ version: 0, + name, modules: { [FACADE_MODULE_NAME]: { get: relativePathToFacade, @@ -158,9 +179,10 @@ const getDefault: ( export const initNewReadonlyServiceConfig = ( configPath: string, commandObj: CommandObj, - relativePathToFacade: string + relativePathToFacade: string, + name: string ): Promise | null> => getReadonlyConfigInitFunction( getInitConfigOptions(configPath), - getDefault(relativePathToFacade) + getDefault(relativePathToFacade, name) )(commandObj); diff --git a/src/lib/const.ts b/src/lib/const.ts index a9720dfc4..8ab6be88f 100644 --- a/src/lib/const.ts +++ b/src/lib/const.ts @@ -39,6 +39,7 @@ export const TMP_DIR_NAME = "tmp"; export const VSCODE_DIR_NAME = ".vscode"; export const NODE_MODULES_DIR_NAME = "node_modules"; export const AQUA_DIR_NAME = "aqua"; +export const AQUA_SERVICES_DIR_NAME = "services"; export const TS_DIR_NAME = "ts"; export const JS_DIR_NAME = "js"; export const MODULES_DIR_NAME = "modules"; @@ -118,6 +119,7 @@ export const TIMEOUT_FLAG = { } as const; export const FORCE_FLAG_NAME = "force"; +export const NAME_FLAG_NAME = "name"; export type CommandObj = Readonly>; diff --git a/src/lib/deployedApp.ts b/src/lib/deployedApp.ts index 13d94eb43..9c877b04a 100644 --- a/src/lib/deployedApp.ts +++ b/src/lib/deployedApp.ts @@ -18,11 +18,11 @@ import fsPromises from "node:fs/promises"; import color from "@oclif/color"; import { CliUx } from "@oclif/core"; -import camelcase from "camelcase"; import type { AquaCLI } from "./aquaCli"; import type { ServicesV2 } from "./configs/project/app"; import { FS_OPTIONS } from "./const"; +import { capitalize } from "./helpers/capitilize"; import { replaceHomeDir } from "./helpers/replaceHomeDir"; import { ensureFluenceJSAppPath, @@ -32,19 +32,19 @@ import { ensureFluenceTSDir, } from "./paths"; -const APP = "App"; -const SERVICE_IDS = "services"; +const APP_SERVICE_NAME = "App"; +const SERVICES_FUNCTION_NAME = "services"; const SERVICE_IDS_ITEM = "ServiceIdsItem"; -const SERVICES = "Services"; +const SERVICES_DATA_AQUA_TYPE = "Services"; export const getAppJson = (services: ServicesV2): string => JSON.stringify( { - name: APP, - serviceId: APP, + name: APP_SERVICE_NAME, + serviceId: APP_SERVICE_NAME, functions: [ { - name: SERVICE_IDS, + name: SERVICES_FUNCTION_NAME, result: services, }, ], @@ -74,7 +74,11 @@ const generateRegisterAppTSorJS = async ({ import { registerApp as registerAppService } from "./deployed.app"; const service = { - ${SERVICE_IDS}: () => (${JSON.stringify(deployedServices, null, 2)}), + ${SERVICES_FUNCTION_NAME}: () => (${JSON.stringify( + deployedServices, + null, + 2 + )}), }; ${ @@ -133,7 +137,7 @@ export const generateRegisterApp = async ( }; const getDeploysDataName = (serviceName: string): string => - `${camelcase(serviceName, { pascalCase: true })}Deploys`; + `${capitalize(serviceName)}Deploys`; export const generateDeployedAppAqua = async ( services: ServicesV2 @@ -161,16 +165,16 @@ ${Object.entries(services) ) .join("\n\n")} -data ${SERVICES}: +data ${SERVICES_DATA_AQUA_TYPE}: ${Object.keys(services) .map( (serviceName): string => - ` ${camelcase(serviceName)}: ${getDeploysDataName(serviceName)}` + ` ${serviceName}: ${getDeploysDataName(serviceName)}` ) .join("\n")} -service ${APP}("${APP}"): - ${SERVICE_IDS}: -> ${SERVICES} +service ${APP_SERVICE_NAME}("${APP_SERVICE_NAME}"): + ${SERVICES_FUNCTION_NAME}: -> ${SERVICES_DATA_AQUA_TYPE} `; await fsPromises.writeFile(appServicesFilePath, appServicesAqua, FS_OPTIONS); diff --git a/src/lib/helpers/capitilize.ts b/src/lib/helpers/capitilize.ts new file mode 100644 index 000000000..d9dc9eacb --- /dev/null +++ b/src/lib/helpers/capitilize.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const capitalize = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); diff --git a/src/lib/helpers/downloadFile.ts b/src/lib/helpers/downloadFile.ts index 0955b08d2..06e7ed355 100644 --- a/src/lib/helpers/downloadFile.ts +++ b/src/lib/helpers/downloadFile.ts @@ -19,13 +19,13 @@ import fsPromises from "node:fs/promises"; import path from "node:path"; import color from "@oclif/color"; -import camelcase from "camelcase"; import decompress from "decompress"; import filenamify from "filenamify"; import fetch from "node-fetch"; import { WASM_EXT } from "../const"; import { ensureFluenceModulesDir, ensureFluenceServicesDir } from "../paths"; +import { input } from "../prompt"; export const getHashOfString = (str: string): Promise => { const md5Hash = crypto.createHash("md5"); @@ -58,29 +58,53 @@ const downloadFile = async (path: string, url: string): Promise => { return path; }; -export const stringToCamelCaseName = (string: string): string => { - const cleanString = string.replace(".tar.gz?raw=true", ""); - const withoutTrailingSlash = cleanString.replace(/\/$/, ""); - - const lastPortionOfPath = - withoutTrailingSlash - .split(withoutTrailingSlash.includes("/") ? "/" : "\\") - .slice(-1)[0] ?? ""; +type EnsureValidAquaNameArg = { + stringToValidate: string | undefined; +} & Parameters[0]; + +export const AQUA_NAME_REQUIREMENTS = + "must start with a lowercase letter and contain only letters, numbers, and underscores"; + +export const ensureValidAquaName = async ({ + stringToValidate, + ...inputArg +}: EnsureValidAquaNameArg): Promise => { + if ( + stringToValidate === undefined || + validateAquaName(stringToValidate) !== true + ) { + return input({ + ...inputArg, + message: `${inputArg.message} (${AQUA_NAME_REQUIREMENTS})`, + validate: validateAquaName, + }); + } - const validName = filenamify(lastPortionOfPath); - return camelcase(validName); + return stringToValidate; }; -const ARCHIVE_FILE = "archive.tar.gz"; +export const validateAquaName = (text: string): true | string => + /^[a-z]\w*$/.test(text) || AQUA_NAME_REQUIREMENTS; -const getHashPath = async (get: string, dir: string): Promise => - path.join(dir, `${stringToCamelCaseName(get)}_${await getHashOfString(get)}`); +const ARCHIVE_FILE = "archive.tar.gz"; const downloadAndDecompress = async ( get: string, - dir: string + pathStart: string ): Promise => { - const dirPath = await getHashPath(get, dir); + const hash = await getHashOfString(get); + const cleanPrefix = get.replace(".tar.gz?raw=true", ""); + const withoutTrailingSlash = cleanPrefix.replace(/\/$/, ""); + + const lastPortionOfPath = + withoutTrailingSlash + .split(withoutTrailingSlash.includes("/") ? "/" : "\\") + .slice(-1)[0] ?? ""; + + const prefix = + lastPortionOfPath === "" ? "" : `${filenamify(lastPortionOfPath)}_`; + + const dirPath = path.join(pathStart, `${prefix}${hash}`); try { await fsPromises.access(dirPath); diff --git a/src/lib/helpers/generateServiceInterface.ts b/src/lib/helpers/generateServiceInterface.ts new file mode 100644 index 000000000..94cb7c03d --- /dev/null +++ b/src/lib/helpers/generateServiceInterface.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fsPromises from "node:fs/promises"; +import path from "node:path"; + +import { AQUA_EXT, FS_OPTIONS } from "../const"; +import type { MarineCLI } from "../marineCli"; +import { ensureFluenceAquaServicesDir } from "../paths"; + +type GenerateServiceInterfaceArg = { + pathToFacade: string; + marineCli: MarineCLI; + serviceName: string; +}; + +export const generateServiceInterface = async ({ + serviceName, + pathToFacade, + marineCli, +}: GenerateServiceInterfaceArg): Promise => { + const interfaceString = await marineCli({ + command: "aqua", + args: [pathToFacade], + }); + + const aquaInterfacePath = path.join( + await ensureFluenceAquaServicesDir(), + `${serviceName}.${AQUA_EXT}` + ); + + return fsPromises.writeFile(aquaInterfacePath, interfaceString, FS_OPTIONS); +}; diff --git a/src/lib/marineCli.ts b/src/lib/marineCli.ts index a34bc19c7..9775af171 100644 --- a/src/lib/marineCli.ts +++ b/src/lib/marineCli.ts @@ -22,6 +22,7 @@ import { import { execPromise } from "./execPromise"; import { getMessageWithKeyValuePairs } from "./helpers/getMessageWithKeyValuePairs"; import { unparseFlags } from "./helpers/unparseFlags"; +import { getProjectRootDir } from "./paths"; import { ensureCargoDependency } from "./rust"; import type { Flags } from "./typeHelpers"; @@ -29,10 +30,17 @@ export type MarineCliInput = | { command: "generate"; flags: Flags<"init" | "name">; + args?: never; + } + | { + command: "aqua"; + flags?: never; + args: Array; } | { command: "build"; flags: Flags<"release">; + args?: never; }; export type MarineCLI = { @@ -53,6 +61,8 @@ export const initMarineCli = async ( commandObj, }); + const projectRootDir = getProjectRootDir(); + await ensureCargoDependency({ name: CARGO_GENERATE_CARGO_DEPENDENCY, commandObj, @@ -61,25 +71,26 @@ export const initMarineCli = async ( return async ({ command, flags, + args, message, keyValuePairs, workingDir, }): Promise => { - const cwd = process.cwd(); - if (workingDir !== undefined) { process.chdir(workingDir); } const result = await execPromise( - `${marineCliPath} ${command ?? ""}${unparseFlags(flags, commandObj)}`, + `${marineCliPath} ${command ?? ""}${ + args === undefined ? "" : ` ${args.join(" ")}` + } ${unparseFlags(flags ?? {}, commandObj)}`, message === undefined ? undefined : getMessageWithKeyValuePairs(message, keyValuePairs) ); if (workingDir !== undefined) { - process.chdir(cwd); + process.chdir(projectRootDir); } return result; diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 2f010fa63..8bd39c376 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -23,6 +23,7 @@ import { APP_SERVICE_JSON_FILE_NAME, APP_TS_FILE_NAME, AQUA_DIR_NAME, + AQUA_SERVICES_DIR_NAME, CARGO_DIR_NAME, CommandObj, CONFIG_TOML, @@ -117,6 +118,9 @@ export const ensureFluenceDir = (): Promise => export const ensureFluenceAquaDir = async (): Promise => ensureDir(path.join(await ensureFluenceDir(), AQUA_DIR_NAME)); +export const ensureFluenceAquaServicesDir = async (): Promise => + ensureDir(path.join(await ensureFluenceAquaDir(), AQUA_SERVICES_DIR_NAME)); + export const ensureFluenceAquaDeployedAppPath = async (): Promise => path.join(await ensureFluenceAquaDir(), DEPLOYED_APP_AQUA_FILE_NAME); diff --git a/src/lib/rust.ts b/src/lib/rust.ts index 86b09acad..320ccfbdc 100644 --- a/src/lib/rust.ts +++ b/src/lib/rust.ts @@ -131,7 +131,7 @@ const hasRequiredRustTarget = async (): Promise => const cargoInstall = async ({ packageName, version, - isNightly, + isNightlyX86, isGlobalDependency, commandObj, message, @@ -142,7 +142,7 @@ const cargoInstall = async ({ }): Promise => execPromise( `${CARGO}${ - isNightly === true ? " +nightly" : "" + isNightlyX86 === true ? " +nightly-x86_64" : "" } install ${packageName} ${unparseFlags( { version, @@ -163,7 +163,7 @@ const cargoInstall = async ({ type CargoDependencyInfo = { recommendedVersion: string; packageName: string; - isNightly?: true; + isNightlyX86?: true; isGlobalDependency?: true; }; @@ -171,12 +171,12 @@ export const cargoDependencies: Record = { [MARINE_CARGO_DEPENDENCY]: { recommendedVersion: MARINE_RECOMMENDED_VERSION, packageName: MARINE_CARGO_DEPENDENCY, - isNightly: true, + isNightlyX86: true, }, [MREPL_CARGO_DEPENDENCY]: { recommendedVersion: MREPL_RECOMMENDED_VERSION, packageName: MREPL_CARGO_DEPENDENCY, - isNightly: true, + isNightlyX86: true, }, [CARGO_GENERATE_CARGO_DEPENDENCY]: { recommendedVersion: CARGO_GENERATE_RECOMMENDED_VERSION,