# Osmose documentation

Public documentation: https://osmose.sh/pages/docs

This reference tracks the current source. Beta installation and account operations still require authorization.

## Contents

- [Getting started](https://osmose.sh/pages/docs#getting-started)
- [Components](https://osmose.sh/pages/docs#components)
- [Frameworks](https://osmose.sh/pages/docs#frameworks)
- [Deploying](https://osmose.sh/pages/docs#deploy)
- [Command reference](https://osmose.sh/pages/docs#commands)

## Getting started

Osmose compiles Liquid components and optional client-side islands into a
Shopify-native theme. Development runs against a Shopify store; it is not an
offline storefront renderer.

This page covers installation, scaffolding, store configuration, and the dev loop.

### Install

Request private-beta access at [osmose.sh](https://osmose.sh), then replace
`YOUR_BETA_KEY` below with your approved key. Treat the keyed URL as a credential.

On macOS or Linux:

```bash
curl -fsSL 'https://get.osmose.sh/install.sh?key=YOUR_BETA_KEY' | bash
```

On Windows, in PowerShell:

```powershell
irm 'https://get.osmose.sh/install.ps1?key=YOUR_BETA_KEY' | iex
```

The default destination is `~/.local/bin` (`$HOME\.local\bin` on Windows).
Follow the installer's PATH instructions and open a new terminal if needed.
Editor integration installation is optional.

The Bash installer checks SHA-256 when the checksum file, matching entry, and
hashing tool are available; it warns and continues if they are unavailable.
The PowerShell installer does not perform that checksum check. Neither installer
verifies a release signature. Activate your machine explicitly after installation:

```bash
osmose --version
osmose login
```

`login` uses your beta key, not your Shopify account. Released builds require an
active license for development, builds, packaging, and store operations. CI can
provide `OSMOSE_LICENSE_KEY` for headless activation. Scaffolding with `new`,
diagnostics, and update checks do not require activation.

### Prerequisites

Install a current Node.js 22 LTS patch release and a supported package manager
(`npm`, `pnpm`, `yarn`, or `bun`) for Vite and client-side integrations. The
scaffolder downloads Shopify's skeleton, so it needs network access; dependency
installation also needs access to your package registry.

The Shopify CLI backend and `build --check` additionally require Shopify CLI:

```bash
npm install -g @shopify/cli
```

An existing theme needs its Vite/framework dependencies installed too; `init`
only writes configuration and performs access setup, not a template or dependency
scaffold.

### Scaffold a project

For a new theme:

```bash
osmose new my-theme --yes --pm npm --skip-store
cd my-theme
```

This downloads the Shopify skeleton, overlays the Osmose starter (including
`components/`, `package.json`, and `osmose.toml`), and runs `npm install`.
The starter has a `development` environment with empty store credentials.
`--yes` skips interactive prompts; `--skip-install` also skips dependency
installation, in which case run `npm install` inside the project before development.
Use a new or empty target directory. `--force` permits overwriting an existing
one, and scaffolding removes the target's `.git` directory; do not use it to
adopt an existing project.

### Configure a store

Use a disposable, unpublished theme while developing. In the generated
`osmose.toml`, fill in the existing environment's `storefront_url` with your
`your-store.myshopify.com` host and `theme_id` with its numeric theme ID.
For token-based access, set `access_token` to a Theme Access password or an
Admin API token authorized for theme operations. Keep credentials out of version control.

Alternatively, leave `access_token` empty and sign in to your Shopify account:

```bash
osmose auth login
```

This is separate from `osmose login`. The account must have access to the store.
The default `theme_backend = "auto"` chooses GraphQL when a usable token or
Osmose Shopify sign-in is available, otherwise Shopify CLI with its own access setup.

For an existing theme **without** an Osmose config, supply your real store,
theme ID, and Theme Access token in the following environment variables, then run:

```bash
# Set SHOPIFY_FLAG_STORE, SHOPIFY_FLAG_THEME_ID, and SHOPIFY_CLI_THEME_TOKEN first.
osmose init
```

`init` creates a local config with a `dev` environment. It requires a store and
theme ID; it is not an interactive credential wizard. `--skip-auth` skips access
setup, not those required fields. Do not use `init --force` to add an environment:
it replaces the config. Edit the existing file instead.

`osmose config` lists configured environments, or writes a placeholder config if
none exist; it does not open an editor or prompt for credentials. Add named
`[environments.<name>]` tables manually and select one with the top-level
`active = "<name>"` setting or the TUI environment switcher. There is no global
`--env` flag (`init --env` only names the environment being created).

### Start the dev loop

From the configured theme's source directory:

```bash
osmose doctor
osmose doctor --remote
osmose dev
```

`doctor` checks local tooling, theme structure, and configuration; `--remote`
also contacts Shopify to check access without uploading theme files.

`dev` starts a watcher, attempts to start Vite, and starts a local WebSocket
server. It compiles and syncs on startup, not only after the first save.
With the Shopify CLI backend it also runs `shopify theme dev` against a generated
mirror. Open the configured theme's Shopify preview or the Shopify CLI preview
URL shown in the terminal; do not depend on a browser opening automatically.

Development **writes to the target theme**, including runtime assets and subsequent
file changes. Keep the process running and allow the preview browser to reach
the local dev servers. Missing Node/Vite is reported rather than always fatal,
but client development requires a working Vite server.

Section updates can replace rendered sections in place, and client modules use
Vite HMR. Layout/template changes, broader dependencies, or failed section
refreshes can require a full page reload; not every save preserves page state.

### Your first component

Create `components/price-badge/server.liquid`. This example is server-only;
add a client entry only when you need browser behavior.

```
components/
  price-badge/
    server.liquid
```

`server.liquid` declares its typed contract and its markup:

```liquid
{% interface PriceBadgeProps %}
  amount: number
  currency: string
{% endinterface %}
{% props PriceBadgeProps %}
  currency: "USD"
{% endprops %}

<span class="price-badge">
  {{ amount | money }} <span>{{ currency | escape }}</span>
</span>
```

Mount it from any section or layout. Pass amounts in the same units as
`product.price`; `currency` here is a display label, not currency conversion.

```liquid
{% component "price-badge", amount: 3900, currency: "USD" %}
```

Save the section and inspect the configured theme preview. The next page,
[components](https://osmose.sh/pages/docs#components), covers the tag syntax, props contract, and
hydration strategies in full. To produce deployable files or upload a production
build, continue to [deploying](https://osmose.sh/pages/docs#deploy).

Read on the web: https://osmose.sh/pages/docs#getting-started

---

## Components

Components are the unit of composition in an Osmose theme. Each one lives
under `components/<kebab-name>/`, with a required server template and an optional
client entry:

```
components/product-card/
  server.liquid      # server-rendered Liquid + typed props
  client.ts          # optional client-side island script
```

Mount a component from any section, layout, or other component using the
`{% component %}` tag. The tag is compiled away at build time. Server markup
becomes standard Shopify Liquid; interactive islands also include browser assets.

Use one client entry per directory and a valid custom-element name containing
a hyphen, such as `product-card`. The examples below all use that directory.

### The component tag

```liquid
{% component "product-card", title: product.title, price: product.price %}
```

For a typed component, compilation:

1. Locates `components/product-card/server.liquid` and validates the call against
   its named props contract.
2. Binds supplied values and omitted-input defaults to private Liquid names.
3. Inlines the component body, with its props and locals namespaced per invocation.

This is a build-time rewrite, not a separate network request or a generated
Shopify snippet. Contract declarations are erased from Liquid output; no theme
snippets or blocks are generated for them. Client type declarations are described below.

### Typed props

Typed server inputs are opt-in: declare an interface, then select it with
`{% props InterfaceName %}`. Interface members declare types; the named props
body supplies Liquid default expressions, not JavaScript or type annotations.

```liquid
{% interface CardProps %}
  title: string
  featured: boolean
  price: number
  label: string
  subtitle?: string
{% endinterface %}

{% props CardProps %}
  featured: false
  price: 0
  label: ''
{% endprops %}

<article>
  <h2>{{ title | escape }}</h2>
  {% if subtitle %}<p>{{ subtitle | escape }}</p>{% endif %}
  {% if featured %}<strong>Featured</strong>{% endif %}
  <span>{{ price | money }}</span>
  <span>{{ label | escape }}</span>
</article>
```

Mount this `product-card` with the required title and any overrides:

```liquid
{% component "product-card", title: product.title %}
{% component "product-card", title: "Sale", featured: true, price: 1200 %}
```

- `title` has neither `?` nor a default, so every call must supply it.
- A valid default makes an input omittable at the call site, but its resolved
  value remains definite: `featured`, `price`, and `label` are still boolean,
  number, and string. Defaults apply only to omitted inputs, not falsy values;
  explicit `false`, `0`, and `''` are preserved.
- `subtitle?: string` permits omission, which resolves to Liquid `nil`. It does
  **not** permit an explicit `subtitle: nil` argument. Declare
  `subtitle?: string | nil` to accept both omission and explicit nil.
  A required `subtitle: string | nil` accepts nil but still requires an argument.

Top-level names in the interface selected by named props cannot be the exact
lowercase Liquid literals `true`, `false`, `nil`, `null`, `empty`, or `blank`:
Liquid evaluates these as constants, not server input bindings. Nested object
keys, unselected interface members, and bare props payload keys are unaffected.
Use a name such as `label` for an input whose default is `''`.

`load`, `media`, `client`, `target`, and `trigger` are reserved island directive
names, not props. They cannot be interface members or named defaults.

Types include primitives, literal unions, arrays, local interfaces, and Shopify
Liquid objects such as `Product`. The syntax is data-only, not arbitrary
TypeScript: `string[]`, `('small' | 'large')[]`, and structural objects are
supported, but TypeScript generics, functions, and interface inheritance are not.

Defaults may reference other props and are evaluated in dependency order, not
declaration order; dependency cycles are errors. A same-name reference is
deliberately ambient:

```liquid
{% interface ProductProps %}
  product: Product
  title: string
{% endinterface %}
{% props ProductProps %}
  title: product.title
  product: product
{% endprops %}
```

Here `product: product` reads the caller/global Product before private inputs
are initialized, and `title` reads the resolved product, including a caller's
override. Used defaults are checked against known caller types: shadowing the
ambient `product` with a number is an error when this default is needed.
Explicitly supplying a valid product bypasses that unused default.

In a named props body, separate the next default after an **unwrapped filter
pipeline** with a newline or semicolon: commas belong to Liquid filter arguments.
For example:

```liquid
{% props CardProps %}
  label: "Sale" | append: "!"
  price: 0
  featured: false
{% endprops %}
```

Component arguments remain comma-separated, including after a filtered value:

```liquid
{% component "product-card", title: product.title | append: "!", price: product.price %}
```

#### Bare props compatibility

An unnamed `{% props %}` block retains its existing raw JavaScript-plus-Liquid
client payload behavior and best-effort inference:

```liquid
{% props %}
{% assign json_escape = 'XHUwMDNj' | base64_decode %}
  title: {{ product.title | json | replace: '<', json_escape }},
  featured: false,
{% endprops %}
```

This body is passed through as an object expression, not a typed server
contract. Its keys create no server bindings, and invocation arguments are not
merged into the payload as defaults; existing caller-argument assignments remain
separate. Bare JavaScript syntax is unchanged. Declaring an interface alone does
not opt in: only a named props block enables the typed contract.

The compiler emits the legacy payload before the component body. Keep Liquid
preparation inside the `{% props %}` block, as above; an `assign` elsewhere in the
component body runs too late to prepare its seed.

#### Diagnostics and editor support

The compiler and LSP report malformed declarations/defaults, unknown types,
duplicate or unknown inputs, missing required arguments, and provable type
mismatches. Calls are checked at their authored source locations, including
nested calls whose inputs come from known props, property paths, assignments,
or loop aliases. Unsaved contract edits propagate to dependent caller diagnostics.
Completion offers interfaces, types, and props; hover explains declared types,
defaults, and requiredness; definition navigation links calls and prop references
back to their declarations.

This is static checking, not Liquid execution. Unresolved globals, dynamic
lookups, stateful tags, or filters with unknown results may remain unknown rather than produce
a type mismatch. Conditions do not narrow types; a successful check does not
prove an unknown runtime value satisfies the contract.

#### Liquid scope and supported constructs

Typed props and ordinary locals (`assign`, `capture`, and loop aliases) receive
private names. Locals reset on **every execution**, including repeated execution
of the same call inside a loop. Nested components cannot overwrite a parent's
bindings. Legacy children inside a typed subtree also have their executable
Liquid names isolated, including Liquid embedded in a bare props payload; the
JavaScript text itself remains raw. Legacy-only trees retain their existing scope
behavior.
Free reads can still see caller/global values; isolation protects bindings that
the component writes, rather than creating a Shopify `render`-style scope.
Explicitly passing an input is clearer than depending on an ambient caller local.
Loop aliases are lexical: an outer input, local, or global remains available
before and after the loop, and in its empty-loop `else` branch. `assign` and
`capture` write the component's root-local binding even when an active loop
alias shadows reads of the same name.


Ordinary conditionals, component-owned `for`/`tablerow` loops, and multiline
`{% liquid %}` statements are supported, but isolation is not a promise to accept
every native Liquid construct. Unsupported scope or state semantics are explicit
compile errors:

- `break`/`continue` cannot target an outer caller's loop. Implicit loop, form,
  and pagination objects require their owning tag inside the component.
- `forloop` requires a static property access; whole-object or dynamic access,
  and `parentloop` traversal outside component-owned loops, are rejected.
- `include` shares dynamic caller scope and is rejected; use literal-target
  `render` with explicit arguments. Dynamic `render`/`component` targets and
  unknown tags with unrecognized binding semantics are rejected.
- `offset: continue` and `ifchanged` use persistent native state and are rejected.
- `increment`, `decrement`, and explicitly literal-grouped `cycle` are lowered to
  resettable private state only as standalone tags outside `for`, `tablerow`,
  `form`, and `paginate`. They are rejected inside those scopes or a
  `{% liquid %}` block. Implicit/dynamic cycle groups and filtered cycle values
  are unsupported.

#### Client values and Shopify types

Named props serialize the resolved values into the island's `<script data-props>`
seed. Every declared key is present: omitted optional
values become JSON `null`, not absent keys or `undefined`. Serialization tests
for nil explicitly, preserving
false, zero, and empty strings, and escapes `<` so serialized values cannot terminate
the script element. The seed is a JavaScript object expression containing JSON
values, not a standalone JSON document; the browser evaluates it rather than
calling `JSON.parse`. Bare props are not automatically escaped: apply `json` and
script-safe escaping to untrusted Liquid output in a legacy payload.

The generated root `osmose.d.ts` exposes two views:

- `ComponentInputs["product-card"]` exposes caller requiredness: defaulted and
  optional inputs may be omitted. Its value types are conservative, JSON-safe
  approximations; the compiler and LSP check the actual declared Liquid types.
- Component-specific `Props` interfaces describe resolved client `.props`:
  every typed key exists, and optional-without-default values include `null`.

Shopify Liquid types come from the bundled Shopify object/filter registry, with
a validated user-cache revision preferred when available. The LSP automatically
checks for registry updates in the background; failed or offline refreshes keep
the last good data. Compiler lookups use the available registry snapshot. No
project/theme type regeneration or additional generated files are needed to
refresh this registry.

A Liquid `Product` is a server Drop, not a promise about the object produced by
Shopify's `json` filter. Whole Drops and structural object values therefore have
an honest `unknown` client wire type (arrays of Drops become `unknown[]`), not
invented TypeScript Shopify interfaces. Project scalar fields such as
`title: product.title` and `price: product.price` into typed props when client code
needs those fields.

#### Native client contract types

Osmose generates `osmose.d.ts` from the server contracts during build and dev;
the Liquid LSP also updates it as contracts change, including unsaved edits.
You do not need to launch the LSP before building. Treat the declaration file as
generated output, not a second place to maintain your interface.

Bind the generated resolved type using your framework's normal TypeScript API.
For `components/product-card/client.tsx`, this React example selects React's
automatic JSX runtime explicitly:

```tsx
/** @jsxImportSource react */
import type { ProductCardProps } from '../../osmose';

export default function ProductCard({ title, price }: ProductCardProps) {
  return <span>{title}: {price}</span>;
}
```

For Preact, use the same function with `/** @jsxImportSource preact */` instead.
Both require the matching integration and a TypeScript configuration with
`"jsx": "react-jsx"`; see [native type checking](https://osmose.sh/pages/docs#frameworks-native-type-checking).

The name comes from the component (`product-card` → `ProductCardProps`), not the
name of the Liquid interface selected by `{% props %}`. Use this **resolved**
type for client values, not `ComponentInputs["product-card"]`: caller inputs
can omit defaulted keys, while the client receives those keys with definite values.

Native web components and Lit can declare their payload without emitting a
field initializer that would overwrite the runtime value:

```ts
import type { ProductCardProps } from '../../osmose';

class ProductCard extends HTMLElement {
  declare props: ProductCardProps;

  connectedCallback() {
    this.textContent = this.props.title;
  }
}
customElements.define('product-card', ProductCard);
```

For Lit, declare the same container inside a complete `LitElement` class:

```ts
import { LitElement, html } from 'lit';
import type { ProductCardProps } from '../../osmose';

class ProductCard extends LitElement {
  declare props: ProductCardProps;

  render() {
    return html`<span>${this.props.title}: ${this.props.price}</span>`;
  }
}
customElements.define('product-card', ProductCard);
```

If you prefer named Lit properties, declare them in `static properties` and type
each from the generated type, such as `declare price: ProductCardProps['price']`.
Declaring a TypeScript field alone does not make it reactive. Native DOM names,
methods, and readonly properties are protected from automatic named assignment;
the full payload remains available in `.props`.
For Solid, annotate the `customElement()` callback parameter:

```tsx
/** @jsxImportSource solid-js */
import { customElement } from 'solid-element';
import type { ProductCardProps } from '../../osmose';

customElement('product-card', (props: ProductCardProps) => {
  return <span>{props.title}: {props.price}</span>;
});
```

Solid's native checker uses `"jsx": "preserve"` and
`"jsxImportSource": "solid-js"`; Vite's Solid plugin performs the JSX transform.

Vue 3.3+ single-file components can import the type into their native props API:

```vue
<script setup lang="ts">
import type { ProductCardProps } from '../../osmose';
const props = defineProps<ProductCardProps>();
</script>

<template>
  <span>{{ props.title }}: {{ props.price }}</span>
</template>
```

Svelte 5 uses its native props rune:

```svelte
<script lang="ts">
import type { ProductCardProps } from '../../osmose';
let { title, price }: ProductCardProps = $props();
</script>

<span>{title}: {price}</span>
```

JavaScript clients can use standard JSDoc, for example
`@param {import('../../osmose').ProductCardProps} props`, with `checkJs` enabled.
Existing author annotations are not rewritten; use or reference the generated
type wherever you want the Liquid contract checked.

These small bindings let stock `tsc`, `vue-tsc`, and `svelte-check`, and your
normal TypeScript/Vue/Svelte editor extensions, consume the same declarations.
Enable strict checking (including `strictNullChecks`) to catch unsafe nullable
access. Native rename, references, signature help, formatting, and SFC style
tooling remain with their native services. Osmose supplies Liquid tooling and
declarations, not a second native client diagnostic service, special client
language modes, or an editor takeover.

Type bindings are erased at runtime. Mount-time prop delivery remains automatic,
independently of whether a client uses TypeScript or imports a generated type.

These types describe the JSON boundary above, not server Drops. A required
`Product` prop remains `unknown` on the client; project the scalar fields your
client needs. See [framework mounting](https://osmose.sh/pages/docs#frameworks-per-framework-specifics) for
when values are available and which registration forms are supported.

### Imports

`{% import %}` loads an **asset**, not a component. Paths are relative to the
theme's `assets/` directory:

```liquid
{% import "theme.css" %}
{% import "theme.js" %}
```

Create `assets/theme.css` and browser-ready `assets/theme.js` before using these examples.
CSS imports emit stylesheet links; script imports emit module script tags.
Development points to Vite, while production uses Shopify asset URLs.
Linked script imports keep their source filenames and contents in production;
they do not turn a `.ts` URL into compiled JavaScript. To compile a TypeScript
asset into the page, use the supported inline path:

```liquid
{% import "theme.ts" | inline %}
```

`{% import "theme.css" | inline %}` embeds CSS in production; dev keeps it linked.
Without Tailwind, plain CSS is returned as authored. Precompile Sass/SCSS/Less
to CSS before linking: the current non-Tailwind inline style path also returns
preprocessor source unchanged.

Components need no import declaration: `{% component "product-card", title: "Sale" %}`
resolves the directory directly and records its use for the dependency graph.
`{% import "product-card" %}` is invalid because it has no supported asset extension.

Use normal JavaScript imports inside a client entry for dependencies and styles.
Framework-local styles follow that framework's DOM model: Lit, Vue custom
elements, and Svelte custom elements normally use shadow DOM, which page-level
CSS does not cross. Load CSS needed by the initial Liquid markup separately;
do not rely on a deferred client import for the first server-rendered paint.

### Hydration strategies

Adding a `client.*` entry creates an island. Supported extensions are `.js`,
`.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts`, `.vue`, and `.svelte`.
Named contract metadata and generated client types cover all of these entries.

Osmose emits an `<island-element>` around the component's custom-element tag
and server markup. With a client file, **omitting `load:` defaults to `idle`**:

- `idle` — schedule with `requestIdleCallback`, falling back to a short timer.
- `load` or `eager` — schedule on the next animation frame after the wrapper
  connects; neither waits for the window `load` event.
- `visible` — use an intersection observer configured with a `1.0` threshold;
  it starts loading when a delivered entry is intersecting. This is not a
  configurable near-viewport prefetch margin.
- `media` — check `media:` on connection, then on window resize until it matches.
  Changes without a resize, such as a color-scheme change, are not watched.
  Without a media query it loads immediately.

Pick the strategy at the mount site, still supplying required typed inputs:

```liquid
{% component "product-card", title: product.title, load: "idle" %}
{% component "product-card", title: product.title, load: "eager" %}
{% component "product-card", title: product.title, load: "visible" %}
{% component "product-card", title: product.title, load: "media", media: "(min-width: 768px)" %}
```

Without a client file, the compiler emits the component tag and server content
without an island wrapper. There is no documented server-only switch for a
component that has a client file. Unknown nonempty strategies currently load
immediately rather than disabling JavaScript; use the listed values.

`load` and `media` configure loading. `target` and `trigger` are optional literal
CSS selectors describing elements an island controls and controls that open it;
they supply visual-ownership hints to the editor, not event listeners.
`client` is reserved for compatibility but does not select an entry or disable
hydration. None of these directives becomes a Liquid prop binding.

### Server vs client

The compiled contents of `server.liquid` run on Shopify when the theme renders,
with the Liquid globals available in that rendering context, subject to the
typed component scope restrictions above.
The browser prepares the `<script data-props>` seed, imports the client module
unless the custom element is already registered, installs the resolved values,
and replaces the island wrapper with the component element. Register the matching
tag or use a supported automatic registration form; an ordinary script that
never registers the tag cannot mount an island.

The rule of thumb: server-render everything you can, and reach for
`client.ts` only when a component genuinely needs interactivity, state, or a
framework primitive. The initial paint should show the correct content
without JavaScript. This is island activation, not a guarantee of framework SSR
hydration: React/Preact mount a new client tree, and a shadow-DOM framework may
replace or hide the Liquid fallback. Design and style both states deliberately.

Read on the web: https://osmose.sh/pages/docs#components

---

## Frameworks

Osmose supports native custom elements plus Lit, Preact, React, Vue, Svelte,
and Solid client entries. Tailwind is available for styling. Each island must
ultimately register a custom element whose tag matches its component directory.

Supported integrations:

- **Native custom elements**: browser APIs in JavaScript or TypeScript; no framework package.
- **Lit**: web components authored in TypeScript.
- **Preact**: JSX components and `preact-custom-element` registrations.
- **React**: JSX components with `react` and `react-dom`.
- **Vue**: `.vue` single-file components via `@vitejs/plugin-vue`.
- **Svelte**: `.svelte` components via `@sveltejs/vite-plugin-svelte`.
- **Solid**: JSX components with `solid-js` and `solid-element`.
- **Tailwind**: utility-first CSS via `@tailwindcss/vite`.

### Auto-detection

Osmose preserves the **active environment's** configured integrations and adds
frameworks detected from `package.json` dependencies/devDependencies or immediate
`components/*/client.*` filenames:

| Integration | Package signals | Filename signal | Required Vite plugin |
| --- | --- | --- | --- |
| Preact | `preact`, `preact-custom-element` | None | `@preact/preset-vite` |
| React | `react`, `react-dom` | None | `@vitejs/plugin-react` |
| Vue | `vue` | `.vue` | `@vitejs/plugin-vue` |
| Svelte | `svelte` | `.svelte` | `@sveltejs/vite-plugin-svelte` |
| Solid | `solid-js`, `solid-element` | None | `vite-plugin-solid` |

Filename detection does not install packages. JSX imports help the mount
transform distinguish React from Preact; they do **not** independently configure
a Vite integration. `.tsx` alone does not identify a framework.

Lit and native custom elements do not need a dedicated Vite plugin. A saved
`lit` integration is accepted without loading one. Tailwind is not
package-auto-detected; `osmose add tailwindcss` records its configuration as shown
[below](https://osmose.sh/pages/docs#frameworks-tailwind). These plugins run with Osmose's built-in options, not
per-component include/exclude rules; see [mixing frameworks](https://osmose.sh/pages/docs#frameworks-mixing-frameworks).

### Adding a framework

Use the `add` subcommand to install and wire up a new framework:

```bash
osmose add lit
osmose add preact
osmose add react
osmose add vue
osmose add svelte
osmose add solid
osmose add tailwindcss
```

`osmose add` selects the package manager from project/global configuration or
package-manager detection (npm, pnpm, yarn, bun), then installs the integration's
dependencies. It then attempts to save the integration in the active environment,
printing a warning if configuration persistence fails. It does not rewrite
client source or `tsconfig.json`. Package detection also enables the corresponding
JavaScript integration; Tailwind depends on its configured entry.

The command also supplies missing Vite, TypeScript, and `cjs-module-lexer`
tooling without changing existing pins. React adds its runtime, Vite plugin, and React type
packages; Vue adds `vue-tsc`, and Svelte adds `svelte-check`. React is also
available when scaffolding a new project. `osmose deps` reports any required
package a project lacks, `osmose deps --install` adds them, and `osmose update`
runs that reconciliation after installing a new version. If configuring a
project by hand, install TypeScript even for JavaScript-only islands: Osmose's
mount transform uses its parser. `cjs-module-lexer` is required for production
framework bundles: it discovers CommonJS exports without executing packages,
so native named imports such as React hooks remain available through the import map.

### Native type checking

Runtime registration and prop delivery below do not require type annotations.
For editor and compiler checking, bind the generated resolved type from the
root `osmose.d.ts` using standard TypeScript:

- **Native web components and Lit**: `declare props: ProductCardProps`.
- **React and Preact**: `function ProductCard(props: ProductCardProps)`.
- **Solid**: `(props: ProductCardProps) => …` in `customElement()`.
- **Vue**: `defineProps<ProductCardProps>()`.
- **Svelte 5**: `let { title, price }: ProductCardProps = $props()`.

Import it with `import type { ProductCardProps } from '../../osmose'` inside
`components/product-card/client.*`. No copied props interface is needed.
`ProductCardProps` describes resolved JSON values; `ComponentInputs["product-card"]`
describes values supplied by a Liquid caller, including omittable defaulted inputs.
See [client contract types](https://osmose.sh/pages/docs#components-native-client-contract-types) for complete examples.

Build/dev generates these declarations without an LSP prerequisite. Your normal
native editor services and stock `tsc`, `vue-tsc`, or `svelte-check` consume them.
Keep the usual TypeScript, Vue, and Svelte language modes and extensions; native
refactoring, formatting, and style tooling remain available. There is no separate
Osmose native diagnostic service to run.

Your checker still needs a native `tsconfig.json`. For a React client at
`components/product-card/client.tsx`, a minimal starting point is:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  },
  "include": ["osmose.d.ts", "components/product-card/client.tsx"]
}
```

Use TypeScript 5+ for this configuration. For Preact change `jsxImportSource` to
`preact`. For Solid use `"jsx": "preserve"` and `"jsxImportSource": "solid-js"`.
Native custom elements and Lit do not need JSX options. Vue and Svelte need their
SFCs included instead of just `client.tsx`, and their native checkers rather
than plain `tsc`. Separate checker configurations avoid competing JSX namespace
types in a mixed-source project; they do not isolate Vite's runtime transforms.

After Osmose has generated `osmose.d.ts`, run the installed checker from the
theme root (these commands assume the matching `tsconfig.json`):

```bash
npx --no-install tsc --noEmit -p tsconfig.json
npx --no-install vue-tsc --noEmit -p tsconfig.json
npx --no-install svelte-check --tsconfig ./tsconfig.json
```

Choose one command for the corresponding framework, not all three for every
project. For JavaScript clients, enable `allowJs` and `checkJs` and bind the
generated type through JSDoc.

The examples use current native APIs: React 18+ (`react-dom/client`), Vue 3.3+
(imported `defineProps` types), and Svelte 5 (`$props`). A Svelte legacy
`export let` declaration can also reference the generated type, but the automatic
contract projection inserts Svelte 5 runes when no props declaration is present.
Do not assume these examples support older major versions. Keep each framework,
Vite plugin, and checker on mutually compatible versions.

### Per-framework specifics

- **Lit and native web components**: register a custom element whose tag matches
  the component directory. Read the full payload from `this.props`, or declare
  named properties such as Lit reactive properties. Values are installed after
  construction and before the supported connected lifecycle; do not read server
  props in a constructor or field initializer.
- **Preact and React**: default-export your `client.tsx` component. Osmose
  registers the directory's tag and passes the payload as the component's props.
  React uses `react-dom/client`; Preact uses its native renderer. Existing
  explicit `customElements.define()` registrations are not auto-wrapped again.
  If you use `preact-custom-element`'s default import, its two-argument
  `register(Component, 'product-card')` form gets inferred contract prop names
  only when the component has no `observedAttributes` or `propTypes`. An explicit
  observed-props argument wins. Register the directory's actual tag.
- **Vue**: a `client.vue` single-file component is registered through
  `defineCustomElement`. Server values arrive as named Vue props. Osmose runs
  the Vue plugin in custom-element mode so SFC styles can accompany the element.
- **Svelte**: a `client.svelte` component is compiled as a custom element and
  registered with the directory's tag. An explicit `<svelte:options
  customElement="…" />` or `customElement={false}` remains an author decision;
  an explicit tag must match the tag your Liquid island mounts.
  `customElement={false}` opts out of the required automatic registration:
  provide your own registering adapter if the entry is still used as an island.
- **Solid**: register the directory's tag with `customElement()` from
  `solid-element`. Contract prop names are merged into its defaults without
  replacing explicit defaults; the callback receives the server-resolved values.
  The transform recognizes direct imported calls (including aliased/namespaced
  imports), not arbitrary wrappers around the registration function.

#### Values at the first render

When Osmose's runtime is installed **before the client element is registered**,
it defers that element's connected lifecycle while it is inside an island
wrapper. The seed is applied before the connected render, including when another
island has already registered the same tag. A custom element registered before
the Osmose runtime was loaded is outside this lifecycle guarantee.

`false`, `0`, `""`, `null`, arrays, and nested objects are values, not signals to
use an Osmose fallback. Nested values are not JSON-stringified into HTML
attributes. The runtime sets `.props` plus eligible named properties; it protects
native DOM members unless explicitly declared, and does not overwrite methods,
getter-only properties, or special keys such as `constructor`. Read `.props` for
the complete payload in native/Lit elements. Framework-owned prop declarations
and coercion still matter; use the generated type with compatible native APIs.

For Vue/Svelte, the build may inject missing runtime-facing props declarations
from a named Liquid contract. This does not edit the authored file or make a
stock editor infer undeclared variables. Keep the explicit generated-type
bindings in source for native checking.

The browser uses the values produced by Liquid, not an independent copy of the
server defaults. See [typed props](https://osmose.sh/pages/docs#components-typed-props) for required inputs,
omitted values, and the JSON-safe client type boundary.

### Tailwind

Tailwind is added the same way as the JS integrations:

```bash
osmose add tailwindcss
```

The command installs `tailwindcss` and `@tailwindcss/vite` and records the plugin
in the active environment. If you installed the packages by hand or the command
warned that it could not save configuration, add this entry manually:

```toml
[[environments.dev.integrations]]
name = "tailwindcss"
```

Replace `dev` with your active environment's name. For Tailwind 4, create a CSS
entry such as `assets/theme.css`:

```css
@import "tailwindcss";
```

Load it from the layout with `{% import "theme.css" %}`. `osmose add` does not
generate this file, insert CSS directives, or configure source scanning for you.
Follow the installed Tailwind version's setup for source detection and any
shadow-root styling needs.

### Mixing frameworks

Separate custom-element tags can coexist on a page, and Lit/native elements can
sit alongside `.vue` and `.svelte` entries. However, all effective integration
plugins are loaded into the same Vite configuration with their default options.
Osmose does not give React, Preact, and Solid separate JSX include/exclude rules.
Installing multiple JSX frameworks may therefore make their transforms compete;
per-file imports or TypeScript `jsxImportSource` alone are not a guarantee that
the combined build is supported.

Use one JSX framework per theme unless you have verified the exact combination
against a real build and browser mount. Per-framework examples and native
typechecking are not proof of mixed-JSX compatibility. Production component
entries reference shared dependencies through the theme's bundles/import map;
this is not a promise that selecting a framework adds zero unused output.

Read on the web: https://osmose.sh/pages/docs#frameworks

---

## Deploying

Osmose compiles source into Shopify-native theme files. `build` validates or
compiles locally, `package` writes a distributable directory, and `push` compiles
and uploads. These are distinct operations: `build` does not automatically write
`dist/`, and `push` should normally receive your **source theme**, not `dist/`.

Released builds require an activated Osmose beta license for these commands.
Production asset compilation needs the project's Node/Vite/framework dependencies.
Run commands from the configured source theme directory unless an override is
shown below.

### Build and package locally

These commands do not upload theme files:

```bash
osmose build
osmose package --output dist
```

`build` compiles the full production theme and reports the result. With a Shopify
CLI backend it also creates a local mirror under `.osmose/`; GraphQL builds do
not write that mirror. For a predictable output directory, use `package`.
Its `--output` path is relative to the active theme's source directory unless
absolute. It writes plain theme files, **not a ZIP**, and does not run Theme Check.
Use a fresh output directory because packaging overwrites matching files without
removing stale ones.

To run Shopify Theme Check on compiled output, install Shopify CLI first:

```bash
osmose build --check --fail-level error
```

Theme Check requires no Shopify store access. It uses a generated mirror, not
your uncompiled Osmose source. Compilation may still write local artifacts.
`--profile` additionally measures the already-remote theme, not this local build;
profiling is skipped if Shopify sign-in or `[settings.perf]` budgets are absent.

You can hand the packaged `dist/` theme to Shopify CLI directly. For example,
after installing Shopify CLI and supplying your real store host and a disposable
theme ID in `SHOPIFY_FLAG_STORE` and `SHOPIFY_FLAG_THEME_ID` (with CLI authentication
or a Theme Access token configured), this **uploads**:

```bash
shopify theme push --path ./dist \
  --store "$SHOPIFY_FLAG_STORE" --theme "$SHOPIFY_FLAG_THEME_ID" --nodelete
```

### Select a target and backend

In `osmose.toml`, the top-level `active` value selects an
`[environments.<name>]` table containing `storefront_url`, `theme_id`,
`access_token`, and `directory`. `access_token` may be empty with an appropriate
Shopify sign-in. Use the store's `your-store.myshopify.com` host and a numeric
theme ID. Change `active` in the file or use the TUI environment switcher;
there is no global `--env` flag.

Backend selection is configured under `[settings]`:

```toml
[settings]
theme_backend = "auto"
```

Merge that key into an existing settings table rather than duplicating it.
You can override it per invocation with `--backend`:

- `auto` (default): GraphQL when the active environment has a usable token or
  Osmose-managed Shopify sign-in; otherwise Shopify CLI.
- `graphql`: Direct Shopify API operations. No Shopify CLI dependency, but the
  build still needs its local asset tooling. Use a Theme Access password, an
  appropriately authorized Admin API token, or `osmose auth login`.
- `shopify-cli`: Requires the `shopify` executable and access through its token
  or account flow. Osmose compiles a mirror and runs Theme Check before pushing.
  This is not the same session store as `osmose auth login`.

Theme reads and writes remain subject to Shopify permissions and API restrictions;
an arbitrary Admin API token is not enough. Theme-file writes need authorized
theme access (including `write_themes` for Admin API credentials). Storefront
passwords are separate from API/Theme Access tokens.

### Local push

After verifying the active environment targets the intended theme:

```bash
osmose push
```

**This writes remote theme files and can update a live theme.** There is no
general confirmation step to rely on. Prefer an unpublished test theme.

The command builds production output and uploads through the selected backend.
GraphQL sends the prepared files through batch upserts; it does not first use the
dry-run plan to upload only changed files. Shopify CLI handles its own transfer.
Normal push does not delete remote-only files; Shopify CLI is invoked with
`--nodelete`.

Shopify CLI push uses a temporary mirror and cleans that mirror afterwards.
Use `--keep-artifacts` to retain it, or `--mirror-dir .osmose/review-theme`
for a persistent mirror. Diagnostics can remain under `.osmose/`.

### Dry run

With the same store access required for remote reads:

```bash
osmose push --dry-run
```

This compiles locally, reads the remote theme, and prints create/update/delete/skip
comparisons without uploading or deleting remote theme files. It is **not offline**
and may write local artifacts. The Shopify CLI backend also runs Theme Check and
downloads a local snapshot for comparison.

`DELETE` entries describe remote-only files in the comparison, not actions that a
subsequent normal `push` will execute. Remote deletion requires a separate explicit
command, such as the following **destructive example** after replacing the path:

```bash
osmose delete assets/obsolete-file.js --yes
```

Do not treat a dry-run result as a transaction or a guarantee of a later upload's
success: the remote theme and access permissions can change.

### Pull

```bash
osmose pull
```

This reads Shopify and writes into the active environment's local source
directory. It can overwrite local files, so commit or back up your work first.
Liquid files containing Osmose component/import directives are protected by
default; `--force` explicitly permits replacing that authored source with remote
output. Pull does not reconstruct components from compiled Liquid and is not the
inverse of compilation.

### Headless push

Supply real values through your CI secret/configuration system:

- `OSMOSE_LICENSE_KEY`: an approved beta key if the runner is not activated.
- `SHOPIFY_FLAG_STORE`: the store's `your-store.myshopify.com` host.
- `SHOPIFY_FLAG_THEME_ID`: the target theme's numeric ID.
- `SHOPIFY_CLI_THEME_TOKEN`: its Theme Access token.

With these set, from a theme source directory whose dependencies are installed:

```bash
osmose push --backend graphql --directory . --dry-run --json
# Remove --dry-run only when you intend to upload to the configured theme.
```

For push, `--url`, `--theme`, and `--password` take precedence over the corresponding
environment variables in the headless target path. Both explicit `--url` and
`--theme`, or both store/theme environment variables, activate that path.
`--directory` is a push **headless-mode** flag; it defaults to `.` there.
Otherwise push uses the active environment's source directory.

Headless push can run without `osmose.toml`; when present, config can still supply
integrations and settings. These flags are not generic overrides for every config
field. `dev` does not accept these store/directory flags and requires a configured
active environment. A fresh CI runner cannot complete interactive Shopify sign-in;
use a token rather than assuming a developer's cached login exists there.

### Publish: choose the backend explicitly

`publish` does **not** mean the same thing for both backends:

- **GraphQL:** Requires `--name`. Creates a theme, then compiles and pushes to it.
  `--role` defaults to `unpublished`; `--role live` can change the live storefront.
  Creation happens before the build/upload, so a later failure can leave the new
  theme behind.
- **Shopify CLI:** Uses `--name` as the target theme selector, or falls back to
  configured `theme_id`. It pushes that target and runs
  `shopify theme publish --force`, making it **live**. It does not create a new
  unpublished preview theme, and `--role unpublished` does not make this path safe.

For a new unpublished theme using the configured store and authorized GraphQL
access, replace the example name if desired:

```bash
osmose publish --backend graphql --name "Osmose preview" --role unpublished
```

This still creates and uploads a remote theme. Use `package`, not `publish`, when
you only want local distributable files.

Read on the web: https://osmose.sh/pages/docs#deploy

---

## Command reference

Common CLI entry points and their important constraints. Run
`osmose <command> --help` for that command's flags. Most project/store commands
in released builds require an activated Osmose beta license; see
[getting started](https://osmose.sh/pages/docs#getting-started).

### Project

- `osmose new [directory]` — Download Shopify's skeleton, overlay the starter,
  and install dependencies. Use `--yes` without a terminal, `--pm npm` (or
  `pnpm`, `yarn`, `bun`) to choose a package manager, `--frameworks react,tailwindcss`
  to choose integrations, and `--skip-install` to defer installation.
  `--force` permits a non-empty destination; scaffolding removes the target's
  `.git` directory, so do not use it to adopt an existing project.
- `osmose init` — Write config for an existing theme. Requires `--store` and
  `--theme` (or their environment variables); `--password` supplies a token.
  `--env` names the new environment (default `dev`), `--directory` sets its
  source directory, and `--skip-auth` skips backend access setup.
  Existing config is rejected unless `--force` is supplied, which replaces it.
  `--global` writes global rather than project config.
- `osmose config` — List existing environments/config location, or create a
  placeholder when there are no environments. Edit the file yourself to supply
  credentials or add environments; this command is not an interactive editor.
- `osmose add <integration>` — Add and install a framework or Tailwind integration:
  `lit`, `preact`, `react`, `vue`, `svelte`, `solid`, or `tailwindcss`.

### Development and local output

- `osmose dev` — Start the configured environment's development loop, compile,
  and sync to Shopify immediately and on changes. Starts local WebSocket/Vite
  services and, for the Shopify CLI backend, `shopify theme dev`. This writes
  remote theme files. `--debug` also writes compiled files under the source
  theme's `debug-output/`. It has no store, directory, or `--env` override flags.
- `osmose build` — Compile the full production theme without uploading.
  `--directory` selects a source theme/config; `--check` also runs Shopify Theme
  Check with `--fail-level` (default `error`). Theme Check needs Shopify CLI but
  not store access. This is not the command that writes `dist/`; use `package`.
- `osmose build <file> [files...]` — Compile individual files and print labelled
  output to stdout. File arguments cannot be combined with `--check` or JSON
  output; this is not a full production asset build.
- `osmose package --output dist` — Compile production files into a directory
  (default `dist`, relative to the active source theme). No ZIP is created,
  no remote upload occurs, and no Theme Check is run. Use a fresh output directory:
  existing files are overwritten but stale files are not cleared.
- `osmose lsp --stdio` — Run the Liquid language server for an editor client.
- `osmose doctor` — Check local dependencies, theme structure, and configuration.
  `--directory` overrides the theme location; `--remote` adds a Shopify access
  check without uploading theme files. Errors produce a nonzero exit status.
- `osmose deps` — Compare `package.json` with the packages this version
  requires: shared build tooling plus each configured or detected integration's
  packages. Missing packages are listed with the exact install command and
  produce a nonzero exit status. `--install` records and installs them with the
  project's package manager; `--json` prints the report as one object.

Build/package can write generated local artifacts even though they do not write
to Shopify. Shopify CLI builds use a mirror under `.osmose/`; GraphQL builds do
not emit that mirror. Use `package` when you need a predictable output directory.

### Store operations

- `osmose push` — Compile production files and upload to the selected theme.
  `--dry-run` instead reads remote files and prints a comparison.
  `--theme`, `--url`, `--password`, and `--directory` support a headless target;
  see [deploying](https://osmose.sh/pages/docs#deploy) for their precedence and safety limitations.
- `osmose pull` — Download remote theme files into the configured source directory.
  This can overwrite local files. Local Liquid containing Osmose component/import
  directives is protected unless `--force` is supplied. It is not a source-code
  decompiler or a guaranteed backup operation.
- `osmose delete <theme-file> [theme-file...] --yes` — Delete explicitly named
  remote files. `--theme` overrides the target theme ID.
- `osmose publish` — **Backend-dependent and potentially live-store-changing.**
  GraphQL requires `--name` and creates a theme before pushing; `--role` defaults
  to `unpublished`. Shopify CLI pushes the target named by `--name` or configured
  `theme_id`, then publishes it live with force. See [deploying](https://osmose.sh/pages/docs#deploy).

`push` and `publish` use temporary Shopify CLI mirrors by default. Their
`--keep-artifacts` flag retains the temporary mirror; `--mirror-dir` chooses a
persistent mirror under `.osmose`.

### Profiling

- `osmose profile` — Request Shopify's Liquid render profile for `/` on the
  active environment's remote theme. Requires `osmose auth login`; a Theme Access
  token alone cannot profile. It does not upload your current source.
- `--url /products/your-handle` selects a real storefront path; without `--all`,
  only the first supplied URL is used. `--theme` overrides the preview theme.
- `--samples 3` selects repeated measurements (maximum 10); the report uses one
  median-total request, choosing the lower median for an even sample count.
  `--top 0` shows all files, `--json` prints JSON, and `--raw profile.json` writes
  a raw speedscope report for a single-page profile.
- `osmose profile --all` profiles routable local templates against the store,
  reports a section-by-template matrix, and caches it at
  `.osmose/reports/profile-all.json`. Templates without a generic route may be
  skipped; `--url` supplies extra paths.
- `osmose build --check --profile` runs Theme Check, then profiles the remote
  storefront against `[settings.perf]` budgets. Profiling is skipped when budgets
  or an Osmose Shopify sign-in are missing; a successful build alone is not proof
  that profiling ran. This does not push the just-built theme.

### License and Shopify sign-in

- `osmose login` — Activate the machine with an Osmose beta key, supplied
  interactively, through `OSMOSE_LICENSE_KEY`, or with `--key`.
- `osmose logout` — Remove the machine's Osmose license activation.
- `osmose auth login` — Sign in to Shopify via device flow. `--no-browser`
  prints the link instead of opening a browser.
- `osmose auth status` — Show Shopify sign-in state and cached store access.
- `osmose auth logout` — Forget the machine's Osmose-managed Shopify sign-in.

### Maintenance

- `osmose update` — Check for updates, review notes, and confirm installation.
  Inside a project, the update then runs `osmose deps --install` with the new
  binary so packages the new version requires are recorded and installed. When
  already current, the same reconciliation runs in place.
- `osmose update --notes` — Read the latest release's notes without downloading
  a binary or installing. This still contacts the release service.
- `osmose update --force` — Download and replace the executable without
  confirmation. The updater does not verify a checksum or release signature.
- `osmose version` (or `osmose --version`) — Print the installed version.
- `osmose check-term` — Report terminal suitability; `--try-resize` attempts
  resizing and `--help-resize` prints guidance.
- `osmose bug --print` — Print the issue-tracker URL without opening a browser.
  `--include-context` also prints diagnostic context for review before sharing.
- `osmose release verify` — Maintainer check of local release metadata and
  public release endpoints; it does not publish or install a release.
  Use `--root` for the tooling release directory containing `VERSION`, optionally
  `--version` for the expected version. `--get-base` and `--cdn-base` override
  endpoint bases; `--legacy=false` skips the legacy `/releases` URL check.

### Shared flags and CI inputs

`--backend auto|graphql|shopify-cli` is inherited by subcommands that use a theme
backend. `auto` prefers GraphQL when the active environment has a usable token or
Osmose Shopify sign-in; otherwise it selects Shopify CLI.

There is **no global `--env` or `--debug` flag**. Select a configured environment
with top-level `active` in `osmose.toml` or the TUI. `init --env` names an
environment being created; `dev --debug` is specific to development.

`build` (without file arguments), `package`, `push`, `pull`, and `doctor` support
`--json` or `OSMOSE_JSON=1`. `profile --json` has its own report format.
For supported store commands, `SHOPIFY_FLAG_STORE`, `SHOPIFY_FLAG_THEME_ID`,
and `SHOPIFY_CLI_THEME_TOKEN` provide store inputs. These are not generic
overrides for every `osmose.toml` setting or every command.

### The TUI

Running `osmose` with no arguments opens the interactive dashboard, with
development, sync, environment switching, diagnostics, and release-note views.
The palette exposes TUI actions; it is not a wrapper for every CLI subcommand.

#### Release notes

Press `r` on the TUI home screen or choose **release notes** in the command
palette to read the embedded history offline. Unreleased work is labelled
separately from published versions. Use arrow keys or `j`/`k` to scroll,
Page Up / Page Down to move by a page, Home / End to jump, and Escape to close.

Press `u` to check for an update and review the target release before deciding
whether to install. Reading notes never installs an update. From the CLI:

```bash
osmose update --notes
```

An interactive `osmose update` opens the notes and asks for confirmation.
When output is redirected, it prints plain-text notes and a `[y/N]` prompt.

Read on the web: https://osmose.sh/pages/docs#commands
