# SDK & CLI

## Các gói

| Gói | Dùng ở | Vai trò |
|---|---|---|
| `@pancake-pos/ui-extensions` | `extensions/`, `page/` | Type cho `pos` và element `pos-*`, helper `defineExtension`, client cho trang iframe (`/page`) |
| `@pancake-pos/ui` | `page/` | Theme và component theo style POS cho trang iframe |
| `@pancake-pos/app-cli` | Máy dev, CI | Lệnh `pos-app`: đăng nhập, chạy thử, kiểm tra, build, upload |
| `@pancake-pos/eslint-plugin` | Editor, `pos-app check` | Luật kiểm tra code cho app; editor báo lỗi ngay khi gõ |
| `create-pos-app` | Một lần | Tạo dự án mẫu, cài sẵn các gói trên và `AGENTS.md` |

Phiên bản gói đi theo `api_version`: gói `2026.1.x` dùng cho `"api_version": "2026-01"`. CLI báo lỗi nếu hai giá trị không khớp.

## SDK và runtime của POS

Code chạy trong sandbox gồm hai phần, do hai bên khác nhau cung cấp:

```text
Worker trong sandbox của app
┌─────────────────────────────────────────────────────┐
│ Bundle của app (shop build và upload)               │
│   module extension + Preact hoặc JS thuần           │
├─────────────────────────────────────────────────────┤
│ Runtime POS (POS cung cấp, không nằm trong bundle)  │
│   DOM tối giản · element pos-* · biến toàn cục pos  │
│   kênh postMessage · app token · gọi Open API       │
└─────────────────────────────────────────────────────┘
```

- **Runtime POS** do POS nạp sẵn vào worker trước module của app. Vì runtime không nằm trong bundle, POS sửa lỗi bảo mật hoặc cải thiện runtime mà app không phải deploy lại, và module của app nhỏ.
- **`@pancake-pos/ui-extensions`** khi dùng trong `extensions/` chủ yếu cung cấp **type** và helper; gần như không thêm code vào bundle.
- **Trang iframe** không có runtime nạp sẵn: app import `connect()` từ `@pancake-pos/ui-extensions/page`, đoạn client nhỏ này được bundle cùng trang.

### Việc SDK và runtime lo thay app

| Việc | Chi tiết |
|---|---|
| Kênh giao tiếp | postMessage giữa sandbox và POS, kiểm tra nguồn gửi, gửi lệnh và sự kiện hai chiều |
| Xác thực | Lấy app token, gia hạn trước khi hết hạn, gặp 401 thì lấy token mới và thử lại 1 lần |
| Gọi Open API | `pos.api.*`, gắn header `Authorization`, chặn method ngoài `access.methods`, đổi lỗi thành `PosApiError` |
| Worker UI | DOM tối giản, định nghĩa element `pos-*`, gom thay đổi gửi về POS theo lô, chuyển sự kiện từ POS vào worker |
| Ngữ cảnh | Cập nhật `pos.data`, phát sự kiện `data-change` |
| Trang iframe | Đồng bộ URL với POS, `setTitle`, `setPrimaryAction` |
| Chế độ phát triển | Nạp bundle từ `localhost`, tự nạp lại extension và trang sau mỗi lần build |

## Cài đặt

Dự án tạo bằng `create-pos-app` đã cài sẵn. Thêm vào dự án có sẵn:

```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   # chỉ khi có trang iframe
```

## TypeScript

`tsconfig.json` ở gốc dự án dùng cho `extensions/`:

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

- `types` khai báo biến toàn cục `pos` và các element `pos-*` cho JSX (thuộc tính, sự kiện, `event.detail`).
- Trang `page/` có `page/tsconfig.json` riêng với `"jsxImportSource": "react"`, do template sinh sẵn; không gộp chung vào tsconfig gốc.
- `extensions/` hỗ trợ **Preact** (khuyến nghị, nhẹ) và **JS thuần** dùng DOM API của runtime. **Chưa hỗ trợ React** trong `extensions/`: React 18 truyền thuộc tính dạng mảng/đối tượng thành chuỗi và không nghe được sự kiện của element `pos-*`; React 19 đang được đánh giá. Trang iframe dùng React bình thường.

## defineExtension

