# SDK & CLI

## Packages

| Package | Used in | Role |
|---|---|---|
| `@pancake-pos/ui-extensions` | `extensions/`, `page/` | Types for `pos` and `pos-*` elements, the `defineExtension` helper, the client for iframe pages (`/page`) |
| `@pancake-pos/ui` | `page/` | Theme and POS-styled components for iframe pages |
| `@pancake-pos/app-cli` | Dev machine, CI | The `pos-app` command: sign in, try in POS, check, build, upload |
| `@pancake-pos/eslint-plugin` | Editor, `pos-app check` | Code rules for apps; editors show problems as you type |
| `create-pos-app` | Once | Creates a starter project with the packages above and an `AGENTS.md` |

Package versions follow `api_version`: package `2026.1.x` is for `"api_version": "2026-01"`. The CLI reports an error when they do not match.

## SDK and the POS runtime

Code running in the sandbox has two parts, provided by two different parties:

```text
Worker inside the app sandbox
┌─────────────────────────────────────────────────────┐
│ App bundle (built and uploaded by the shop)         │
│   extension modules + Preact or plain JS            │
├─────────────────────────────────────────────────────┤
│ POS runtime (provided by POS, not in the bundle)    │
│   minimal DOM · pos-* elements · global pos object  │
│   postMessage channel · app token · Open API calls  │
└─────────────────────────────────────────────────────┘
```

- **The POS runtime** is loaded into the worker before the app's modules. Because it is not part of the bundle, POS can ship security fixes or improvements without apps redeploying, and app modules stay small.
- **`@pancake-pos/ui-extensions`** mostly provides **types** and helpers in `extensions/`; it adds almost no code to the bundle.
- **Iframe pages** have no preloaded runtime: the app imports `connect()` from `@pancake-pos/ui-extensions/page`, and this small client is bundled with the page.

### What the SDK and runtime handle for you

| Concern | Details |
|---|---|
| Messaging | postMessage between the sandbox and POS, sender verification, commands and events in both directions |
| Authentication | Fetches the app token, renews it before it expires, refreshes and retries once on 401 |
| Open API calls | `pos.api.*`, adds the `Authorization` header, blocks methods outside `access.methods`, turns errors into `PosApiError` |
| Worker UI | Minimal DOM, `pos-*` element definitions, batching changes sent to POS, delivering POS events to the worker |
| Context | Updates `pos.data`, emits `data-change` events |
| Iframe pages | Syncs the URL with POS, `setTitle`, `setPrimaryAction` |
| Development mode | Loads the bundle from `localhost`, reloads extensions and pages after every build |

## Installation

Projects created with `create-pos-app` already have everything. To add the SDK to an existing project:

```bash
npm install @pancake-pos/ui-extensions preact
npm install -D @pancake-pos/app-cli typescript eslint @pancake-pos/eslint-plugin
npm install @pancake-pos/ui react react-dom   # only if you have an iframe page
```

## TypeScript

The `tsconfig.json` at the project root covers `extensions/`:

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "strict": true,
    "types": ["@pancake-pos/ui-extensions"]
  },
  "include": ["extensions"]
}
```

- `types` declares the global `pos` object and the `pos-*` elements for JSX (properties, events, `event.detail`).
- The `page/` folder has its own `page/tsconfig.json` with `"jsxImportSource": "react"`, generated by the template; do not merge it into the root tsconfig.
- `extensions/` supports **Preact** (recommended, small) and **plain JS** using the runtime's DOM API. **React is not supported** in `extensions/` yet: React 18 turns array/object properties into strings and cannot listen to `pos-*` element events; React 19 is under evaluation. Iframe pages can use React as usual.

## defineExtension

An optional helper that makes the type of `data` and of the return value match the target.

```ts
import { defineExtension } from '@pancake-pos/ui-extensions';

export default defineExtension('order.before_submit', async (data) => {
  // data: DetailsData<'order'>; must return BeforeSubmitResult
  if (data.draft.total_price > 20_000_000 && !data.draft.bill_phone_number) {
    return { ok: false, message: 'Large orders need a phone number.' };
  }
  return { ok: true };
});
```

`defineExtension` returns the function you pass in, so it is equivalent to `export default async function (data)`.

The resource comes from the target name, so the same helper types `data.draft` as a `PurchaseDraft` in `purchase.before_submit` or `data.records` as `WarehouseTransferRef[]` in `warehouse_transfer.index.selection-action`.

Exported types: `Resource`, `DetailsData`, `IndexData`, `SelectionData`, `AfterSaveInput`, `BeforeSubmitResult`, `HeadlessActionResult`, `PageData`, the `Ref`/`Draft` pair of each resource (`OrderRef`, `OrderDraft`, `ProductRef`, `ProductDraft`, `PurchaseRef`, `PurchaseDraft`, `ExportRef`, `ExportDraft`, `WarehouseTransferRef`, `WarehouseTransferDraft`), `PosApiError`, `PosRuntimeError`.

## Iframe pages

```ts
import { connect } from '@pancake-pos/ui-extensions/page';

