Skip to content

Command line

The package has a command line with seven commands. It has no dependencies, and you do not need to install anything extra:

Terminal window
npx @lo-tek/interfaces <command>

When the package is in package.json, the command is called lotek, so npx lotek check and a script with lotek check do the same thing. npx @lo-tek/interfaces without a command, or with --help, shows the full overview.

CommandDoes
checkchecks markup against LO-TEK, in files, from standard input or from an address
agentwrites the rulebook for coding agents for your environment
takeovercopies the source code of one component into the project
thememakes a colour theme from your brand colours
check-themechecks that a theme meets the contrast requirements
manifestwrites the manifest the check uses, with the fragments from takeover
lspthe language server for editors, see The editor

The commands are written once, in Rust, and built as a WebAssembly module for WASI. npx runs it with Node, but the same module also runs without it, and gives the same answer, byte for byte. Pick what is already on the machine:

If you haveRun
Java 17 or newerjava -jar lotek.jar check form.html
wasmtimewasmtime run --dir=. --env LOTEK_PACKAGE=package lotek.wasm check form.html
Rustcargo install --git https://github.com/MarkusAugust/lo-tek lotek, then lotek check form.html

lotek.jar and lotek.wasm are attached to every release on GitHub, with the same version as the npm package. Neither is signed with a certificate. Instead, GitHub attests which commit and which workflow each file was built from, and you can check that yourself:

Terminal window
gh attestation verify lotek.jar --repo MarkusAugust/lo-tek

The jar contains everything and needs no dependencies. The executable and lotek.wasm carry no data: they read the manifest, the colour contract, the rulebooks and the components from @lo-tek/interfaces in node_modules, or from the folder LOTEK_PACKAGE points to. Without Node, you fetch the package from the npm registry and unpack it, and it ends up in package/:

Terminal window
curl -sL https://registry.npmjs.org/@lo-tek/interfaces/-/interfaces-0.36.0.tgz | tar xz
LOTEK_PACKAGE=package lotek check form.html

Replace the version with the one you want to check against. Without the package, the command stops and says what it is missing.

An address is fetched by the host, since WASI has no network. npx and java -jar fetch both http:// and https://. The executable from cargo install fetches http://, which is what a local server answers on. For https:// with cargo install, you send the page in yourself: curl -s https://… | lotek check --rendered.

wasmtime is there for those who already have it, or who do not have Node, Java or Rust. It is the weakest of the hosts, and you should know why before you choose it:

  • You have to trust a third party. wasmtime is installed from wasmtime.dev, outside npm, Maven and cargo, and is made by the Bytecode Alliance, not by LO-TEK.
  • No addresses. wasmtime has no network, and no host fetches the page for the module. lotek check http://… says it could not connect. Send the page in yourself: curl -s http://localhost:8080/form | wasmtime run --dir=. --env LOTEK_PACKAGE=package lotek.wasm check --rendered.
  • Only the folders you open. wasmtime sees only what you give it with --dir. With --dir=., paths under the working folder work, and a file outside gives “File not found”, even though it exists. The package must also be under a folder you open, and LOTEK_PACKAGE is given with --env, since wasmtime does not pass your environment to the module. The language server needs --dir=/::/, which gives it the whole file system.
  • No notice about standard input. The other hosts say “Reading markup from standard input” when you run check without files in a terminal. wasmtime cannot tell, so the command waits silently until you end it with Ctrl-D.
  • Windows is not tested. The other hosts translate C:\… into what the module understands. In wasmtime you have to open the drive yourself with --dir.
  • Not tested in CI. npx, java -jar and the executable are tested on every change, with the same runs. wasmtime has been tested by hand.

check is the same check the editor runs while you type. It knows every class, every element, every attribute and every allowed value in the package you have installed.

