Command Line

muzak is the framework's command. It creates a project laid out the way the framework's own example is, runs it while you write it and restarts it when a file changes, and reads the OpenAPI document an application publishes: as a table of routes, as a compatibility report for CI, or as TypeScript for a frontend.

go install muzak.dev/framework/cmd/muzak@latest

The command lives in the framework's own module, so the muzak installed at a version is the one that matches the framework of that version, and the project it creates requires that version. Like the framework, it uses the standard library and nothing else.

CommandWhat it does
muzak new <dir>Creates a project in dir
muzak devBuilds and runs the application, and rebuilds and restarts it on every change
muzak routesPrints the operations a document describes
muzak diff old.json new.jsonReports what changed between two documents, and fails on breaking changes
muzak tsWrites TypeScript declarations, and optionally a client, for a document
muzak versionPrints the framework version it belongs to, which a new project requires, and the Go toolchain it was built with
muzak help [command]Prints help; muzak <command> -h prints the same

Flags may come before or after the other arguments, so muzak new shop -module example.com/shop reads as it is written. A -- ends the flags.

Creating a project

muzak new shop -module example.com/shop
cd shop
go mod tidy
go test ./...
muzak dev
shop/
  .gitignore
  README.md
  cmd/server/main.go        composition, and nothing else
  go.mod                    requires muzak.dev/framework at this version
  handlers/hello.go         the function that answers GET /hello
  handlers/hello_test.go    a test that serves the application with testclient
  routers/hello.go          which handler answers which path
  routers/routers.go
  schemas/hello.go          the request and response models, with their rules

It is the layout First Steps describes, with one route in it, and the test is written the way Testing recommends. Two things differ from the page's picture: the main package is in cmd/server, which is where muzak dev looks by default, and there is no core/ yet; add it when the application gains configuration, guards or managed resources. A project with its main package in cmd/, as the framework's example has, runs with muzak dev -pkg ./cmd. go mod tidy downloads the framework and records its checksum in go.sum, which muzak new cannot do without the network.

FlagDefaultEffect
-module paththe name of dirThe module path written to go.mod and every import

A module path is checked the way the go command checks one: elements of ASCII letters, digits and - . _ ~, none empty, none starting or ending with a dot, none a name Windows reserves for a device. A directory called My App is not a module path, so it needs -module.

What it will not write over

muzak new writes into a directory it creates, or into one that exists and is empty. Anything else is refused, and nothing is written:

  • a directory that holds anything, even only a .git;
  • a file at the path;
  • a symbolic link at the path, which would put the project somewhere other than where the command line says;
  • a path whose parent does not exist.

Every file is created through an os.Root opened on the directory, and created exclusively, so no name and no symbolic link placed inside the directory while it is being written can lead a write outside it or over a file that appeared meanwhile. If a write fails part way, everything the command created is removed again, newest first, and the directory too when the command created it: a failed run leaves the file system as it found it. Files are created 0644 and directories 0755, less whatever your umask takes away.

Running while you write

muzak dev
muzak dev -pkg ./cmd/api -- -config dev.yaml

muzak dev builds the package with go build into a temporary directory, runs it, and watches the tree. When a file changes it waits for the editing to settle, builds again, and, if the build succeeds, stops the running application and starts the new build. A build that fails prints what the compiler said and leaves the application that was running alone, so the server stays up while you fix the mistake.

muzak dev: building ./cmd/server
muzak dev: started ./cmd/server (pid 41207)
muzak dev: building ./cmd/server
# example.com/shop/handlers
handlers/hello.go:12:9: undefined: greeting
muzak dev: the build failed; the application keeps running the previous build
muzak dev: building ./cmd/server
muzak dev: started ./cmd/server (pid 41388)
FlagDefaultEffect
-pkg package./cmd/serverThe package to build and run
-watch directory.The tree to watch
-poll duration300msHow often to look for changes, at least 10ms
-grace duration5sHow long a stopping application has before it is killed
-ext listgo,mod,sum,work,yaml,yml,envThe extensions of the files whose change rebuilds

Arguments after -- are passed to the application. It runs in the current directory, with the same environment as muzak dev, and with no standard input.

How a change is found

Changes are found by polling: every -poll, the tree is walked and each watched file's size and modification time compared with the last walk. That needs nothing from the operating system but stat, so it works the same on every platform and on every file system, network mounts and container bind mounts included.