const pos = await connect({ timeout: 10_000 });
```

- `connect()` waits for POS to send the context and token; after `timeout` (10 seconds by default) it throws, usually because the page was opened outside POS.
- The page runs only inside a POS iframe. Opening a sandbox URL in its own tab or window, including a popup the page opens itself, is refused (`403`). A popup sign-in flow must return to a page of yours outside the sandbox and report back to the app through your server.
- The page's `pos` has the same API as in the worker, plus `pos.page.*`. See [Runtime API](/developers/apps/en/runtime-api#iframe-pages).

## CLI (pos-app)

| Command | Description |
|---|---|
| `pos-app login --token <token> [--shop <shop_id>]` | Stores the CLI session on this machine. Copy the full command, token included, from the **Connect CLI** button on the custom apps management page (requires the Settings permission); the token only works for that shop and the CLI's own commands (upload, versions, errors, sample data; it cannot activate apps or sign in to POS), lasts 30 days and stops working when you sign out of POS or when it is revoked from the **Connect CLI** button |
| `pos-app logout` | Revokes the CLI token, then removes the session from this machine |
| `pos-app dev [--port 5174] [--page-url <url>]` | Watch and build, serve at `http://localhost:<port>` for POS development mode; POS reloads after every build. `--page-url`: a [self-hosted page](/developers/apps/en/runtime-api#self-hosted-pages) running on your machine (for example `http://localhost:3000`) |
| `pos-app check` | Validates the manifest and code (see below) without uploading |
| `pos-app build` | Builds into `dist/` |
| `pos-app deploy [--shop <shop_id>] [--message "..."]` | Runs `check` and `build`, uploads a new (inactive) version |
| `pos-app versions [--shop <shop_id>]` | Lists uploaded versions and the active one |
| `pos-app mcp [--shop <shop_id>]` | Starts the MCP server for AI tools, see [MCP](#mcp) |

Every command accepts `--json` for machine-readable output (CI, scripts, the MCP server).

The `pos-app` command is available once `@pancake-pos/app-cli` is installed, so `npx pos-app …` works inside the project folder. Outside a project, call it by package name: `npx @pancake-pos/app-cli <command>`. The `pos-app` package on npm is not published by Pancake.

Activation and rollback happen on the [custom apps page](/developers/apps/en/manage-apps) in POS, so an admin always approves permissions before an app runs.

### pos-app check

Static checks that run automatically before `deploy`. Especially useful for AI-generated code:

- **Manifest:** valid against the [schema](/developers/apps/en/manifest), reported with line numbers; targets exist in `api_version`; `module`/`entry` files exist; `conditions` keys are valid for the resource.
- **Code:** runs the [code rules](#code-rules) with a built-in configuration; nothing to configure in the project.
- **Sizes** of modules and bundles are within limits.

`error` findings block `deploy`; `warning` findings are only reported. Each finding has a file, line, rule id and a suggested fix (`--json` for CI and MCP). `pos-app check` ignores `eslint-disable` comments, so disabling a rule in code does not bypass this check.

### Code rules

The rules ship in `@pancake-pos/eslint-plugin`. Element rules are generated from the component catalog of the exact `api_version`, so they always match POS.

| Group | Rules | Level |
|---|---|---|
| Worker environment | No `window`, `document.querySelector`, `localStorage`, `alert`…; no `react`, `react-dom`, CSS files or UI libraries imported in `extensions/` | error |
| `pos-*` elements | Elements, properties and events exist in the [catalog](/developers/apps/en/components) (with a suggestion for typos); valid enum values; valid children and slots; text only inside elements that accept text | error |
| Data and security | `pos.api.*` methods are listed in `access.methods`; literal `fetch` URLs and `pos-image` sources belong to `network.allowed_origins` (images may also use `public/`); no hardcoded shop id; no strings that look like API keys, tokens or secrets | error |
| Behavior | Modules `export default` a function; rendered lists have a `key`; input `value` is not set after an `await`; `before_submit` does not chain several API calls | warning |
| Iframe pages | No `window.top` or `window.parent`; no routes starting with `/_pos/` | error |

Static checks cannot catch dynamic cases such as element names built from variables. In development mode POS also logs warnings to the console at run time.

### Editor feedback

Projects created with `create-pos-app` include an `eslint.config.js`:

```js
import posApp from '@pancake-pos/eslint-plugin';

// recommended reads pos-app.json for api_version, access and allowed_origins
export default [...posApp.configs.recommended];
```

Install the ESLint extension in your editor (VS Code, Cursor…) to see problems as you type. `npx eslint .` gives the same results as the code part of `pos-app check`.

### Build output

```text
dist/
├─ pos-app.json              # manifest with module paths pointing to built files
├─ ext/
│  ├─ order-block.js         # one ES module per module, Preact included
│  └─ push-erp.js
├─ public/                   # images for pos-image, copied as is
└─ page/                     # iframe page built by Vite
   ├─ index.html
   └─ assets/...
```

- Extension modules are built with esbuild, targeting ES2020, without code splitting and without imports from CDNs.
- Iframe pages are built with Vite using the configuration that ships with the template.

## MCP

`pos-app mcp` starts an [MCP](https://modelcontextprotocol.io) server (stdio) so AI tools can look up the docs, see the shop's real metadata, check the app, try it, read errors, deploy and report bugs to Pancake without leaving the chat.

```bash
npx @pancake-pos/app-cli mcp --shop 123          # run in the app's project folder, or add --cwd <folder>
```

How to add it to Claude Code, Cursor, VS Code, Codex and Claude Desktop, the tool list and sample prompts: [Build with AI › MCP](/developers/apps/en/ai-guide#mcp).
