From 81781ee199baac479bb7e4af3e85c82b51fdecda Mon Sep 17 00:00:00 2001 From: Jaya Krishna Namburu Date: Wed, 6 Sep 2023 18:04:36 +0530 Subject: [PATCH] Add guided flow using prompts for cli (#2252) ## Description This PR adds prompts, when the cli is ran using `webstudio` and without any commands. It requests the users if they want to create a folder. If yes, it will create a folder and changes the directory to the newly created ones. This PR even allows `webstudio link` command to be executed without prompts using ```sh webstudio link -l ``` ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - test it on preview - [ ] hi @TrySound , I need you to do - conceptual review (architecture, feature-correctness) ## Before requesting a review - [x] made a self-review --- packages/cli/package.json | 2 ++ packages/cli/src/cli.ts | 16 ++++++--- packages/cli/src/commands/build.ts | 4 ++- packages/cli/src/commands/init-flow.ts | 50 ++++++++++++++++++++++++++ packages/cli/src/commands/link.ts | 28 ++++++++++++--- packages/cli/src/fs-utils.ts | 9 +++++ pnpm-lock.yaml | 13 +++++++ 7 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/commands/init-flow.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index b0deb4a695..f27d21dc27 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,11 +34,13 @@ "env-paths": "^3.0.0", "ora": "^7.0.1", "p-limit": "^4.0.0", + "prompts": "^2.4.2", "strip-indent": "^4.0.0", "yargs": "^17.7.2" }, "devDependencies": { "@types/node": "^18.13.0", + "@types/prompts": "^2.4.4", "@webstudio-is/form-handlers": "workspace:^", "@webstudio-is/scripts": "workspace:^", "@webstudio-is/tsconfig": "workspace:^", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c38eb80a57..b1b1b95f5c 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -3,9 +3,10 @@ import { exit, argv } from "node:process"; import { hideBin } from "yargs/helpers"; import { GLOBAL_CONFIG_FILE } from "./config"; import { ensureFileInPath } from "./fs-utils"; -import { link } from "./commands/link"; +import { link, linkOptions } from "./commands/link"; import { sync } from "./commands/sync"; import { build, buildOptions } from "./commands/build"; +import { initFlow } from "./commands/init-flow"; import makeCLI from "yargs"; import packageJson from "../package.json" assert { type: "json" }; import type { CommonYargsArgv } from "./commands/yargs-types"; @@ -34,18 +35,23 @@ export const main = async () => { type: "boolean", }) .option("h", { - describe: "Show version number", + describe: "Show all commands", alias: "help", type: "boolean", }) - .scriptName("webstudio") - .demandCommand(1, "Command is required"); + .scriptName("webstudio"); cmd.version(packageJson.version).alias("v", "version"); cmd.command(["build"], "build the project", buildOptions, build); - cmd.command(["link"], "link the project from your workspace", {}, link); + cmd.command( + ["link"], + "link the project from your workspace", + linkOptions, + link + ); cmd.command(["sync"], "sync your project", {}, sync); + cmd.command("$0", "setup the project", {}, initFlow); await cmd.parse(); } catch (error) { diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index b9c8ecb735..5af9920559 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -15,7 +15,9 @@ export const buildOptions = (yargs: CommonYargsArgv) => // @todo: use options.assets to define if we need to download assets export const build = async ( - options: StrictYargsOptionsToInterface + options: + | StrictYargsOptionsToInterface + | { assets?: boolean } ) => { try { await access(LOCAL_DATA_FILE); diff --git a/packages/cli/src/commands/init-flow.ts b/packages/cli/src/commands/init-flow.ts new file mode 100644 index 0000000000..16f886dd59 --- /dev/null +++ b/packages/cli/src/commands/init-flow.ts @@ -0,0 +1,50 @@ +import prompts, { type PromptObject } from "prompts"; +import { ensureFolderExists, isFileExists } from "../fs-utils"; +import { chdir, cwd } from "node:process"; +import { join } from "node:path"; +import { link } from "./link"; +import { sync } from "./sync"; +import { build } from "./build"; + +export const initFlow = async () => { + const isProjectConfigured = await isFileExists(".webstudio/config.json"); + const prompsList: PromptObject[] = []; + + if (isProjectConfigured === false) { + prompsList.push( + { + type: "confirm", + name: "folder", + message: "Do you want to create a folder", + initial: true, + }, + { + type: (prev) => (prev === true ? "text" : null), + name: "folderName", + message: "Enter a project name", + }, + { + type: (prev) => (prev === undefined ? null : "text"), + name: "projectLink", + message: "Enter a project link", + } + ); + } + + const response = await prompts(prompsList); + + /* + If users wanted to create a folder, we need to create it and link the project. + */ + if (response.folderName) { + await ensureFolderExists(join(cwd(), response.folderName)); + chdir(join(cwd(), response.folderName)); + await link({ link: response.projectLink }); + } + + /* + If the project is already set up, we can sync and build it. + */ + await sync(); + await build({ assets: true }); +}; diff --git a/packages/cli/src/commands/link.ts b/packages/cli/src/commands/link.ts index ddbc0c77a1..12ee993db8 100644 --- a/packages/cli/src/commands/link.ts +++ b/packages/cli/src/commands/link.ts @@ -4,10 +4,31 @@ import * as readline from "node:readline/promises"; import { readFile, writeFile } from "node:fs/promises"; import { GLOBAL_CONFIG_FILE, LOCAL_CONFIG_FILE } from "../config"; import { ensureFileInPath } from "../fs-utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "./yargs-types"; -export const link = async () => { - const rl = readline.createInterface({ input: stdin, output: stdout }); - const shareLink = await rl.question(`Paste share link (with build access): `); +export const linkOptions = (yargs: CommonYargsArgv) => + yargs.option("link", { + alias: "l", + type: "string", + describe: "Link to a webstudio project", + }); + +export const link = async ( + options: StrictYargsOptionsToInterface | { link?: string } +) => { + let shareLink: string; + if (options.link) { + shareLink = options.link; + } else { + const rl = readline.createInterface({ input: stdin, output: stdout }); + shareLink = await rl.question( + `Please paste a link from the Share Dialog in the builder: ` + ); + rl.close(); + } const shareLinkUrl = new URL(shareLink); const host = shareLinkUrl.origin; @@ -34,7 +55,6 @@ export const link = async () => { }; await writeFile(GLOBAL_CONFIG_FILE, JSON.stringify(newConfig, null, 2)); - rl.close(); console.info(`Saved credentials for project ${projectId}. You can find your config at ${GLOBAL_CONFIG_FILE} `); diff --git a/packages/cli/src/fs-utils.ts b/packages/cli/src/fs-utils.ts index efbbe2077a..8c5f8d7d75 100644 --- a/packages/cli/src/fs-utils.ts +++ b/packages/cli/src/fs-utils.ts @@ -9,6 +9,15 @@ import { import { join } from "node:path"; import type { Folder } from "./args"; +export const isFileExists = async (filePath: string) => { + try { + await access(filePath, constants.F_OK); + return true; + } catch { + return false; + } +}; + export const ensureFileInPath = async (filePath: string, content?: string) => { const dir = dirname(filePath); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49503ec047..b14df51687 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,9 @@ importers: p-limit: specifier: ^4.0.0 version: 4.0.0 + prompts: + specifier: ^2.4.2 + version: 2.4.2 strip-indent: specifier: ^4.0.0 version: 4.0.0 @@ -490,6 +493,9 @@ importers: '@types/node': specifier: ^18.13.0 version: 18.13.0 + '@types/prompts': + specifier: ^2.4.4 + version: 2.4.4 '@webstudio-is/form-handlers': specifier: workspace:^ version: link:../form-handlers @@ -7579,6 +7585,13 @@ packages: resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} dev: true + /@types/prompts@2.4.4: + resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==} + dependencies: + '@types/node': 18.17.1 + kleur: 3.0.3 + dev: true + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}