@zzznpm/mens
Version:
All about idea management.
433 lines (391 loc) • 10.1 kB
JavaScript
import meow from 'meow'
import Mens from './Mens.js'
import { createSpinner } from 'nanospinner'
import { confirm, editor, input, select } from '@inquirer/prompts'
import { green, red, yellow } from 'ansis'
import markdownit from 'markdown-it'
import terminal from 'markdown-it-terminal'
import honshinSelect from 'inquirer-honshin-select'
import Entity from './Entity.js'
import logger from './logger.js'
import { ensureFileExists as ensureConfigFileExists, getConfig, setConfig } from './configer.js'
import { createGist, sync } from './remote.js'
const spinner = createSpinner(' ')
let config,
mens
const md = markdownit()
md.use(terminal)
const listActions = [
{
value: 'open', name: 'Open', key: 'o',
},
{
value: 'edit', name: 'Edit', key: 'e',
},
{
value: 'delete', name: 'Delete', key: 'd',
},
{
value: 'quit', name: 'Quit', key: 'q',
},
]
/**
* Opens an entity and logs its details to the console.
* @param {Object} entity - The entity to open.
*/
const doOpen = (entity)=> {
console.log(Entity.toRaw(entity))
}
const cmdAdd = async()=> {
let content
if (args.length < 1){
content = await editor({
message: 'Add a new entity with the editor:',
waitForUseInput: false,
})
}else{
content = args[0]
}
content = content.trim()
const addedEntity = await mens.add(content)
console.log(Entity.toRaw(addedEntity))
logger.info(`[main][cmdAdd] Added: ${Entity.toRaw(addedEntity)}`)
}
const cmdRemove = async()=> {
if (args.length < 1){
console.error(red('ID is required for removing an entity.'))
logger.error('[main][cmdRemove] ID is required for removing an entity.')
return null
}
const allEntities = await mens.getAllEntities()
const entity = allEntities.find(item=> item.id === args[0])
doDelete(entity)
}
/**
* Edits an entity's content. Opens an editor if new content is not provided.
* @param {string} id - The ID of the entity to edit.
* @param {Object} entity - The entity object to edit.
* @param {string} newContent - The new content for the entity.
*/
const doEdit = async(id, entity, newContent)=> {
if (!(entity instanceof Entity)){
entity = Entity.fromRaw(entity)
}
if(!id){
console.error(red('ID is required for modifying an entity.'))
logger.error('[main][doEdit] ID is required for modifying an entity.')
return null
}
const originalContent = entity.content
if(!newContent){
newContent = await editor({
message: 'Modify the entity with the editor:',
default: originalContent,
waitForUseInput: false,
})
}
newContent = newContent.trim()
if (newContent === originalContent){
console.log(yellow('No changes, modification aborted.'))
logger.info('[main][doEdit] No changes, modification aborted.')
return null
}
entity.content = newContent
const modifiedEntity = await mens.modify(entity)
console.log(Entity.toRaw(modifiedEntity))
}
const cmdModify = async()=> {
const id = args[0]
const entityDuplicated = await mens.getEntity(id)
doEdit(id, entityDuplicated, args[1])
}
const cmdGet = async()=> {
if (args.length < 1){
console.error(red('ID is required for getting an entity.'))
logger.error('[main][cmdGet] ID is required for getting an entity.')
return null
}
const entity = await mens.getEntity(args[0])
console.log(entity)
logger.info(`[main][cmdGet] Entity: ${entity}`)
}
const cmdList = async()=> {
const allEntities = await mens.getAllEntities()
showList(allEntities)
}
/**
* Displays a list of entities and allows the user to perform actions on them.
* @param {Array} entities - The list of entities to display.
*/
const showList = async(entities)=> {
if (!entities.length){
console.log(yellow('No entities found!'))
logger.warn('[main][showList] No entities found!')
return null
}
const contents = entities.map((entity)=> {
return {
name: md.render(entity.content).trim(),
value: entity.id,
}
})
const result = await honshinSelect({
message: 'Choose an entity to perform an action:',
actions: listActions,
choices: contents,
})
const selectedRaw = entities.find(item=> item.id === result.answer)
if(result.action === 'open'){
doOpen(selectedRaw)
}else if(result.action === 'edit'){
doEdit(selectedRaw.id, selectedRaw, undefined)
} else if(result.action === 'delete'){
doDelete(selectedRaw)
}else if (result.action === 'quit'){
process.exit()
}
}
/**
* Deletes an entity after confirming with the user.
* @param {Object} entity - The entity to delete.
*/
const doDelete = async(entity)=> {
const answer = await confirm({
message: `Delete entity '${entity.id}', continue?`,
default: false,
})
if(answer){
const removedIds = await mens.remove(entity.id)
console.log(removedIds)
logger.info(`[main][doDelete] Removed: ${removedIds}`)
}
}
/**
* Displays meta information about the entities and the application.
*/
const cmdInfo = async()=> {
const info = {
entity: {
total: (await mens.getAllEntities()).length,
},
meta: { ...mens.meta },
version: 3,
time: {
created: new Date(mens.local.meta.cTime).toString(),
modified: new Date(mens.local.meta.mTime).toString(),
},
}
console.log('Version:\t', green(info.version))
console.log('Total entities:\t', green(info.entity.total))
if(config.isTest){
console.warn(yellow('Test mode is enabled.'))
}
console.log(`Create time:\t${info.time.created}`)
console.log(`Last modified:\t${info.time.modified}`)
}
/**
* Searches for entities by a keyword and displays the results.
*/
const cmdSearch = async()=> {
if (args.length < 1){
logger.error('[main][cmdSearch] Keyword is required for searching entities!')
return null
}
const results = await mens.search(args[0])
if(!results.length){
console.log('No results found.')
logger.warn('[main][cmdSearch] No results found.')
return null
}
showList(results)
}
const todo = {
NOTHING: 0,
TOKEN: 1,
GISTID: 2,
}
const cmdConfig = async()=> {
if (args.length < 2){
console.error(red('Key and value are required for setting a configuration value.'))
logger.error('[main][cmdConfig] Key and value are required for setting a configuration value.')
return null
}
const keyString = args[0].trim()
let value = args[1]
if(cli.flags.smart){
if(value === 'true'){
value = true
}else if(value === 'false'){
value = false
}else if(!isNaN(value)){
value = Number(value)
}
}
setConfig(keyString, value)
}
const cmdSync = async()=> {
spinner.start('Syncing...')
const result = await sync(config, mens)
if(result === 0){
spinner.success('Synced successfully.')
}else{
spinner.error('Failed to sync.')
}
}
const cmdClear = async()=> {
const answer = await confirm({
message: 'Clear all entities? This action cannot be undone.',
default: false,
})
if(answer){
await mens.clear()
console.log('All entities have been cleared.')
logger.info('[main][cmdClear] All entities have been cleared.')
}
}
const defination = `
Usage
$ mens <command> [options]
Commands
add <content> Add a new entity with the given content
remove <id> Remove an entity by its ID
modify <id> <content> Modify an entity by its ID with new content
get <id> Get an entity by its ID
info Show meta info
search <keyword> Search entities by a keyword
list List all entities
config <key> <value> Set a configuration value
clear Clear all entities
sync Sync with remote
Options
--help -s Show help
--version v Show version
--smart, -s Parsing values as smart
`
const mconfig = {
importMeta: import.meta,
flags: {
help: {
type: 'boolean',
shortFlag: 'h',
},
version: {
type: 'boolean',
shortFlag: 'v',
},
smart: {
type: 'boolean',
shortFlag: 's',
description: 'Parsing values as smart',
default: false,
},
},
}
// shortcuts
const commandAliases = {
rm: 'remove',
r: 'remove',
d: 'remove',
delete: 'remove',
mod: 'modify',
m: 'modify',
a: 'add',
s: 'search',
l: 'list',
}
const cli = meow(defination, mconfig)
const [command, ...args] = cli.input
const cmd = commandAliases[command] || command
/**
* Parses and executes the command provided by the user.
*/
const parseCommand = async()=> {
switch (cmd){
case 'add':
cmdAdd()
break
case 'remove':
cmdRemove()
break
case 'modify':
cmdModify()
break
case 'get':
cmdGet()
break
case 'search':
cmdSearch()
break
case 'list':
cmdList()
break
case 'info':
cmdInfo()
break
case 'clear':
cmdClear()
break
case 'config':
cmdConfig()
break
case 'sync':
cmdSync()
break
default:
cli.showHelp()
break
}
}
const init = async()=> {
await ensureConfigFileExists()
config = await getConfig()
mens = await new Mens(config)
if(!config.token){
return todo.TOKEN
}
if(!config.gist?.id){
return todo.GISTID
}
return todo.NOTHING
}
const chore = await init()
if(chore === todo.TOKEN){
console.warn(yellow('Token has not been set!'))
logger.warn('[main] Set token first time.')
const answer = await input({ message: 'Enter your token' })
setConfig('token', answer)
}else if (chore === todo.GISTID){
console.warn(yellow('Gist ID has not been set!'))
logger.warn('[main] Set gist ID the first time.')
const choice = await select({
message: 'You can do the following:',
choices: [
{
name: 'Use an existing gist',
value: 'exsiting',
description: 'Input the Gits ID.',
},
{
name: 'Create a new gist',
value: 'creating',
description: 'Create a new gist for mens automatically.',
},
],
})
if(choice === 'creating'){
spinner.start('Creating a new Gist...')
const { id, node } = await createGist(config.token)
spinner.success('Created a new Gist.')
await setConfig('gist.id', id)
await setConfig('gist.node', node)
}else{
const answer = await input({ message: 'Enter your Gist ID' })
setConfig('gist.id', answer)
}
}else if(chore === todo.NOTHING){
parseCommand()
.catch((err)=> {
logger.error(`[main][parseCommand] Got error: ${err}`)
})
}