diff --git a/package-lock.json b/package-lock.json index 18c4702..c4c395d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,24 @@ { "name": "foji", - "version": "0.4.1", + "version": "0.0.0-development", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "foji", - "version": "0.4.1", + "version": "0.0.0-development", "license": "MIT", "dependencies": { "@inquirer/prompts": "^5.4.0", - "commander": "^12.1.0" + "commander": "^12.1.0", + "fast-levenshtein": "^3.0.0" }, "bin": { "fj": "bin/index.js", "foji": "bin/index.js" }, "devDependencies": { + "@types/fast-levenshtein": "^0.0.4", "@types/inquirer": "^9.0.7", "@types/node": "^22.5.1", "semantic-release": "^24.1.1", @@ -761,6 +763,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/fast-levenshtein": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.4.tgz", + "integrity": "sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/inquirer": { "version": "9.0.7", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", @@ -1658,6 +1667,24 @@ "node": ">=8.6.0" } }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", diff --git a/package.json b/package.json index 068df61..3fc58d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foji", - "version": "0.4.1", + "version": "0.0.0-development", "main": "src/index.ts", "bin": { "foji": "./bin/index.js", @@ -27,6 +27,7 @@ "license": "MIT", "description": "Forge your code in a new way.", "devDependencies": { + "@types/fast-levenshtein": "^0.0.4", "@types/inquirer": "^9.0.7", "@types/node": "^22.5.1", "semantic-release": "^24.1.1", @@ -34,6 +35,7 @@ }, "dependencies": { "@inquirer/prompts": "^5.4.0", - "commander": "^12.1.0" + "commander": "^12.1.0", + "fast-levenshtein": "^3.0.0" } } diff --git a/release.config.mjs b/release.config.mjs index 1632787..f902b49 100644 --- a/release.config.mjs +++ b/release.config.mjs @@ -2,5 +2,5 @@ * @type {import('semantic-release').GlobalConfig} */ export default { - branches: ['master', 'next'], + branches: ['master', 'next', { name: 'beta', prerelease: true }], }; diff --git a/src/commands/index.ts b/src/commands/index.ts index bbb078c..5ceec2d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,7 +6,12 @@ import removeCommand from './removeCommand'; import openConfig from './openConfig'; import uploadConfig from './uploadConfig'; import downloadConfig from './downloadConfig'; -import { getConfig, logList, runUserCommand } from '../lib/utils'; +import { + getClosestWord, + getConfig, + logList, + runUserCommand, +} from '../lib/utils'; import syncConfig from './syncConfig'; import renameCommand from './renameCommand'; @@ -51,7 +56,20 @@ const program = new Command() const command = configCommands[commandName]; if (!command) { - console.error(`command "${commandName}" not found`); + const closestWord = getClosestWord( + commandName, + Object.keys(configCommands) + ); + + let suggestion: string; + + if (isFinite(closestWord.distance)) + suggestion = `Did you mean: "${closestWord.word}"?`; + + console.error(`command "${commandName}" not found.`); + + if (suggestion) console.log(suggestion); + process.exit(1); } diff --git a/src/commands/syncConfig.ts b/src/commands/syncConfig.ts index 1995463..944a618 100644 --- a/src/commands/syncConfig.ts +++ b/src/commands/syncConfig.ts @@ -1,14 +1,10 @@ import { Command } from 'commander'; import { uploadConfiguration, - login, updateCloudConfiguration, - hasGithubCli, - isGithubLogged, getConfiguration, basicGithubVerifications, } from '../lib/github'; -import { confirm } from '@inquirer/prompts'; import { changeGistUrl, createConfig, getConfig } from '../lib/utils'; const syncConfig = new Command('sync') diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 53f1674..2042707 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ import { confirm } from '@inquirer/prompts'; import { spawn, exec } from 'node:child_process'; +import levenshtein from 'fast-levenshtein'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -150,12 +151,6 @@ export function formatCommand( args = args.map((arg) => (arg === '_' ? undefined : arg)); - if (debug) { - console.log('Command:', command); - console.log('Command arguments:', commandArguments); - console.log('Received arguments:', args); - } - for (let index = 0; index < commandArguments.length; index++) { const arg = commandArguments[index]; let argValue = arg.alternativeValue @@ -167,6 +162,16 @@ export function formatCommand( splitCommand[index * 2 + 1] = argValue; } + if (debug) { + console.log('Command:', command); + console.log('Command arguments:', commandArguments); + console.log('Received arguments:', args); + console.log(`Formatted command: ${splitCommand.join('')}`); + console.log(); + console.log('Running command...'); + console.log(); + } + return splitCommand.join(''); } @@ -205,3 +210,19 @@ export function openDirectory(path: string) { exec(`${command} "${path}"`); } + +export function getClosestWord( + searchWord: string, + words: string[], + defaultWord?: '' +): { distance: number; word: string } { + return words.reduce( + (data, word) => { + const distance = levenshtein.get(searchWord, word); + + if (distance < data.distance) return { distance, word }; + return data; + }, + { distance: Infinity, word: defaultWord } + ); +}