@cerkel/cli 0.0.0-stage → 1.0.1
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 +502 -2
- package/dist/api-url.js +42 -0
- package/dist/api.js +48 -0
- package/dist/bin.js +157 -0
- package/dist/chunk.js +31 -0
- package/dist/cli.js +497 -0
- package/dist/client.js +125 -0
- package/dist/command.js +2 -0
- package/dist/commands/args.js +107 -0
- package/dist/commands/auth.js +255 -0
- package/dist/commands/call.js +67 -0
- package/dist/commands/collection.js +121 -0
- package/dist/commands/content.js +281 -0
- package/dist/commands/customer.js +126 -0
- package/dist/commands/dev.js +261 -0
- package/dist/commands/draft.js +101 -0
- package/dist/commands/file.js +53 -0
- package/dist/commands/index.js +43 -0
- package/dist/commands/inventory.js +103 -0
- package/dist/commands/offer.js +92 -0
- package/dist/commands/order-args.js +37 -0
- package/dist/commands/order.js +219 -0
- package/dist/commands/price.js +55 -0
- package/dist/commands/product.js +265 -0
- package/dist/commands/promo.js +140 -0
- package/dist/commands/redirect.js +94 -0
- package/dist/commands/report.js +93 -0
- package/dist/commands/section.js +233 -0
- package/dist/commands/store.js +23 -0
- package/dist/commands/theme-tools.js +432 -0
- package/dist/commands/theme.js +388 -0
- package/dist/commands/whatsapp.js +39 -0
- package/dist/commands/zone.js +90 -0
- package/dist/commerce.js +138 -0
- package/dist/content-files.js +246 -0
- package/dist/context.js +5 -0
- package/dist/credentials.js +201 -0
- package/dist/diff.js +146 -0
- package/dist/env.js +24 -0
- package/dist/errors.js +73 -0
- package/dist/format.js +13 -0
- package/dist/layout.js +122 -0
- package/dist/logins.js +49 -0
- package/dist/raizn-alias.js +6 -0
- package/dist/redact.js +27 -0
- package/dist/session.js +266 -0
- package/dist/state.js +153 -0
- package/dist/sync.js +121 -0
- package/dist/zip.js +88 -0
- package/package.json +37 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { bool, str } from "../command.js";
|
|
2
|
+
import { READ_HELP, READ_OPTIONS, WRITE_HELP, WRITE_OPTIONS, flagsOf, moneyFlag, runRead, runWrite, show } from "../commerce.js";
|
|
3
|
+
import { CliError } from "../errors.js";
|
|
4
|
+
import { table } from "../format.js";
|
|
5
|
+
import { discountFlag, listFlag, need, pairsFlag } from "./order-args.js";
|
|
6
|
+
const draftLines = (d) => [
|
|
7
|
+
`Draft ${d.id} — ${d.status}`,
|
|
8
|
+
`Customer: ${d.customer.name ?? "—"} ${d.customer.phone ?? ""} · ${[d.address.governorate, d.address.city, d.address.address].filter(Boolean).join(", ") || "no address"}`,
|
|
9
|
+
...table([["Item", "SKU", "Qty", "Total"], ...d.items.map((l) => [l.title, l.sku ?? "—", String(l.quantity), l.total.formatted])]),
|
|
10
|
+
`Subtotal ${show(d.totals.subtotal)} · delivery ${show(d.totals.delivery)}${d.totals.deliveryZone ? ` (${d.totals.deliveryZone})` : ""} · discount ${show(d.totals.discount)} · total ${show(d.totals.total)}`,
|
|
11
|
+
...(d.problem ? [`Cannot be placed yet: ${d.problem}`] : []),
|
|
12
|
+
];
|
|
13
|
+
export const DRAFT_COMMANDS = {
|
|
14
|
+
"draft list": {
|
|
15
|
+
usage: "cerkel draft list [--status open|completed|cancelled] [--store <slug>] [--json]",
|
|
16
|
+
summary: "List the store's draft orders",
|
|
17
|
+
help: [READ_HELP],
|
|
18
|
+
options: { ...READ_OPTIONS, status: { type: "string" } },
|
|
19
|
+
run: (s, v) => runRead(s, flagsOf(v), "list_draft_orders", { ...(str(v, "status") ? { status: str(v, "status") } : {}) }, (d) => d.drafts.length
|
|
20
|
+
? table([["Id", "Created", "Customer", "Total"], ...d.drafts.map((x) => [x.id, x.createdAt.slice(0, 16).replace("T", " "), x.customer.name ?? "—", show(x.totals.total)])])
|
|
21
|
+
: ["No drafts."]),
|
|
22
|
+
},
|
|
23
|
+
"draft get": {
|
|
24
|
+
usage: "cerkel draft get <id> [--store <slug>] [--json]",
|
|
25
|
+
summary: "Show one draft order, priced",
|
|
26
|
+
help: [READ_HELP],
|
|
27
|
+
options: READ_OPTIONS,
|
|
28
|
+
positionals: true,
|
|
29
|
+
run: (s, v, pos) => {
|
|
30
|
+
need(pos, 1, "cerkel draft get <id>");
|
|
31
|
+
return runRead(s, flagsOf(v), "list_draft_orders", { draft: pos[0] }, draftLines);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"draft create": {
|
|
35
|
+
usage: "cerkel draft create --name <name> --phone <phone> --governorate <code> --address <text> --item <sku>=<n>… [--offer <id>=<n>]… [--addon <id>=<n>]… [--city …] [--district …] [--landmark …] [--phone-alt …] [--email …] [--discount <amount|n%>] [--promo <code>] [--delivery-fee <amount>] [--note …] [--tag <t>]… [--whatsapp] [--locale ar|en|ckb] [--store <slug>] [--yes] [--dry-run] [--json]",
|
|
36
|
+
summary: "Build a draft order for a phone or WhatsApp sale",
|
|
37
|
+
help: [
|
|
38
|
+
"The governorate is an ISO code (JO-AM for Amman, IQ-BG for Baghdad): it decides the delivery fee.",
|
|
39
|
+
"Nothing is sold until `cerkel draft complete <id>`. --whatsapp sends the customer the order-received message then.",
|
|
40
|
+
WRITE_HELP,
|
|
41
|
+
],
|
|
42
|
+
options: {
|
|
43
|
+
...WRITE_OPTIONS,
|
|
44
|
+
name: { type: "string" }, phone: { type: "string" }, "phone-alt": { type: "string" }, email: { type: "string" },
|
|
45
|
+
governorate: { type: "string" }, city: { type: "string" }, district: { type: "string" }, landmark: { type: "string" }, address: { type: "string" },
|
|
46
|
+
item: { type: "string", multiple: true }, offer: { type: "string", multiple: true }, addon: { type: "string", multiple: true },
|
|
47
|
+
discount: { type: "string" }, promo: { type: "string" }, "delivery-fee": { type: "string" }, note: { type: "string" },
|
|
48
|
+
tag: { type: "string", multiple: true }, whatsapp: { type: "boolean" }, locale: { type: "string" },
|
|
49
|
+
},
|
|
50
|
+
run: (s, v) => {
|
|
51
|
+
for (const k of ["name", "phone", "governorate", "address"])
|
|
52
|
+
if (!str(v, k))
|
|
53
|
+
throw new CliError(`--${k} is required. See cerkel draft create --help.`);
|
|
54
|
+
const items = [
|
|
55
|
+
...pairsFlag(v, "item", "<sku>").map((p) => ({ variant: p.key, quantity: p.quantity })),
|
|
56
|
+
...pairsFlag(v, "offer", "<offer id>").map((p) => ({ offer: p.key, quantity: p.quantity })),
|
|
57
|
+
...pairsFlag(v, "addon", "<add-on id>").map((p) => ({ addon: p.key, quantity: p.quantity })),
|
|
58
|
+
];
|
|
59
|
+
if (!items.length)
|
|
60
|
+
throw new CliError("Add at least one --item <sku>=<quantity>.");
|
|
61
|
+
const discount = discountFlag(v);
|
|
62
|
+
const fee = moneyFlag(str(v, "delivery-fee"));
|
|
63
|
+
return runWrite(s, flagsOf(v), "create_draft_order", {
|
|
64
|
+
customer: { name: str(v, "name"), phone: str(v, "phone"), ...(str(v, "phone-alt") ? { phone_alt: str(v, "phone-alt") } : {}), ...(str(v, "email") ? { email: str(v, "email") } : {}) },
|
|
65
|
+
address: {
|
|
66
|
+
governorate: str(v, "governorate"), address: str(v, "address"),
|
|
67
|
+
...(str(v, "city") ? { city: str(v, "city") } : {}), ...(str(v, "district") ? { district: str(v, "district") } : {}), ...(str(v, "landmark") ? { landmark: str(v, "landmark") } : {}),
|
|
68
|
+
},
|
|
69
|
+
items,
|
|
70
|
+
...(discount ? { discount } : {}),
|
|
71
|
+
...(str(v, "promo") ? { promo_code: str(v, "promo") } : {}),
|
|
72
|
+
...(fee ? { delivery_fee: fee } : {}),
|
|
73
|
+
...(str(v, "note") ? { note: str(v, "note") } : {}),
|
|
74
|
+
...(listFlag(v, "tag") ? { tags: listFlag(v, "tag") } : {}),
|
|
75
|
+
...(bool(v, "whatsapp") ? { send_whatsapp: true } : {}),
|
|
76
|
+
...(str(v, "locale") ? { locale: str(v, "locale") } : {}),
|
|
77
|
+
}, {
|
|
78
|
+
after: (a) => {
|
|
79
|
+
const r = a.result;
|
|
80
|
+
return [`Done: ${a.summary}`, ...(r?.id ? [`Draft id: ${r.id} — place it with: cerkel draft complete ${r.id}`] : [])];
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
"draft complete": {
|
|
86
|
+
usage: "cerkel draft complete <id> [--store <slug>] [--yes] [--dry-run] [--json]",
|
|
87
|
+
summary: "Place a draft as a real order (number, stock, cash-on-delivery checks)",
|
|
88
|
+
help: ["Needs permission to edit and to confirm orders.", WRITE_HELP],
|
|
89
|
+
options: WRITE_OPTIONS,
|
|
90
|
+
positionals: true,
|
|
91
|
+
run: (s, v, pos) => {
|
|
92
|
+
need(pos, 1, "cerkel draft complete <id>");
|
|
93
|
+
return runWrite(s, flagsOf(v), "complete_draft_order", { draft: pos[0] }, {
|
|
94
|
+
after: (a) => {
|
|
95
|
+
const r = a.result;
|
|
96
|
+
return [`Done: order ${r?.number ?? "?"} placed.`];
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { str } from "../command.js";
|
|
2
|
+
import { READ_HELP, READ_OPTIONS, WRITE_HELP, WRITE_OPTIONS, flagsOf, runRead, runWrite } from "../commerce.js";
|
|
3
|
+
import { CliError } from "../errors.js";
|
|
4
|
+
import { table } from "../format.js";
|
|
5
|
+
const size = (b) => (b == null ? "—" : b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MB` : `${Math.max(1, Math.round(b / 1024))} KB`);
|
|
6
|
+
/** `cerkel file …` — the store's media library (list_files, upload_file, delete_file). */
|
|
7
|
+
export const FILE_COMMANDS = {
|
|
8
|
+
"file list": {
|
|
9
|
+
usage: "cerkel file list [--kind image|video|document] [--search <text>] [--store <slug>] [--json]",
|
|
10
|
+
summary: "List the files in the store's media library",
|
|
11
|
+
help: [READ_HELP],
|
|
12
|
+
options: { ...READ_OPTIONS, kind: { type: "string" }, search: { type: "string" } },
|
|
13
|
+
run: (s, v) => {
|
|
14
|
+
const kind = str(v, "kind");
|
|
15
|
+
if (kind && !["image", "video", "document"].includes(kind))
|
|
16
|
+
throw new CliError("--kind is image, video or document.");
|
|
17
|
+
return runRead(s, flagsOf(v), "list_files", { limit: 200, ...(kind ? { kind } : {}), ...(str(v, "search") ? { q: str(v, "search") } : {}) }, (d) => [
|
|
18
|
+
...table([["ID", "NAME", "SIZE", "STATUS", "USED BY", "URL"], ...d.files.map((f) => [f.id, f.filename ?? "—", size(f.bytes), f.status, String(f.usedBy), f.url ?? f.sourceUrl ?? "—"])]),
|
|
19
|
+
...(d.total > d.files.length ? [`… ${d.total - d.files.length} more (use --search)`] : []),
|
|
20
|
+
]);
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
"file upload": {
|
|
24
|
+
usage: "cerkel file upload <https-url> [--name <filename>] [--store <slug>] [--yes] [--dry-run] [--json]",
|
|
25
|
+
summary: "Copy an image, video or PDF from a public URL into the store's media library",
|
|
26
|
+
help: ["Up to 25 MB. Prints the file's permanent URL to use in pages, products and theme settings.", WRITE_HELP],
|
|
27
|
+
options: { ...WRITE_OPTIONS, name: { type: "string" } },
|
|
28
|
+
positionals: true,
|
|
29
|
+
run: (s, v, pos) => {
|
|
30
|
+
if (pos.length !== 1)
|
|
31
|
+
throw new CliError("Usage: cerkel file upload <https-url> [--name <filename>]");
|
|
32
|
+
return runWrite(s, flagsOf(v), "upload_file", { url: pos[0], ...(str(v, "name") ? { filename: str(v, "name") } : {}) }, {
|
|
33
|
+
after: (o) => [
|
|
34
|
+
`Done: ${o.summary}`,
|
|
35
|
+
...(o.file ? [`${o.file.status === "mirrored" ? "Ready" : `Status: ${o.file.status}`}: ${o.file.url ?? o.file.sourceUrl ?? ""}${o.file.error ? ` (${o.file.error})` : ""}`] : []),
|
|
36
|
+
...(o.note ? [o.note] : []),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"file delete": {
|
|
42
|
+
usage: "cerkel file delete <file-id> [--store <slug>] [--yes] [--dry-run] [--json]",
|
|
43
|
+
summary: "Delete a file from the media library (refused while something shows it)",
|
|
44
|
+
help: [WRITE_HELP],
|
|
45
|
+
options: WRITE_OPTIONS,
|
|
46
|
+
positionals: true,
|
|
47
|
+
run: (s, v, pos) => {
|
|
48
|
+
if (pos.length !== 1)
|
|
49
|
+
throw new CliError("Usage: cerkel file delete <file-id>");
|
|
50
|
+
return runWrite(s, flagsOf(v), "delete_file", { file: pos[0] });
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { COLLECTION_COMMANDS } from "./collection.js";
|
|
2
|
+
import { INVENTORY_COMMANDS } from "./inventory.js";
|
|
3
|
+
import { OFFER_COMMANDS } from "./offer.js";
|
|
4
|
+
import { CUSTOMER_COMMANDS } from "./customer.js";
|
|
5
|
+
import { DRAFT_COMMANDS } from "./draft.js";
|
|
6
|
+
import { ORDER_COMMANDS } from "./order.js";
|
|
7
|
+
import { PRICE_COMMANDS } from "./price.js";
|
|
8
|
+
import { PRODUCT_COMMANDS } from "./product.js";
|
|
9
|
+
import { CALL_COMMANDS } from "./call.js";
|
|
10
|
+
import { CONTENT_COMMANDS } from "./content.js";
|
|
11
|
+
import { FILE_COMMANDS } from "./file.js";
|
|
12
|
+
import { PROMO_COMMANDS } from "./promo.js";
|
|
13
|
+
import { REDIRECT_COMMANDS } from "./redirect.js";
|
|
14
|
+
import { REPORT_COMMANDS } from "./report.js";
|
|
15
|
+
import { STORE_COMMANDS } from "./store.js";
|
|
16
|
+
import { ZONE_COMMANDS } from "./zone.js";
|
|
17
|
+
import { WHATSAPP_COMMANDS } from "./whatsapp.js";
|
|
18
|
+
/**
|
|
19
|
+
* Every commerce command group (`cerkel store …`, `cerkel price …`, …), each in its own file built on
|
|
20
|
+
* ../commerce.ts (runRead / runWrite). cli.ts merges these after the theme commands; a new group is
|
|
21
|
+
* one import and one spread here.
|
|
22
|
+
*/
|
|
23
|
+
export const COMMERCE_COMMANDS = {
|
|
24
|
+
...STORE_COMMANDS,
|
|
25
|
+
...PRODUCT_COMMANDS,
|
|
26
|
+
...PRICE_COMMANDS,
|
|
27
|
+
...OFFER_COMMANDS,
|
|
28
|
+
...INVENTORY_COMMANDS,
|
|
29
|
+
...COLLECTION_COMMANDS,
|
|
30
|
+
...CONTENT_COMMANDS,
|
|
31
|
+
...REDIRECT_COMMANDS,
|
|
32
|
+
...FILE_COMMANDS,
|
|
33
|
+
...PROMO_COMMANDS,
|
|
34
|
+
...ZONE_COMMANDS,
|
|
35
|
+
...REPORT_COMMANDS,
|
|
36
|
+
...CALL_COMMANDS,
|
|
37
|
+
// Phase C: orders, draft orders, customers and reviews.
|
|
38
|
+
...ORDER_COMMANDS,
|
|
39
|
+
...DRAFT_COMMANDS,
|
|
40
|
+
...CUSTOMER_COMMANDS,
|
|
41
|
+
// WhatsApp template tests.
|
|
42
|
+
...WHATSAPP_COMMANDS,
|
|
43
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { bool, str } from "../command.js";
|
|
2
|
+
import { READ_HELP, READ_OPTIONS, WRITE_HELP, WRITE_OPTIONS, flagsOf, runRead, runWrite } from "../commerce.js";
|
|
3
|
+
import { CliError, ToolRefusal } from "../errors.js";
|
|
4
|
+
import { table } from "../format.js";
|
|
5
|
+
import { defined, int, onOff } from "./args.js";
|
|
6
|
+
const REASONS = "correction, damage, theft, received, return, count";
|
|
7
|
+
export const INVENTORY_COMMANDS = {
|
|
8
|
+
"inventory get": {
|
|
9
|
+
usage: "cerkel inventory get <product|sku> [--location <name|code|id>] [--store <slug>] [--json]",
|
|
10
|
+
summary: "Show stock per variant and location: on hand, reserved, available",
|
|
11
|
+
help: ["A product handle shows all its variants; a SKU shows that variant.", READ_HELP],
|
|
12
|
+
options: { ...READ_OPTIONS, location: { type: "string" }, sku: { type: "boolean" } },
|
|
13
|
+
positionals: true,
|
|
14
|
+
run: async (s, v, pos) => {
|
|
15
|
+
const [ref, extra] = pos;
|
|
16
|
+
if (!ref || extra !== undefined)
|
|
17
|
+
throw new CliError("Usage: cerkel inventory get <product|sku>");
|
|
18
|
+
const render = (d) => [
|
|
19
|
+
`${d.product.title} (${d.product.handle})${d.quantitiesFromShopify ? " — quantities come from the Shopify sync" : ""}`,
|
|
20
|
+
...table([["SKU", "VARIANT", "LOCATION", "ON HAND", "RESERVED", "AVAILABLE", "TRACKED"], ...d.variants.flatMap((x) => {
|
|
21
|
+
const rows = x.locations.length ? x.locations : [{ name: "—", onHand: 0, reserved: 0, available: 0 }];
|
|
22
|
+
return rows.map((l) => [x.sku ?? "—", x.label, l.name ?? "—", String(l.onHand), String(l.reserved), String(l.available), x.tracked ? (x.continueSelling ? "yes, sells when out" : "yes") : "no"]);
|
|
23
|
+
})]),
|
|
24
|
+
];
|
|
25
|
+
const location = str(v, "location");
|
|
26
|
+
// A handle first (product), then the same text as a SKU (variant): the server says which exists.
|
|
27
|
+
if (!bool(v, "sku")) {
|
|
28
|
+
try {
|
|
29
|
+
return await runRead(s, flagsOf(v), "get_inventory_levels", defined({ product: ref, location }), render);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (!(e instanceof ToolRefusal) || e.code !== "not_found")
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return runRead(s, flagsOf(v), "get_inventory_levels", defined({ variant: ref, location }), render);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"inventory adjust": {
|
|
40
|
+
usage: "cerkel inventory adjust <sku> <+N|-N> [--reason <reason>] [--location <name>] [--note <text>] [--yes] [--dry-run] [--json]",
|
|
41
|
+
summary: "Add or take away units of a variant at a location",
|
|
42
|
+
help: [`Reasons: ${REASONS} (default correction; received for a supplier delivery).`, WRITE_HELP],
|
|
43
|
+
options: { ...WRITE_OPTIONS, reason: { type: "string" }, location: { type: "string" }, note: { type: "string" } },
|
|
44
|
+
positionals: true,
|
|
45
|
+
run: (s, v, pos) => {
|
|
46
|
+
const [variant, qty, extra] = pos;
|
|
47
|
+
if (!variant || qty === undefined || extra !== undefined)
|
|
48
|
+
throw new CliError("Usage: cerkel inventory adjust <sku> <+N|-N> [--reason received]");
|
|
49
|
+
return runWrite(s, flagsOf(v), "adjust_inventory", defined({
|
|
50
|
+
variant, quantity: int(qty.replace(/^\+/, ""), "The quantity"), reason: str(v, "reason") ?? "correction", location: str(v, "location"), note: str(v, "note"),
|
|
51
|
+
}));
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"inventory set": {
|
|
55
|
+
usage: "cerkel inventory set <sku> <N> [--reason <reason>] [--location <name>] [--note <text>] [--yes] [--dry-run] [--json]",
|
|
56
|
+
summary: "Set the units on hand of a variant at a location to an exact number",
|
|
57
|
+
help: [`Reasons: ${REASONS} (default correction).`, WRITE_HELP],
|
|
58
|
+
options: { ...WRITE_OPTIONS, reason: { type: "string" }, location: { type: "string" }, note: { type: "string" } },
|
|
59
|
+
positionals: true,
|
|
60
|
+
run: (s, v, pos) => {
|
|
61
|
+
const [variant, qty, extra] = pos;
|
|
62
|
+
if (!variant || qty === undefined || extra !== undefined)
|
|
63
|
+
throw new CliError("Usage: cerkel inventory set <sku> <N>");
|
|
64
|
+
return runWrite(s, flagsOf(v), "set_inventory", defined({
|
|
65
|
+
variant, quantity: int(qty, "The quantity"), reason: str(v, "reason"), location: str(v, "location"), note: str(v, "note"),
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"inventory tracking": {
|
|
70
|
+
usage: "cerkel inventory tracking <product> [--track on|off] [--continue-selling on|off] [--variant <sku>]… [--yes] [--dry-run] [--json]",
|
|
71
|
+
summary: "Turn Track quantity / Continue selling when out of stock on or off",
|
|
72
|
+
help: ["All the product's variants, or the ones named with --variant.", WRITE_HELP],
|
|
73
|
+
options: { ...WRITE_OPTIONS, track: { type: "string" }, "continue-selling": { type: "string" }, variant: { type: "string", multiple: true } },
|
|
74
|
+
positionals: true,
|
|
75
|
+
run: (s, v, pos) => {
|
|
76
|
+
const [product, extra] = pos;
|
|
77
|
+
if (!product || extra !== undefined)
|
|
78
|
+
throw new CliError("Usage: cerkel inventory tracking <product> --track on|off");
|
|
79
|
+
const variants = Array.isArray(v["variant"]) ? v["variant"] : undefined;
|
|
80
|
+
return runWrite(s, flagsOf(v), "set_inventory_tracking", defined({
|
|
81
|
+
product, variants: variants?.length ? variants : undefined, track: onOff(str(v, "track"), "--track"), continueSelling: onOff(str(v, "continue-selling"), "--continue-selling"),
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
"inventory locations": {
|
|
86
|
+
usage: "cerkel inventory locations [--all] [--store <slug>] [--json]",
|
|
87
|
+
summary: "Show the store's stock locations (--all includes inactive ones)",
|
|
88
|
+
help: [READ_HELP],
|
|
89
|
+
options: { ...READ_OPTIONS, all: { type: "boolean" } },
|
|
90
|
+
run: (s, v) => runRead(s, flagsOf(v), "list_locations", bool(v, "all") ? { includeInactive: true } : {}, (d) => table([["NAME", "CODE", "TYPE", "DEFAULT", "ACTIVE", "SHARED"], ...d.locations.map((l) => [
|
|
91
|
+
l.name, l.code ?? "—", l.type, l.isDefault ? "yes" : "", l.active ? "yes" : "no", l.sharedByOrg ? "yes" : "",
|
|
92
|
+
])])),
|
|
93
|
+
},
|
|
94
|
+
"inventory low": {
|
|
95
|
+
usage: "cerkel inventory low [--out] [--limit N] [--store <slug>] [--json]",
|
|
96
|
+
summary: "Show tracked variants that are low or out of stock",
|
|
97
|
+
help: ["--out lists only what has nothing left to sell.", READ_HELP],
|
|
98
|
+
options: { ...READ_OPTIONS, out: { type: "boolean" }, limit: { type: "string" } },
|
|
99
|
+
run: (s, v) => runRead(s, flagsOf(v), "list_low_stock", defined({ status: bool(v, "out") ? "out" : undefined, limit: int(str(v, "limit"), "--limit") }), (d) => d.variants.length === 0 ? ["Nothing is low or out of stock."] : table([["SKU", "PRODUCT", "VARIANT", "STATUS", "AVAILABLE", "REORDER AT"], ...d.variants.map((x) => [
|
|
100
|
+
x.sku ?? "—", x.product.handle, x.variant || "—", x.status, String(x.available), x.reorderPoint === null ? "—" : String(x.reorderPoint),
|
|
101
|
+
])])),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { str } from "../command.js";
|
|
2
|
+
import { READ_HELP, READ_OPTIONS, WRITE_HELP, WRITE_OPTIONS, flagsOf, moneyFlag, runRead, runWrite, show } from "../commerce.js";
|
|
3
|
+
import { CliError } from "../errors.js";
|
|
4
|
+
import { table } from "../format.js";
|
|
5
|
+
import { LANG_OPTION, TEXT_HELP, defined, int, lines, localized, pairs, textOf } from "./args.js";
|
|
6
|
+
const activeFlag = (v) => (v["active"] === true ? true : v["inactive"] === true ? false : undefined);
|
|
7
|
+
export const OFFER_COMMANDS = {
|
|
8
|
+
"offer list": {
|
|
9
|
+
usage: "cerkel offer list [product] [--store <slug>] [--json]",
|
|
10
|
+
summary: "Show bundle tiers and add-ons (of one product, or the whole store)",
|
|
11
|
+
help: [READ_HELP],
|
|
12
|
+
options: READ_OPTIONS,
|
|
13
|
+
positionals: true,
|
|
14
|
+
run: (s, v, pos) => runRead(s, flagsOf(v), "list_offers", defined({ product: pos[0] }), (d) => [
|
|
15
|
+
...(d.offers.length
|
|
16
|
+
? table([["PRODUCT", "KEY", "LABEL", "PRICE", "COMPARE-AT", "PIECES", "ACTIVE"], ...d.offers.map((o) => [
|
|
17
|
+
o.product, o.key, textOf(o.label), show(o.price), show(o.compareAt), String(o.lines.reduce((n, l) => n + l.quantity, 0)), o.active ? "yes" : "no",
|
|
18
|
+
])])
|
|
19
|
+
: ["No bundle tiers."]),
|
|
20
|
+
"",
|
|
21
|
+
...(d.addons.length
|
|
22
|
+
? table([["PRODUCT", "ADD-ON", "PRICE", "ACTIVE"], ...d.addons.map((a) => [a.product, a.variant.label ?? a.variant.sku ?? "—", show(a.price), a.active ? "yes" : "no"])])
|
|
23
|
+
: ["No add-ons."]),
|
|
24
|
+
]),
|
|
25
|
+
},
|
|
26
|
+
"offer set": {
|
|
27
|
+
usage: "cerkel offer set <product> [<key>] [--label …] [--badge …] [--description …] [--price <amount>] [--compare-at <amount|none>] [--line <sku>:<qty>]… [--composition name=N]… [--position N] [--active|--inactive] [--yes] [--dry-run] [--json]",
|
|
28
|
+
summary: "Create a bundle tier, or change one (name it by its key)",
|
|
29
|
+
help: [
|
|
30
|
+
"New tier: --label, --price and at least one --line (e.g. --line JL-SQ:5 --line JL-CYL:1). Existing tier: only what you pass changes.",
|
|
31
|
+
"Amounts are in the store's currency (IQD in steps of 250). --new-key sets the key of a new tier (else one is generated).",
|
|
32
|
+
TEXT_HELP, WRITE_HELP,
|
|
33
|
+
],
|
|
34
|
+
options: {
|
|
35
|
+
...WRITE_OPTIONS, ...LANG_OPTION, label: { type: "string", multiple: true }, badge: { type: "string", multiple: true }, description: { type: "string", multiple: true },
|
|
36
|
+
price: { type: "string" }, "compare-at": { type: "string" }, line: { type: "string", multiple: true }, composition: { type: "string", multiple: true },
|
|
37
|
+
position: { type: "string" }, active: { type: "boolean" }, inactive: { type: "boolean" }, "new-key": { type: "string" },
|
|
38
|
+
},
|
|
39
|
+
positionals: true,
|
|
40
|
+
run: (s, v, pos) => {
|
|
41
|
+
const [product, offer, extra] = pos;
|
|
42
|
+
if (!product || extra !== undefined)
|
|
43
|
+
throw new CliError("Usage: cerkel offer set <product> [<key>] [flags]");
|
|
44
|
+
return runWrite(s, flagsOf(v), "upsert_offer", defined({
|
|
45
|
+
product, offer, key: str(v, "new-key"), label: localized(v, "label"), badge: localized(v, "badge"), description: localized(v, "description"),
|
|
46
|
+
price: str(v, "price"), compareAt: moneyFlag(str(v, "compare-at")), lines: lines(v, "line"), composition: pairs(v, "composition"),
|
|
47
|
+
position: int(str(v, "position"), "--position"), active: activeFlag(v),
|
|
48
|
+
}));
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"offer delete": {
|
|
52
|
+
usage: "cerkel offer delete <product> <key> [--yes] [--dry-run] [--json]",
|
|
53
|
+
summary: "Delete a bundle tier (or hide it with offer set --inactive)",
|
|
54
|
+
help: [WRITE_HELP],
|
|
55
|
+
options: WRITE_OPTIONS,
|
|
56
|
+
positionals: true,
|
|
57
|
+
run: (s, v, pos) => {
|
|
58
|
+
const [product, offer, extra] = pos;
|
|
59
|
+
if (!product || !offer || extra !== undefined)
|
|
60
|
+
throw new CliError("Usage: cerkel offer delete <product> <key>");
|
|
61
|
+
return runWrite(s, flagsOf(v), "delete_offer", { product, offer });
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"addon set": {
|
|
65
|
+
usage: "cerkel addon set <product> <addon-sku> [--price <amount>] [--compare-at <amount|none>] [--position N] [--active|--inactive] [--yes] [--dry-run] [--json]",
|
|
66
|
+
summary: "Offer a variant as an add-on with a product's order, or change it",
|
|
67
|
+
help: ["A new add-on needs --price (0 = a free gift). Only what you pass changes.", WRITE_HELP],
|
|
68
|
+
options: { ...WRITE_OPTIONS, price: { type: "string" }, "compare-at": { type: "string" }, position: { type: "string" }, active: { type: "boolean" }, inactive: { type: "boolean" } },
|
|
69
|
+
positionals: true,
|
|
70
|
+
run: (s, v, pos) => {
|
|
71
|
+
const [product, variant, extra] = pos;
|
|
72
|
+
if (!product || !variant || extra !== undefined)
|
|
73
|
+
throw new CliError("Usage: cerkel addon set <product> <addon-sku> [--price <amount>]");
|
|
74
|
+
return runWrite(s, flagsOf(v), "upsert_addon", defined({
|
|
75
|
+
product, variant, price: str(v, "price"), compareAt: moneyFlag(str(v, "compare-at")), position: int(str(v, "position"), "--position"), active: activeFlag(v),
|
|
76
|
+
}));
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"addon delete": {
|
|
80
|
+
usage: "cerkel addon delete <product> <addon-sku> [--yes] [--dry-run] [--json]",
|
|
81
|
+
summary: "Stop offering a variant as an add-on with a product",
|
|
82
|
+
help: [WRITE_HELP],
|
|
83
|
+
options: WRITE_OPTIONS,
|
|
84
|
+
positionals: true,
|
|
85
|
+
run: (s, v, pos) => {
|
|
86
|
+
const [product, variant, extra] = pos;
|
|
87
|
+
if (!product || !variant || extra !== undefined)
|
|
88
|
+
throw new CliError("Usage: cerkel addon delete <product> <addon-sku>");
|
|
89
|
+
return runWrite(s, flagsOf(v), "delete_addon", { product, variant });
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { str } from "../command.js";
|
|
2
|
+
import { CliError } from "../errors.js";
|
|
3
|
+
// Argument helpers for the phase C groups (order, draft, customer, review); args.ts holds the catalogue ones.
|
|
4
|
+
/** A repeatable flag (`--tag a --tag b`) or a comma list (`--tag a,b`), trimmed, empties dropped. */
|
|
5
|
+
export function listFlag(v, k) {
|
|
6
|
+
const raw = v[k];
|
|
7
|
+
if (raw === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const parts = (Array.isArray(raw) ? raw : [raw]).filter((x) => typeof x === "string");
|
|
10
|
+
return parts.flatMap((p) => p.split(",")).map((p) => p.trim()).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
/** `KEY=N` pairs (`--add JL-90=2`), the number a whole quantity. */
|
|
13
|
+
export function pairsFlag(v, k, what) {
|
|
14
|
+
return (listFlag(v, k) ?? []).map((p) => {
|
|
15
|
+
const m = /^(.+?)=(\d{1,3})$/.exec(p);
|
|
16
|
+
if (!m)
|
|
17
|
+
throw new CliError(`--${k} takes ${what}=<quantity>, e.g. --${k} JL-90=2 (got "${p}").`);
|
|
18
|
+
return { key: m[1].trim(), quantity: Number(m[2]) };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** `--discount 10%` → percent, `--discount 2.500` → amount (as typed; the server parses money), `none` → null. */
|
|
22
|
+
export function discountFlag(v, k = "discount") {
|
|
23
|
+
const raw = str(v, k);
|
|
24
|
+
if (raw === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const t = raw.trim();
|
|
27
|
+
if (["none", "null", "-", ""].includes(t.toLowerCase()))
|
|
28
|
+
return null;
|
|
29
|
+
return t.endsWith("%") ? { type: "percent", value: t.slice(0, -1).trim() } : { type: "amount", value: t };
|
|
30
|
+
}
|
|
31
|
+
/** The rest of the positionals as one text (`cerkel order note JL-1001 Customer called back`). */
|
|
32
|
+
export const textOf = (pos, from) => pos.slice(from).join(" ").trim();
|
|
33
|
+
/** Fails with the usage line when a positional is missing. */
|
|
34
|
+
export function need(pos, n, usage) {
|
|
35
|
+
if (pos.length < n || pos.slice(0, n).some((p) => !p?.trim()))
|
|
36
|
+
throw new CliError(`Usage: ${usage}`);
|
|
37
|
+
}
|