Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI polish #494

Merged
merged 19 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ In previous releases, the name "Hypermode" was used for all three._
- Improve dev first use log messages [#489](https://github.com/hypermodeinc/modus/pull/489)
- Highlight endpoints when running in dev [#490](https://github.com/hypermodeinc/modus/pull/490)
- Fix data race in logging adapter [#491](https://github.com/hypermodeinc/modus/pull/491)
- Simplify and polish `modus new` experience [#494](https://github.com/hypermodeinc/modus/pull/494)
- Move hyp settings for local model invocation to env variables [#495](https://github.com/hypermodeinc/modus/pull/495)

## 2024-10-02 - Version 0.12.7
Expand Down
9 changes: 9 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ This package contains the Modus CLI, which is responsible for the Modus local de
## Getting Started

Please refer to the docs at: https://docs.hypermode.com/modus/quickstart

## Contributing

```bash
npm i
npm run watch
```

Make changes and then run `./bin/modus.js` to test changes.
5 changes: 4 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
"scripts": {
"pack": "npm pack",
"build": "rm -rf dist && tsc -b",
"watch": "rm -rf dist && tsc -b -w",
"postpack": "rm -f oclif.manifest.json",
"prepack": "npm i && npm run build && oclif manifest"
},
"dependencies": {
"@inquirer/prompts": "^7.0.0",
"@oclif/core": "^4",
"chalk": "^5.3.0",
"chokidar": "^4.0.1",
Expand Down Expand Up @@ -61,5 +63,6 @@
"description": "Modus Runtime Management"
}
}
}
},
"packageManager": "pnpm@8.14.1+sha512.856c4ecd97c5d3f30354ebc373cd32fb37274295c489db2d0f613a1e60f010cadfbb15036d525f3a06dec1897e4219b3c4f6d3be8f2f62fb0a366bee6aaa7533"
}
143 changes: 71 additions & 72 deletions cli/src/commands/new/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import { GitHubOwner, GitHubRepo, MinGoVersion, MinNodeVersion, MinTinyGoVersion
import { ask, clearLine, withSpinner } from "../../util/index.js";
import SDKInstallCommand from "../sdk/install/index.js";
import { getHeader } from "../../custom/header.js";
import * as inquirer from "@inquirer/prompts";

const MODUS_NEW_GO_NAME = "modus-go-app";
const MODUS_NEW_AS_NAME = "modus-as-app";

const MODUS_DEFAULT_TEMPLATE_NAME = "default";

export default class NewCommand extends Command {
static description = "Create a new Modus app";
Expand All @@ -45,10 +51,10 @@ export default class NewCommand extends Command {
char: "s",
description: "SDK to use",
}),
template: Flags.string({
char: "t",
description: "Template to use",
}),
// template: Flags.string({
// char: "t",
// description: "Template to use",
// }),
force: Flags.boolean({
char: "f",
default: false,
Expand All @@ -70,79 +76,53 @@ export default class NewCommand extends Command {
}

this.log(chalk.hex("#A585FF")(NewCommand.description) + "\n");
const name = flags.name || (await this.promptAppName());
if (!name) {
this.logError("An app name is required.");
this.exit(1);
}
const dir = flags.dir ? path.join(process.cwd(), flags.dir) : await this.promptInstallPath("." + path.sep + name);
if (!dir) {
this.logError("An install directory is required.");
this.exit(1);
}
const sdk = parseSDK(
flags.sdk
? Object.values(SDK)[
Object.keys(SDK)
.map((v) => v.toLowerCase())
.indexOf(flags.sdk?.trim().toLowerCase())
]
: await this.promptSdkSelection()
);
const template = flags.template || (await this.promptTemplate("default"));
if (!template) {
this.logError("A template is required.");
this.exit(1);
}
if (!flags.force && !(await this.confirmAction("Continue? [y/n]"))) {
this.log(chalk.dim("Aborted."));
this.exit(1);

let sdk: SDK;
if (flags.sdk) {
sdk = parseSDK(flags.sdk);
} else {
const sdkInput = await inquirer.select({
message: "Select a SDK",
default: SDK.Go,
choices: [
{
value: SDK.Go,
},
{
value: SDK.AssemblyScript,
},
],
});

sdk = parseSDK(sdkInput);
}
this.log();
await this.createApp(name, dir, sdk, template, flags.force, flags.prerelease);
}

private async promptAppName(): Promise<string> {
this.log("App Name?");
const name = ((await ask(chalk.dim(" -> "))) || "").trim();
clearLine(2);
this.log("App Name: " + chalk.dim(name.length ? name : "Not Provided"));
return name;
}
const defaultName = sdk === SDK.Go ? MODUS_NEW_GO_NAME : MODUS_NEW_AS_NAME;

private async promptInstallPath(defaultValue: string): Promise<string> {
this.log("Install Directory? " + chalk.dim(`(${defaultValue})`));
const dir = ((await ask(chalk.dim(" -> "))) || defaultValue).trim();
clearLine(2);
this.log("Directory: " + chalk.dim(dir));
return path.resolve(dir);
}
const name =
flags.name ||
(await inquirer.input({
message: "Pick a name for your app:",
default: defaultName,
}));

private async promptSdkSelection(): Promise<string> {
this.log("Select an SDK");
for (const [index, sdk] of Object.values(SDK).entries()) {
this.log(chalk.dim(` ${index + 1}. ${sdk}`));
}
const dir = flags.dir || "." + path.sep + name;

const selectedIndex = Number.parseInt(((await ask(chalk.dim(" -> "))) || "1").trim(), 10) - 1;
const sdk = Object.values(SDK)[selectedIndex];
clearLine(Object.values(SDK).length + 2);
if (!sdk) this.exit(1);
this.log("SDK: " + chalk.dim(sdk));
return sdk;
}
if (!flags.force) {
const confirm = await inquirer.confirm({ message: "Continue?", default: true });
if (!confirm) {
this.log(chalk.dim("Aborted"));
this.exit(1);
}
}

private async promptTemplate(defaultValue: string): Promise<string> {
this.log("Template? " + chalk.dim(`(${defaultValue})`));
const template = ((await ask(chalk.dim(" -> "))) || defaultValue).trim();
clearLine(2);
this.log("Template: " + chalk.dim(template));
return template;
this.log();
await this.createApp(name, dir, sdk, MODUS_DEFAULT_TEMPLATE_NAME, flags.force, flags.prerelease);
}

private async createApp(name: string, dir: string, sdk: SDK, template: string, force: boolean, prerelease: boolean) {
if (!force && (await fs.exists(dir))) {
if (!(await this.confirmAction("Attempting to overwrite a folder that already exists.\nAre you sure you want to continue? [y/n]"))) {
if (!(await this.confirmAction("Attempting to overwrite a folder that already exists.\nAre you sure you want to continue? [Y/n]"))) {
clearLine();
return;
} else {
Expand Down Expand Up @@ -229,13 +209,13 @@ export default class NewCommand extends Command {

let updateSDK = false;
if (!installedSdkVersion) {
if (!(await this.confirmAction(`You do not have the ${sdkText} installed. Would you like to install it now? [y/n]`))) {
if (!(await this.confirmAction(`You do not have the ${sdkText} installed. Would you like to install it now? [Y/n]`))) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
updateSDK = true;
} else if (latestVersion !== installedSdkVersion) {
if (await this.confirmAction(`You have ${installedSdkVersion} of the ${sdkText}. The latest is ${latestVersion}. Would you like to update? [y/n]`)) {
if (await this.confirmAction(`You have ${installedSdkVersion} of the ${sdkText}. The latest is ${latestVersion}. Would you like to update? [Y/n]`)) {
updateSDK = true;
}
}
Expand Down Expand Up @@ -316,11 +296,18 @@ export default class NewCommand extends Command {
this.log(chalk.red(" ERROR ") + chalk.dim(": " + message));
}

private async confirmAction(message: string): Promise<boolean> {
private async confirmAction(message: string, defaultToContinue = true): Promise<boolean> {
this.log(message);
const cont = ((await ask(chalk.dim(" -> "))) || "n").toLowerCase().trim();
const input = await ask(chalk.dim(" -> "));

if (input === "") {
clearLine(2);
return defaultToContinue;
}

const shouldContinue = (input || "n").toLowerCase().trim();
clearLine(2);
return cont === "yes" || cont === "y";
return shouldContinue === "yes" || shouldContinue === "y";
}
}

Expand Down Expand Up @@ -348,3 +335,15 @@ async function getTinyGoVersion(): Promise<string | undefined> {
return parts.length > 2 ? parts[2] : undefined;
} catch {}
}

function toValidAppName(input: string): string {
// Remove any characters that aren't alphanumeric, spaces, or a few other valid characters.
// Replace spaces with hyphens.
return input
.trim() // Remove leading/trailing spaces
.toLowerCase() // Convert to lowercase for consistency
.replace(/[^a-z0-9\s-]/g, "") // Remove invalid characters
.replace(/\s+/g, "-") // Replace spaces (or multiple spaces) with a single hyphen
.replace(/-+/g, "-") // Replace multiple consecutive hyphens with a single hyphen
.replace(/^-|-$/g, ""); // Remove leading or trailing hyphens
}
17 changes: 12 additions & 5 deletions cli/src/commands/runtime/remove/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class RuntimeRemoveCommand extends Command {
if (versions.length === 0) {
this.log(chalk.yellow("No Modus runtimes are installed."));
this.exit(1);
} else if (!flags.force && !(await this.confirmAction("Really, remove all Modus runtimes? [y/n]"))) {
} else if (!flags.force && !(await this.confirmAction("Really, remove all Modus runtimes? [y/N]"), false)) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
Expand All @@ -62,7 +62,7 @@ export default class RuntimeRemoveCommand extends Command {
if (!isInstalled) {
this.log(chalk.yellow(runtimeText + "is not installed."));
this.exit(1);
} else if (!flags.force && !(await this.confirmAction(`Really, remove ${runtimeText} ? [y/n]`))) {
} else if (!flags.force && !(await this.confirmAction(`Really, remove ${runtimeText} ? [y/N]`))) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
Expand All @@ -89,10 +89,17 @@ export default class RuntimeRemoveCommand extends Command {
this.log(chalk.red(" ERROR ") + chalk.dim(": " + message));
}

private async confirmAction(message: string): Promise<boolean> {
private async confirmAction(message: string, defaultToContinue = true): Promise<boolean> {
this.log(message);
const cont = ((await ask(chalk.dim(" -> "))) || "n").toLowerCase().trim();
const input = await ask(chalk.dim(" -> "));

if (input === "") {
clearLine(2);
return defaultToContinue;
}

const shouldContinue = (input || "n").toLowerCase().trim();
clearLine(2);
return cont === "yes" || cont === "y";
return shouldContinue === "yes" || shouldContinue === "y";
}
}
19 changes: 13 additions & 6 deletions cli/src/commands/sdk/remove/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default class SDKRemoveCommand extends Command {
this.exit(1);
}

if (!flags.force && !(await this.confirmAction(`Really, remove all Modus SDKs${flags.runtimes ? " and runtimes" : ""}? [y/n]`))) {
if (!flags.force && !(await this.confirmAction(`Really, remove all Modus SDKs${flags.runtimes ? " and runtimes" : ""}? [y/N]`, false))) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
Expand All @@ -89,7 +89,7 @@ export default class SDKRemoveCommand extends Command {
if (versions.length === 0) {
this.log(chalk.yellow(`No Modus ${sdk} SDKs are installed.`));
this.exit(1);
} else if (!flags.force && !(await this.confirmAction(`Really, remove all Modus ${sdk} SDKs? [y/n]`))) {
} else if (!flags.force && !(await this.confirmAction(`Really, remove all Modus ${sdk} SDKs? [y/N]`, false))) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
Expand All @@ -106,7 +106,7 @@ export default class SDKRemoveCommand extends Command {
if (!isInstalled) {
this.log(chalk.yellow(sdkText + "is not installed."));
this.exit(1);
} else if (!flags.force && !(await this.confirmAction(`Really, remove ${sdkText} ? [y/n]`))) {
} else if (!flags.force && !(await this.confirmAction(`Really, remove ${sdkText} ? [y/N]`, false))) {
this.log(chalk.dim("Aborted."));
this.exit(1);
}
Expand Down Expand Up @@ -148,10 +148,17 @@ export default class SDKRemoveCommand extends Command {
this.log(chalk.red(" ERROR ") + chalk.dim(": " + message));
}

private async confirmAction(message: string): Promise<boolean> {
private async confirmAction(message: string, defaultToContinue = true): Promise<boolean> {
this.log(message);
const cont = ((await ask(chalk.dim(" -> "))) || "n").toLowerCase().trim();
const input = await ask(chalk.dim(" -> "));

if (input === "") {
clearLine(2);
return defaultToContinue;
}

const shouldContinue = (input || "n").toLowerCase().trim();
clearLine(2);
return cont === "yes" || cont === "y";
return shouldContinue === "yes" || shouldContinue === "y";
}
}
3 changes: 1 addition & 2 deletions cli/src/custom/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
* SPDX-License-Identifier: Apache-2.0
*/

import chalk from "chalk";
import { getLogo } from "./logo.js";

export function getHeader(cliVersion: string): string {
let out = "";
out += getLogo();
out += "\n";
out += chalk.dim(`Modus CLI v${cliVersion}`);
out += `Modus CLI v${cliVersion}`;
out += "\n";
return out;
}
15 changes: 11 additions & 4 deletions cli/src/custom/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class CustomHelp extends Help {
}

formatRoot(): string {
return `${chalk.bold("Usage:")} modus ${chalk.blueBright("<command or tool> [...flags] [...args]")}`;
return `${chalk.bold("Usage:")} modus ${"<command or tool> [flags] [args]"}`;
}

formatCommands(commands: Command.Loadable[]): string {
Expand Down Expand Up @@ -77,11 +77,18 @@ export default class CustomHelp extends Help {

formatFooter(): string {
let out = "";
out += "View the docs:" + " ".repeat(Math.max(1, this.pre_pad + this.post_pad - 12)) + chalk.blueBright("https://docs.hypermode.com/modus") + "\n";
out += "View the repo:" + " ".repeat(Math.max(1, this.pre_pad + this.post_pad - 12)) + chalk.blueBright("https://github.com/hypermodeinc/modus") + "\n";
const links = [
{ name: "Docs", url: "https://docs.hypermode.com/modus" },
{ name: "GitHub", url: "https://github.com/hypermodeinc/modus" },
{ name: "Discord", url: "https://discord.hypermode.com" },
];

for (const link of links) {
out += `${link.name}: ${" ".repeat(Math.max(1, this.pre_pad + this.post_pad - link.name.length))}${link.url}\n`;
}

out += "\n";
out += "Made with 💖 by " + chalk.hex("#602AF8")("https://hypermode.com");
out += "Made with ♥︎ by Hypermode";
out += "\n";

return out;
Expand Down