Helper không bắt buộc, giúp type của tham số `data` và giá trị trả về khớp với target.

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

export default defineExtension('order.before_submit', async (data) => {
  // data: DetailsData<'order'>, phải trả BeforeSubmitResult
  if (data.draft.total_price > 20_000_000 && !data.draft.bill_phone_number) {
    return { ok: false, message: 'Đơn lớn cần số điện thoại.' };
  }
  return { ok: true };
});
```

`defineExtension` chỉ trả lại đúng hàm truyền vào, nên tương đương `export default async function (data)`.

Resource lấy theo tên target, nên cùng helper này cho `data.draft` kiểu `PurchaseDraft` trong `purchase.before_submit`, hay `data.records` kiểu `WarehouseTransferRef[]` trong `warehouse_transfer.index.selection-action`.

Các type xuất ra từ gói: `Resource`, `DetailsData`, `IndexData`, `SelectionData`, `AfterSaveInput`, `BeforeSubmitResult`, `HeadlessActionResult`, `PageData`, cặp `Ref`/`Draft` của từng resource (`OrderRef`, `OrderDraft`, `ProductRef`, `ProductDraft`, `PurchaseRef`, `PurchaseDraft`, `ExportRef`, `ExportDraft`, `WarehouseTransferRef`, `WarehouseTransferDraft`), `PosApiError`, `PosRuntimeError`.

## Trang iframe

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

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

- `connect()` chờ POS gửi ngữ cảnh và token; quá `timeout` (mặc định 10 giây) thì ném lỗi, thường do mở trang ngoài POS.
- Trang chỉ chạy trong iframe của POS. Mở URL sandbox trên tab hay cửa sổ riêng, kể cả popup do trang tự mở, sẽ bị từ chối (`403`). Luồng đăng nhập kiểu popup phải quay về trang của bạn ngoài sandbox, rồi báo lại cho app qua server của bạn.
- `pos` của trang có cùng API như trong worker, thêm `pos.page.*`. Xem [Runtime API](/developers/apps/runtime-api#trang-iframe).

## CLI (pos-app)

| Lệnh | Mô tả |
|---|---|
| `pos-app login --token <mã> [--shop <shop_id>]` | Lưu phiên đăng nhập của CLI trên máy. Lệnh đầy đủ kèm mã lấy ở nút **Kết nối CLI** trên trang quản lý ứng dụng tuỳ biến (cần quyền Cấu hình); mã chỉ dùng cho shop đó và các lệnh của CLI (upload, xem version, lỗi, dữ liệu mẫu; không kích hoạt app hay đăng nhập POS được), hạn 30 ngày, hết hiệu lực khi bạn đăng xuất khỏi POS hoặc khi bị thu hồi ở nút **Kết nối CLI** |
| `pos-app logout` | Thu hồi token CLI rồi xóa phiên đăng nhập trên máy |
| `pos-app dev [--port 5174] [--page-url <url>]` | Build theo dõi thay đổi và phục vụ tại `http://localhost:<port>` cho chế độ phát triển của POS; POS tự nạp lại sau mỗi lần build. `--page-url`: [trang tự host](/developers/apps/runtime-api#trang-tu-host) đang chạy trên máy (ví dụ `http://localhost:3000`) |
| `pos-app check` | Kiểm tra manifest và code (xem bên dưới), không upload |
| `pos-app build` | Build ra `dist/` |
| `pos-app deploy [--shop <shop_id>] [--message "..."]` | Chạy `check` và `build`, upload thành version mới (chưa kích hoạt) |
| `pos-app versions [--shop <shop_id>]` | Liệt kê version đã upload và version đang kích hoạt |
| `pos-app mcp [--shop <shop_id>]` | Chạy MCP server cho công cụ AI, xem [MCP](#mcp) |

Mọi lệnh nhận `--json` để trả kết quả dạng máy đọc được (CI, script, MCP server).

Lệnh `pos-app` có trong dự án sau khi cài `@pancake-pos/app-cli`, nên `npx pos-app …` chạy được trong thư mục dự án. Ngoài dự án, gọi theo tên gói: `npx @pancake-pos/app-cli <lệnh>`. Gói `pos-app` trên npm không phải của Pancake.

Kích hoạt và rollback làm trên [trang quản lý ứng dụng tuỳ biến](/developers/apps/manage-apps) trong POS, để admin luôn duyệt quyền trước khi app chạy.

### pos-app check

Kiểm tra tĩnh, chạy tự động trước `deploy`. Đặc biệt hữu ích khi code do AI sinh:

- **Manifest:** hợp lệ theo [schema](/developers/apps/manifest), báo đúng dòng; target có trong `api_version`; file `module`/`entry` tồn tại; khoá `conditions` hợp lệ theo resource.
- **Code:** chạy bộ [luật kiểm tra code](#luat-kiem-tra-code) với cấu hình có sẵn, không cần cấu hình gì trong dự án.
- **Kích thước** module và bundle trong giới hạn.

Lỗi mức `error` chặn `deploy`; `warning` chỉ báo. Mỗi lỗi có file, dòng, mã luật và gợi ý sửa (`--json` cho CI và MCP). `pos-app check` bỏ qua comment `eslint-disable`, nên tắt luật trong code không vượt được bước kiểm tra này.

### Luật kiểm tra code

Bộ luật nằm trong gói `@pancake-pos/eslint-plugin`. Luật về element sinh từ danh mục component của đúng `api_version`, nên luôn khớp với POS.

| Nhóm | Luật | Mức |
|---|---|---|
| Môi trường worker | Không dùng `window`, `document.querySelector`, `localStorage`, `alert`…; không import `react`, `react-dom`, file CSS hay thư viện UI trong `extensions/` | error |
| Element `pos-*` | Element, thuộc tính, sự kiện có trong [danh mục](/developers/apps/components) (gợi ý tên đúng khi gõ sai); giá trị enum hợp lệ; element con và slot hợp lệ; text nằm trong element chứa được text | error |
| Dữ liệu và an toàn | Method `pos.api.*` có trong `access.methods`; URL `fetch` và `src` của `pos-image` viết cố định thuộc `network.allowed_origins` (ảnh được dùng thêm `public/`); không viết cứng mã shop; không có chuỗi giống API key, token, secret | error |
| Hành vi | Module `export default` một hàm; danh sách render có `key`; không đặt `value` của ô nhập sau `await`; `before_submit` không gọi API nối tiếp nhiều lần | warning |
| Trang iframe | Không dùng `window.top`, `window.parent`; không dùng route bắt đầu bằng `/_pos/` | error |

Kiểm tra tĩnh không bắt được trường hợp động, ví dụ tên element tạo từ biến. Ở chế độ phát triển, POS cảnh báo thêm trong console khi chạy.

### Báo lỗi trong editor

Dự án tạo bằng `create-pos-app` có sẵn `eslint.config.js`:

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

// recommended đọc pos-app.json để biết api_version, access và allowed_origins
export default [...posApp.configs.recommended];
```

Cài extension ESLint cho editor (VS Code, Cursor…) để thấy lỗi ngay khi gõ. `npx eslint .` cho cùng kết quả với phần code của `pos-app check`.

### Kết quả build

```text
dist/
├─ pos-app.json              # manifest, đường dẫn module đã đổi sang file build
├─ ext/
│  ├─ order-block.js         # mỗi module một file ES module, đã gồm Preact
│  └─ push-erp.js
├─ public/                   # ảnh cho pos-image, chép nguyên từ dự án
└─ page/                     # trang iframe do Vite build
   ├─ index.html
   └─ assets/...
```

- Module extension build bằng esbuild, đích ES2020, không tách chunk, không import từ CDN.
- Trang iframe build bằng Vite với cấu hình có sẵn trong template.

## MCP

`pos-app mcp` chạy một [MCP](https://modelcontextprotocol.io) server (stdio) để công cụ AI tra tài liệu, xem dữ liệu nền thật của shop, kiểm tra app, chạy thử, đọc lỗi, deploy và báo lỗi cho Pancake ngay trong cuộc trò chuyện.

```bash
npx @pancake-pos/app-cli mcp --shop 123          # chạy trong thư mục dự án app, hoặc thêm --cwd <thư mục>
```

Cách thêm server vào Claude Code, Cursor, VS Code, Codex, Claude Desktop, danh sách tool và câu lệnh mẫu: [Làm app với AI › MCP](/developers/apps/ai-guide#mcp).
