@pancake-pos/app-cli 2026.1.4 → 2026.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.js +25 -26
- package/docs/llms-full.txt +42 -20
- package/package.json +2 -2
- package/runtime/frame.js +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,13 @@ The `pos-app` command for Pancake POS apps: `login`, `dev`, `check`, `build`, `d
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @pancake-pos/app-cli login --shop 123 --token <token> # from POS › Settings › Advanced › Custom apps › Connect CLI
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
npm run dev # in an app created by create-pos-app (scripts call pos-app)
|
|
8
|
+
npm run deploy
|
|
9
|
+
npx @pancake-pos/app-cli versions # any command, from anywhere
|
|
9
10
|
```
|
|
10
11
|
|
|
12
|
+
Do not run `npx pos-app`: the `pos-app` package on npm is not published by Pancake, and npx downloads it outside an installed project.
|
|
13
|
+
|
|
11
14
|
Docs: https://pos.pages.fm/developers/apps/en (English), https://pos.pages.fm/developers/apps (Tiếng Việt). For AI tools: https://pos.pages.fm/developers/apps/llms-full.txt.
|
|
12
15
|
|
|
13
16
|
Part of Pancake POS Apps. Use is limited to building apps for Pancake POS.
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// src/api.ts
|
|
18
18
|
async function apiRequest(credentials, method, path, body) {
|
|
19
|
-
if (!credentials.token) throw new ApiError(401,
|
|
19
|
+
if (!credentials.token) throw new ApiError(401, 'Ch\u01B0a \u0111\u0103ng nh\u1EADp: ch\u1EA1y l\u1EC7nh \u1EDF n\xFAt "K\u1EBFt n\u1ED1i CLI" tr\xEAn trang \u1EE8ng d\u1EE5ng tu\u1EF3 bi\u1EBFn c\u1EE7a POS (npx @pancake-pos/app-cli login \u2026)');
|
|
20
20
|
const headers = { Authorization: `Bearer ${credentials.token}`, Accept: "application/json" };
|
|
21
21
|
let payload;
|
|
22
22
|
if (body instanceof FormData) payload = body;
|
|
@@ -73,7 +73,7 @@ function targets(apiVersion) {
|
|
|
73
73
|
"app.page",
|
|
74
74
|
...RESOURCES.flatMap((r) => [...locations, "index.column"].map((l) => `${r}.${l}`)),
|
|
75
75
|
...PRINT_RESOURCES.map((r) => `${r}.print-action`),
|
|
76
|
-
|
|
76
|
+
...CUSTOMER_LOCATIONS.map((l) => `customer.${l}`)
|
|
77
77
|
];
|
|
78
78
|
}
|
|
79
79
|
function validateManifest(manifest, files) {
|
|
@@ -181,7 +181,7 @@ function validateManifest(manifest, files) {
|
|
|
181
181
|
} else if (target.endsWith(".print-action") && PRINT_RESOURCES.includes(target.slice(0, -".print-action".length))) {
|
|
182
182
|
requiredString(e, "label", 40, path);
|
|
183
183
|
file("module", true);
|
|
184
|
-
} else if (target.endsWith(".index.column") && RESOURCES.includes(target.slice(0, -".index.column".length))) {
|
|
184
|
+
} else if (target.endsWith(".index.column") && [...RESOURCES, "customer"].includes(target.slice(0, -".index.column".length))) {
|
|
185
185
|
requiredString(e, "title", 30, path);
|
|
186
186
|
file("module", true);
|
|
187
187
|
if (e.format !== void 0 && !COLUMN_FORMATS.includes(e.format)) add("invalid_field", `${path}.format`, `format ph\u1EA3i l\xE0 ${COLUMN_FORMATS.join(", ")}`);
|
|
@@ -192,7 +192,8 @@ function validateManifest(manifest, files) {
|
|
|
192
192
|
const dot = target.indexOf(".");
|
|
193
193
|
const resource = target.slice(0, dot);
|
|
194
194
|
const location = target.slice(dot + 1);
|
|
195
|
-
|
|
195
|
+
const customer = resource === "customer" && CUSTOMER_LOCATIONS.includes(location);
|
|
196
|
+
if (!RESOURCES.includes(resource) && !customer) return add("unknown_target", `${path}.target`, `Target ${target} kh\xF4ng t\u1ED3n t\u1EA1i`);
|
|
196
197
|
if (ACTION_LOCATIONS.includes(location)) {
|
|
197
198
|
requiredString(e, "label", 40, path);
|
|
198
199
|
optionalString(e, "title", 60, path);
|
|
@@ -229,7 +230,7 @@ function validOrigin(origin) {
|
|
|
229
230
|
return false;
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
|
-
var API_VERSIONS, RESERVED_DOMAINS, RESOURCES, ACTION_LOCATIONS, DETAIL_LOCATIONS, PRESENTATIONS, METHODS, ACCESS_FIELDS, CONDITION_RESOURCES, UUID, PRINT_RESOURCES, COLUMN_FORMATS, COLUMN_ALIGNS, isString, isObject;
|
|
233
|
+
var API_VERSIONS, RESERVED_DOMAINS, RESOURCES, ACTION_LOCATIONS, DETAIL_LOCATIONS, PRESENTATIONS, METHODS, ACCESS_FIELDS, CONDITION_RESOURCES, UUID, CUSTOMER_LOCATIONS, PRINT_RESOURCES, COLUMN_FORMATS, COLUMN_ALIGNS, isString, isObject;
|
|
233
234
|
var init_manifest = __esm({
|
|
234
235
|
"src/manifest.ts"() {
|
|
235
236
|
"use strict";
|
|
@@ -252,6 +253,7 @@ var init_manifest = __esm({
|
|
|
252
253
|
has_customer: ["order"]
|
|
253
254
|
};
|
|
254
255
|
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
256
|
+
CUSTOMER_LOCATIONS = ["details.block", ...ACTION_LOCATIONS, "index.column"];
|
|
255
257
|
PRINT_RESOURCES = ["order", "purchase", "export", "warehouse_transfer"];
|
|
256
258
|
COLUMN_FORMATS = ["text", "number", "money", "date", "datetime"];
|
|
257
259
|
COLUMN_ALIGNS = ["left", "center", "right"];
|
|
@@ -643,7 +645,7 @@ function createSiteHandler(options) {
|
|
|
643
645
|
if (req.method !== "GET" && req.method !== "HEAD") return send(req, res, 405, { ...textHeaders, Allow: "GET, HEAD" }, "Method not allowed");
|
|
644
646
|
const url = new URL(req.url ?? "/", "http://sandbox");
|
|
645
647
|
const path = url.pathname;
|
|
646
|
-
if (path === "/_pos/health") return send(req, res, 200, textHeaders, "ok");
|
|
648
|
+
if (path === "/_pos/health") return send(req, res, 200, options.runtime.version ? { ...textHeaders, "X-Pos-Runtime": options.runtime.version } : textHeaders, "ok");
|
|
647
649
|
if (options.rejectTopLevel && req.headers["sec-fetch-dest"] === "document") {
|
|
648
650
|
return send(req, res, 403, textHeaders, "This app only runs inside Pancake POS");
|
|
649
651
|
}
|
|
@@ -1145,7 +1147,7 @@ var CLI_VERSION;
|
|
|
1145
1147
|
var init_version = __esm({
|
|
1146
1148
|
"src/version.ts"() {
|
|
1147
1149
|
"use strict";
|
|
1148
|
-
CLI_VERSION = true ? "2026.1.
|
|
1150
|
+
CLI_VERSION = true ? "2026.1.6" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1149
1151
|
}
|
|
1150
1152
|
});
|
|
1151
1153
|
|
|
@@ -1246,6 +1248,8 @@ function isSaved(resource, record) {
|
|
|
1246
1248
|
}
|
|
1247
1249
|
function recordRef(resource, record) {
|
|
1248
1250
|
switch (resource) {
|
|
1251
|
+
case "customer":
|
|
1252
|
+
return customerRef(record);
|
|
1249
1253
|
case "order": {
|
|
1250
1254
|
const systemId = orderSystemId(record) ?? 0;
|
|
1251
1255
|
return { id: str(record.custom_id) ?? systemId, system_id: systemId, status: num(record.status), updated_at: str(record.updated_at) };
|
|
@@ -1374,11 +1378,12 @@ function hideOrderFields(data, hidden) {
|
|
|
1374
1378
|
for (const field of hidden) delete draft[field];
|
|
1375
1379
|
return { ...data, draft };
|
|
1376
1380
|
}
|
|
1377
|
-
var RESOURCES2, MAX_SELECTION, num, str, idOf, ids, list, orderPrice, orderSystemId;
|
|
1381
|
+
var RESOURCES2, LIST_RESOURCES, MAX_SELECTION, num, str, idOf, ids, list, orderPrice, orderSystemId;
|
|
1378
1382
|
var init_records = __esm({
|
|
1379
1383
|
"../app-host/src/records.ts"() {
|
|
1380
1384
|
"use strict";
|
|
1381
1385
|
RESOURCES2 = ["order", "product", "purchase", "export", "warehouse_transfer"];
|
|
1386
|
+
LIST_RESOURCES = [...RESOURCES2, "customer"];
|
|
1382
1387
|
MAX_SELECTION = 500;
|
|
1383
1388
|
num = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : Number(value) || fallback;
|
|
1384
1389
|
str = (value) => value === void 0 || value === null || value === "" ? null : String(value);
|
|
@@ -1514,17 +1519,18 @@ function describeTarget(target, all = sections()) {
|
|
|
1514
1519
|
const [resource, ...rest] = target.split(".");
|
|
1515
1520
|
const location = target === "nav.item" || target === "app.page" ? target : rest.join(".");
|
|
1516
1521
|
const plain = target === "nav.item" || target === "app.page";
|
|
1517
|
-
const
|
|
1518
|
-
const
|
|
1522
|
+
const customer = resource === "customer";
|
|
1523
|
+
const headings = plain ? [target] : customer ? ["`customer.*`", ...location === "details.block" ? [] : [`\`*.${location}\``]] : [`\`*.${location}\``];
|
|
1524
|
+
const docs = all.filter((section) => section.page === "targets" && headings.some((heading) => section.heading === heading || section.heading.startsWith(`${heading} `)));
|
|
1519
1525
|
const listData = location === "print-action" || location === "index.column";
|
|
1520
|
-
const data =
|
|
1526
|
+
const data = customer && location.startsWith("details.") ? "CustomerData ({ resource: 'customer', record: CustomerRef })" : DATA[location] ? `${DATA[location]}<'${resource}'>` : target === "app.page" ? "PageData ({ path, query })" : "none (no code)";
|
|
1521
1527
|
const result = location === "before_submit" ? "BeforeSubmitResult: `{ ok: true, changes?: { note?, add_tags? } }` or `{ ok: false, message }`" : location === "print-action" ? "PrintResult: `{ html, title? }` (POS strips scripts, then prints)" : location === "index.column" ? `ColumnResult: \`{ values: { [${resource === "order" ? "system_id" : "id"}]: string | number | null } }\`` : location.endsWith("action") ? "headless: `{ message?, refresh? }` or nothing; modal/fullscreen: render, call pos.close()" : "nothing (blocks and pages render themselves)";
|
|
1522
|
-
const types =
|
|
1528
|
+
const types = customer ? [typeBlock("CustomerRef", all)].filter(Boolean) : DATA[location] ? [typeBlock(`${pascal(resource)}Ref`, all), location === "index.action" || location.endsWith("selection-action") || location === "after_save" || listData ? null : typeBlock(`${pascal(resource)}Draft`, all)].filter(Boolean) : [];
|
|
1523
1529
|
return [
|
|
1524
1530
|
`# ${target}`,
|
|
1525
1531
|
`- Context data (\`pos.data\` and the module argument): \`${data}\``,
|
|
1526
1532
|
`- Returns: ${result}`,
|
|
1527
|
-
`- Manifest fields: ${target === "customer.details.block" ? "`module` (required), `should_render`. No `conditions`." : MANIFEST[location] ?? ""}`,
|
|
1533
|
+
`- Manifest fields: ${target === "customer.details.block" ? "`module` (required), `should_render`. No `conditions`." : `${MANIFEST[location] ?? ""}${customer ? " No `conditions` for customer targets." : ""}`}`,
|
|
1528
1534
|
...types.length ? ["", "```ts", ...types, "```"] : [],
|
|
1529
1535
|
"",
|
|
1530
1536
|
"## Example",
|
|
@@ -1583,7 +1589,7 @@ function createTools(ctx) {
|
|
|
1583
1589
|
let dev = null;
|
|
1584
1590
|
const devLog = [];
|
|
1585
1591
|
const shop = () => {
|
|
1586
|
-
if (!ctx.shop) throw new Error("Missing shop: start the server with --shop <shop_id> or run pos-
|
|
1592
|
+
if (!ctx.shop) throw new Error("Missing shop: start the server with --shop <shop_id> or run npx @pancake-pos/app-cli login --shop <shop_id>");
|
|
1587
1593
|
return ctx.shop;
|
|
1588
1594
|
};
|
|
1589
1595
|
const get = (path) => apiRequest(ctx.credentials, "GET", `/shops/${shop()}/${path}`);
|
|
@@ -1684,20 +1690,12 @@ ${hit.text.slice(0, 4e3)}`).join("\n\n---\n\n") };
|
|
|
1684
1690
|
async run(args) {
|
|
1685
1691
|
const target = String(args.target ?? "");
|
|
1686
1692
|
if (target === "app.page") return ok({ target, data: { path: "/", query: {} } });
|
|
1687
|
-
if (target === "customer.details.block") {
|
|
1688
|
-
const id = args.record_id === void 0 ? void 0 : String(args.record_id);
|
|
1689
|
-
const response = id ? await get(`customers/${encodeURIComponent(id)}`) : await get(`customers?page_size=1&page_number=1`);
|
|
1690
|
-
const record2 = id ? response.data ?? response : itemsOf(response)[0];
|
|
1691
|
-
if (!record2 || typeof record2 !== "object") return fail("The shop has no customers the signed-in user can read");
|
|
1692
|
-
return ok({ target, data: customerData(record2) });
|
|
1693
|
-
}
|
|
1694
1693
|
const [resource, ...rest] = target.split(".");
|
|
1695
1694
|
const location = rest.join(".");
|
|
1696
|
-
if (!
|
|
1695
|
+
if (!LIST_RESOURCES.includes(resource) || !targets(API_VERSION2).includes(target)) return fail(`Unknown target ${target}`);
|
|
1697
1696
|
const record = await sampleRecord(resource, args.record_id === void 0 ? void 0 : String(args.record_id));
|
|
1698
1697
|
if (!record) return fail(`The shop has no ${resource} records the signed-in user can read`);
|
|
1699
|
-
const
|
|
1700
|
-
const data = location === "index.action" ? indexData(r, record) : location === "index.selection-action" || location === "print-action" || location === "index.column" ? selectionData(r, [record]) : location === "after_save" ? afterSaveInput(r, record, "edit") : detailsData(r, record);
|
|
1698
|
+
const data = location === "index.action" ? indexData(resource, record) : location === "index.selection-action" || location === "print-action" || location === "index.column" ? selectionData(resource, [record]) : resource === "customer" ? customerData(record) : location === "after_save" ? afterSaveInput(resource, record, "edit") : detailsData(resource, record);
|
|
1701
1699
|
const hidden = readManifest(ctx.root).access?.hidden_order_fields;
|
|
1702
1700
|
return ok({ target, data: "draft" in data ? hideOrderFields(data, hidden) : data });
|
|
1703
1701
|
}
|
|
@@ -1883,7 +1881,8 @@ var init_tools = __esm({
|
|
|
1883
1881
|
product: "products",
|
|
1884
1882
|
purchase: "purchases",
|
|
1885
1883
|
export: "export",
|
|
1886
|
-
warehouse_transfer: "transfers"
|
|
1884
|
+
warehouse_transfer: "transfers",
|
|
1885
|
+
customer: "customers"
|
|
1887
1886
|
};
|
|
1888
1887
|
META_PATHS = {
|
|
1889
1888
|
order: { warehouses: "warehouses", tags: "orders/tags", order_sources: "order_source" },
|
|
@@ -1968,7 +1967,7 @@ async function run(argv, io = defaultIo) {
|
|
|
1968
1967
|
const credentials = readCredentials();
|
|
1969
1968
|
const shopOf = () => {
|
|
1970
1969
|
const shop = str2(flags.shop) ?? credentials.shop;
|
|
1971
|
-
if (!shop) throw new Error("Thi\u1EBFu --shop <shop_id> (ho\u1EB7c l\u01B0u b\u1EB1ng `pos-
|
|
1970
|
+
if (!shop) throw new Error("Thi\u1EBFu --shop <shop_id> (ho\u1EB7c l\u01B0u b\u1EB1ng `npx @pancake-pos/app-cli login --shop`)");
|
|
1972
1971
|
return shop;
|
|
1973
1972
|
};
|
|
1974
1973
|
switch (command) {
|
package/docs/llms-full.txt
CHANGED
|
@@ -171,7 +171,7 @@ Details: [Data access](/developers/apps/en/data-access).
|
|
|
171
171
|
|---|---|
|
|
172
172
|
| PoC | Sandbox, app tokens, order detail block, order action menu item, iframe page, a sample app written by AI |
|
|
173
173
|
| Phase 1 | Upload, versions, rollback, visibility permissions, audit, rate limits, component catalog v1, SDK, CLI |
|
|
174
|
-
| Phase 2 | Available: `before_submit` hook (block saves, set the note and tags), Worker UI pages (`render: "worker"`), app print templates (orders, purchases, exports, transfers), list columns, customer
|
|
174
|
+
| Phase 2 | Available: `before_submit` hook (block saves, set the note and tags), Worker UI pages (`render: "worker"`), app print templates (orders, purchases, exports, transfers), list columns, customer boxes, actions and columns. Not yet: per-app webhooks and service keys, app storage |
|
|
175
175
|
| Phase 3 | Consider the mobile app, install one app across shops of the same owner |
|
|
176
176
|
|
|
177
177
|
---
|
|
@@ -259,21 +259,21 @@ npx @pancake-pos/app-cli login --shop 123 --token <token>
|
|
|
259
259
|
Then run:
|
|
260
260
|
|
|
261
261
|
```bash
|
|
262
|
-
|
|
262
|
+
npm run dev # watch and build, serve at http://localhost:5174
|
|
263
263
|
```
|
|
264
264
|
|
|
265
265
|
On the same management page, click **Development mode** and enter `http://localhost:5174` (see [Manage apps › Development mode](/developers/apps/en/manage-apps#development-mode)). While it is on, POS loads the app from `localhost` **in your browser only**; everyone else in the shop keeps using the active version. When you edit code, POS reloads the extensions without reloading the page. Use Chrome or Edge: Safari may block an `https` page from loading `http://localhost`.
|
|
266
266
|
|
|
267
|
-
In development mode, `pos.api` can call the Open API only after the app has been uploaded at least once (`
|
|
267
|
+
In development mode, `pos.api` can call the Open API only after the app has been uploaded at least once (`npm run deploy`, no activation needed), and it uses the `access` of the latest uploaded version. Deploy again after changing `access`. After each such deploy, reload the POS page so POS gets a new token.
|
|
268
268
|
|
|
269
269
|
Development mode runs against the real data of the open shop, so try a test shop first. To see the app as a staff member with narrow permissions, deploy and activate it on a test shop and sign in with that account.
|
|
270
270
|
|
|
271
|
-
While you write code, an editor with the ESLint extension shows problems as you type. Run `
|
|
271
|
+
While you write code, an editor with the ESLint extension shows problems as you type. Run `npm run check` to validate the manifest as well.
|
|
272
272
|
|
|
273
273
|
## 4. Build and upload
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
|
|
276
|
+
npm run deploy
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
This command:
|
|
@@ -420,7 +420,7 @@ The `pos-app` command (and the MCP server) needs a token to upload apps to the s
|
|
|
420
420
|
|
|
421
421
|
Development mode lets you try the code you are writing inside real POS without affecting anyone else.
|
|
422
422
|
|
|
423
|
-
1. On your machine, run `
|
|
423
|
+
1. On your machine, run `npm run dev` in the app folder (served at `http://localhost:5174` by default).
|
|
424
424
|
2. Click **Development mode**, enter the **Dev server address** and click **Enable development mode**.
|
|
425
425
|
|
|
426
426
|

|
|
@@ -651,8 +651,8 @@ and a "Send reminder" button that calls https://crm.example.com/api/remind. Read
|
|
|
651
651
|
- [ ] Modules in `extensions/` use no real DOM, UI libraries or CSS.
|
|
652
652
|
- [ ] No tokens, API keys or secrets in code.
|
|
653
653
|
- [ ] Errors and loading states are handled.
|
|
654
|
-
- [ ] `
|
|
655
|
-
- [ ] Tested with `
|
|
654
|
+
- [ ] `npm run check` reports no errors.
|
|
655
|
+
- [ ] Tested with `npm run dev` (an account with the Settings permission), then deployed to a test shop and tried with a narrow-permission staff account.
|
|
656
656
|
|
|
657
657
|
## Sample AGENTS.md
|
|
658
658
|
|
|
@@ -677,10 +677,11 @@ Docs: https://pos.pages.fm/developers/apps/llms-full.txt (read before changing c
|
|
|
677
677
|
- Catch errors on every API call; handle 403 and 429.
|
|
678
678
|
|
|
679
679
|
## Commands
|
|
680
|
-
- npx pos-
|
|
680
|
+
- Run the CLI through the npm scripts below, or as `npx @pancake-pos/app-cli <command>` (login, versions, mcp). Never run `npx pos-app`: the `pos-app` package on npm is not Pancake's, and npx downloads it when the project is not installed.
|
|
681
|
+
- npm run dev: try in POS (turn on Development mode on the custom apps management page); POS reloads when code changes.
|
|
681
682
|
- npx eslint .: check the code after every change; do not add eslint-disable to silence rules.
|
|
682
|
-
-
|
|
683
|
-
-
|
|
683
|
+
- npm run check: validate the manifest and code; fix every error before deploying.
|
|
684
|
+
- npm run deploy: build, validate the manifest, upload a new version (options after --, for example npm run deploy -- --message "...").
|
|
684
685
|
- With the pos-app MCP server: get_target/list_shop_meta/sample_context before writing code, check_app after every change, get_errors after testing in POS, deploy when done (uploads an inactive version; an admin activates it). For a platform bug or a missing feature: draft a report, show it to the user, and call send_feedback only after they agree.
|
|
685
686
|
````
|
|
686
687
|
|
|
@@ -1158,6 +1159,8 @@ Apply to `*.details.block`.
|
|
|
1158
1159
|
| Target | Field | Description |
|
|
1159
1160
|
|---|---|---|
|
|
1160
1161
|
| `customer.details.block` | `module` (required), `should_render` | As for blocks; no `conditions`. |
|
|
1162
|
+
| `customer.details.action`, `customer.index.action`, `customer.index.selection-action` | As for actions | No `conditions`. |
|
|
1163
|
+
| `customer.index.column` | As for `<resource>.index.column` | `values` is keyed by the customer's `id`. |
|
|
1161
1164
|
| `<resource>.print-action` | `label` (required) | Template name in the "Print template" picker, up to 40 characters. |
|
|
1162
1165
|
| `<resource>.print-action` | `module` (required) | Module returning `{ html, title? }`. |
|
|
1163
1166
|
| `<resource>.index.column` | `title` (required) | Column title, up to 30 characters. |
|
|
@@ -1166,7 +1169,7 @@ Apply to `*.details.block`.
|
|
|
1166
1169
|
| `<resource>.index.column` | `align` | `left`, `center`, `right`. |
|
|
1167
1170
|
| `<resource>.index.column` | `width` | Column width in px, 60–400, default 140. |
|
|
1168
1171
|
|
|
1169
|
-
See [Extension points](/developers/apps/en/targets#customer
|
|
1172
|
+
See [Extension points](/developers/apps/en/targets#customer) for the context data and return values.
|
|
1170
1173
|
|
|
1171
1174
|
### Hook fields
|
|
1172
1175
|
|
|
@@ -1261,7 +1264,7 @@ Targets attached to a POS record are named `<resource>.<location>`, for example
|
|
|
1261
1264
|
| `<resource>.after_save` | After a record is created/updated on the detail form | hook, headless |
|
|
1262
1265
|
| `<resource>.index.column` | A column in the list | hook, headless |
|
|
1263
1266
|
| `<resource>.print-action` | A print template in the "Print template" picker (except `product`) | headless, returns HTML |
|
|
1264
|
-
| `customer
|
|
1267
|
+
| `customer.<location>` | Customer profile and list: `details.block`, `details.action`, `index.action`, `index.selection-action`, `index.column` | See [`customer.*`](#customer) |
|
|
1265
1268
|
|
|
1266
1269
|
`<resource>` is one of `order`, `product`, `purchase`, `export`, `warehouse_transfer`. Products have no `print-action` because they only print barcodes.
|
|
1267
1270
|
|
|
@@ -1345,6 +1348,7 @@ Example `pos.data` in `purchase.details.action`:
|
|
|
1345
1348
|
| `purchase` | `record.id` | `purchase_id`, `purchase_ids` |
|
|
1346
1349
|
| `export` | `record.id` | `export_id`, `export_ids` |
|
|
1347
1350
|
| `warehouse_transfer` | `record.id` | `warehouse_transfer_id`, `warehouse_transfer_ids` |
|
|
1351
|
+
| `customer` | `record.id` | `customer_id`, `customer_ids` |
|
|
1348
1352
|
|
|
1349
1353
|
### Types per resource
|
|
1350
1354
|
|
|
@@ -1663,9 +1667,19 @@ export default defineExtension('order.before_submit', async (data) => {
|
|
|
1663
1667
|
|
|
1664
1668
|
`*.before_submit` and `*.after_save` only run when the user saves on the detail form in POS web. They do **not** run for changes made elsewhere: the status menu on list rows, bulk updates, Excel import, the Open API, the mobile app or marketplace sync. Rules that must always hold belong on the server.
|
|
1665
1669
|
|
|
1666
|
-
## `customer
|
|
1670
|
+
## `customer.*`
|
|
1667
1671
|
|
|
1668
|
-
|
|
1672
|
+
Customers have boxes, actions and columns like the other resources, but **no** `before_submit`, `after_save` (the profile saves each field as soon as it changes; there is no shared Save button) and no print templates.
|
|
1673
|
+
|
|
1674
|
+
| Target | Location | Context data |
|
|
1675
|
+
|---|---|---|
|
|
1676
|
+
| `customer.details.block` | A box in the customer profile, right column below Notes and Addresses | `CustomerData` |
|
|
1677
|
+
| `customer.details.action` | An "Apps" button at the top of the customer profile, shown only when an app adds an action here | `CustomerData` |
|
|
1678
|
+
| `customer.index.action` | The "⋯" menu at the end of each customer list row, shown only when an app adds an action here | `IndexData<'customer'>` |
|
|
1679
|
+
| `customer.index.selection-action` | An "Apps" button on the action bar when several customers are selected | `SelectionData<'customer'>` |
|
|
1680
|
+
| `customer.index.column` | A column in the customer list; `values` is keyed by the customer's `id` | `ColumnData<'customer'>` |
|
|
1681
|
+
|
|
1682
|
+
The profile has the box and the actions wherever it opens: the Customers page, the order form and other screens. When the customer list is filtered to deleted customers, app actions and columns are hidden.
|
|
1669
1683
|
|
|
1670
1684
|
```ts
|
|
1671
1685
|
type CustomerRef = {
|
|
@@ -1678,12 +1692,14 @@ type CustomerRef = {
|
|
|
1678
1692
|
updated_at: string | null;
|
|
1679
1693
|
};
|
|
1680
1694
|
|
|
1681
|
-
type CustomerData = { resource: 'customer'; record: CustomerRef };
|
|
1695
|
+
type CustomerData = { resource: 'customer'; record: CustomerRef }; // same shape as IndexData<'customer'>
|
|
1682
1696
|
```
|
|
1683
1697
|
|
|
1684
|
-
- Manifest:
|
|
1685
|
-
-
|
|
1686
|
-
-
|
|
1698
|
+
- Manifest: the same fields as the target at the same location for other resources (`*.details.action`, `*.details.block`, `*.index.column`…). Customer targets have **no** `conditions`; use `should_render` to hide by data.
|
|
1699
|
+
- `refresh` of a headless action and `pos.refresh()` reload the customer list page being viewed; a profile opened from the list gets the new data. A profile opened from the order form or other screens does not reload yet.
|
|
1700
|
+
- `navigate` adds `customer_id` or `customer_ids` to the query.
|
|
1701
|
+
- When the customer is edited in the profile, the box receives `data-change` with the new data.
|
|
1702
|
+
- As with `*.details.block`, the box's worker starts when the box scrolls into view; POS shows a placeholder meanwhile.
|
|
1687
1703
|
|
|
1688
1704
|
## `*.print-action`
|
|
1689
1705
|
|
|
@@ -2647,7 +2663,13 @@ const pos = await connect({ timeout: 10_000 });
|
|
|
2647
2663
|
|
|
2648
2664
|
Every command accepts `--json` for machine-readable output (CI, scripts, the MCP server).
|
|
2649
2665
|
|
|
2650
|
-
|
|
2666
|
+
How to run the CLI:
|
|
2667
|
+
|
|
2668
|
+
- **In a project** created by `create-pos-app`: `npm run dev`, `npm run check`, `npm run build`, `npm run deploy`. Put options after `--`, for example `npm run deploy -- --message "Fix the debt box"`.
|
|
2669
|
+
- **Other commands, or from anywhere:** `npx @pancake-pos/app-cli <command>`, for example `npx @pancake-pos/app-cli versions`. In an installed project this uses the project's own version.
|
|
2670
|
+
- **Short command everywhere:** install globally with `npm i -g @pancake-pos/app-cli`, then run `pos-app <command>`.
|
|
2671
|
+
|
|
2672
|
+
Do not run `npx pos-app`: the `pos-app` package on npm is not published by Pancake. Outside an installed project, npx downloads and runs that package.
|
|
2651
2673
|
|
|
2652
2674
|
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.
|
|
2653
2675
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pancake-pos/app-cli",
|
|
3
|
-
"version": "2026.1.
|
|
3
|
+
"version": "2026.1.6",
|
|
4
4
|
"description": "pos-app command for Pancake POS apps: login, dev, check, build, deploy, versions and an MCP server for AI tools",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"esbuild": "^0.28.2",
|
|
25
25
|
"eslint": "^10.12.0",
|
|
26
26
|
"fflate": "^0.8.3",
|
|
27
|
-
"@pancake-pos/eslint-plugin": "2026.1.
|
|
27
|
+
"@pancake-pos/eslint-plugin": "2026.1.6"
|
|
28
28
|
}
|
|
29
29
|
}
|
package/runtime/frame.js
CHANGED
|
@@ -37,5 +37,5 @@ window.addEventListener("message", (event) => {
|
|
|
37
37
|
else if (data?.t === "kill") kill(data.instance);
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
|
-
window.parent.postMessage({ t: "frame-ready", protocol: PROTOCOL_VERSION, runtime: "2026.1.
|
|
40
|
+
window.parent.postMessage({ t: "frame-ready", protocol: PROTOCOL_VERSION, runtime: "2026.1.6" }, "*");
|
|
41
41
|
//# sourceMappingURL=frame.js.map
|