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.
| Command | What it does |
|---|---|
muzak new <dir> | Creates a project in dir |
muzak dev | Builds and runs the application, and rebuilds and restarts it on every change |
muzak routes | Prints the operations a document describes |
muzak diff old.json new.json | Reports what changed between two documents, and fails on breaking changes |
muzak ts | Writes TypeScript declarations, and optionally a client, for a document |
muzak version | Prints 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.
| Flag | Default | Effect |
|---|---|---|
-module path | the name of dir | The 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)
| Flag | Default | Effect |
|---|---|---|
-pkg package | ./cmd/server | The package to build and run |
-watch directory | . | The tree to watch |
-poll duration | 300ms | How often to look for changes, at least 10ms |
-grace duration | 5s | How long a stopping application has before it is killed |
-ext list | go,mod,sum,work,yaml,yml,env | The 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. The application is killed at
once, without the chance to shut down gracefully, and a process it started is left
running, since stopping it would take a job object, which the standard library does not
offer. On Unix, a process that leaves the application's group, as a daemon calling
setsid does, is not stopped either.
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.
| Flag | Effect |
|---|---|
-url URL | Fetches the document from a running application |
-file file | Reads 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 status | Meaning |
|---|---|
0 | No change as serious as -fail-on names |
1 | At least one change that serious, or the report cannot be written, even under -fail-on never |
2 | The command line is wrong, or a document cannot be read |
-fail-on | Fails on |
|---|---|
breaking (default) | Breaking changes |
possibly | Breaking and possibly breaking changes |
never | Nothing: 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.
| Flag | Effect |
|---|---|
-url URL / -file file | Where the document is read from, as for muzak routes |
-client | Also writes the client |
-o file | Writes 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.
| Bound | Value |
|---|---|
| Document size | 16 MiB |
| Document nesting | 128 levels |
Schema nesting muzak ts generates from | 64 levels |
| Fetch time | 30 seconds, every redirect and the body included |
| Redirects | 5, never from https to http |
| Addresses fetched from | Public, loopback and private; never cloud metadata |
Files muzak dev watches | 10,000 |
| Shortest poll | 10ms |
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.