Introducing jd, the Command Line

June 23, 2026

What happened to all the software that let you script it?

There was a stretch, roughly the 1990s, when this was table stakes. Photoshop had actions. Illustrator had scripting. Every serious Mac application shipped an AppleScript dictionary, and Excel had a macro recorder that would write the macro by watching you work. The idea was uncontroversial: a program is a set of verbs, and if a human can invoke them, a script should be able to invoke them too.

Then, mostly, it stopped. Not all at once, and not by decree. The applications moved to the browser, and the browser was a place where the application ran but nothing else did, and the scripting layer quietly became an HTTP API maintained by a different team, exposing a different subset of the verbs, on a different release cycle. Which is a polite way of saying it drifted, and then it rotted, and then it got deprecated.

I wanted the 1990s bargain back, so justdraw.fyi has a command line. It’s called jd, and here is what using it looks like.

$ jd proj create logo
0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40

$ jd draw 0192a4e1 rect 0 0 120 80
0192a4e2-118f-7c04-a6d3-9e7b2c115a03

$ jd draw 0192a4e1 circle 60 40 25
0192a4e3-9a20-7be1-8c47-51d0f3ab7726

$ jd style 0192a4e3 fill=#5b7cfa
ok

$ jd group create 0192a4e2 0192a4e3
0192a4e4-4d78-7a55-b2f9-08c31e6b7d92

Look at what every command does when it finishes: it prints the id of the thing it just made, on stdout, by itself. That is the entire design. jd draw prints an id, and that id is the argument to jd style, and both of those ids are the arguments to jd group create, which prints another id you can hand to jd align.

This is not a novel idea. It is the oldest idea in Unix, and the reason a shell pipeline works at all: each thing emits a name for what it produced, in a form the next thing can eat. Everything that survived from that era survived because it did this. The programs that printed a friendly summary — “Successfully created your rectangle!” — did not compose with anything, and are gone.

So jd list prints one element per line, id first. Ids are opaque UUIDv7s, and you are meant to pass them around, never to parse them. [1]

The other decision worth defending is that an element id works anywhere a project id is expected. When you type jd style 0192a4e3 fill=#5b7cfa, you have not told the system which project that shape lives in, and you shouldn’t have to. The shape knows. The server looks it up. A group id works anywhere an element id does, for the same reason: the thing you are holding should be sufficient to identify itself, and making the human carry a second identifier around so the computer doesn’t have to do a lookup is exactly backwards.

The verbs are the ones you’d guess. draw takes line, rect, square, ellipse, circle, triangle, polygon, star and text. move, clone, delete, name, style. group create and group ungroup. align in six directions, distribute horizontally or vertically, order to push things front and back. proj create, list, show, rename, destroy. And export, which writes the drawing out as YAML or SVG.

Now the honest part, which is the constraint that surprises everybody.

The drawing commands require the project to be open in a browser. If no tab is open on that project, jd draw will tell you so and exit 1.

That sounds like a bug. It’s the central design decision of the whole system, and I’ll defend it properly in the next post, but the short version is this: the editor in the browser already knows what “align these three shapes” means. It has the geometry, the layers, the groups, the undo stack, the renderer. If the CLI were to compute alignment on the server, there would be two implementations of alignment, they would disagree, and the day they disagreed would be the day a customer found out. So jd doesn’t compute anything. It sends a command, the server relays it down the WebSocket the editor is holding open, the editor does what it does when you press the align button, and the answer comes back.

Which means that when you run jd style … fill=#5b7cfa, someone watching that tab sees the circle turn blue. Your terminal and their screen are the same session. The edit lands in their undo history. It autosaves like anything else.

The commands that don’t touch the drawing don’t need a tab, because they’re answered from the database: proj create, list, show, rename, destroy, and export. You can mint a project and dump its contents to YAML from a cron job at three in the morning with no browser anywhere in the world. [2]

That’s the trade. A CLI that needs an open tab is a strange thing to ship, and I have thought about it more than any other decision in this project. But the alternative was a second implementation of a vector editor, written in Go, slightly wrong, drifting a little further from the real one every month — which is precisely the fate that befell every scripting layer I mentioned at the top of this post.

I would rather have one editor and an unusual constraint than two editors and an ordinary lie.

[1] UUIDv7s happen to sort by creation time, which is handy, and it is still true that nothing in the system is allowed to depend on it. The moment a program parses meaning out of an id, the id has stopped being an id.

[2] jd export <project> drawing.svg at 3 a.m. is, in fact, the thing that made me build export first.

justdraw.fyi is a vector editor that opens in a tab. No account, nothing to install, and every drawing is a link you can send to anyone.

← All posts