Skip to content

Instantly share code, notes, and snippets.

@swyxio
Created May 3, 2019 08:04
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save swyxio/3af1e264b8460af8897768045b2c229f to your computer and use it in GitHub Desktop.
Save swyxio/3af1e264b8460af8897768045b2c229f to your computer and use it in GitHub Desktop.
an adaptive, intent based CLI "state machine"

an adaptive, intent based CLI "state machine"

one realization from working on Netlify's CLI is that the CLI framework we used, oclif, didn't provide a great user experience out of the box.

Emphasis on great: it does a lot of nice things, like offering flag and argument parsing, help documentation, and pluggability. That's good for the CLI developer. But what about the CLI user?

  • Idiomatic oclif code often checks for required preconditions, and if it doesn't exist, it prints a warning and then process.exit(1).
  • Decent code prints a helpful warning telling the user what they got wrong. It is informative.
  • Better code offers a prompt, creates a file, or something similar to solve the precondition before proceeding. (possibly recursively). It is intent-based.
  • Great code remembers past inputs to prompts and uses that to offer useful defaults. It is adaptive.
  • Fantastic code steps beginners through so they don't need to read docs, but also offers escape hatches and shortcuts for pro users to super effective. It embraces progressive disclosure.

It is possible to code all of these individually within each oclif command, but at some point the criss-crossing needs of various CLI commands start to cause a lot of overlap. This is where we need a "state machine" to coordinate and deduplicate precondition resolution.

Intent based

Bear with me here. What this term tries to encapsulate is the realization that most CLIs are just "procedure calls". You type a few letters in a console, it runs code. If every precondition is met, the code runs as intended. If preconditions are not met, errors occur and the command fails. If we are lucky, these errors are caught and a helpful message is displayed. Often we are not so lucky.

An "intent based" CLI doesn't put the onus on the user to get every precondition exactly right to proceed. Momentum does not die the moment something is underspecified. Intents have explicitly specified preconditions, and specified precondition resolutions. Commands don't directly execute code, they spawn intents. Intents can spawn nested intents (a weak point of oclif), possibly to resolve preconditions.

By decoupling commands from intents, we allow mistakes. We allow multiple paths to feed into the right code paths. We allow "did you mean...?" intent resolution. We allow non-CLI reuse of the code, e.g. with chatbots/voice UI's, anything with generative and iterative prompting and a need to carry the ball in a task from start to finish (this typically rules out graphical/web UI's).

Adaptive

CLIs can access a lot of context to gain power:

  • project config/filesystem
  • machine cache level
  • user account level
  • global telemetry-based level

The more context we can read, the better defaults we can offer, the less documentation the user needs, the faster the user can achieve their intent. My favorite CLI is rupa/z, and it has just one command that gets more useful the longer i use it, because it records my past actions and preferences and takes maximum advantage of that. We should all aspire to be as useful.

But the code to read and modify this context is laborious to write. You'd think we'd want to make this easy to write so that we can offer this power easily. It isn't.

"State Machine"

The reason I think this.. thing, whatever it turns out to be in the end, doesnt require a new framework to replace oclif, is because it is just an intelligent layer sitting behind oclif. It starts to look like a state machine with active data flow (instead of passive, like Redux or XState, where motive power lies outside the state machine, this is active, where it always wants to keep going, and only pauses to wait for user input).

This "state machine" combines the context and intents we specify above and describes the flow of intent through state, kicking off other intents if needed, modifying state after intents are executed, finally bringing the user to the successful conclusion of their specified intent (or the user can always cancel). This is a stretch, but it may also offer the ability to go back on some reversible state changes if the user changes their mind.

Conclusion

these are just three things i think are necessary to have to make it easier to build CLI with great UX. It should be easier than it is today.

@dfosco
Copy link

dfosco commented May 3, 2019

Great read! Do you know of any project that's pushing this direction to some extent? Would love to keep an eye on this space :)

@swyxio
Copy link
Author

swyxio commented May 3, 2019

@dfosco not right now. but we may do this for the Netlify CLI...

@swyxio
Copy link
Author

swyxio commented Aug 25, 2020

eventually gave this as a talk at Heroku's oclifconf https://www.youtube.com/watch?v=1_w1YWCHXFg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment