@pancake-pos/app-cli 2026.1.4 → 2026.1.5

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/dist/cli.js CHANGED
@@ -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
- "customer.details.block"
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
- if (!RESOURCES.includes(resource)) return add("unknown_target", `${path}.target`, `Target ${target} kh\xF4ng t\u1ED3n t\u1EA1i`);
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.4" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
1150
+ CLI_VERSION = true ? "2026.1.5" : 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 docHeading = plain ? target : target === "customer.details.block" ? `\`${target}\`` : `\`*.${location}\``;
1518
- const docs = all.filter((section) => section.page === "targets" && (section.heading === docHeading || section.heading.startsWith(`${docHeading} `)));
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 = target === "customer.details.block" ? "CustomerData ({ resource: 'customer', record: CustomerRef })" : DATA[location] ? `${DATA[location]}<'${resource}'>` : target === "app.page" ? "PageData ({ path, query })" : "none (no code)";
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 = target === "customer.details.block" ? [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) : [];
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",
@@ -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 (!RESOURCES2.includes(resource) || !targets(API_VERSION2).includes(target)) return fail(`Unknown target ${target}`);
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 r = resource;
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" },
@@ -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 blocks. Not yet: per-app webhooks and service keys, app storage |
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
  ---
@@ -1158,6 +1158,8 @@ Apply to `*.details.block`.
1158
1158
  | Target | Field | Description |
1159
1159
  |---|---|---|
1160
1160
  | `customer.details.block` | `module` (required), `should_render` | As for blocks; no `conditions`. |
1161
+ | `customer.details.action`, `customer.index.action`, `customer.index.selection-action` | As for actions | No `conditions`. |
1162
+ | `customer.index.column` | As for `<resource>.index.column` | `values` is keyed by the customer's `id`. |
1161
1163
  | `<resource>.print-action` | `label` (required) | Template name in the "Print template" picker, up to 40 characters. |
1162
1164
  | `<resource>.print-action` | `module` (required) | Module returning `{ html, title? }`. |
1163
1165
  | `<resource>.index.column` | `title` (required) | Column title, up to 30 characters. |
@@ -1166,7 +1168,7 @@ Apply to `*.details.block`.
1166
1168
  | `<resource>.index.column` | `align` | `left`, `center`, `right`. |
1167
1169
  | `<resource>.index.column` | `width` | Column width in px, 60–400, default 140. |
1168
1170
 
1169
- See [Extension points](/developers/apps/en/targets#customer-details-block) for the context data and return values.
1171
+ See [Extension points](/developers/apps/en/targets#customer) for the context data and return values.
1170
1172
 
1171
1173
  ### Hook fields
1172
1174
 
@@ -1261,7 +1263,7 @@ Targets attached to a POS record are named `<resource>.<location>`, for example
1261
1263
  | `<resource>.after_save` | After a record is created/updated on the detail form | hook, headless |
1262
1264
  | `<resource>.index.column` | A column in the list | hook, headless |
1263
1265
  | `<resource>.print-action` | A print template in the "Print template" picker (except `product`) | headless, returns HTML |
1264
- | `customer.details.block` | A box in the customer profile | Worker UI |
1266
+ | `customer.<location>` | Customer profile and list: `details.block`, `details.action`, `index.action`, `index.selection-action`, `index.column` | See [`customer.*`](#customer) |
1265
1267
 
1266
1268
  `<resource>` is one of `order`, `product`, `purchase`, `export`, `warehouse_transfer`. Products have no `print-action` because they only print barcodes.
1267
1269
 
@@ -1345,6 +1347,7 @@ Example `pos.data` in `purchase.details.action`:
1345
1347
  | `purchase` | `record.id` | `purchase_id`, `purchase_ids` |
1346
1348
  | `export` | `record.id` | `export_id`, `export_ids` |
1347
1349
  | `warehouse_transfer` | `record.id` | `warehouse_transfer_id`, `warehouse_transfer_ids` |
1350
+ | `customer` | `record.id` | `customer_id`, `customer_ids` |
1348
1351
 
1349
1352
  ### Types per resource
1350
1353
 
@@ -1663,9 +1666,19 @@ export default defineExtension('order.before_submit', async (data) => {
1663
1666
 
1664
1667
  `*.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
1668
 
1666
- ## `customer.details.block`
1669
+ ## `customer.*`
1667
1670
 
1668
- A box in the customer profile, in the right column below Notes and Addresses. The profile has this box wherever it opens: the Customers page, the order form and other screens.
1671
+ 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.
1672
+
1673
+ | Target | Location | Context data |
1674
+ |---|---|---|
1675
+ | `customer.details.block` | A box in the customer profile, right column below Notes and Addresses | `CustomerData` |
1676
+ | `customer.details.action` | An "Apps" button at the top of the customer profile, shown only when an app adds an action here | `CustomerData` |
1677
+ | `customer.index.action` | The "⋯" menu at the end of each customer list row, shown only when an app adds an action here | `IndexData<'customer'>` |
1678
+ | `customer.index.selection-action` | An "Apps" button on the action bar when several customers are selected | `SelectionData<'customer'>` |
1679
+ | `customer.index.column` | A column in the customer list; `values` is keyed by the customer's `id` | `ColumnData<'customer'>` |
1680
+
1681
+ 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
1682
 
1670
1683
  ```ts
1671
1684
  type CustomerRef = {
@@ -1678,12 +1691,14 @@ type CustomerRef = {
1678
1691
  updated_at: string | null;
1679
1692
  };
1680
1693
 
1681
- type CustomerData = { resource: 'customer'; record: CustomerRef };
1694
+ type CustomerData = { resource: 'customer'; record: CustomerRef }; // same shape as IndexData<'customer'>
1682
1695
  ```
1683
1696
 
1684
- - Manifest: `module` (required), `should_render`. No `conditions`.
1685
- - When the customer is edited in the profile, POS emits `data-change` with the new data.
1686
- - As with `*.details.block`, the worker starts when the box scrolls into view; POS shows a placeholder meanwhile.
1697
+ - 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.
1698
+ - `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.
1699
+ - `navigate` adds `customer_id` or `customer_ids` to the query.
1700
+ - When the customer is edited in the profile, the box receives `data-change` with the new data.
1701
+ - As with `*.details.block`, the box's worker starts when the box scrolls into view; POS shows a placeholder meanwhile.
1687
1702
 
1688
1703
  ## `*.print-action`
1689
1704
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pancake-pos/app-cli",
3
- "version": "2026.1.4",
3
+ "version": "2026.1.5",
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.4"
27
+ "@pancake-pos/eslint-plugin": "2026.1.5"
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.4" }, "*");
40
+ window.parent.postMessage({ t: "frame-ready", protocol: PROTOCOL_VERSION, runtime: "2026.1.5" }, "*");
41
41
  //# sourceMappingURL=frame.js.map