Only files with one of the -ext extensions are watched. The default covers the Go sources and the files the go command reads to build them, YAML, which is what locale files are and which an application usually embeds, and .env, which configuration is read from at start-up. An application that writes a SQLite database, a log or an upload into its own directory therefore does not restart itself every time it writes. Add the extensions of anything else you embed:

muzak dev -ext go,mod,sum,yaml,html,tmpl

Directories whose names start with a dot (.git, .idea), node_modules, vendor and testdata are not walked, nor is the directory muzak dev builds into. A -watch root that is a symbolic link, or a directory reached through one, is followed, so a project you reach through a link is watched as the directory it names. Links inside the tree are not: a symbolic link to a file is watched by what it points to, and one to a directory is not followed, so a link cannot make the walk loop.

A poll reads every directory under -watch that is not skipped, and costs one stat per watched file. The number of watched files is bounded: a tree with more than 10,000 is refused with a message rather than polled ever more slowly. A project that size is one where -watch is pointing at something it should not, such as a build output or another checkout. Only watched files count toward the bound, so a large data directory whose files have other extensions is not refused, but it is still read on every poll: keep it outside the watched tree.

A rebuild waits for one poll in which nothing changed, so a save that touches several files builds once. A file that changes on every poll cannot put the build off for ever: after ten polls in a row it builds anyway.

Stopping

Ctrl-C or SIGTERM stops muzak dev, and so do the hang-up (SIGHUP) a terminal sends when it closes and the quit (SIGQUIT) that Ctrl-\ sends; left to their defaults, those two would end muzak dev alone and leave the application running, holding its port. The application is sent the same signal, given -grace to shut down, and killed if it has not. A restart after a change sends SIGINT in the same way, which a Muzak application started with RunSignals answers by draining its requests and stopping its lifecycle components, as it would in production; see Server Configuration.

On Unix the application runs in a process group of its own, and the signal goes to the whole group, so a process the application started (a worker, a sh -c) is stopped with it. A signal from the terminal, Ctrl-C, Ctrl-\ or the hang-up, reaches muzak dev alone and is forwarded from there, so it is not delivered twice. An application that exits by itself is reported, whatever it started is stopped, and muzak dev waits for a change rather than starting it again. Every process is waited for, so none is left behind as a zombie, and the build directory is removed when muzak dev exits.

On Windows

Windows has no interrupt one process can send another, so the application is stopped at once, without the chance to shut down gracefully, and -grace has nothing to wait for. It runs in a job object of its own, which Windows puts every process it starts into as well, and stopping it terminates the job: a process the application started is stopped with it, as it is on Unix. The application starts suspended and joins the job before it runs, so nothing it starts can be outside it. On Unix, a process that leaves the application's group, as a daemon calling setsid does, is not stopped.

Listing the routes

muzak routes -url http://localhost:8080/openapi.json
muzak routes -file openapi.json
go run ./cmd/openapi | muzak routes -file -

cmd/openapi is the small command OpenAPI shows, which writes the application's document to standard output without starting a server.

METHOD  PATH         OPERATION           SUMMARY                      SECURITY
GET     /health      get_health          -                            -
GET     /items       get_items           List items                   none
POST    /items       post_items          Create an item               oauth[items:write,items:read]
GET     /items/{id}  get_items_by_id     Read an item                 bearer | bearer+key
DELETE  /items/{id}  delete_items_by_id  (deprecated) Delete an item  oauth[items:write]

Rows are sorted by path and then by method, in the order GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, so the same document always prints the same table. A summary longer than 72 characters is cut, and a deprecated operation's is marked.

The security column lists the alternatives a client satisfies one of, separated by |. Each names the schemes it needs together, joined by +, with the scopes in brackets: bearer | bearer+key accepts a bearer token alone or one with an API key. none is an operation that declares it needs no credentials, which is what muzak.Public() writes, and - one that declares nothing at all.

FlagEffect
-url URLFetches the document from a running application
-file fileReads the document from a file, or from standard input when it is -

Exactly one of the two is given.

Fetching from a running application

-url fetches through a muzak.Client, so the fetch has every bound the client has: it gives up after 30 seconds, reads at most 16 MiB, follows at most five redirects and never from https down to http. The client is allowed to connect to loopback and private addresses, which a client of a URL from somewhere else never should be, because a developer points it at their own application, on localhost or inside their network. Cloud metadata addresses stay refused all the same: no document is served there, and a redirect to one is how a fetch would read a machine's credentials.

