If you are using any of Aleph's local development frameworks, this package is for you. It
makes the nds
command available globally in your terminal. which then gives you access
to a multitude of shortcuts that you can run on your project to do lots of important
tasks without looking up long, laborious strings and flags.
Install globally with npm i -g @aleph/nought-dev-scripts
and away you'll go! Verify
it's working with a simple nds
in your terminal. This should give you a colorful overview
of the commands.
This tool is a shortcut that mainly leverages other tools. You will need to install those other tools for this to work. Some commands will work fine without all the tools, but you should strive to install all of these just to make sure:
pv
(pipe viewer) for progress bars in terminalThe command nds
is essentially a namespace that gates subcommands, kind of like a router. We use
the fun project Meow to parse this into commands (inputs) and flags,
so like if you go nds do something --fun
Meow runs a callback including input
and flags
like:
{
input: ['do', 'something'],
flags: {
fun: true
}
}
Yeah, it's JS! To make it easier for us to work on this, we're using JavaScript to do all of this stuff. isn't it grand?
Now, at this point we could just run the callback any way we want, but that's not that much fun for us. Instead we use the totally bonkers and awesome Ink to pass the arguments to React components which render in the terminal. This allows us to use all the familiar tools like hooks, forms and component states, and to practice our basic React skills while making it easier to work at Aleph. Bitchin!
Ink does a very specific thing and is obviously limited to rendering "components" using what's available in a terminal, so you're not gonna be building web UIs here. We can of course build little boxes and colored text and rainbows. Check out the docs.
The real fun part is the ability to build interactive CLI stuff that looks nice and is easy to use. The power
of this paradigm becomes readily apparent when we are using actual react form elements to do stuff, and hooks
like useEffect
and useState
to determine what to do next or what commands to run.
So specifically, here's how it all breaks down:
nds
command is first run through the bootstrap script at src/index.js
, where it is parsed by Meow.cmds
.input
and flags
from the command are passed through as props, where we can choose to do many,
many things. If you'd like to contribute by creating or modifying commands, that's awesome. You really should! Steps to get started:
nds
globally, remove it with npm uninstall -g @aleph/nought-dev-scripts
.yarn
and then npm link
This will alias npm
on your command line with whatever is in your local project directory. Note
that after making any changes, you need to run yarn compile
for those changes to appear in your
command line.
Let's build the sample nds hello
command, which has a few important aspects that will
help us learn a bit about this wacky setup.
First, decide what you'd like this command to do. In our case, we want the following:
nds hello
we'd like it to respond by asking us which famous dead
poet we'd like to say hi to.nds hello yeats
should skip the question, assuming saying hello to Yeats is supported.Our naming conventions here are to create a directory with the same name (all lowercase) as the
command within the src/cmds
directory with at least an index.js
inside it, so we start by creating
src/cmds/hello/index.js
with some important dependencies:
import React, { useState, useEffect } from 'react' // real hooks!
import { Box, Text, Newline } from 'ink' // components from Ink for formatting
import chalk from 'chalk' // for colors
import TextInput from 'ink-text-input' // real input form field
import Gradient from 'ink-gradient'
import SelectInput from 'ink-select-input' // a select box!
import poesyQuips from './poesyQuips' // we'll create this in a second
import ErrorMsg from '../error'
Next we want an info object (by convention called helloInfo
) that stores all the vital info
including help text about our command:
export const helloInfo = { // export it, we'll use it elsewhere
name: 'hello', // the actual command
desc: 'Maybe say hello to a dead poet, why not?', // a description for help purposes
help: chalk`
{cyan.dim Usage}
{bold nds hello <dead poet last name>}
{cyan.dim Flags}
{bold -y} || {bold --yes} Don't prompt for an answer}
{cyan.dim Supoorted Dead Poets}
{bold bishop} (Elizabeth)
{bold borges} (Jorge Luis)
{bold plath} (Sylvia)
{bold poe} (Edgar Allen)
{bold shakespeare} (Bill)
{bold yeats} (William Butler)
{bold wcw} (WCW)
` // the proceeding literal is formatted with Chalk, really looks nice.
}
We should follow some basic guidelines when formatting our strings. The excellent Chalk
project has good docs on how to use the string literals with brackets to do nice
things. Right now, we like our headings to be dim.cyan
and any actual command info
to be bold
, but do what you feel.'
Lets create an array of objects describing the values and labels for our select input:
const deadPoets = [
{
label: 'Elizabeth Bishop',
value: 'bishop'
},
{
label: 'Jorge Luis Borges',
value: 'borges'
},
{
label: 'Sylvia Plath',
value: 'plath'
},
{
label: 'Edgar Allen Poe',
value: 'poe'
},
{
label: 'Bill Shakespeare',
value: 'shakespeare'
},
{
label: 'W.C.W.',
value: 'wcw'
},
]
Next, you want to create the actual component that your terminal will render when the command is run. It looks like this:
const Hello = ({input, flags}) => { // as promised, input and flags as props!
const [sayHello, setSayHello] = useState(flags.yes ? 'y' : false) // a boolean mapping to our '-y' flag
const [deadPoet, setDeadPoet] = useState(input[1]) // for whom the bell hath tolled
const [deadPoetSay, setDeadPoetSay] = useState() // an empty vessel
const [errorMsg, setErrorMsg] = useState(false); // used to push into an error component
useEffect(() => { // only run this when specific things change, just like in real React
// All our commands will have conditionals, they should probably run here.
// Switches are nice for subcommands, but here we can check against an object.
if(sayHello === 'y' && deadPoet && poesyQuips.hasOwnProperty(deadPoet)){
setDeadPoetSay(poesyQuips[deadPoet])
}
// if the user somehow hits something other than y or n, taunt them with an error
if(sayHello && sayHello !== 'y' && sayHello !== 'n'){
setErrorMsg('All you had to do was hit "y" or "n". What have you done?!')
}
}, [deadPoet, sayHello]) // just like real React, only run when these two things change
return (
<>
{/* First, we ask which poet with a select input, if none has yet been chosen */}
{!deadPoet && <>
<Box paddingY={2}>
<Text>
Which dead poet would you like to say "hello" to?
</Text>
</Box>
<SelectInput items={deadPoets} onSelect={(poet) => setDeadPoet(poet.value)} />
</>}
{/* Assuming that is set, we can now confirm with a simple input that tracks our every keystroke */}
{deadPoet && !sayHello && <>
<Box paddingY={2}>
<Text>
Are you absolutely sure you'd like to say hello to a dead poet? Dead?! (y/n)
</Text>
</Box>
<TextInput value={''} onChange={setSayHello} />
</>}
{/* Made it! We can now show what the poet says from the grave. */}
{sayHello === 'y' && deadPoetSay && <Box borderStyle='classic' borderColor='magentaBright' justifyContent='center' marginY='1' padding='1' width='50%'>
<Gradient name='mind'>
{deadPoetSay}
</Gradient>
</Box>}
{errorMsg && <ErrorMsg msg={errorMsg} />}
</>
)
}
export default Hello
That's your whole component!
To allow your command to be called by nds
you'll need to add it to the src/index.js
file
as a property of the nds
object. Note that all commands take this approach, and the hello
command gets bootstrapped like so. First include both the info block and the component itself
as dependencies in index.js
:
import Hello, { helloInfo } from './cmds/hello'
Then, down where the other commands are, include hello
like so (alphabetical order is nice):
nds.hello = () => ({ // nds will pick this up as a subcommand
cli: meow(helloInfo.help, { // meow takes the help text as the first argument
// description is an option that we'll show when the help text is called with --help
description: chalk`{bold.cyan "${helloInfo.name}"} {cyan.dim ${helloInfo.desc}}`,
}),
// finally, the action property is a callback where we can pass input, flags, etc. We use it
// to render the component we created, courtesy of Ink!
action: ({input, flags, showHelp}) => render(<Hello input={input} flags={flags} showHelp={showHelp} />)
})
This command is an example so we don't want to include it in our global help text, but if
we wanted to, we'd simply add the helloInfo
object to the listedCmds
array.