Terminal window
npx @lo-tek/interfaces check templates/*.html
npx @lo-tek/interfaces check http://localhost:8080/form
curl -s http://localhost:8080/form | npx @lo-tek/interfaces check --rendered

It reads three kinds of input:

  • files, given as arguments;
  • standard input, when no files are given;
  • an address starting with http:// or https://. It is fetched by the command line, see Without Node. A page that answers with anything other than 2xx, or with anything other than HTML, gives an error code instead of being checked.

Each finding is written the way a compiler writes it, with file, line and column:

form.html:4:27: warning: data-variant cannot be "outline" on button. Allowed values: secondary, ghost, danger, and primary without the attribute.
form.html:5:13: warning: The class "lt-modal" does not exist in LO-TEK.
2 findings in 1 file.
FlagMeaning
--renderedcheck files and standard input as whole pages, with the wiring
--css=<file>also check that the stylesheets style the markup. See With the stylesheets
--manifest=<file>also check a component you have taken over. See takeover

The exit code is 0 when there are no findings, and 1 otherwise, also when all the findings are warnings. A warning in a template is an error no compiler sees. Empty standard input, files that are all empty, a file that does not exist, and an address that does not answer or answers with an empty page also give 1. One empty file among several, such as an empty partial in a glob, is not an error.

A template is often split into pieces. A label can be in one file and the field in another, and template syntax such as {{ }}, {% %}, <?= ?>, @variable and <%= %> stands where the value comes later. Without flags, check therefore checks only what can be decided in one file: elements, classes, attributes, values, that every <lt-field> has a label, and that a part that should appear once does not appear more than once. An <lt-field> with two controls wires only the first, and an lt-table with two <caption> elements gets its name from the first. Which parts this applies to is in the manifest as unique. Other parts can appear several times, such as the buttons in the panel of <lt-popover>. If there is template syntax in the component, including v-if or an expression in {…}, the rule says nothing, since a condition can give two parts in the template and one on the page.

A page as the server sends it is whole. Then check can also require that the wiring is right:

  • every id in for, aria-describedby, aria-labelledby and aria-controls exists on the page, and for points to a field or a button;
  • no id appears more than once;
  • every field with an lt- class has a label: a <label for>, a <label> around it, aria-label or aria-labelledby;
  • every visible help text and error text is pointed to from aria-describedby. A text is not visible when it or a parent has hidden, or is in a closed <dialog> or <details>. A status element, with role="status", role="alert" or aria-live, is announced on its own and needs no wiring;
  • every link in <lt-error-summary> goes to an id that exists.

An address is always checked this way. Files and standard input are checked this way with --rendered. Bare markup inside <lt-field> gives no findings, since the web component sets the wiring in the browser. The same applies to a field inside <lt-suggestion>. An id that is written and points to the wrong place is reported wherever it appears.

Two things are not read as part of the page. The content of a <template> is not in the document, and in a shadow root, <template shadowrootmode>, the ids have their own scope. Markup in an attribute value, such as a code example behind a copy button, or inside a <textarea>, is text. None of them are checked.

http://localhost:8080/form:12:30: error: for="email" points to the id "email", which does not exist on the page. The screen reader loses the label. Write for on the label and id on the field in the template, or put the label around the field. If the markup is made with JavaScript, lt.field({ id }) does this.

This is the check that works the same in every language. The HTML the server sends looks the same whether it comes from Kotlin, Go, PHP or Node, and that is what is checked, not the code that makes it.

The markup can be right and still look wrong: the component’s stylesheet is not loaded, or a variant has no rule. With --css, check reads the stylesheets the page loads, with what they import, and reports both:

Terminal window
npx @lo-tek/interfaces check --css=public/app.css --manifest=src/ui/button/lotek-manifest.json templates/*.html
templates/button.html:1:28: warning: data-variant="glass" on button has no rule in the stylesheets, so it does nothing visible. Style the value, or remove it.
templates/card.html:1:13: warning: The class "lt-card" is in the markup, but none of the stylesheets style it. Is lotek.css, or the stylesheet for card, loaded?
2 findings in 2 files.

The first message comes for a value the manifest knows, but which none of the stylesheets style. A value LO-TEK does not have, such as data-variant="glass" on lt-button, is rejected before the stylesheets are read, with a message listing the allowed values. The message is most useful for a component you have taken over: if you add a variant to the fragment, but not to the CSS, the markup is valid and looks like the default. The output above is from such a copy: button.html is <button class="app-button" data-variant="glass">, with glass added to the fragment. @import is followed to neighbouring files and to packages in node_modules, such as @lo-tek/interfaces/lotek.css. --css can be given several times. The stylesheets are read with a real CSS parser, so nesting, :is(), :where(), @layer and @media are understood the way the browser understands them.

What the server sends is not always what the user sees. The web components add markup, a script can set a class, and the stylesheets are the ones the browser actually loaded. diagnoseRendered checks the page as the browser has rendered it, with Playwright:

import { diagnoseRendered } from "@lo-tek/interfaces/diagnostics"
test("the form is right as it is shown", async ({ page }) => {
await page.goto("http://localhost:8080/form")
expect(await diagnoseRendered(page)).toEqual([])
})

It gives what diagnosePage gives, and what the stylesheets do not style. A stylesheet from a CDN is also read when it answers with Access-Control-Allow-Origin, as jsDelivr and unpkg do. With LO-TEK for Kotlin it is called assertLotekRendered.

With a running server, one line is enough:

Terminal window
npx @lo-tek/interfaces check http://localhost:8080/ http://localhost:8080/form

The same check exists as functions, for tests written in JavaScript or TypeScript:

import { diagnoseMarkup, diagnosePage } from "@lo-tek/interfaces/diagnostics"
expect(diagnoseMarkup(template)).toEqual([]) // a template or a piece of a page
expect(diagnosePage(html)).toEqual([]) // a whole page, with the wiring

Each finding has start, end, line, column, severity, rule and message, and some have a fix the editor can offer. The check is the same Rust core the editors use, built to WebAssembly and included in the package.

If the app is written in another language, the test runs the command over the HTML it renders. In Kotlin:

@Test
fun `the form is wired correctly`() {
val html = app.render("/form")
val check = ProcessBuilder("npx", "@lo-tek/interfaces", "check", "--rendered")
.redirectErrorStream(true)
.start()
check.outputStream.use { it.write(html.toByteArray()) }
val output = check.inputStream.bufferedReader().readText()
assertEquals(0, check.waitFor(), output)
}

The test fails with the findings as its message, and that is the message a coding agent reads and fixes from.

Without Node in CI, you replace npx @lo-tek/interfaces with java -jar lotek.jar. With LO-TEK for Kotlin it becomes assertLotek(html), with the same check run in the JVM.

Writes the rulebook for coding agents to standard output. No file is written anywhere.

Terminal window
npx @lo-tek/interfaces agent
npx @lo-tek/interfaces agent --framework=datastar

Without flags, the environment is read from the dependencies in package.json: Astro, React, Datastar, or a build step such as Vite. If there is no package.json, as in a Kotlin or Go app, it is the rulebook for templates. Which one it chose, and why, is written to standard error.

--frameworkThe rulebook for
htmlhand-written HTML without a build step, where the browser loads the files directly
templatesserver-written HTML: Go templates, Razor, PHP, Jinja, ERB and others, and HTML written in code
bundlesa build step where the markup is plain HTML: Vue, Svelte, Solid, Lit and Vite
reactReact
astroAstro
datastarHTML with Datastar

How the agent finds the rulebook is described on Coding agents.

Copies the source code of one component into the project: the TypeScript file and the stylesheet. The copy is yours from the moment it is written, and the command does three things so that it can be:

  • The copy gets a new name. lt-button becomes app-button in the classes, the tags, customElements.define and the selectors. The copy can therefore stand beside the original, and you change the name in the markup where the copy is to be used. The variables from the theme, --lt-*, are the same, so the copy follows your theme as before.
  • The references out of the component are rewritten to the package, so the rest of the system still comes from @lo-tek/interfaces and can be upgraded.
  • A fragment of the manifest is written next to it, lotek-manifest.json, with the classes, elements and values of the copy under the new name. With it, check checks the markup for the copy too:
Terminal window
npx @lo-tek/interfaces takeover button --out=src/ui
npx @lo-tek/interfaces check --manifest=src/ui/button/lotek-manifest.json templates/*.html

Without --manifest, check leaves alone everything that does not start with lt-. With it, only the copy’s own names are checked, app-button with its variants and elements, not the rest of the project’s classes. If you change the copy, for example with a new variant, you write it into the fragment too. --manifest can be given several times, once for each copy.

FlagMeaning
--out=<folder>where the copy goes. Default: src/lotek
--overwrite=yesoverwrite a copy that already exists

Without a name, the command lists the components. When you should take over a component, and what you should try first, is described on Customising.

Writes the manifest check uses: LO-TEK’s classes, elements and allowed values, with the fragments from takeover added. That is what an editor or another tool needs to know the vocabulary the project actually has.

Terminal window
npx @lo-tek/interfaces manifest --manifest=src/ui/button/lotek-manifest.json --out=build/lotek/manifest.json
FlagMeaning
--manifest=<file>a fragment from takeover. Can be given several times
--out=<file>write to a file instead of to standard output

The Gradle plugin writes the same file to build/lotek/manifest.json when manifests has fragments.

Makes a colour theme from your brand colours, with a light and a dark appearance and the contrast requirements met. The theme is written to standard output, or to a file.

Terminal window
npx @lo-tek/interfaces theme --accent=#7c3aed --danger=#b3261e \
--success=#2b6940 --warning=#8a5a00 --out=theme.css
npx @lo-tek/interfaces theme lotek.theme.json --out=theme.css
FlagMeaning
--accent, --danger, --success, --warning, --neutral, --visitedthe colours for the roles, in hexadecimal
--brand1, --brand2, --brand3brand colours for surfaces and categories
--fontthe font stack the theme uses
--button-radius, --field-radius, --surface-radiusthe corners of buttons, fields and surfaces
--button-border, --button-weightthe border width and the weight of the button text
--out=<file>write to a file instead of to standard output

What each role controls, and how the theme is put to use, is described on Your own theme.

Checks a theme you have written yourself. The command reads the --lt-color-* values in each block and says which cell fails, and why.

Terminal window
npx @lo-tek/interfaces check-theme theme.css

One violation gives an error code. A theme made with theme already meets the requirements.

A theme without colours, with only font and shape, has no contrast to check. The command says so and exits without an error code. A file without any --lt-* values gives an error code, because then the check has not looked at anything.