The document is read with muzak.ReadDocument, strictly and within its bounds. It reads what Muzak writes, so a document that is not OpenAPI 3.1, that uses a keyword Muzak never writes, such as oneOf, or an extension, or that is larger or deeper than a document is read at, is refused with a message saying what and where. A document another framework generated is therefore usually refused rather than half understood.

Comparing two documents

muzak diff baseline.json current.json
muzak diff -fail-on possibly baseline.json current.json
Breaking (1):
  /paths/~1items/post: The operation POST /items is no longer served. [operation-removed]
Possibly breaking (1):
  /paths/~1items~1{id}: The path "/items/{id}", which was deprecated, is no longer served. [path-removed]
Compatible (1):
  /paths/~1users: The path "/users" is new. [path-added]
muzak: the comparison found 1 breaking change, and -fail-on breaking fails on that

This is muzak.CompareDocuments and muzak.WriteChanges, so it judges exactly what testclient.AssertCompatible judges in a test; see API Compatibility for how each change is classified. The report goes to standard output and the verdict to standard error.

Exit statusMeaning
0No change as serious as -fail-on names
1At least one change that serious, or the report cannot be written, even under -fail-on never
2The command line is wrong, or a document cannot be read
-fail-onFails on
breaking (default)Breaking changes
possiblyBreaking and possibly breaking changes
neverNothing: the report only informs

A document that cannot be read exits 2 rather than 1, so a CI step can tell a missing baseline from a breaking change. Either document may be read from standard input by naming it -:

go run ./cmd/openapi | muzak diff openapi.json -

In a pipeline, compare the document a pull request generates with the one its base branch committed:

- run: go install muzak.dev/framework/cmd/muzak@latest
- run: git show origin/main:openapi.json > baseline.json
- run: go run ./cmd/openapi > openapi.json
- run: muzak diff baseline.json openapi.json

Generating TypeScript

muzak ts -url http://localhost:8080/openapi.json -o web/src/api.ts
muzak ts -file openapi.json -client -o web/src/api.ts

muzak ts writes what the tsgen package generates: an interface for every component schema and the parameter, body, response and error types of every operation, and with -client, a small client built on fetch. See TypeScript for what is written and why.

FlagEffect
-url URL / -file fileWhere the document is read from, as for muzak routes
-clientAlso writes the client
-o fileWrites to the file rather than to standard output

The file -o names is written to a temporary file beside it, .<name>.<random>.tmp, and renamed into place, so a frontend build watching it never reads half a file and an interrupted run leaves the previous one intact, though it may leave the temporary file behind. A new file is created 0644, less whatever your umask takes away; a file that is there already keeps its mode. A symbolic link at the path is replaced rather than written through.

muzak ts refuses a schema nested more than 64 levels deep, as tsgen does, even though a document nested up to 128 levels is read: a document muzak routes and muzak diff accept can still be one it cannot generate from.

What a hostile document cannot do

muzak routes, diff and ts read documents someone else may have written: a partner's Muzak service, or a URL in a ticket. Everything the command prints that came from a document, a server or a file is escaped for the terminal first: control characters, including the escape that starts an ANSI sequence, the tab and line break that would split the table, the Unicode controls that reorder or hide text, and bytes that are not UTF-8 are printed as the Go escape that names them. A path named /a\x1b[2J is shown as exactly that, and cannot clear your screen, retitle the window or forge a row of the report.

BoundValue
Document size16 MiB
Document nesting128 levels
Schema nesting muzak ts generates from64 levels
Fetch time30 seconds, every redirect and the body included
Redirects5, never from https to http
Addresses fetched fromPublic, loopback and private; never cloud metadata
Files muzak dev watches10,000
Shortest poll10ms

Exit status

Every command exits 0 when it did what it was asked, 1 when it could not, and 2 when the command line was wrong: an unknown command, an unknown flag, a missing or extra argument. An unknown command is answered with the nearest one:

$ muzak rotues
muzak: there is no command "rotues"; did you mean "routes"?
Run "muzak help" for the list of commands.

muzak diff is the exception that a CI step relies on: 1 means the documents differ by more than -fail-on allows, or that the report could not be written, and 2 means they could not be compared.

Where to go next

First Steps walks through the project muzak new creates. API Compatibility covers the comparison muzak diff runs, and TypeScript the declarations muzak ts writes.

Open source under MIT / Apache-2.0.Built on net/http, and nothing else.