zhuli
If you saw the Legend of Korra, you may remember the charismatic Varrick and his very capable assistant, Zhu Li. She is the namesake of this tool.
Note: install instructions are located in the Appendix section.
What is this this tool?
It's a flexible task running tool. One use might be to set up project templates for you. Generally speaking, the tool can execute a tree of tasks and collect user input from interactive promps or from CLI arguments.
The zhuli executable runs your task trees ("presets") which you save in a particular place. To define a preset, you create a new directory with at least one special file in it and then zhuli will be able to find it and do a new thing.
Here's how it works.
zhuli <preset-name>will look for files within the directory~.zhuli/presets/<preset-name>- the one required file in your preset directory is
task.hbr, which defines your task tree. This file should contain a javascript object which is compatible with the following interface. The file isn't valid JS by itself, since the object is not being assigned to anything, but don't let that trouble you. This is the "shape" of that object:
// all properties are optional
export interface TaskNode {
args?: string[]; // ignore this, used internally
parent?: TaskNode | null; // tasks can access their parent (perhaps to get input passed to the parent task)
label?: string; // ignore, used internally for console output
description?: string; // set this to something that describes what the task does
inputMeta?: ObjectMeta; // this is how you specify what user input is required
input?: InputParams; // not created by you, zhuli collects info and puts it here
tasks?: TaskNode[]; // child tasks
fn?: () => Promise<void>; // the task's "action", can access this.input property prepared by zhuli.
}Here's an example of a simple task.hbr file:
{
description: "generate nanoid",
inputMeta: {
properties: [
{ name: "howMany", display: "How Many?", type: "number", default: 1 },
{ name: "length", display: "Length?", type: "number", default: 21 }
]
},
fn: async function() {
const { howMany, length } = this.input;
for(let i=0; i<howMany; i++){
console.log(nanoid(length));
}
}
}That task was made to test using a 3rd party library (nanoid) from within the task tree. More info in the appendix.
Anyways, that's all zhuli needs to do the thing.
Here's what zhuli can do when you have defined this task tree within a preset directory named nanoid:
km@km-AERO-15-KC:~/dev/deno/zhuli$ zhuli nanoid
┌[nanoid] generate nanoid
|How Many? (1): 5
|Length? (21):
|{ howMany: 5, length: 21 }
|IlTCswsjP6IYXA5YKfGoU
|auKX_BtmW-VAWYvgo43Hk
|bvVCUz7Kix3DnFEar22en
|HyV34UAD8TK4TPAiAwXAt
|jCojlXjOqgSrmi75o0253
└[nanoid] completed
All done!
km@km-AERO-15-KC:~/dev/deno/zhuli$ Okay, this particular preset isnt very useful. Let's see one more example of how zhuli can also scaffold a project.
This time, let's see the output first.
The output:
km@km-AERO-15-KC:~/dev$ mkdir taco-generator
km@km-AERO-15-KC:~/dev$ cd taco-generator/
km@km-AERO-15-KC:~/dev/taco-generator$ zhuli node-ts
┌[node-ts] Create a new node+TypeScript project 📝
|Project Name (taco-generator):
|{ projectName: "taco-generator" }
|
|┌[executing 4 subtasks]
||┌[node-ts:1] Create package file
||└[node-ts:1] completed
||┌[node-ts:2]
|||┌[basic-tsconfig] Create tsconfig file
|||└[basic-tsconfig] completed
||└[node-ts:2] completed
||┌[node-ts:3] Install node packages
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^2.3.2 (node_modules/jest-haste-map/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN taco-generator@0.0.1 No description
npm WARN taco-generator@0.0.1 No repository field.
+ ts-jest@27.0.3
+ @types/jest@26.0.23
+ jest@27.0.4
+ typescript@4.3.2
added 423 packages from 297 contributors and audited 424 packages in 11.054s
24 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
||└[node-ts:3] completed
||┌[node-ts:4] Create source files
||└[node-ts:4] completed
|└[completed 4 subtasks]
└[node-ts] completed
All done!
km@km-AERO-15-KC:~/dev/taco-generator$ Ah yes, how nice. Thanks, zhuli.
The "tree" part of "task tree" has now revealed itself. In fact, the main task has no fn property, it collects some input but other than that, the child tasks do all the work.
Here's the task tree:
{
description: "Create a new node+TypeScript project 📝",
inputMeta: {
properties: [
{
name: "projectName",
display: "Project Name",
default: "{{cwdName}}"
}
]
},
tasks: [
{
description: "Create package file",
fn: async function() {
const templatePath = path.join("{{templateDirPath}}", "package.json.hbr");
await forgeFile(templatePath, "package.json", this.parent.input);
}
},
{
fn: async function() {
await zhuli("basic-tsconfig", {strict: true});
}
},
{
description: "Install node packages",
fn: async function() {
const cmdTokens = "npm install -D jest typescript ts-jest @types/jest".split(" ");
await exec(cmdTokens);
}
},
{
description: "Create source files",
fn: async function() {
await copyDirectoryContents(path.join("{{templateDirPath}}", "bones"));
}
}
]
}As you can see, there is much more going on here, but each task is only a line or two of code. You can get more information on the various convenience functions I used here in the appendix below.
Side note for context: the directory for this preset has a few static project files in it in an arbitrarily named "bones" folder, and also a template file called package.json.hbr which is a template for a node package json file.
One thing you may notice is the {{cwdName}} and {{templateDirPath}}.
As the .hbr file extension hinted, your task tree definition file is indeed a handlebars template, and a few parameters will be passed to it by zhuli herself so that the template knows what directory it is being invoked in, for example.
You probably also noticed that subtask 2 calls... zhuli?! That's right. In fact zhuli the program is the command line interface for zhuli the function, and zhuli the function can call herself! And when she calls herself this way she is even more useful because you can supply some of the input data for the top level task in the preset you are calling, so the user won't have to. That's what the deal is with the second argument you see above in the zhuli call.
Aside: you can also skip user input from the CLI by calling zhuli like this, for example: zhuli nanoid --howMany 10 --length 21
Appendix
This tool uses the deno javascript runtime, so go grab it if you are interested. Install from the shell is a one-liner and deno is just a single executable of a relatively small size!
https://deno.land/
After you have deno installed, you can install zhuli by running the ./install.sh script from this repo. You may need to rename it to .ps1 on windows, but please note I've never tested zhuli on windows. This will not create your ~.zhuli directory for you. You have to create that directory and the ~.zhuli/presets directory yourself. Someday zhuli might help you out there, but not today.
Disclaimer: you give zhuli the ability to do a lot of stuff when you install it. You have the power to mess up your file system and more. NEVER run other people's presets without carefull inspecting the code, including any modules that code may import. Ideally you should only write your own tasks.
functions exposed to the global scope for your zhuli tasks:
- exec(tokens: string[]): lets you execute arbitrary commands (i.e. invoke other programs)
- copyDirectoryContents(sourceDirPath, destDirPath="."): useful for project scaffolding
- forgeFile(templatePath, destPath, data): create a file from a template file and some data to fill it in with
path,exists,walk,Colorsare all in global scope (imported from deno standard libraries)- the
Denoobject is also available
exposing your own functions to the global scope:
- you can do this by defining a
context.tsfile in your preset directory. - You should have one named export called
contextwithin yourcontext.tsfile - all properties of
contextwill be available in the global scope for your task tree functions