Create Custom CLIs - Live Workshop 2019-11-18 Part 3

Shawn Wang
InstructorShawn Wang
Share this video with your friends

Social Share Links

Send Tweet
Published 5 years ago
Updated 3 years ago

Create Custom CLIs with Shawn Wang

Instructor: [00:00] OK. I don't seem to have gotten [00:03] any questions over the break. I [00:05] hope people are up to speed, and [00:12] maybe you played around a little [00:13] bit with scaffolding and at [00:17] least some of the other prompts [00:19] in Inquirer. Definitely, if [00:23] you're working on something, [00:27] throw it up. I'm happy to take a [00:29] look and offer suggestions. [00:31] This is a very safe space, as to [00:35] we're all learning here, and I'm [00:36] no stranger to showing my crappy [00:38] code to everyone else as well. [00:42] The better part of our workshop [00:44] is being able to share, and get [00:46] some live feedback, and [00:50] collaborate, because a lot of [00:52] times, we're alone on this. [00:54] In [00:55] fact, over the break, I actually [00:57] saw Ben Awad. Famous is his name. [01:01] He released his own CLI as well. [01:05] That's not him. '87? '97. Holy [01:11] shit, he's that young. He's so [01:14] young. I forgot about the lower. [01:19] Let's do that. He created [01:24] GraphQL API. It incorporates [01:26] TypeScript, Apollo, Type GraphQL, [01:28] and Type ORM. [01:33] The way I look [01:34] into this is I go to MPM first [01:37] because he didn't publish any [01:39] GitHub. There it is. He did [01:45] publish in GitHub. I'm sure the [01:48] documentation is minimal. Yep. [01:50] [laughs] Classic. [inaudible] [01:56] GraphQL API by running one [01:58] command. Looks like he's using a [02:00] monorepo setup. Yep. [02:05] Let's head [02:05] to packages, and creat GraphQL [02:07] API, package JSON. It's going to [02:11] have the bin folder that we [02:12] always know and love now. It's [02:14] going to source index.js and [02:16] uses chalk commander FS extra. [02:18] That's about it. [02:20] Let's look at [02:21] his CLI. It's got to be like 10 [02:24] lines. It's ridiculous. No, it's [02:27] 60 lines. Using commander, he's [02:31] creating a new command using [02:33] package json.name. Adding [02:36] versioning arguments product [02:38] directory, using Chalk -- we're [02:40] going to use Chalk in this part [02:42] of the workshop -- and then [02:46] doing some prompting if stuff is [02:47] not supplied, and he is copying [02:52] a template. [02:54] This is using fs. [02:56] copySync. There may be some [02:59] cross-platform issues, or [03:01] recursive directory issues. I [03:03] use copy-template-dir, because [03:05] if I ever need to swap out [03:07] anything, I can also swap out [03:10] the variable names. That's very [03:12] helpful as well. This is a very [03:15] common utility function as well, [03:18] like, "Should I use Yarn or npm?" [03:20] There are Yarn detection [03:21] libraries out there that you can [03:24] use as well. [03:28] SQLite? Why is he [03:30] using SQLite? He's git ignoring [03:33] it, and then he's doing npm [03:40] install execSync. Very cool. [03:45] We're going to use some of this. [03:48] We have implemented quite a few [03:52] pieces of this. We have [03:57] understood how to parse flags [04:00] and arguments, and stuff like [04:01] that. [04:02] We've copied stuff from a [04:04] source template folder into a [04:07] destination folder. We need to [04:10] execute installation. That's the [04:14] missing step, that's something [04:16] that I need to do. Let's get [04:18] right into it. This is [04:20] definitely a nice little...it's [04:21] one of those CLI projects that... [04:23] It's just for him. He does a lot [04:24] of projects, and it's super [04:26] helpful. [04:27] It didn't take that [04:27] long, like we're not taking that [04:31] long. I'm taking my sweet time [04:32] explaining everything, because I [04:33] have the luxury of time. You can [04:36] tell that, once you're used to [04:37] this, that you copy on this [04:39] template folder. Ben Awad, he [04:42] does really good work with [04:44] TypeScript and GraphQL. Follow [04:46] him. [04:49] We're on to executing and [04:51] piping child processes with [04:53] Execa, and then we're going to [04:57] go onto the Polish and react-ink [04:59] section. These two will be much [05:02] faster, because we're not even [05:04] going to mess around with code [05:05] in some of these. I wanted to [05:06] tell you that they exist and [05:07] they're very easy. [laughs] [05:12] Let's go to the guide, and go to [05:16] child process. What we're doing [05:19] here...What is a child process? [05:21] A lot of times, your CLI wants [05:23] to call other CLIs or execute [05:24] code in parallel. Your [05:27] JavaScript is single-threaded, [05:29] but not really because you can [05:30] do threading or,run multiple [05:35] concurrent child processes. [05:41] The [05:41] communication between the back [05:43] and forth is sufficiently [05:44] advanced that I've seen some [05:46] really cool stuff done in [05:48] parallel and taking advantage of [05:51] the multicores of your machine. [05:53] This is really awesome. [05:54] The [05:55] node itself actually has a [05:57] simple child process API, [05:59] actually is very usable. You [06:01] just require execSync like you [06:03] saw Ben Awad did, or you can use [06:05] spawn center, like the stream [06:09] version of this. You spawn and [06:12] then you call some base command [06:14] and then you pass in some flags [06:16] similar to what you already saw. [06:18] Then LS represents three [06:21] different streams. As some of [06:23] you may or may not know, a lot [06:24] of CLIs depend on piping in and [06:28] out three different streams. [06:29] There's one standard in, you [06:31] represent a user input or the [06:33] previous CLI's input, standard [06:36] in coming in, it says CLI. [06:37] Coming out is two streams, [06:38] standard out and standard error. [06:40] Standard out is for successful [06:42] logs. Standard error is for [06:44] error logs. It's merely a [06:49] setting that you, the developer, [06:50] set like as to like, "Hey, this [06:52] is an error. Pipe it to standard [06:53] error. Hey, this is a log. pipe [06:55] it to standard up." [06:57] You can [06:58] accept data piped in from other [06:59] CLIs. That's why you have a pipe [07:01] command. One of the ones I use a [07:02] lot is lsof. I'm taking lsof, [07:10] and then I'm piping it into grip, [07:12] and then I'm piping into grip [07:13] again. That's a very common type [07:16] of unique C approach that you [07:19] frankly don't really use a lot [07:20] in node CLIs for most of your [07:22] use cases. [07:24] If that's a design [07:25] goal of yours, absolutely take [07:26] advantage of that. Most of the [07:29] time, you actually care about [07:30] piping out so that people can [07:32] grip your input. Something like [07:37] myCLI generate or stream, and [07:43] then and then you grip, like [07:45] grip errors only, something like [07:50] that, whatever. [07:53] You care about [07:54] the exhaust more than the [07:55] ingestion, because you're [07:56] typically the primary source of [07:59] CLI operation. There's some [08:05] explanation about oclif and why [08:06] we use this.log instead of [08:08] console.log. Not super important. [08:13] We are going to deal with child [08:16] processes. The main reason we [08:18] don't use the raw node child [08:21] process is because of cross [08:22] platform nuances, which nobody [08:24] has time for. It's documented in [08:27] the execa docs. [08:29] We're just [08:29] going to use execa. I's from [08:30] Sindre Sorhus who is the most [08:33] prolific node author on the [08:35] planet. We could do a lot worse [08:37] than relying on his celebrities [08:39] because everyone else does. [08:42] We're going to incorporate... [08:45] Let's just stick it in here. [08:50] Just CD out of this. zcli- [08:54] workshop. I'm going to use Z [08:56] again. I think I'm back in [09:01] business. I have my dev command. [09:08] I could use as my generate [09:09] command. I can generate. I can [09:13] name my folder. I need a cheap [09:14] command. I'm Just going to [09:15] create a new test command test. [09:18] ts, check it in there. [09:22] I don't [09:22] want this to copy out [09:23] directories. I just want it to [09:24] run stuff so I can show you [09:28] things. Let's dump out inquirer. [09:37] I'm going to install execa. [09:39] Execa is a nice crossplatform. [09:46] They are over the default node [09:50] process. [09:52] Literally, you're just [09:54] running commands like you would [09:58] in a terminal except you break [09:59] it up into the base command and [10:01] then an array featuring all your [10:03] arguments and flags. Obviously, [10:05] there's no native conception of [10:07] the difference between arguments [10:08] and flags, so they're all [10:09] arguments. Bear that in mind. [10:20] Let's see what this says, when [10:23] we pipe it out. Here, we're [10:25] piping out the stdout of the sub- [10:27] process. We're taking stdout of [10:30] this sub-process and then we're [10:31] piping it to the parent process [10:34] stdout. It's going to echo [10:36] whatever we have over there. cli- [10:40] workshop-six generate test. [10:48] Again, the reason it takes so [10:49] long to compile, A is, because [10:50] I'm streaming, B is, because [10:53] it's compiling TypeScript every [10:55] time. "Cannot find the name [10:56] execa." Of course, I didn't [10:58] install it. Is it a top-level [11:04] import? Yeah. I hope that works. [11:07] It should work. Sometimes it's [11:09] usually very fastidious about [11:10] the default export. [11:24] [pause] Instructor: [11:24] I need to add the [11:24] esModuleInterop flag. Did I not [11:26] already add it? I added it. Nope. [11:31] This is a flag that lets you [11:33] interrupt with ES modules, like [11:35] it says inside of TypeScript. [11:38] It's a good idea to always have [11:39] it. It's too bad that oclif [11:44] doesn't scaffold it for you. If [11:47] you are familiar enough with [11:48] TypeScript, you should be able [11:49] to run that. [11:53] It does stdout [11:54] pipe, and then echoes, and then [11:59] it crushes it. If I wanted to [12:02] run foo but then not say [12:06] anything, I could say, for [12:09] example, standard...I could [12:12] silence this and not pipe [12:13] anything, and it would just say [12:16] foo, but then it won't pipe out [12:20] anything. [12:21] As a parent, the [12:23] child is saying foo, but no [12:24] one's listening, so I don't have [12:29] any place to print it. We can [12:31] obviously pipe it through the [12:33] file. Here we can create a write [12:34] stream. This is one of my [12:38] favorite ways to log out stuff [12:40] inside of a CLI, especially if [12:41] it's a super long thing and you [12:43] just want a sessions log, you [12:46] can definitely pipe it into a [12:48] stdout. I need to import fs. [13:07] [sings] [13:07] Here's the stdout, [13:08] saying foo. That's something [13:09] that we piped out over time. [13:13] That's great. Let's make it do [13:18] something more interesting. [13:21] Let's switch back to our [13:23] generate function. Over here, [13:28] what we're going to do is, we're [13:31] copying over the directory. [13:38] Remember, there's no Node [13:39] modules here, so you want to run [13:42] npm install, or Yarn add, or [13:44] Yarn install for these users. [13:50] The task would be to run npm [13:54] install. This Zoom bar is [14:06] disturbing me, even though you [14:08] can't see it. [sings] [14:17] I'm [14:17] cheating here and assuming Yarn. [14:20] Obviously, you should run some [14:21] Yarn test. For example, the one [14:25] that we saw with Yarn. You can [14:35] also use Yarn or npm, that's a [14:37] npm package, and then do the [14:39] appropriate npm or Yarn [14:41] installing. Not super [14:43] interesting, but whatever. Yarn [14:49] install...Yeah, that's it. [14:59] You [15:00] have to be inside of a directory. [15:08] This is something I'm personally [15:10] not super familiar with. Let's [15:18] look at Execa, and see how we [15:20] can set a directory inside of [15:22] Execa. I forgot how to do this. [15:27] Give me one second. [sings] [15:59] Local dir? OK. I want local dir, [16:17] I'm not 100 percent sure. I [16:20] thought I had this. Clearly, I [16:24] forgot already. Let's look at [16:28] options, local dir, and then [16:31] destination dir. There's a high [16:38] chance this is wrong, but we'll [16:40] try to figure it out and hope [16:47] for the best. [16:52] Just like before, [16:53] we're going to step out of the [16:56] workshop repo for this one and [16:57] head to a new project. Now we're [17:02] going to say, cli-workshop [17:05] generate myApp, get us name [17:09] myApp. Did I use a flag? I'm [17:14] going to prompt for it anyway. [17:17] Generally, you want to tie your [17:18] flag to your prompt. There's a [17:21] better developer experience [17:23] there to use. I still haven't [17:24] [laughs] resolved this one. I [17:25] forgot to dive into that. I'm [17:27] sure it's a very small issue. [17:31] MyApp, I'm going to call that [17:33] myApp. It's supposed to run Yarn [17:35] install for me. I doubt that it [17:37] did, because it executed so [17:38] quickly. Let's look at my new [17:43] project...No. Where's myApp? [17:56] It's supposed to run Yarn [17:57] install for me and inside of [18:00] that new folder, which it did [18:03] not. [18:09] I'm not really sure where [18:11] to configure this. I'm happy to [18:14] take suggestions right now [18:15] because I'm a little bit...I [18:17] can't believe I forgot. Let me [18:22] see the chat if there is a chat. [18:24] Await execa. Sorry. All right. [18:29] John Clark says, "Await execa." [18:31] This is not the one I'm looking [18:32] for. [18:39] It's sync, right? Our [18:42] default is async, you're right. [18:49] It should be synced. It really [18:52] doesn't matter in this scenario, [18:54] but you may want to make it [18:56] async for your purposes. Let's [19:01] delete my app again. [19:06] I'm not [19:07] confident that I'm running this [19:08] local der right. This was [19:12] bothering me that I don't have a [19:13] way to debug this. We may have [19:22] to punt on this because this is [19:26] just something I assume that I [19:27] knew, so I didn't practice this, [19:30] but we'll see where this takes [19:33] us. [19:34] I actually happen to work [19:35] with the [inaudible] of execa so [19:38] I can just ask him, but where's [19:41] the fun in that? This is not [19:46] running. We need to run Yarn [19:52] install and I need to make this [19:54] local dir. Maybe it is that [19:57] other der. Should I say prefer [20:06] local true? Is that it? [sings] [20:26] No. [20:33] I'm not sure what I was...I [20:37] forgot. Jesus. Let's wire one [20:45] more thing. Let's try this Yarn [20:50] or npm library, because I've [20:51] used this one before. It has an [20:56] embedded spawning mechanism, [20:59] which is nice. It's helpful. [21:05] The problem is, so I'm trying to [21:06] run the local dir...The key goal [21:16] I'm trying to solve is, I'm [21:17] trying to run the Yarn inside of [21:18] that foreign directory. Let's, [21:21] in fact, see how Ben Awad did it, [21:24] because I'm completely blanking [21:26] right now. This is so bad. [21:32] The [21:32] nice to have is that, this code [21:34] is all over the place. You need [21:35] to go perve at other people's [21:37] code for a little bit, and then [21:38] you're like, "Ah!" I've never [21:46] had to think about it. [laughs] [21:51] This is installing it to this [21:55] place. Does that mean I should [22:01] cd? Yeah. I can do that. [22:14] [pause] Instructor: [22:14] Node share dir. That's awkward. [22:26] That means I need this one line, [22:29] and all of my problems go away. [22:38] Let's delete this myApp again. [22:42] Remember, the goal is to [22:44] replicate create-react-app's [22:45] functionality. I type in myApp, [22:47] and it's going to scaffold over. [23:05] [pause] Instructor: [23:05] I need to wait for the copying [23:07] to be done, because it creates [23:08] the folder. I called the change [23:12] dir on a folder that doesn't [23:13] exist. One more time. We're [23:17] recreating the create-react-app [23:18] functionality, where we're [23:20] scaffolding that thing and then [23:21] we're running npm install on it, [23:23] for all of the dependencies. [23:28] It's supposed to run npm install, [23:30] but we don't see any of [laughs] [23:33] the install messages, because we [23:37] haven't piped anything. Remember, [23:38] this is the whole thing that we [23:39] started out with, that we needed [23:40] to pipe stuff out to stdout. [23:44] The good thing is that now this [23:45] project works, because the Node [23:47] modules are installed properly. [23:49] I can run Yarn start. Fantastic. [23:53] Now let's do the final step of [23:54] piping the input and output. [23:57] There are a number of different [23:59] ways that we already explored, [24:00] how to do this. I will go for [24:04] the standard piping out to [24:06] stdout. We should be able to see [24:08] everything on there as well. [24:13] This is a general discipline of [24:16] your CLI might rep other CLIs, [24:18] and you may need to run a few of [24:20] them in parallel. This is how to [24:22] control them. What the hell is [24:25] this? Probably pipe does not [24:26] exist in type string. [24:35] Execa. [24:40] [sings] Execa.sync. Here we go. [24:52] Returns or throws a child [24:54] process result. What the hell is [24:55] a child process result? It's an [24:57] object with a standard out. Yeah. [25:08] Probably pipe does not exist in [25:09] type string. Execa.sync. Yeah, [25:17] executed this thing [25:19] synchronously. Returns or throws [25:21] a child process result. [25:33] No. [25:34] We're piping it to the parent [25:34] process, so it's the process. I [25:38] might have to remove .sync. That [25:41] might be it. Don't look so smart. [25:49] Although it might prematurely [25:50] execute. I don't know. Hey, look [25:54] at that. It's yawn installing. [25:57] Whoa. I think we created create [26:05] react app. Creation. [laughs] [26:12] Cool. Now we have some feedback. [26:14] We can add some nice messages [26:17] saying we've completed and [26:18] [inaudible] to do, but now we [26:22] have a nice little scaffolding [26:24] CLI. This is a standalone folder. [26:28] We can use this project like a [26:33] create react app, might be. [26:35] That's fantastic. That's [26:37] wonderful. A bit gnarly. I [26:40] apologize for that. I am rusty [26:42] on it, to be honest. [26:44] Let's keep [26:45] going because we have a lot to [26:47] do. You may want to process [26:49] skill or handle other signals of [26:55] closing. I'm the parent, I want [26:56] to close you. Because your CLI [26:59] may be run by other CLIs. If you [27:01] want to gracefully handle that, [27:03] the default is to just exit [27:05] anyway. [27:06] If you want to do any [27:07] clean-up code, that's your [27:08] opportunity. You can do [27:09] suppresses on SIGINT. All the [27:14] links are there for you to [27:17] figure it out, as and when you [27:18] need it. Most of the time, [27:19] you're not going to need it. [27:21] Quick one on update-notifier. [27:23] This is a nice one for telling [27:28] your users that your CLI might [27:30] be updated. Literally it looks [27:34] like this. I think if I show you [27:36] the [laughs] docs, you'll see [27:39] what I mean. You've definitely [27:42] seen this in some of your CLIs. [27:46] This one, "Update available, [27:47] blah-blah-blah. Run npm ig to [27:50] update." That's the one. It [27:53] literally looks like that. [27:55] You [27:55] can run that anywhere inside of [27:58] here. Source, dev...I'm going to [28:04] chuck it somewhere. Honestly, [28:05] chuck it in here, at the top [28:07] level. Doesn't matter. It's [28:09] supposed to run it first anyway. [28:13] Make sure that the paths are [28:14] correctly run. It's so simple [28:17] I'm not even going to show you, [28:18] because it's so dumb. [28:21] One thing [28:21] that is helpful, I mentioned [28:23] this before, is sharing the code [28:26] with the base oclif commands. [28:28] This is something that is [28:29] documented by oclif. If you head [28:31] over here...The core idea is, [28:35] let's say I have some common [28:38] code that I use in three [28:39] different commands. Maybe I [28:41] should extract that somewhere, [28:42] into one shared directory. [28:45] That's accounted for with the [28:48] share commands. [28:49] You can go over [28:50] here. It's tucked way down there, [28:52] but it's super useful. This is [28:54] class inheritance. [laughs] It [28:56] is really dumb once you figure [28:58] it out. Basically, you put it in [29:00] a base command, and then you [29:02] import the base command, instead [29:03] of importing the direct command [29:06] from oclif. [29:07] You have an [29:07] intermediate inheritance. It [29:09] sets some fields there, and then [29:10] you pass it on for the final [29:13] implementation. How that looks [29:16] for us is, we have a base.ts [29:19] class, for example. I'll write a [29:25] base.ts class. You don't have to [29:27] follow along on this one, this [29:28] one is really simple. [29:31] I'll [29:31] import the commands here, and [29:32] then it exports another command. [29:34] This is an abstract class, [29:36] meaning, it's not meant to be [29:37] used directly. I can do some [29:40] initializations. For example, [29:42] one good candidate for the share [29:44] command is update-notifier. [29:47] Where is my update-notifier? I [29:51] forgot it. These guys. [29:55] I can [29:56] chuck it in here, and then I'll [29:59] run it on initialization of [30:01] every command. For this command, [30:07] instead of importing directly [30:10] from oclif/command, I can import [30:12] command from ../base. [30:20] This will [30:21] extend anything base that I do. [30:24] This command can do it, the [30:26] generate command can do it, the [30:27] test command can do it, every [30:28] other command can do it. This is [30:30] a way to, "Let's put everything [30:31] boilerplate-y on here, even [30:35] instances." We have static [30:37] netlify api. [30:42] Here is a object [30:44] with all the methods that we [30:46] might possibly use. Over here, [30:49] we do this.netlify.getUser, and [30:55] it's a simple command over there. [30:57] That's a very nice way of [31:00] separating out the concerns from... [31:02] This is hooking stuff up in the [31:04] base, and then this is [31:06] implementing the implementation [31:08] details of your commands. [31:11] A lot [31:11] of ways to do that. Telling your [31:16] user that there is an update [31:18] makes sense. You give them a [31:19] sense of the SemVer change. I'll [31:23] delete all of this, because I [31:24] don't need this. Give them a [31:26] sense of SemVer change. Say, "Oh, [31:28] I've gone from version 092 to 1. [31:35] 2. That's a major version change. [31:37] That might be worth updating. [31:38] Let me go read about that." [31:43] CLI, [31:43] especially if it's globally [31:44] installed, you probably want [31:45] something like this, to nag [31:46] people, especially for security [31:48] issues. That's about it. This is [31:53] pretty simple. You can do this [31:54] on your own time. I'm going to [31:56] speed ahead, because we don't [31:58] have that much time. [32:01] Now we're [32:01] in the Polish section...That was [32:05] the Polish section. We were [32:06] Polishing the update experience. [32:07] Now we're going to explain how [32:11] to store state. Most CLI [32:14] experiences can be improved by [32:16] storing state. [32:17] For example, [32:19] let's say I go over to my dev [32:20] command. Remember, dev command [32:22] is the one with the autocomplete. [32:25] I say dev, and it's showing me [32:29] this long list. It's got [32:31] autocomplete, it's really nice. [32:48] [sings] [32:48] I'm going to cheat and [32:53] comment this out, because it's [32:54] not relevant to the stuff I'm [32:56] talking about right now. That [33:05] must have been it, that was the [33:06] source of the error just now. [33:09] I [33:09] have this long list of [33:11] autocomplete, and it's really [33:12] nice. It's the same list every [33:15] time. Let's say I pick pineapple [33:17] a lot, and then I pick pineapple [33:19] again, and I pick pineapple [33:20] again, and I pick pineapple [33:21] again. Wouldn't it be nice to [33:25] have pineapple be the first [33:31] response, because that's [33:32] something that...It's learned me. [33:34] That's what I've previously [33:38] implemented in the past. [33:41] In [33:41] order to do that, we need to [33:42] have this concept of memory. A [33:44] lot of CLIs today are [33:46] implemented as stateless scripts. [33:49] In fact, we're in the file [33:50] system, we can have as much [33:52] state as we want. It's free real [33:56] estate. Let's go for it. [33:59] Things [34:00] you can store, authentication [34:01] tokens. You log people in one [34:03] time, and then you have a token [34:06] that you can use to do [34:08] authenticative stuff with. [34:09] That's a very useful case of [34:11] CLIs. Preferences, like do I [34:14] want extra logging, do I want [34:16] dark mode, do I want to be [34:21] called swyx or Shaun or whatever. [34:24] Prior selection, so this is [34:25] relevant to our use case for [34:27] autocomplete. Performance caches, [34:29] like, "I calculated this once, [34:31] let me pull from cache instead [34:33] of recalculating it again," if [34:35] it's expensive. Offline sync, [34:37] for stuff that happened offline [34:38] that you might want to sync [34:39] online later. Frecency storage, [34:41] we'll talk about that next, and [34:42] whatever you want. [34:45] Where do you [34:46] store state? This is where it [34:48] starts to get tricky. CLIs want [34:52] to store state globally. You may [34:53] want to store some state locally [34:55] in your projects, but when you [34:56] move onto the next project, you [34:58] lose that. It's most helpful if [35:00] you can remember between [35:02] projects, that's the whole point. [35:04] You have to figure out where to [35:06] store state. There is a spec for [35:08] that. It's not the most friendly [35:12] spec, [laughs] but it is a spec. [35:14] To the furthest extent of your [35:15] abilities, if you follow those [35:17] specs, then the whole ecosystem [35:19] will help you implement things [35:21] freely. [35:23] I like following [35:24] community conventions, unless I [35:26] have a very strong reason not to. [35:28] This is when I don't. I've [35:30] definitely seen people get very [35:32] finicky about where you randomly [35:33] store state on disk. If you [35:35] [laughs] randomly show up on [35:37] your desktop and like, "Hey, I'm [35:39] just gonna dump my stuff here," [35:40] that's not great. [35:42] There is the [35:43] XDG spec, which lets users [35:45] specify, "OK, here is my config [35:48] cache. If you're gonna store [35:49] anything for caching or [35:51] configuration or data, store in [35:52] these three places, and we'll be [35:55] copacetic." Those are the main [35:58] three ones. There's an [36:00] additional one, which I never [36:01] use. [36:03] There's the question of, [36:04] you could write file, read file [36:06] all the time, or you could use a [36:08] library. Again, this is tour of [36:11] all the libraries. We're going [36:12] to use Sindre Sorhus's conf [36:14] library, short for config. [36:18] [laughs] It is the most simplest, [36:20] dumbest config library I've ever [36:21] seen. We're going to try and [36:23] implement it for our selection, [36:26] for some of these APIs. [36:32] First [36:33] of all, we have to cd back, CLI [36:34] workshop six. Again, I'm using [36:36] that z command that's so helpful. [36:39] I'm going to add Yarn add conf. [36:41] I'm going to basically copy from [36:45] the docs. There's no reason for [36:47] me to be original here. [36:51] Config. [36:52] set('unicorn'). He likes his [36:53] unicorns, that Sindre, that's [36:57] because he is one. Every time we [37:03] have a name selection, we're [37:11] going to store this in [37:21] selections. We'll say, "This is [37:24] an array of names." In fact, [37:28] we'll have to get this array [37:30] first, and then assign it [37:37] somewhere else. [37:39] Where is the [37:39] get? There's no get. Now I'll [37:46] spread it, and I'll add the last [37:50] name here. That's the best way [37:52] to do it. I could also push, but [37:55] I don't care. That's that. Let's [38:01] also add a nice debug, saying, " [38:03] selections, selections, [38:08] selection saved." Does that work? [38:17] I may have to stringify it. [38:29] I [38:29] can run debug now. The debug key [38:32] is, cliworkshop dev. Let's try [38:38] this. debug = cliworkshop dev. [38:57] I'm going to pick blackberry... [38:59] or banana, I like banana. "Use [39:01] delete to clear values." What am [39:05] I doing? Where did I go wrong? [39:14] This mapping doesn't help me. [39:21] I [39:21] should get, goddammit. We do [39:28] need to solve that source [39:30] mapping thing. That's very bad, [39:32] that it's pointing me to the [39:33] wrong source map. "Names is not [39:35] iterable." Of course, you [39:39] default to the... [39:53] [pause] Instructor: [39:53] Good. There's the question of, [39:56] where did it save that [39:58] preferences? I ran it here. It's [40:03] not saved there. The best way to [40:05] figure out where it's saved, is [40:07] to check config.path. Let's [40:09] check config.path. We're going [40:15] to say, "Debug config is saved [40:24] in config.path." Sweet. [40:42] We've [40:42] uncovered an edge case over here, [40:44] that two bananas are selected, [40:46] so we should probably de-dupe, [40:49] whatever. Also, note that it's [40:53] stored in /Users/Swyx/Library/ [40:56] Preferences/CLIWorkshop/Swyx/ [40:58] NodeJS/config.json. [40:59] This is [40:59] something that is picked from a [41:02] combination of both package.json, [41:04] as well as the proprietary Node. [41:06] js label that's slapped on my [41:08] conf. You can configure that, [41:11] but it doesn't matter, because [41:15] as long as your package name is [41:17] unique, that's fine. It'll be [41:19] saved there for good. [41:23] Let's [41:24] pull it up. You can see that [41:25] it's a JSON file with an array, [41:28] which is nice. Now we can add [41:34] some kind of recent history for [41:37] autocomplete. We can store and [41:38] retrieve our last five [41:39] selections. I'll leave the [41:43] duplication to another day, [41:46] because that's just [41:48] implementation detail. [41:51] Let's [41:51] also have a nice little [41:52] separator value here. We don't [42:00] have the colors module, so I'm [42:03] going to use star, recent picks, [42:09] and then I'll do that. We can [42:13] spread the names, they're down [42:18] here. [42:31] [pause] Instructor: [42:31] Let's see where that gets us. Oh, [42:45] we didn't spread it. [laughs] [42:52] Comma. [43:03] [pause] Instructor: [43:03] We have some memory in here. If [43:05] pick cherry, and then I rerun [43:07] again. It's a very rudimentary [43:11] way of...I should probably [43:15] reverse this, as well, then I [43:17] can pick cherry again. It's a [43:21] very rudimentary way of [43:22] improving your implementation of [43:26] autocomplete. [43:27] Obviously, keep [43:28] this to five. Try to make them [43:29] unique, and then use the [43:32] autocomplete to your advantage. [43:35] Honestly, this is such an easy [43:36] win. It's such a joke that most [43:39] people don't do this. That's [43:42] because they're not thinking [43:44] about user experience in the [43:46] same way that we treat a [43:48] frontend. These are common sense [43:51] things. [43:53] The principles here I'm [43:54] promoting are, offer to persist [43:56] state when possible, so you can [43:58] create nice defaults. Literally, [44:00] the goal is, someone should [44:03] benefit from using your CLI over [44:05] time. The only way to do that is [44:09] to have some memory. What the [44:12] hell's wintergreen? [44:15] If you have [44:16] an input, try to have an [44:17] autocomplete with three to five [44:19] things, so your users can press [44:20] down and get the thing that they [44:23] want. That's the fastest way of [44:24] doing it. Obviously, I have some [44:28] [laughs] more work to do here, [44:29] in terms of user experience. For [44:30] example, you should reverse the [44:31] order so the most recent one is [44:32] first. [44:34] You should also [44:34] deduplicate anything that's in [44:36] there, and limit it to five, [44:38] because you don't want to have [44:39] an infinite history of stuff. [44:41] That's all easy, you can do that. [44:43] I trust that you can do that. I [44:44] wanted to introduce you to the [44:45] idea that it didn't take that [44:46] much, and we already have a much [44:49] more usable CLI than we did [44:51] before. [44:54] The next step in our [44:55] Polish story is frecency. That's [45:01] the second-last step. This one, [45:02] we won't even...This is too [45:05] advanced. This is the PhD-level [45:10] CLI user experience. If you want [45:13] to have that galaxy brain [45:14] experience, remember that whole [45:16] thing with the Rupa z CLI that [45:19] I've been using to jump around [45:20] different commands and app, [45:23] whatever this thing is? It [45:25] doesn't matter. It learns me. [45:28] This is an application of [45:35] history, like remembering things [45:38] that I've done in the past, and [45:39] then sorting and ranking by [45:41] recency. You can also see this [45:43] in Slack as well. Here's a blog [45:45] post from Slack. You can also [45:46] click through this to see. [45:48] Imagine you have a 100 percent [45:49] company. You have five different [45:50] Matts because you're very [45:51] diverse. You only speak to one [45:54] Matt, Matt Hogkins, because [45:56] that's the guy that you speak to. [45:58] Instead, your UI keeps insisting [46:00] on alphabetically sorting the [46:01] Matts. Bylander keeps coming up [46:03] top, but you never talk to that [46:05] guy, he's an asshole. [46:07] What [46:07] Slack should do for you is, your [46:10] buddy should be up top. Even if [46:13] you have autocomplete here, [46:15] that's not good enough. You also [46:16] want a recency sort. It's a [46:18] combination of frequency and [46:20] recency. If by raw frequency, [46:24] you speak to Matt Hogkins the [46:26] most, then you put them up top. [46:29] That makes a lot of sense. [46:31] For [46:31] recency, if a new Matt joins and [46:33] he's your new bud, and Matt [46:35] Hogkins is falling behind in the [46:39] race, then you start referring [46:40] to that new Matt. That's what's [46:44] underlying Slack, and that's why [46:45] it's so intuitive. [46:48] I love this [46:48] chart. They did some research [46:50] because they have the logs. For [46:52] them, when they switched to this [46:54] frecency chart, the amount of [46:56] time spent, it used to be 80 [46:58] milliseconds, that's fine, but [46:59] now it's dropped [laughs] down [47:02] so much, by 10x. I don't like [47:05] the word 10x faster when you're [47:06] doing something smaller, but [47:08] whatever. [47:10] It obviously is [47:11] faster because you get to the [47:13] person that you mean so much [47:14] quicker. That's CLI in a UI, but [47:18] you can have a CLI inside of CLI. [47:21] That is faster. There is a [47:23] project for this, called [47:25] frecency. It is made for the [47:28] browser, so you need to adapt it [47:31] to do local storage. [47:32] I was lazy, [47:33] so I showed you the no local [47:36] storage package, but you can [47:37] also use conf as well. You have [47:40] to implement the API interface [47:42] on your own. Perfectly fine. [47:44] You're storing it somewhere, and [47:46] then you're sorting your [47:48] frecency results. [47:51] Basically, [47:52] nobody does this. I didn't [47:56] manage to do it, because I had [47:58] other priorities to do. This is [48:00] super interesting. It's also [48:02] another easy win. If you thought [48:04] the conf was easy, then you have [48:06] this frecency stuff, and then [48:07] you talk it up. Sounds pretty [48:10] fancy, it's a nice win. [48:13] The [48:13] overall principle is, use [48:15] information about the user, your [48:17] very intimate and privileged [48:19] position to learn from the user, [48:21] reflect it back to them and [48:22] they'll reflect that [inaudible] [48:22] back to you, is a nice overall [48:25] principle. That's frecency. I [48:29] cheated by not talking about it, [48:30] not putting any code. [laughs] [48:33] We'll talk about the three other [48:36] libraries that I picked up. [48:37] There are a ton of libraries for [48:38] Polish. All we're doing is [48:39] Polish, Polish, Polish. [48:41] Oh, [48:42] Natalya. We have people leaving. [48:44] Thank you so much for joining in. [48:46] This will obviously be recorded. [48:49] I'm always appreciative of your [48:51] feedback and emails, and [48:54] whatever. Tell me what you think, [48:56] ask me your questions. I'm here [48:58] for you. [49:00] Let's get this going. [49:02] CLI, I'll put it with ora-cli. [49:05] These are ways of taming this [49:08] mess. What we've worked on is [49:14] nice and all, but it is a lot of [49:16] junk. The more complex stuff you [49:21] make, the more unintuitive it [49:26] can be. This is fine, because we [49:27] have colors, how do we put in [49:29] colors. We'll talk a little bit [49:32] about that. [49:33] I think I talked [49:33] about Chalk? No. Chalk is the de [49:36] facto library. There's Clare, [49:37] there's other libraries as well, [49:39] but this by far the default. [49:42] Stick with that and you'll be [49:44] well set. To show you what Chalk [49:47] does, I can Yarn add chalk, [49:55] const chalk = require('chalk'). [50:00] You can import, too, but [50:01] whatever. [50:02] The simple API is [50:03] chalk.color...you chain whatever, [50:08] and then you put in a string at [50:08] the end. Let's do this, node... [50:15] If you didn't know, you can run [50:16] a node repo down in here. You [50:17] can say const chalk = require(' [50:21] chalk'). Let's do a console. [50:25] chalk.blue. What? Why is this [50:34] undefined? [50:43] [pause] Instructor: [50:43] Do I have to do a default import? [50:49] Yay, chalk.blue, bold, and then [51:00] you can look at the API. Let's [51:01] do green. I have blue on purple, [51:05] it's hard to read. Maybe a more [51:07] accessible color might be green [51:11] or magenta. These are things you [51:15] might want to think about. [51:16] You [51:16] can also take other modifiers, [51:18] like strikethrough. This doesn't [51:22] show up on my screen. You can [51:24] also see italics. italic "Hello [51:29] World!" Italics are a really [51:32] nice effect. There's a lot of [51:36] styling that you can do in here. [51:39] I tend to do this on any string [51:40] that has user input. For example, [51:44] if I have user input over here, [51:46] then I can say, "Config is saved [51:49] in chalk.cyan.config.path," and [51:55] then selection save chalk.yellow. [51:59] JSON.stringify. I'll give you an [52:01] example of what the workflow is. [52:03] If you use tag template literals [52:08] as strings, this is a really [52:14] easy workflow, because you can [52:15] just us the technical rules. In [52:18] future, you can supply this via [52:19] the name flag. For example, here [52:21] I want to highlight the name [52:22] flag to draw some attention, tog [52:24] that yellow.bold. Oh, and then [52:33] this is not template literal, so [52:35] I've got to convert it. Hang on. [52:39] The zoom window's getting in the [52:40] way. [sings] . All right, so [52:46] let's see how this looks. All [52:53] right, let's get rid of the dev. [52:55] Let's see it. I expect to see a [53:00] little bit more color in the [53:03] stuff that I choose to emphasize. [53:06] I'm going to choose ermine, cyan, [53:10] undefined. [53:12] Oh, I have to fix my [53:14] default. Sorry, guys. [sings] [53:23] Let's try that again. [sings] [53:34] You see those colors coming in? [53:36] This is new. I made this cyan, [53:38] and I made this yellow. For this [53:42] normal log, I made this flag [53:44] stand out. It just marks the [53:46] user, like this is not normal [53:48] text from the rest of the CLI [53:51] output. [53:52] Your eye just [53:53] gravitates to that more. That's [53:55] just a really good use of color. [53:58] Obviously, there are color blind [53:59] people that won't be able to see [54:00] this. Just don't depend on just [54:03] color alone to do that. I've [54:05] done that before. I regreted it. [54:07] Here are some other tips as well. [54:09] Save red for errors and green [54:11] for success. Cyan is a good [54:13] informational color. Blah, blah, [54:15] blah. Too many colors is also [54:17] confusing. Don't go overboard [54:19] with that. You know all this. [54:22] Cli-ux, this is oclif's first- [54:25] party utilities library. Hang on. [54:28] Let me check the questions. OK, [54:29] there are no questions. Cli-ux [54:32] is oclif's utilities library. It [54:34] just has a grab bag of other [54:36] stuff that you might want. They [54:39] have a spinner that's involved. [54:41] They have a wait. They have some [54:43] iterm-specific stuff that you [54:47] may want to use. It's all in the [54:52] docs. [54:54] Probably the most [54:54] interesting one is the table [54:56] that they have. I won't even [55:00] install this. I'll just show it [55:01] to you. The principle for tables [55:07] is that presenting tables of [55:10] information in CLIs, is that you [55:12] want to be able to grep it. For [55:15] grepping, you want to make it [55:17] horizontal rather than...You [55:20] want to make your columns like [55:22] columns array. What the hell? [55:26] Cli-ux. Ah. Goddamn it. Cli-ux. [55:34] It's case-sensitive. That's [55:34] weird. Let me see the tables. [55:41] Tables are like this. This is [55:43] the way that they recommend [55:46] parsing tables, which means we [55:48] want every line to be filterable [55:50] so that if people grep it they [55:52] only get the lines that they [55:54] want to see. [55:57] The head of the [55:59] name of the table is here. [56:00] Basically, the table API, it [56:02] just aligns everything [56:04] vertically for you. If you need [56:06] to present information, this is [56:08] your best bet. Obviously, you [56:10] see this natively a lot. This is [56:14] something I use a lot. Grep TCP. [56:18] These tables that you can just [56:19] grep as well. Let's say I want [56:21] to grep only the 505 ports. [56:31] These are the five-something [56:33] ports, something like that. Very [56:37] simple, but something you should [56:41] keep aware of when designing CLI [56:43] tables. The last one is Ora. Ora [56:51] or is centuries CLI library, [56:53] again, very, very simple. You [56:56] can see all the spinners here [56:58] that you can do with. [57:00] Anytime [57:01] you're kicking something async, [57:02] you probably want a spinner. You [57:04] can have immediate response. The [57:06] principle is that in CLIs, [57:08] people expect something to [57:11] happen really quickly. If you [57:13] can give them a CLI, a visual [57:15] indication, then the perceived [57:20] performance will be much faster. [57:22] Cool. [laughs] I struggled with [57:25] that so much. Holy crap. [57:29] I like [57:30] this CLI spinners. Check this [57:31] out. CLI spinners has a whole [57:36] bunch of others. Ora just has [57:38] like some of these circle-y [57:40] things. You've definitely seen [57:42] this before in Yarn and some [57:44] other CLIs. [57:45] Some of these are [57:46] just nuts. Look at this Pong [57:47] implementation, or Runner. My [57:49] favorite is Moon or Earth. Look [57:51] at Earth. I really like this. [57:56] I'm going to try and implement [57:58] this and not explode. [58:03] The API [58:04] is super simple. It just looks [58:05] so professional, like you've [58:07] just spent that little bit of [58:09] thought, just a few minutes of [58:11] your time polishing it up. It [58:14] pays off so nicely in terms of [58:16] your look and feel. It's worth [58:20] exploring. You just need to know [58:22] where to go. That's why I'm here. [58:36] I actually don't know. [laughs] [58:39] I'm more familiar with the Ora [58:40] API. I hope this is the right [58:42] one. Const CLI spinners. What [58:50] I'm going to do is inside the [58:52] generate command that we have, [58:55] I'm going to stick it in there. [59:03] When we kick off the expensive [59:05] action, which is installing Yarn, [59:09] we just say CLI spinners dots. [59:15] How do I kill it? Why are you [59:22] giving me an SE cinema thing? [59:26] Yeah, I'm using it through the [59:26] Ora module. Forget this. [59:34] [inaudible] . I'm looking at the [59:39] Ora. I know there's some [59:41] integration which I am supposed [59:43] to do. Ora. There we go. Got it. [59:57] I have to implement Ora. That's [60:00] what I'm supposed to do. I'm [60:02] supposed to install Ora, and [60:03] then load up the spinners in one [60:07] of those ways. Forget those. [60:11] We're going to start spinner. [60:20] Installing React and other [60:23] packages. [60:27] Once we're done, we [60:32] have to refer to the subprocess [60:38] exit. I'm going to go refer to [60:41] my own notes here. When the [60:53] subprocess closes, then close [61:01] the spinner. The spinner is [61:04] spinner.stop, I think. Let's see. [61:15] This is an object. I have text [61:18] here. The type or the spinner, [61:25] I'll have moon. How do I stop? [61:32] There we go, stop. [61:35] This one [61:35] will clear the spinner. I can [61:40] type succeed, spinner.succeed, [61:48] and that should give me a nice [61:49] green checkmark. I don't need [61:56] that debug anymore. I'm using [62:01] the dev command. Let's switch [62:03] back to newProject. I'm using [62:14] the generate command, cli- [62:17] workshop-six generate. It's [62:22] going to ask me for a name. I'm [62:26] going to call it the very [62:27] creative name of myApp2. [62:30] You [62:30] look at that moon, look at that [62:31] moon! Look at that! What's [62:36] happening here is that, every [62:38] time some child process pops [62:40] something out, we're going to [62:43] freeze whatever is on screen at [62:45] the time. This is what you'll [62:48] see, this little information, [62:52] intermingling that. [62:53] This is not [62:53] very nice, so we should probably [62:55] want to...We can have a few ways, [62:59] we can accumulate all these [63:00] standard out piping ins, and [63:02] then we'll dump it at the start. [63:04] We could clear this every single [63:07] time. We could clear the spinner [63:10] every single time, and then [63:11] restart the spinner. That's a [63:13] very long, laborious process. [63:15] It's up to you. [63:16] For now, I'm [63:17] going to comment out the [63:19] standard out, and then show you [63:22] how it looks, because it is nice [63:29] to see something moving on the [63:31] screen when work is being done. [63:33] myApp3. [63:36] Something's moving on [63:36] the screen, something is doing [63:37] in the background, you're [63:38] swallowing all those messages. [63:39] It may be a lot of messages that [63:41] you may not want, but you know [63:43] work is being done while this is [63:45] going on. [63:46] There's obviously [63:47] other tricks you can do to stop [63:49] and clear and re-render. This is [63:52] how you install spinners, and [63:54] give some nice...where heavy [63:57] stuff is being done in the [63:58] background. I've overused those [64:01] terms, but hopefully you [64:02] understand that we are adding [64:05] Polish. It just takes a few [64:06] minutes to add some flair onto [64:11] the CLI. That's a Polish thing. [64:15] That's the end of our Polish [64:17] section. We are about to start [64:18] on to the React Ink section, [64:22] which is the final section, and [64:24] the newest and the most [64:25] experimental. I'm going to take [64:28] a pause here, and ask if anyone [64:30] has any questions. I know I've [64:33] gone through a lot. I hope it's [64:36] interesting to you, because it's [64:37] not that hard to make nice CLIs. [64:43] Questions, questions, questions. [64:49] I know I had a couple of people [64:50] leave because they had to get [64:51] back to work. Hopefully, whoever [64:54] is watching this, asynchronously [64:56] as well, feel free to ask me [64:58] questions. Head to this repo, [65:01] file an issue, I'll answer [65:03] whatever you've got. I've been [65:06] following this for a while now. [65:09] I might have something that [65:10] might save you some time. [65:13] Honestly, I feel if we ever had [65:17] industry intro to node JS, I [65:20] think this should be in there [65:21] because it's so fundamental to [65:23] everything that we do. [65:27] Final [65:27] section, React Ink. We're going [65:30] to use React in CLIS. It's [65:33] absurd, it's ridiculous, it's [65:37] totally experimental and [65:38] sometimes buggy but it's super [65:41] exciting because of the layout [65:43] that it achieves. What is React [65:45] Ink? React Ink is a React [65:47] renderer to the CLI. [65:49] It lets [65:50] you componentized parts of your [65:52] CLI as React components which is [65:56] the same reason you want to use [65:58] components when you're writing [66:00] web interfaces. You might want [66:01] to use that for your CLI [66:03] interfaces, especially talking [66:05] about the [inaudible] engine. [66:08] If you think centering stuff in [66:11] CSS is hard, try centering [66:13] things in the terminal. It's [66:16] near impossible. It's actually [66:18] not. You can measure the width [66:20] of the terminal or calculate [66:21] that half of it and then [66:23] calculate the offsets and so on [66:24] and so forth or you could use [66:27] layout engine and just use the [66:28] Flexbox model which you as web [66:30] developers probably already know. [66:33] That's the main attractiveness [66:37] of this. There is little bit of [66:39] fudging. React Ink wasn't meant [66:42] to be used with OCLIF and OCLIF [66:44] had no idea that people would be [66:47] using React for writing UCLIS. [66:50] There's a little bit fudging [66:51] right now to have to do this. [66:52] I [66:52] think that using the best [66:55] command line, command parsing [66:57] framework and then the best [66:58] front and rendering tool, you [67:00] can get the best of both worlds [67:02] in terms of the technology stack [67:04] that you used to write your CLIS. [67:07] How do we write React Ink [67:09] projects? We're going to jump [67:11] over to back to the workshop [67:17] repo. We're going to install [67:20] React and Ink. React is this box [67:23] center React project but instead [67:25] of ReactDOM, we're now just [67:26] installing Ink which is a very [67:29] cool three-letter npm package [67:32] name. I'm very jealous. [67:35] I have [67:35] a couple as well but this is [67:37] actually getting some use so [67:40] kudos to them. We're also going [67:42] to have to modify tsconfig.json [67:44] if you're following along a [67:45] Typescript to recognize JSX. [67:50] That's just the tsconfig issue. [67:54] OCLIF doesn't recognize TSX [67:56] files. This is the situation [67:59] where if I rename this to dev. [68:03] tsx, this is valid Typescript. [68:07] Typescript will parse this. [68:09] OCLIF will not. You can try this [68:13] by running CLI workshop dev and [68:17] will just tell you, "What is dev. [68:19] I have never seen dev before in [68:20] my life and dev not found." [68:26] What an insult to my [68:26] intelligence. Dev is right there. [68:29] I just renamed Dev to dev.tsx. [68:32] This actually turned me off for [68:34] a little bit. The reason is [68:36] because they the hard coded .ts [68:39] in there. This is where I teach [68:43] you how to monkey patch your [68:44] unknown modules. You'll just [68:49] going to have to do it. [68:50] This is [68:50] what part and parcel of using [68:52] tooling, finding holes and [68:54] you're like, "Maybe, I need to [68:56] PR this upstream." In the [68:57] meantime, you need to maintain a [68:59] fork to fit your use cases [69:01] because your use cases will [69:04] differ from other people's. This [69:06] is a good general skill in [69:08] NodeJS to learn. It's, [69:09] fortunately, also very easy. [69:11] We're going to go the offending [69:13] library. It's node_modules/@ [69:15] oclif/config/lib/plugin. You get [69:26] here by tracing the calls. It [69:28] took me about 15 minutes. It's [69:31] not that hard. We have to get [69:36] command IDs. Here's the patterns [69:40] that it uses to recognize the [69:42] files that are valid. [69:44] It [69:44] understands JS, it understands [69:45] TS, doesn't understand TSX. What [69:49] I'm going to do here is, I'm [69:50] going to add a new line and call [69:51] it jsx and tsx. That would do it. [69:57] Now when I rerun, it's going to [69:59] rerun this exact code. Now it's [70:01] going to magically find the dev [70:02] command, that it previously [70:05] insisted that wasn't there [70:07] before. That's fantastic. [70:10] Now [70:11] the question is, I have this [70:12] janky thing in...I messed inside [70:16] of my Node modules. When I clear [70:18] my Node modules and reinstall, [70:19] that's going to go away. How do [70:21] I reapply my changes on top of [70:22] it? That's where patch-package [70:24] comes in. Definitely read the [70:29] docs for it, but I'm going to [70:31] show you the mechanics of how it [70:33] works. We're going to have to [70:34] reapply this change again. [70:36] Yarn [70:36] add patch-package postinstall- [70:39] postinstall. This is a [70:40] postinstall hook for Yarn, to [70:43] run the patching code after [70:50] modules are installed. We [70:55] installed this, and now we have [70:58] to add a postinstall command to [71:01] the scripts, to our package.json. [71:10] Where's our scripts? Sweet. Now [71:16] we run Yarn oclif/config. Let me [71:20] reinstall the...Rerunning Yarn [71:25] checks the integrity of all the [71:26] Node modules, and then [71:27] reinstalls anything that has [71:28] been compromised, like we did. [71:31] Now I have to apply my fix again, [71:34] and then say, "OK, for this [71:37] module, oclif/config, I have a [71:40] thing which I patched in my [71:43] package. Let me persist that [71:44] package by running this small [71:46] little CLI." It's downloading [71:55] the real package in the [71:57] temporary folder, it's diffing [71:59] the files with the clean files, [72:00] and it's creating the patch. [72:02] Guess what? This is all a CLI. [72:04] This guy did it in his free time, [72:05] just to help himself. You can do [72:07] this too. We just went through [72:10] how to do this. It's really [72:14] super nice whenever you can [72:15] benefit and build your own [72:16] tooling to help you, and you can [72:19] understand your own tooling, as [72:20] well. Let me just show you the [72:23] patch that was generated. [72:25] If [72:25] you're familiar with git, this [72:27] is exactly the same thing. This [72:28] is just a git diff, like here's [72:31] what it was before. Here is what [72:33] it was after. Here's the single [72:35] line that we added. When anyone [72:38] else clones this repo and runs [72:39] npm.install, it's going to find [72:41] this post install thing and run [72:44] patch-package. [72:45] Patch-package [72:45] CLI is going to go and look for [72:48] this patches folder. Inside, [72:49] there's this patch here. It's [72:51] going to reapply this thing on [72:52] top of the raw published [72:57] downloaded npm package. [72:58] That's [72:59] it. It's pretty simple, but a [73:02] lot of people are skip-y when [73:06] they look at something like this. [73:08] Yeah, it is a little bit scary. [73:10] OK, so let's copy and paste a [73:12] nice little Inc rendering layer, [73:14] so we can get a sense of what an [73:16] Inc command looks like. [73:19] I'm [73:19] going to cheat a little bit and [73:21] I'm going to write Inc.TSX over [73:23] here. I'm going to just paste my [73:24] prepared code. You can see, this [73:27] is a React component, right? [73:31] It's using some proprietary [73:34] components like the boxed [73:36] component from React Inc. [73:38] That's just a replacement for [73:38] div, similar to the view in [73:42] React Native. I have some hooks [73:45] here and it's so on, and so [73:47] forth. Very common, bog standard [73:49] React stuff. If you're not too [73:50] familiar with React, don't worry [73:52] about it. But you should be able [73:55] to get a hang of it after some [73:56] looking at it. [73:58] The hook between [73:59] oclif and React Inc. is down in [74:02] here where we just call the [74:03] render function, as part of the [74:05] run. We will use oclif to do the [74:08] documentation, do the flags, do [74:10] the args passing. Then we'll [74:12] parcel this stuff, and we'll [74:13] pass it in to the component and [74:16] we'll just render it. [74:20] Once we [74:20] are rendering it, the command's [74:22] just going to keep running until [74:24] we call exit. The exit command [74:27] is something that we pull off of [74:30] Inc. That's something that we do. [74:34] I'm now going to run our first [74:37] React.Inc command, which is [74:39] called Inc. [74:47] Again, it's slow [74:48] because of typescript and all [74:49] that. You see it's updating that [74:52] counter from 0to 1, to 2, to 9, [74:54] to 10? That's this hook that's [74:59] use effective and it's set time [75:00] updating constantly. That's the [75:08] integration between oclif and [75:09] React. Now we have something [75:11] that we can use componentizing. [75:14] We can build a system of [75:15] components and re-use them all [75:16] over the place to build out a [75:18] full application inside of our [75:19] terminal, which is pretty crazy. [75:22] It's something that's never [75:24] happened in CLI development. [75:27] That's pretty exciting. [75:31] I only [75:31] got two more left and then we [75:34] can break for the workshop and [75:36] more questions. Let's talk a [75:38] little bit about layout. I [75:41] introduced the box component a [75:42] little bit. I just said it's [75:44] like the dev. It basically has [75:46] by default all the flexbox [75:48] properties that you might want [75:49] on it except that the units are [75:52] in terms of characters or lines. [75:55] Here you have the width or four [75:56] characters and it's just four [75:57] characters long and inside of it [76:00] is left aligned to...The text [76:02] content inside of is left [76:03] aligned. You can have a width of [76:06] 50 percent of a parent component [76:10] so it's that as well. You can [76:12] have flex-direction. You can [76:13] switch that to column versus row, [76:16] by default it's row. [76:18] You can [76:18] set text wrapping so by default [76:21] there are certain width length [76:22] and if it exceeds, you can set a [76:26] text wrap for it to wrap around. [76:28] You can set PAT'ing. I really [76:30] like this. This is the box [76:35] PAT'ing of six. Let's see how [76:38] that looks. By the way, one way [76:44] to get TS, Typescript to compile [76:47] much faster is use the [76:48] incremental tag that uses some [76:51] caching. [76:53] I should have done [76:53] that from the start. I'm sorry. [76:56] This is a PATing of six lines. [77:00] Nice big PATing for people to [77:03] gaze at your wisdom. There's [77:09] other inbuilt components as well [77:10] so I can also call it color [77:12] component. This is similar to [77:14] the Chalk, but native to the [77:18] React [inaudible] ecosystem. [77:24] Let's just take this color. [77:39] Let's have slightly less [77:41] obnoxious PATing of two. It's [77:46] just going to output a box with [77:49] the counter. "Cannot find the [77:55] name color." What do you mean? [77:57] Oh, called it the wrong name. [78:15] [sings] [78:15] You see that nice [78:15] little color icon. I did go for [78:18] a black and white. I'm sorry [78:19] that's my bad but I can just BG, [78:23] keyword magenta. Whatever. You [78:28] got my meaning. You can have [78:30] color, you can have text. [78:32] There's native components that [78:34] they export just for all of the [78:37] stuff. There's our pink color [78:41] background. [78:44] This is a component, [78:45] this is a box component. Inside [78:47] of it, we're writing JSX like we [78:49] do normally. There is a lot of [78:52] opportunity for composition of [78:55] components. That might be [78:57] arguably a better way, or the [78:58] future way, to write CLIs. It's [79:01] worth discussing as well. [79:03] I [79:03] wanted to end this chapter with [79:05] one big, long demo. Here's a [79:10] example of the Jest UI. I think [79:14] I introduced some bugs to this. [79:19] [laughs] Any issues with this, I [79:23] take responsibility for. This is [79:24] the idea of, what you can do [79:26] with React Ink, where you're [79:30] creating components, and then [79:31] composing them up individually, [79:33] and then you're writing tests [79:34] like this. [79:35] This is the Jest [79:38] interface, that you're running [79:41] all these tests, and you're [79:42] importing them onto the screen, [79:43] dynamically updating different [79:45] sections of the screen. This is [79:47] a super polished UI. You're [79:50] writing this in these lines of [79:52] code. [79:53] Let me break it down for [79:53] you. There is a git breakdown [79:57] for status. There is a test [80:02] component. It defines the start, [80:06] middle, and end of these tests. [80:09] There's the summary section, [80:11] which gives you the summary of [80:13] all the tests that pass or fail. [80:16] Inside of the main command, the [80:18] jest command, you can declare [80:21] them, in terms of box, and then [80:23] complete a test up top, running [80:24] tests on the bottom. As they [80:25] complete, they would go smaller, [80:28] and then complete a test as [80:29] before. The summary will always [80:30] be at the bottom. [80:32] That layout [80:33] thing, it's become declarative. [80:35] You don't even think about [80:36] anything else. React takes care [80:38] of the re-render for you, which [80:40] is super nice. It's super nice. [80:44] This is super unexplored. Gatsby [80:46] has already put it into [80:47] production. There's a bunch of [80:49] other people as well, like React [80:51] Ink. Let's look at who else is [80:54] using this. [80:56] This is definitely [80:56] still early days. It's less than [80:58] two years old. It's side-project. [81:03] Definitely, it needs more [81:06] development, but there's a fair [81:09] amount of CLIs that make use of [81:10] this. It's really cool. I hope [81:14] more people explore interesting [81:16] UIs in CLIs, that take advantage [81:19] of information density, live [81:22] updating, and showing concurrent [81:24] work going on as well. [81:27] You have [81:27] all the code available here. [81:30] That's the power of the layout [81:32] model, which is pretty [81:33] interesting. On top of layout, [81:36] then you also need to take care [81:38] of interactivity. This is where [81:40] we talk about...in the input [81:42] selection as well. [81:46] There is a [81:46] bunch of first-party components. [81:49] Let's talk about useful [81:51] components. These are all of the [81:53] components that are provided [81:55] first-class, and then these are [81:56] the stuff that they're working [81:57] on to upgrade for Ink v1 to v2. [82:01] All of these are amazing already. [82:03] We'll focus on one, which is the [82:07] ink-text-input. We can show this [82:11] by, Yarn add ink-text-input. I [82:19] feel like I'm going...I pre- [82:21] prepared all this. It'd probably [82:22] take me too long to hand code a [82:25] lot of this. [82:27] I trust that you [82:28] can read React code. It looks [82:32] like [laughs] standard React [82:34] code, which is amazing. You have [82:35] a very thin rendering layer on [82:37] top of the oclif framework of [82:40] CLI parsing, so that's nice. [82:44] Let's swap this out with the [82:45] commands and see what we get. [82:53] [pause] Instructor: [82:53] I have a question from Zach [82:54] Jones about, "Can you use all [82:57] the CLI packages of React Ink [82:58] like cli-spinners?" Broadly, I [83:02] would caution against that, [83:05] because React Ink has control [83:08] over the stdout. If any other [83:11] people interfere, it may cause [83:13] that interspersing issue. That's [83:15] a very common problem with [83:17] terminals. I don't think we have [83:18] it figured out yet. [83:19] I think we [83:20] need a very eager clearing [83:21] policy, to hook into everything, [83:24] to make sure every output stream [83:28] [inaudible] . Right now, we have [83:33] a lot of crossing of the streams. [83:34] If you're a fan of "Ghostbusters," [83:35] you know that's not a good thing. [83:39] Right now, React Ink does [83:40] involve having an Ink version of [83:42] everything. That's not good, [83:44] obviously, but it is what we [83:46] have right now. In future, we [83:49] might figure out a way to [83:50] isolate these things. That would [83:54] be nice. We can reuse everything [83:55] that we already know. [84:00] Let's run [84:00] on new inputs. Again, I copied [84:02] and pasted this ink command. Now [84:06] we're no longer looking at non- [84:08] interactive output, now we're [84:10] interacting with it, and [84:11] updating it from the React [84:13] paradigm of, "I have a use state [84:16] hook over here, and I'm updating [84:17] it." I can have Zach, Natalya, [84:22] John, Chris, William, so on. [84:29] This entry is persistent. You [84:33] can imagine me doing a to-do [84:34] list application on top of this. [84:37] You can have stream on one side [84:40] and text input on the other side. [84:42] There's tab commands that you [84:43] might be able to do. ink-tab is [84:46] a tabable interface. Let's show [84:48] you the demo, because the demo [84:54] is cool. [84:55] You can tab. Look at [84:56] this. That's not it. Look at [84:59] this, you can tab, and then [85:00] change the core content over [85:02] here. You tab on the side panel, [85:04] and you're navigating through [85:06] pages. Maybe it looks like a [85:08] router, I don't know. This is [85:11] interesting. [85:13] The focus transfer [85:14] between different components is [85:16] not worked out yet, so I would [85:17] not recommend this for that, but [85:19] that is clearly the direction, [85:20] that we have accessible user [85:22] interfaces inside of the CLI. [85:27] Everything looks very well- [85:29] positioned and well-presented. [85:32] You have full declarative [85:34] control, on a component level, [85:36] of what you're outputting. [85:38] That's the galaxy brain version [85:41] of how you do React Ink [85:44] components. [85:46] That is it. We have [85:48] covered a hell of a lot with [85:51] input, output, parsing, [85:53] scaffolding, child processes, [85:55] update-notifier, story state. I [86:01] can talk all day long about the [86:03] future of CLIs. I can talk about [86:06] some notes, if you want to [86:08] really go deep. [86:10] Really CLIs are [86:11] just the interface layer. [86:12] Talking about input, output and [86:15] display, presentation, polish [86:17] that kind of thing. Everything [86:19] else inside of that logic is up [86:20] to you. That's your Node.js [86:22] logic running on file systems, [86:25] survey access, whatever. It's [86:29] really up to you. [86:31] You may need [86:32] to reach out to the Node TSE [86:34] working groups. The Node.js [86:35] tooling group is very interested [86:36] in talking to people interested [86:38] in tooling on top of Node.js. [86:41] I [86:42] recommend checking out some of [86:43] the courses. If you want to go [86:46] deep on streams, you want to go [86:47] deep on some of the core Node [86:50] APIs, going to check out [86:52] Frontend Masters as well as some [86:53] again lessons over here. [86:55] There [86:56] weren't too many on ahead which [86:57] is why I proposed this lesson. [86:59] Hopefully, this covers a lot of [87:01] different cool things that you [87:02] can do inside of Node.js, for [87:03] CLIs. There's that. [87:07] Oclif [87:07] itself is a framework that [87:09] continues to see the most [87:11] investment. They just had a [87:12] conference this year which I [87:14] spoke at, and you can see the [87:15] creator of oclif. A bunch of [87:18] other like Stripe and Twilio and [87:20] other big players also talking [87:23] about how they use oclif. [87:26] There's that. Will Johnson I'm [87:30] going to get to you in a second. [87:32] Finally, I also have a chichi [87:33] which is the source of all of [87:36] the wisdom that I've accumulated [87:38] over the years, over surveying [87:42] all this tooling. There's [87:44] alternatives to basically [87:46] everything. [87:47] Feel free to come [87:48] check out some of these links, [87:50] contribute anything that's [87:51] missing. It's really up to you. [87:54] I like this one, Sign Bunny. [87:57] This is a cool one. We actually [87:59] lobbied to get this in to...Look [88:03] at this. It says sign bunny. NPX [88:06] Sign Bunny CLIs are great. [88:14] I [88:14] hope my Internet connection is [88:16] fast. Yay! [laughs] Such a kick. [88:27] I'll get to your questions in a [88:28] sec. Then I just wanted to end [88:30] off with I just want to inspire [88:31] you. This is still a very early [88:34] field. There's no such [88:36] profession as CLI engineer yet. [88:40] Given the amount of investment [88:42] available in the field and in [88:45] developer tooling and in [88:46] developer-focused companies, [88:48] it's clear that this is a [88:49] developing industry. There's a [88:52] lot of opportunity for growth. [88:55] Just when you thought you knew [88:58] everything that was possible to [89:00] be done in CLIs, this thing came [89:03] along which I found today. I'm [89:06] going to add Ink-gradient and [89:08] Ink-big-text. Then I'm just [89:10] going to copy this little, small, [89:12] little command. [89:16] I'm going to [89:16] paste it into the Ink command [89:18] that we've been working with. I [89:22] feel like I can just end of the [89:25] workshop with this call to [89:28] action which is, drum roll, if [89:35] that ever [laughs] gets there, [89:36] go make CLIs. Yeah, that's a [89:39] nice call to action, right, if I [89:42] can reduce the font a little bit. [89:46] [laughs] [89:46] I can take questions. [89:49] Johnson's question is, "Why [89:51] again would you caution against [89:52] using React Ink with spinners?" [89:53] Basically, because React Ink [89:55] wants control over the stdout. I [90:00] want to output to the screen, [90:04] and then cli-spinners, or ora -- [90:06] actually, ora is the main [90:07] package that you want to think [90:08] about -- ora also wants to [90:09] output to the screen. Both of [90:11] them are going to output [90:12] together. [90:13] React Ink and ora, [90:15] both rely on having a strict [90:17] reference to, "This is the line [90:19] that I'm updating." When I do [90:21] animations or when I do re- [90:23] rendering, I can clear that line [90:25] and then re-render again. [90:27] When [90:27] I have crossing of the streams [90:29] like that, the line becomes [90:30] static, frozen. It would create [90:33] a new line and then re-render [90:35] from there. You get that [90:38] intermixed, very ugly visual [90:42] when you mix control streams [90:44] like that. [90:46] Hopefully, that [90:46] answers your question, Will, [90:48] that intermixing of the streams [90:52] doesn't work naively. That's [90:53] because we don't engineer for [90:56] that situation. [90:57] We could [90:57] engineer in such a way that we [90:59] have a sequence, we have central [91:01] store of, "Here's what everyone [91:03] has control over, and whenever [91:06] anyone needs to re-render, we'll [91:07] clear the preceding stuff first, [91:09] and then we'll re-render that [91:10] stuff, and then we'll reinstate [91:11] that preceding stuff." We need [91:15] some coordination between [91:16] libraries in order to take care [91:17] of that cooperation effort. [91:24] "At [91:24] what point in your development [91:26] of CLIs do you start reaching [91:27] for something like React Ink? Do [91:28] you start with oclif and switch [91:29] to something nicer like React [91:30] Ink, or just start with the [91:31] latter?" [91:32] Pretty much the way [91:34] that I've gone through in this [91:36] workshop, is exactly what I [91:37] would recommend. Start with [91:38] oclif, keep it simple. React Ink [91:40] is newer. I have run into bugs [91:43] writing this, like with the [91:44] layout and all that. It'd be [91:47] annoying to debug. The whole [91:49] point is to save productivity. [91:52] All of this stuff at the end is [91:54] nice polish. You want to make it [91:56] look professional and then [91:58] pretty and all that. [91:59] If you [91:59] want just core functionality, go [92:00] with something that's tried and [92:01] tested and deployed in [92:03] production, at scale. What my [92:07] point of trying to prove to you [92:09] is, you don't have to choose. [92:11] You can start with oclif, write [92:13] the commands, and if you want to [92:15] swap it out with a new rendering [92:16] layer, like React Ink, it's a [92:19] simple one-liner. You have the [92:21] code for it now, you've seen me [92:23] do it multiple times. That's it. [92:27] Any other questions before I go? [92:30] I can also talk a little bit [92:31] about the future of CLIs. Feel [92:40] free to use the microphone as [92:41] well to jump on in, because I [92:42] think we're at the discussion [92:44] stage of the chat. [92:49] A couple [92:49] notes on the future of CLIs, and [92:51] then we'll call it a day. [92:57] Everything that we've written is [92:58] in TypeScript and JavaScript. [93:01] That is obviously not the [93:02] fastest runtime. There is a [93:04] project to rewrite everything in [93:05] Rust. There's a prediction that [93:08] the core of everything that we [93:10] use, all the hot paths, should [93:12] be rewritten in something that's [93:13] faster anyway, because we run it [93:15] so much. [93:16] Here's an alternative [93:18] to Babel written in Rust. It's [93:19] 16 times faster, because it [93:23] doesn't have the overhead of V8. [93:27] That makes a lot of sense. [93:33] Speed is one concern, and then [93:34] UI is another concern. The [93:37] question is, we're creating all [93:39] this UI inside of the CLI, maybe [93:40] we should just launch Electron [93:42] app. Vue is leading the way on [93:44] that. Vue CLI has a UI on top of [93:47] the CLI that drives the CLI. [93:50] It's a one-to-one correlation [93:52] with what they do. [93:54] Here's the [93:54] Vue CLI UI. Literally, is you [93:59] can add dashboard you add [94:00] plugins just by clicking point [94:02] and click point and click, add [94:03] plugins. It's just a more [94:05] beginner friendly experience. [94:07] You can see the log streaming in [94:08] from here and you obviously do [94:11] something that works across as [94:12] well. [94:13] The argument is that this [94:14] is faster or just you can use [94:18] real CSS and real visual assets [94:22] to show inside of your UI. If [94:25] you're going to make all these [94:26] UIs anyway maybe you might as [94:27] well just want to run something [94:29] on the screen. There is an [94:31] argument for that. There's an [94:32] argument for not switching [94:33] between terminals and back and [94:35] forth. [94:36] It's totally up to your [94:37] design traces that's what the [94:38] Vue community has done. I really [94:40] respect them for it because it's [94:42] a big undertaking. Dry run CLIS [94:45] these are good ideas for [94:46] anything expensive. Other times [94:49] like sometimes when you need to [94:51] debug your webpack config or [94:53] your rollup config is to just [94:55] run them. [94:56] That's really [94:57] annoying especially if you don't [94:59] know if you've configured things [94:59] right. If you add a dry run [95:02] feature then you just test like, " [95:04] I'm going to run this this this [95:05] this" It's kind of planning [95:06] without actually executing. You [95:08] can run the things a lot faster [95:10] and you can report any errors [95:13] and warn on project structure or [95:16] anything like that. [95:17] It's a [95:17] little bit...I actually compare [95:19] and liken it more to database [95:21] world where before you execute [95:23] the query, you do query planning [95:24] and then you do query [95:25] optimization. You vent against [95:27] anything of that. Once you have [95:29] a valid query, then you execute [95:30] it. [95:32] We jumped straight to the [95:34] execution part but maybe we [95:35] should actually have a dry run [95:36] piece. That's the inspiration [95:40] for the dry run movement in CLIS. [95:44] Then the last bit is my sort of [95:46] think piece bit on adaptive CLIS. [95:50] This idea is that a lot of the... [95:53] This is a talk that I did. You [95:54] can go check it out on what I [95:56] mean by adaptive state machines. [95:59] Basically, the idea is that I [96:00] have all these if then else [96:02] statements. [96:06] Let's say I have [96:07] five different requirements and [96:08] I have like, if this and that [96:10] and then do this and if then [96:12] else if then else if then else [96:13] in a one long script, that's [96:15] basically doing a bunch of [96:18] resolution edge cases. If you do [96:19] this enough in one command and [96:22] the next command and the next [96:22] command, you want to extract [96:24] them and put them in a different [96:26] utils folder. [96:27] Then you want to [96:29] make them intelligent like have [96:31] some logic jumping between like, " [96:33] If this is done, go check the [96:34] next thing, go check the next [96:35] thing and make it a reusable [96:36] library." Pretty soon, you'll [96:38] have a pretty adaptive logic [96:40] that's says, "If this field is [96:43] required and is not available, [96:45] prompt for it and then move on [96:48] to the next thing or the next [96:49] thing." [96:50] That's the adaptive [96:51] CLIS. I actually also have... [97:00] It's a state machine in the [97:01] sense of you start here and you [97:03] want to end state here. You [97:04] should use the state machine to [97:05] draw this where this pass and [97:06] then generate all the prompts [97:11] that are required. [97:12] Instead of [97:12] manually coding the prompts, you [97:14] let a framework take over and [97:16] you only declare the data [97:17] dependencies on that. Now I [97:20] started thinking that I started [97:21] based on this. I have NVP built [97:25] out but again nobody has time to [97:29] invest in all of this yet. [97:30] I do [97:31] think that some part of this is [97:32] in the future because this [97:35] doesn't really scale. This is a [97:36] lot of duplicative edge case [97:39] checking. The only way you can [97:41] really plan all this out is with [97:43] the state machine. [97:46] That's what [97:46] I'm thinking about. Cool. That's [97:49] it. I have a lot of private [97:53] thanks and people leaving. Thank [97:54] you so much for joining. I [97:57] really appreciate you. Please [97:58] come by to Repo and contribute [98:01] any questions and comments. This [98:03] is definitely a working progress. [98:05] Thanks very much.