Skip to content

The hem CLI

Every hem command, its options, its file selection behavior, and its exit conditions.

Updated View as Markdown

Running hem with no arguments prints the command synopsis and exits 0. There is no --help or --version; both are treated as unknown commands.

Commands that take optional file arguments share one selection rule. With no files named, the command discovers every .hem root covered by the nearest tsconfig.json. With files named, those files select their projects, and explicit inputs may span more than one nearest-tsconfig project. inspect requires one explicit module target.

A .hem file named on the command line, or reached through another file’s import, must be covered by that tsconfig’s include or files. A file outside the project is refused with a configuration diagnostic rather than checked in isolation.

hem explain

hem explain <code>
hem explain --all

Prints the diagnostic registry’s own title and explanation for a code.

--all prints every registered code sorted lexically. An unknown code exits 1 and lists the known identifiers. Arguments after the first are ignored.

hem inspect

hem inspect <file.hem[#export]> [--json] checks the selected file’s project and returns its exports, or one named export. Quote targets containing # when your shell requires it. It writes no generated files.

The report has kind: "hem-inspect-report" and schemaVersion: 1. --json writes one object to stdout with empty stderr; without it the same data is pretty-printed.

Status Meaning Exit code
resolved A checked module or export is available 0
unavailable The module or named export could not be resolved; see reason 1
failed Compilation failed; check contains the check report and no provisional exports 1
error Arguments, preparation, or inspection failed; see reason and message 1

Resolved exports include checker-rendered types, all call signatures, documentation, and definition locations. Reexports follow their declarations. Locations distinguish hem-authored from typescript-contract; they include source hashes, one-based UTF-16 line/column, and half-open spans. Hem locations are position-map anchors, which may identify a token or containing construct. Foreign definitions use exact declaration spans. An export’s static contract does not establish whether it exists at runtime.

Recognized Effect values and single-signature operations expose separate success, error, and service-requirement channels with union members. The Effect field explicitly reports unavailable for unrecognized values and overloads. Unavailable does not mean pure or requirement-free. These are static contracts, not runtime permissions or proof that a foreign implementation honors its declaration.

toolchain records Hem and TypeScript versions. snapshotSha256 fingerprints checked Hem sources, the TypeScript program’s source/declaration files, compiler options and Effect import setting. Definition hashes identify the exact source text behind locations. Re-inspect after changes; these hashes are freshness identifiers, not a portable build cache or runtime verification. Inspection uses checked in-memory output from Hem sources; it does not certify existing emitted siblings.

hem fmt

hem fmt [--check] [files...]
hem fmt --stdin

Formats Hem source. Without options it rewrites changed files and prints their paths.

Option Effect
--check Writes nothing; exits non-zero if any file needs formatting or fails to parse
--stdin Reads source from stdin, writes formatted output to stdout

--stdin cannot be combined with --check or with file arguments. Unknown options are rejected.

There is no --print-width. The CLI and the language server both use the formatter default.

hem check

hem check [--json] [files...]

Runs the full project compilation and checking path without writing output. Reports HEM3008 when an existing generated sibling no longer matches the body hash in its banner, including stale siblings recorded in the manifest.

--json may appear before or after file arguments. Unknown options are rejected before project loading. For a filename starting with a dash, use a path such as ./-example.hem; -- is not an option terminator for this command.

Machine-readable checks

hem check --json writes one JSON object to stdout and leaves stderr empty for reported check and project-loading failures. The report has kind: "hem-check-report" and schemaVersion: 1.

Status Meaning Exit code
passed Checking finished without error diagnostics; hints and warnings may remain 0
failed Checking finished with error diagnostics 1
error Arguments, project selection, or preparation failed; checking did not finish 1

files lists the checked source files on completed checks. On an operational error it records the selected or requested inputs, not proof that those files were checked; argument errors leave it empty. An error report also contains error.kind (arguments, project-selection, or project-preparation) and a human-readable error.message. Do not parse that message to identify a failure.

Each entry in diagnostics contains:

  • fileName: the source or project context associated with the diagnostic.
  • diagnostic: the existing structured diagnostic, including its stable code, severity, message, span, and any notes, relatedInformation, or fix.
  • authored: the Hem file, one-based line and column, and half-open UTF-16 span. It is null for operational diagnostics without a precise source location; do not interpret a placeholder diagnostic span as a location in that case.
  • guidance: a checker-backed contextual explanation when available, otherwise the registry explanation, or null when neither is available.

Safety diagnostics may also contain diagnostic.evidence. write-transfer identifies the conflicting property path and, for array widening, both element types. invalidated-refinement identifies the refused read and conservative invalidating operations with authored anchors; its status is unavailable when no mapped cause is available. The list is analysis provenance, not proof of a runtime mutation or an exhaustive execution trace. effect-requirements exposes checked service names for HEM4102 and supported missing-provider TS2345/TS2379 diagnostics. Evidence is additive: its absence on another diagnostic does not imply safety.

For an imported Effect operation passed to a configured Effect runner with missing services, the JSON report names the checked requirements and explains that they need Layers and Effect.provide. Terminal output from hem check and a refused hem build includes the same guidance as a diagnostic note. This guidance does not erase or handle typed failures and does not supply an automatic edit. Lookalike APIs and unrelated argument errors retain their ordinary explanations.

Quickfix edits use offsets into the original Hem source, not generated TypeScript. Apply edits only to the unchanged source that produced the report, from highest offset to lowest, then check again. A fix is not a guarantee that the whole program will pass. When no fix is supplied, use the diagnostic and guidance to choose a repair; do not add an unchecked assertion or move refused code into TypeScript to silence it.

For example, save this as main.hem in a configured Hem project:

let mut count = 0
count++
export let answer = count

Run hem check --json main.hem. HEM1035 supplies a quickfix replacing count++ with count += 1. Apply that edit to main.hem, then run the same check again. It passes; hem build main.hem produces a module exporting answer as 1. The CLI regression suite also checks this repair with CRLF line endings and a preceding Unicode comment, then executes the emitted module.

This is a command-line repair loop, not evidence of language-server update handling or editor completion quality. Process crashes and startup failures are outside this report contract.

hem build

hem build [--overwrite-edited] [files...]
hem build --watch [path]

Writes generated .ts and .tsx siblings, refuses collisions with handwritten files (HEM3002) or edits to generated siblings (HEM3008), removes unedited stale generated alternatives, and updates .hem/generated-files.json.

The banner carries hem:sha256=<16 hex>: the first 16 hex characters of SHA-256 over the body after the banner newline, with CRLF normalized to LF. An edited sibling is preserved, other files still build, and the command exits non-zero. The same guard protects stale-sibling removal. Move the change into the .hem source, then run hem build --overwrite-edited or delete the sibling and rebuild. Legacy banners without a hash remain overwritable and gain a hash on rebuild.

--overwrite-edited discards edits to generated siblings, regenerating them or removing edited stale siblings. Each overwrite or removal is reported on stderr; other errors still cause a non-zero exit. Handwritten-file collisions (HEM3002) remain refused. The flag is only available for one-shot builds: combining it with --watch is a usage error, and it is not exposed by the Vite plugin.

When two or more siblings are refused as edited, build, watch, and check add one note recommending formatter/linter exclusions via hem generated exclusions before bulk recovery. JSON check output includes the note once in diagnostic guidance.

In watch mode the first positional value is the directory to start project discovery from; further positional values are not treated as source files. Watch mode performs an initial build, then rebuilds on changes to .hem files, handwritten TypeScript/JavaScript files, package manifests, or tsconfig until interrupted with SIGINT or SIGTERM. Creating or deleting a sidecar also refreshes diagnostics. Inherited configs are watched by their resolved filenames, including configs outside the project directory and files whose names differ from tsconfig.json. Changing the inheritance chain updates those watchers; fixing an invalid inherited config triggers recovery. Other node_modules changes are ignored. Deleting a known sibling or changing a sibling that the last build refused as edited also triggers a rebuild, allowing deletion or restoration to recover immediately. Other sibling events are ignored. Vite uses the same watch and recovery behavior.

Runtime debugging

Every nonempty generated sibling ends with an inline version-3 source map. Its source URL names the adjacent .hem file, escapes URL delimiters, and contains no absolute filesystem path or authored source content. The ownership hash covers the map as part of the generated body, so map edits receive the same HEM3008 protection as code edits. There is no extra .map file to clean up when a source is removed or switches between TS and TSX.

After hem build, Node 22.18 or later can run an erasable TypeScript sibling with node --enable-source-maps src/task.ts. Error-stack frames point to the authored Hem file. JSX still needs the project’s JSX transform. Lowered control flow and generated scaffolding point to the nearest authored construct; this does not reproduce an authored variable environment or make every generated statement an independent Hem breakpoint.

The Vite plugin supplies the first map when loading the ordinary sibling. Vite’s TypeScript/JSX transform and bundler compose it; enable build.sourcemap for production maps. Local development maps include the successfully compiled Hem source so DevTools displays the correct authored text, including after a failed rebuild. Production maps omit that content by default. hem({ includeSourcesContent: true }) opts into publishing it; the downstream sourcemapExcludeSources option still wins. Publishing production maps is a deployment choice made through Vite’s normal settings.

Plain tsc does not consume a TypeScript input map. For that pipeline, decode the sibling’s final inline map using parseRuntimeSourceMap from @hem-lang/compiler, read the flat map emitted by tsc, and pass the two maps to composeRuntimeSourceMaps(downstream, upstream, intermediateSource). The third argument is the exact generated TS entry in downstream.sources, such as ../src/task.ts for an outDir build. Write the composed map where the JavaScript’s sourceMappingURL points, then run JavaScript with --enable-source-maps. These helpers support flat version-3 maps with no sourceRoot; flatten index maps and normalize source roots with the downstream tool first. A map must be retained/composed at every transformation; tools that discard maps cannot provide authored runtime locations.

hem generated

hem generated activate <project-directory>
hem generated verify <project-directory>
hem generated setup <project-directory...> [--relative-to <root>]
hem generated exclusions <project-directory...> [--relative-to <root>] [--format lines|json|biome]
Subcommand Behavior
activate Verifies the manifest, then canonicalizes and rewrites it
verify Checks the manifest and its source and sibling pairs, writing nothing
setup Registers consumer projects and manages exact Git/Prettier ignore blocks; prints one-time ESLint, Vitest, and Biome wiring
exclusions Prints the generated sibling paths for formatter, linter, and coverage configuration

--relative-to rebases printed paths against a root, and is required when passing more than one project directory. A sibling that falls outside that root is an error.

--format defaults to lines. The alternatives are json and biome.

setup does not accept --format. It writes .hem/tooling.json at the consumer root and preserves existing text outside its hashed blocks in .gitignore and .prettierignore. Subsequent build, watch, and Vite updates commit the ownership manifest and enrolled ignore blocks together. Rerunning setup replaces the registered project set. It requires valid manifests and existing Hem sources; absent siblings are allowed, while existing siblings require current hashed ownership. It refuses edited blocks, ambiguous registrations, unreadable files, and symlink write destinations.

The @hem-lang/cli/generated exports generatedESLintIgnores(root) and generatedExclusions(root, "eslint" | "coverage" | "biome") read current registered manifests without evaluating configuration. Their patterns are based on the consumer root. Follow the consumer setup guide for the required configuration wiring and failure recovery.

Unknown options and invalid combinations print the usage text.

hem migrate

hem migrate [--check] [--json] [--verbose] [--extract-implementation] <files.ts...>

Reads existing TypeScript. In the default apply mode it writes a .hem sibling for every file whose proposed migration both parses and type-checks as Hem, and refuses, leaving untouched, any file that does not. Every written file carries a banner marking it as migrated output.

Option Effect
--check Prints a per-file, per-construct readiness report; writes nothing
--json Emits the versioned machine-readable migration plan; requires --check
--verbose Reports every refusal diagnostic instead of grouped first blockers
--extract-implementation Keeps a TypeScript shell and moves self-contained function implementations into .impl.hem

Inputs must be unique .ts or .tsx paths. Unknown options are rejected. --json without --check is an error, and --extract-implementation is incompatible with --check.

Semantic API redesigns, meaning the truthiness-driven surfaces a real port would rethink, are a stated non-goal of both modes.

hem test

hem test [filters...] [--watch] [--effect <module>] [--jsx-import-source <module>] [--help]

For projects without a Vite or Vitest config, emits the project’s siblings and runs Vitest with the Hem plugin wired in and the emitted TSX transform pinned to a JSX runtime. Selects **/*.{test,spec}.{ts,tsx} under the current directory.

If the current directory contains vitest.config.* or vite.config.* (extensions ts, mts, cts, js, mjs, or cjs), exits 1 before loading configuration, emitting files, or starting tests. Register hem() from @hem-lang/vite-plugin in that config and run the project’s Vitest command to retain setup files, environment, coverage, and projects. Custom config paths also belong to the project’s Vitest command; hem test does not discover or load them.

Option Effect Default
--watch Leaves Vitest running and re-runs on change Off
--effect Overrides hem.effectImportSource The tsconfig value
--jsx-import-source Sets the JSX runtime for emitted TSX react
--help, -h Prints test-command usage without building or running tests Off

The two module options also accept the --option=value spelling. Values must contain a non-whitespace character and not begin with -. Module specifiers are preserved as supplied; quote relative paths containing spaces, for example --effect './local transport.ts'. Repeated module options use the last value. Positional values are passed to Vitest as test file path filters; use -- before a filter beginning with -.

Unsupported options (including --coverage, --config, and -t) and missing or malformed module values exit 2 with usage before emitting or testing. Vitest options are not forwarded. Use the project’s Vitest command for additional options, including test-name selection and coverage.

Vitest and @hem-lang/vite-plugin resolve from the project rather than from the CLI, so the project’s own installed versions run. When either is missing, the command reports what to install and exits 1.

Failed tests, unhandled errors, and no matching test files fail the command. Passing assertions do not erase a Vitest runner failure.

Normative source

Specification 0.2, §1 (compilation model), §13 (modules and interop), §16 (diagnostics).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close