kaching-cli 0.1.0

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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/assets/sdk.json +1 -0
  4. package/assets/template/.env.example +4 -0
  5. package/assets/template/AGENTS.md +54 -0
  6. package/assets/template/CLAUDE.md +1 -0
  7. package/assets/template/README.md +11 -0
  8. package/assets/template/eslint.config.mjs +18 -0
  9. package/assets/template/gitignore +42 -0
  10. package/assets/template/next.config.ts +10 -0
  11. package/assets/template/package.json +30 -0
  12. package/assets/template/postcss.config.mjs +7 -0
  13. package/assets/template/src/app/checkout/success/page.tsx +9 -0
  14. package/assets/template/src/app/favicon.ico +0 -0
  15. package/assets/template/src/app/globals.css +41 -0
  16. package/assets/template/src/app/layout.tsx +47 -0
  17. package/assets/template/src/app/not-found.tsx +13 -0
  18. package/assets/template/src/app/page.tsx +37 -0
  19. package/assets/template/src/app/products/[slug]/page.tsx +62 -0
  20. package/assets/template/src/components/cart-button.tsx +21 -0
  21. package/assets/template/src/components/cart-drawer.tsx +136 -0
  22. package/assets/template/src/components/header.tsx +26 -0
  23. package/assets/template/src/components/order-confirmation.tsx +116 -0
  24. package/assets/template/src/components/price.tsx +21 -0
  25. package/assets/template/src/components/product-card.tsx +44 -0
  26. package/assets/template/src/components/product-form.tsx +123 -0
  27. package/assets/template/src/components/providers.tsx +14 -0
  28. package/assets/template/src/lib/kaching.ts +24 -0
  29. package/assets/template/src/store.config.ts +16 -0
  30. package/assets/template/tsconfig.json +34 -0
  31. package/dist/chunk-TWKERERX.js +283 -0
  32. package/dist/index.js +503 -0
  33. package/dist/mcp-LTF62ASB.js +313 -0
  34. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CliError,
4
+ DEFAULT_BASE_URL,
5
+ accountClient,
6
+ apiUrl,
7
+ c,
8
+ formatMoney,
9
+ handleError,
10
+ isJson,
11
+ log,
12
+ openBrowser,
13
+ out,
14
+ readConfig,
15
+ setJsonMode,
16
+ storeClient,
17
+ table,
18
+ toMinor,
19
+ upsertEnvFile,
20
+ writeConfig
21
+ } from "./chunk-TWKERERX.js";
22
+
23
+ // src/index.ts
24
+ import { Command } from "commander";
25
+
26
+ // src/commands/auth.ts
27
+ import { hostname } from "os";
28
+ async function post(base, path, body) {
29
+ const res = await fetch(base + path, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(body)
33
+ });
34
+ return { status: res.status, data: await res.json() };
35
+ }
36
+ async function login(opts) {
37
+ const base = apiUrl(opts.apiUrl);
38
+ const start = await post(base, "/cli/device", { client_name: `kaching CLI on ${hostname()}` });
39
+ if (start.status !== 200) throw new CliError(`Could not start login against ${base}`, "login_failed");
40
+ const d = start.data;
41
+ log(`
42
+ Confirm this code in your browser: ${c.bold(c.cyan(d.user_code))}
43
+ `);
44
+ const opened = opts.open !== false && openBrowser(d.verification_uri_complete);
45
+ log(` ${opened ? "Opened" : "Open"} ${c.cyan(d.verification_uri_complete)}
46
+ `);
47
+ log(c.dim(" Waiting for approval\u2026"));
48
+ const deadline = new Date(d.expires_at).getTime();
49
+ while (Date.now() < deadline) {
50
+ await new Promise((r) => setTimeout(r, d.interval * 1e3));
51
+ const poll = await post(base, "/cli/token", { device_code: d.device_code });
52
+ if (poll.data.status === "approved" && poll.data.account_token) {
53
+ writeConfig({ account_token: poll.data.account_token, api_url: opts.apiUrl ? base : readConfig().api_url });
54
+ return;
55
+ }
56
+ if (poll.data.status === "denied") throw new CliError("Login was denied in the browser.", "login_denied");
57
+ if (poll.data.status === "expired") break;
58
+ }
59
+ throw new CliError("Login code expired. Run `kaching login` again.", "login_expired");
60
+ }
61
+ function registerAuth(program2) {
62
+ program2.command("login").description("Log in to kaching (opens your browser to approve this device)").option("--api-url <url>", "API base URL (for self-hosted or local development)").option("--no-open", "Print the URL instead of opening a browser").action(async (opts) => {
63
+ await login(opts);
64
+ const { data } = await accountClient().stores.list();
65
+ out(
66
+ { logged_in: true, stores: data.map((s) => s.slug) },
67
+ () => log(`${c.green("\u2714")} Logged in. ${data.length} store${data.length === 1 ? "" : "s"} on this account.`)
68
+ );
69
+ });
70
+ program2.command("logout").description("Forget the saved account token on this machine").action(() => {
71
+ writeConfig({ account_token: void 0 });
72
+ out({ logged_in: false }, () => log(`${c.green("\u2714")} Logged out.`));
73
+ });
74
+ program2.command("whoami").description("Show login status and this account's stores").action(async () => {
75
+ if (!readConfig().account_token && !process.env.KACHING_ACCOUNT_TOKEN) {
76
+ return out({ logged_in: false, api_url: apiUrl() }, () => log("Not logged in. Run `kaching login`."));
77
+ }
78
+ const { data } = await accountClient().stores.list();
79
+ out({ logged_in: true, api_url: apiUrl(), stores: data }, () => {
80
+ log(`Logged in to ${c.cyan(apiUrl())}`);
81
+ for (const s of data) {
82
+ console.log(` ${c.bold(s.slug)} ${s.name} ${s.currency.toUpperCase()} ${s.payments.ready ? c.green("payments ready") : c.yellow("payments not set up")}`);
83
+ }
84
+ });
85
+ });
86
+ }
87
+
88
+ // src/commands/create.ts
89
+ import { spawnSync } from "child_process";
90
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
91
+ import { basename, dirname, join, relative, resolve } from "path";
92
+ import { fileURLToPath } from "url";
93
+ var assetsDir = () => join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
94
+ function detectPackageManager() {
95
+ const ua = process.env.npm_config_user_agent ?? "";
96
+ if (ua.startsWith("pnpm")) return "pnpm";
97
+ if (ua.startsWith("yarn")) return "yarn";
98
+ if (ua.startsWith("bun")) return "bun";
99
+ return "npm";
100
+ }
101
+ function scaffold(dir, slug, pm) {
102
+ const assets = assetsDir();
103
+ if (!existsSync(join(assets, "template"))) throw new CliError("Storefront template missing from this CLI build.", "no_template");
104
+ cpSync(join(assets, "template"), dir, { recursive: true });
105
+ if (existsSync(join(dir, "gitignore"))) renameSync(join(dir, "gitignore"), join(dir, ".gitignore"));
106
+ const vendorSrc = join(assets, "vendor");
107
+ const tarballs = existsSync(vendorSrc) ? readdirSync(vendorSrc).filter((f) => f.endsWith(".tgz")) : [];
108
+ const pkgPath = join(dir, "package.json");
109
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
110
+ pkg.name = slug;
111
+ delete pkg.packageManager;
112
+ if (tarballs.length) {
113
+ mkdirSync(join(dir, "vendor"), { recursive: true });
114
+ const overrides = {};
115
+ for (const file of tarballs) {
116
+ cpSync(join(vendorSrc, file), join(dir, "vendor", file));
117
+ const name = file.includes("-react-") ? "@kaching.sh/react" : "@kaching.sh/sdk";
118
+ overrides[name] = `file:./vendor/${file}`;
119
+ }
120
+ for (const [name, spec] of Object.entries(overrides)) pkg.dependencies[name] = spec;
121
+ pkg.overrides = overrides;
122
+ pkg.resolutions = overrides;
123
+ } else {
124
+ const { version } = JSON.parse(readFileSync(join(assets, "sdk.json"), "utf8"));
125
+ for (const name of ["@kaching.sh/sdk", "@kaching.sh/react"]) pkg.dependencies[name] = `^${version}`;
126
+ }
127
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
128
+ if (pm === "pnpm") {
129
+ const overrides = pkg.overrides ? `overrides:
130
+ ${Object.entries(pkg.overrides).map(([k, v]) => ` "${k}": "${v}"`).join("\n")}
131
+ ` : "";
132
+ writeFileSync(join(dir, "pnpm-workspace.yaml"), `allowBuilds:
133
+ sharp: true
134
+ unrs-resolver: true
135
+ ${overrides}`);
136
+ }
137
+ }
138
+ function install(dir, pm) {
139
+ const res = spawnSync(pm, ["install"], { cwd: dir, stdio: isJson() ? "ignore" : "inherit", shell: process.platform === "win32" });
140
+ return res.status === 0;
141
+ }
142
+ function registerCreate(program2) {
143
+ program2.command("create").description("Create a new store and a ready-to-run Next.js storefront").argument("[directory]", "Folder to create", ".").option("-n, --name <name>", "Store name (defaults to the folder name)").option("-c, --currency <code>", "Three-letter currency code", "usd").option("--support-email <email>", "Customer support email shown in the store and receipts").option("--no-install", "Skip installing dependencies").action(async (directory, opts) => {
144
+ const dir = resolve(directory);
145
+ if (existsSync(dir) && readdirSync(dir).filter((f) => !f.startsWith(".")).length > 0) {
146
+ throw new CliError(`${relative(process.cwd(), dir) || "."} is not empty. Pick a new folder name.`, "dir_not_empty");
147
+ }
148
+ if (!readConfig().account_token && !process.env.KACHING_ACCOUNT_TOKEN) {
149
+ if (!process.stdin.isTTY || isJson()) throw new CliError("Not logged in. Run `kaching login` first.", "not_logged_in");
150
+ await login({});
151
+ }
152
+ const name = opts.name ?? titleCase(basename(dir));
153
+ log(`${c.dim("\u2022")} Creating store ${c.bold(name)}\u2026`);
154
+ const created = await accountClient().stores.create({
155
+ name,
156
+ currency: opts.currency,
157
+ ...opts.supportEmail && { support_email: opts.supportEmail }
158
+ });
159
+ const pm = detectPackageManager();
160
+ log(`${c.dim("\u2022")} Writing storefront to ${c.bold(relative(process.cwd(), dir) || ".")}`);
161
+ mkdirSync(dir, { recursive: true });
162
+ scaffold(dir, created.slug, pm);
163
+ const api = apiUrl();
164
+ upsertEnvFile(join(dir, ".env.local"), {
165
+ NEXT_PUBLIC_KACHING_PUBLISHABLE_KEY: created.keys.publishable,
166
+ NEXT_PUBLIC_KACHING_API_URL: api === DEFAULT_BASE_URL ? void 0 : api,
167
+ // Server-only. Used by the kaching CLI/agents in this folder. Never expose to the browser.
168
+ KACHING_SECRET_KEY: created.keys.secret
169
+ });
170
+ let installed = false;
171
+ if (opts.install) {
172
+ log(`${c.dim("\u2022")} Installing dependencies with ${pm}\u2026`);
173
+ installed = install(dir, pm);
174
+ if (!installed) log(c.yellow(` Install failed \u2014 run \`${pm} install\` in the folder to retry.`));
175
+ }
176
+ const { keys: _, ...store } = created;
177
+ const cd = relative(process.cwd(), dir);
178
+ const run = pm === "npm" ? "npm run dev" : `${pm} dev`;
179
+ const nextSteps = [
180
+ ...cd ? [`cd ${cd}`] : [],
181
+ ...installed ? [] : [`${pm} install`],
182
+ run,
183
+ 'kaching products create --title "..." --price 25',
184
+ "kaching payments connect"
185
+ ];
186
+ out({ store, directory: dir, installed, package_manager: pm, next_steps: nextSteps }, () => {
187
+ log(`
188
+ ${c.green("\u2714")} ${c.bold(name)} is ready.
189
+ `);
190
+ log(" Next:");
191
+ for (const step of nextSteps) log(` ${c.cyan(step)}`);
192
+ log(`
193
+ Keys are in ${c.bold(".env.local")} (gitignored). Storefront runs on http://localhost:3001
194
+ `);
195
+ });
196
+ });
197
+ program2.command("link").description("Connect this folder to an existing store (writes keys to .env.local)").argument("[store]", "Store slug (see `kaching whoami`)").action(async (slug) => {
198
+ const client = accountClient();
199
+ const { data: stores } = await client.stores.list();
200
+ const store = slug ? stores.find((s) => s.slug === slug) : stores.length === 1 ? stores[0] : void 0;
201
+ if (!store) {
202
+ throw new CliError(
203
+ stores.length === 0 ? "This account has no stores yet. Run `kaching create`." : `Specify which store: ${stores.map((s) => s.slug).join(", ")}`,
204
+ "store_required"
205
+ );
206
+ }
207
+ const keys = await issueKeys(store.id);
208
+ const api = apiUrl();
209
+ upsertEnvFile(join(process.cwd(), ".env.local"), {
210
+ NEXT_PUBLIC_KACHING_PUBLISHABLE_KEY: keys.publishable,
211
+ NEXT_PUBLIC_KACHING_API_URL: api === DEFAULT_BASE_URL ? void 0 : api,
212
+ KACHING_SECRET_KEY: keys.secret
213
+ });
214
+ out({ store }, () => log(`${c.green("\u2714")} Linked to ${c.bold(store.name)}. Keys written to .env.local.`));
215
+ });
216
+ }
217
+ async function issueKeys(storeId) {
218
+ const token = process.env.KACHING_ACCOUNT_TOKEN ?? readConfig().account_token;
219
+ const res = await fetch(`${apiUrl()}/stores/${storeId}/keys`, {
220
+ method: "POST",
221
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
222
+ body: JSON.stringify({ name: "cli-link" })
223
+ });
224
+ const data = await res.json();
225
+ if (!res.ok) throw new CliError(data?.error?.message ?? "Could not issue keys", data?.error?.code);
226
+ return data;
227
+ }
228
+ var titleCase = (s) => s.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (ch) => ch.toUpperCase()) || "My Store";
229
+
230
+ // src/commands/store.ts
231
+ import { readFileSync as readFileSync2, statSync } from "fs";
232
+ import { basename as basename2, dirname as dirname2, extname, resolve as resolve2 } from "path";
233
+ var collect = (value, prev = []) => [...prev, value];
234
+ var MIME = {
235
+ ".jpg": "image/jpeg",
236
+ ".jpeg": "image/jpeg",
237
+ ".png": "image/png",
238
+ ".webp": "image/webp",
239
+ ".gif": "image/gif",
240
+ ".avif": "image/avif",
241
+ ".pdf": "application/pdf",
242
+ ".zip": "application/zip",
243
+ ".epub": "application/epub+zip",
244
+ ".mp3": "audio/mpeg",
245
+ ".mp4": "video/mp4"
246
+ };
247
+ function readLocalFile(path) {
248
+ const abs = resolve2(path);
249
+ try {
250
+ statSync(abs);
251
+ } catch {
252
+ throw new CliError(`File not found: ${path}`, "file_not_found");
253
+ }
254
+ const type = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
255
+ return { blob: new Blob([readFileSync2(abs)], { type }), name: basename2(abs) };
256
+ }
257
+ var isUrl = (s) => /^https?:\/\//.test(s);
258
+ async function attachMedia(product, images, files) {
259
+ const client = storeClient();
260
+ let current = product;
261
+ const urls = images.filter(isUrl);
262
+ if (urls.length) current = await client.products.update(current.id, { images: [...current.images, ...urls] });
263
+ for (const path of images.filter((i) => !isUrl(i))) {
264
+ const { blob, name } = readLocalFile(path);
265
+ log(c.dim(` uploading ${name}\u2026`));
266
+ current = await client.products.uploadImage(current.id, blob, name);
267
+ }
268
+ for (const path of files) {
269
+ const { blob, name } = readLocalFile(path);
270
+ log(c.dim(` uploading ${name}\u2026`));
271
+ await client.variants.files.upload(current.variants[0].id, blob, name);
272
+ }
273
+ return files.length ? client.products.get(current.id) : current;
274
+ }
275
+ function printProduct(p, currency) {
276
+ console.log(`${c.bold(p.title)} ${c.dim(`/${p.slug}`)} ${p.type} ${p.status}`);
277
+ for (const v of p.variants) {
278
+ const stock = v.track_inventory ? `${v.inventory_quantity} in stock` : "untracked";
279
+ const files = v.files?.length ? ` ${v.files.length} file(s)` : "";
280
+ console.log(` ${v.title.padEnd(16)} ${formatMoney(v.price, currency).padEnd(14)} ${c.dim(stock)}${files} ${c.dim(v.id)}`);
281
+ }
282
+ }
283
+ function registerStoreCommands(program2) {
284
+ program2.command("store").description("Show the store linked to this folder").action(async () => {
285
+ const store = await storeClient().store.get();
286
+ out(store, (s) => {
287
+ console.log(`${c.bold(s.name)} ${c.dim(`(${s.slug})`)} ${s.currency.toUpperCase()}`);
288
+ console.log(` payments: ${s.payments.ready ? c.green("ready") : c.yellow(s.payments.connected ? `pending (${s.payments.card_payments_status})` : "not connected \u2014 run `kaching payments connect`")}`);
289
+ console.log(` tax: ${s.tax_enabled ? "on" : "off"}`);
290
+ });
291
+ });
292
+ const products = program2.command("products").description("Manage products");
293
+ products.command("list").description("List products").action(async () => {
294
+ const client = storeClient();
295
+ const [{ data }, store] = await Promise.all([client.products.list({ limit: 100 }), client.store.get()]);
296
+ out(data, (list) => {
297
+ if (!list.length) return log("No products yet. Add one with `kaching products create`.");
298
+ table(
299
+ list.map((p) => ({
300
+ slug: p.slug,
301
+ title: p.title,
302
+ type: p.type,
303
+ status: p.status,
304
+ price: p.price_range ? formatMoney(p.price_range.min, store.currency) : "-",
305
+ variants: p.variants.length
306
+ }))
307
+ );
308
+ });
309
+ });
310
+ products.command("get").description("Show a product with its variants").argument("<slug>", "Product slug or id").action(async (slug) => {
311
+ const client = storeClient();
312
+ const [product, store] = await Promise.all([client.products.get(slug), client.store.get()]);
313
+ out(product, (p) => printProduct(p, store.currency));
314
+ });
315
+ products.command("create").description("Create a product. Prices are in normal units (e.g. 24.90)").requiredOption("-t, --title <title>", "Product title").option("-p, --price <amount>", "Price for a single-variant product, e.g. 24.90").option("-d, --description <text>", "Description").option("--type <type>", "physical or digital", "physical").option("--variant <spec>", 'Variant as "Title:price[:stock]", e.g. "Large:39.90:10" (repeatable)', collect).option("--option-name <name>", 'Option name for --variant titles, e.g. "size"').option("--stock <n>", "Track inventory with this quantity (single-variant products)").option("--sku <sku>", "SKU (single-variant products)").option("--image <pathOrUrl>", "Image file or URL (repeatable)", collect).option("--file <path>", "Downloadable file for digital products (repeatable)", collect).option("--draft", "Create as a draft (hidden from the storefront)").action(async (opts) => {
316
+ const client = storeClient();
317
+ const store = await client.store.get();
318
+ if (!opts.price && !opts.variant?.length) throw new CliError("Provide --price or at least one --variant", "price_required");
319
+ if (opts.file?.length && opts.type !== "digital") throw new CliError("--file requires --type digital", "not_digital");
320
+ const input = {
321
+ title: opts.title,
322
+ description: opts.description,
323
+ type: opts.type,
324
+ status: opts.draft ? "draft" : "active"
325
+ };
326
+ if (opts.variant?.length) {
327
+ input.variants = opts.variant.map((spec, i) => {
328
+ const [title, price, stock] = spec.split(":");
329
+ if (!title || !price) throw new CliError(`Invalid --variant "${spec}". Use "Title:price[:stock]"`, "invalid_variant");
330
+ return {
331
+ title,
332
+ price: toMinor(price, store.currency),
333
+ position: i,
334
+ ...opts.optionName && { options: { [opts.optionName]: title } },
335
+ ...stock !== void 0 && { track_inventory: true, inventory_quantity: Number(stock) }
336
+ };
337
+ });
338
+ } else {
339
+ input.variants = [
340
+ {
341
+ price: toMinor(opts.price, store.currency),
342
+ ...opts.sku && { sku: opts.sku },
343
+ ...opts.stock !== void 0 && { track_inventory: true, inventory_quantity: Number(opts.stock) }
344
+ }
345
+ ];
346
+ }
347
+ let product = await client.products.create(input);
348
+ product = await attachMedia(product, opts.image ?? [], opts.file ?? []);
349
+ out(product, (p) => {
350
+ log(`${c.green("\u2714")} Created`);
351
+ printProduct(p, store.currency);
352
+ });
353
+ });
354
+ products.command("update").description("Update a product (and its price/stock if it has a single variant)").argument("<slug>", "Product slug or id").option("-t, --title <title>").option("-d, --description <text>").option("-p, --price <amount>", "New price (single-variant products)").option("--compare-at <amount>", "Original price to show as a sale (single-variant products)").option("--stock <n>", "Set tracked inventory (single-variant products)").option("--status <status>", "active, draft or archived").option("--image <pathOrUrl>", "Add an image (repeatable)", collect).option("--file <path>", "Add a downloadable file (repeatable)", collect).action(async (slug, opts) => {
355
+ const client = storeClient();
356
+ const [store, existing] = await Promise.all([client.store.get(), client.products.get(slug)]);
357
+ let product = existing;
358
+ const fields = { title: opts.title, description: opts.description, status: opts.status };
359
+ if (Object.values(fields).some((v) => v !== void 0)) product = await client.products.update(existing.id, fields);
360
+ if (opts.price !== void 0 || opts.stock !== void 0 || opts.compareAt !== void 0) {
361
+ if (product.variants.length !== 1) {
362
+ throw new CliError("This product has several variants \u2014 update them with `kaching variants update <id>`.", "multiple_variants");
363
+ }
364
+ product = await client.variants.update(product.variants[0].id, {
365
+ ...opts.price !== void 0 && { price: toMinor(opts.price, store.currency) },
366
+ ...opts.compareAt !== void 0 && { compare_at_price: toMinor(opts.compareAt, store.currency) },
367
+ ...opts.stock !== void 0 && { track_inventory: true, inventory_quantity: Number(opts.stock) }
368
+ });
369
+ }
370
+ product = await attachMedia(product, opts.image ?? [], opts.file ?? []);
371
+ out(product, (p) => {
372
+ log(`${c.green("\u2714")} Updated`);
373
+ printProduct(p, store.currency);
374
+ });
375
+ });
376
+ products.command("delete").description("Delete a product (use `update --status archived` to just hide it)").argument("<slug>", "Product slug or id").action(async (slug) => {
377
+ const res = await storeClient().products.delete(slug);
378
+ out(res, () => log(`${c.green("\u2714")} Deleted ${slug}`));
379
+ });
380
+ products.command("import").description("Create products from a JSON file (array of product objects; prices in minor units, image/file paths relative to the JSON)").argument("<file>", "Path to JSON").action(async (file) => {
381
+ const client = storeClient();
382
+ const base = dirname2(resolve2(file));
383
+ const items = JSON.parse(readFileSync2(file, "utf8"));
384
+ if (!Array.isArray(items)) throw new CliError("The JSON file must contain an array of products", "invalid_import");
385
+ const created = [];
386
+ for (const { image_files = [], files = [], ...input } of items) {
387
+ log(`${c.dim("\u2022")} ${input.title}`);
388
+ const product = await client.products.create(input);
389
+ created.push(
390
+ await attachMedia(
391
+ product,
392
+ image_files.map((p) => isUrl(p) ? p : resolve2(base, p)),
393
+ files.map((p) => resolve2(base, p))
394
+ )
395
+ );
396
+ }
397
+ out(created, (list) => log(`${c.green("\u2714")} Imported ${list.length} product${list.length === 1 ? "" : "s"}`));
398
+ });
399
+ const variants = program2.command("variants").description("Manage product variants");
400
+ variants.command("update").description("Update a variant's price or stock").argument("<id>", "Variant id (see `kaching products get <slug>`)").option("-p, --price <amount>").option("--compare-at <amount>").option("--stock <n>").option("-t, --title <title>").action(async (id, opts) => {
401
+ const client = storeClient();
402
+ const store = await client.store.get();
403
+ const product = await client.variants.update(id, {
404
+ ...opts.title && { title: opts.title },
405
+ ...opts.price !== void 0 && { price: toMinor(opts.price, store.currency) },
406
+ ...opts.compareAt !== void 0 && { compare_at_price: toMinor(opts.compareAt, store.currency) },
407
+ ...opts.stock !== void 0 && { track_inventory: true, inventory_quantity: Number(opts.stock) }
408
+ });
409
+ out(product, (p) => printProduct(p, store.currency));
410
+ });
411
+ const shipping = program2.command("shipping").description("Manage shipping rates (physical products)");
412
+ shipping.command("list").action(async () => {
413
+ const client = storeClient();
414
+ const [{ data }, store] = await Promise.all([client.shippingRates.list(), client.store.get()]);
415
+ out(data, (list) => {
416
+ if (!list.length) return log("No shipping rates \u2014 physical orders ship free worldwide. Add one with `kaching shipping create`.");
417
+ table(
418
+ list.map((r) => ({
419
+ name: r.name,
420
+ price: formatMoney(r.amount, store.currency),
421
+ countries: r.countries.length ? r.countries.join(",") : "worldwide",
422
+ days: r.min_delivery_days != null ? `${r.min_delivery_days}-${r.max_delivery_days}` : "-",
423
+ active: r.active ? "yes" : "no",
424
+ id: r.id
425
+ }))
426
+ );
427
+ });
428
+ });
429
+ shipping.command("create").requiredOption("-n, --name <name>", 'e.g. "Standard"').requiredOption("-p, --price <amount>", "e.g. 4.90 (0 for free)").option("--countries <codes>", "Comma-separated ISO codes, e.g. US,CA (default: worldwide)").option("--days <min-max>", 'Delivery estimate in business days, e.g. "2-5"').action(async (opts) => {
430
+ const client = storeClient();
431
+ const store = await client.store.get();
432
+ const [min, max] = opts.days ? String(opts.days).split("-").map(Number) : [];
433
+ const rate = await client.shippingRates.create({
434
+ name: opts.name,
435
+ amount: toMinor(opts.price, store.currency),
436
+ countries: opts.countries ? String(opts.countries).split(",").map((s) => s.trim()) : [],
437
+ ...min !== void 0 && { min_delivery_days: min, max_delivery_days: max ?? min }
438
+ });
439
+ out(rate, (r) => log(`${c.green("\u2714")} Added ${r.name} (${formatMoney(r.amount, store.currency)})`));
440
+ });
441
+ shipping.command("delete").argument("<id>").action(async (id) => {
442
+ out(await storeClient().shippingRates.delete(id), () => log(`${c.green("\u2714")} Deleted`));
443
+ });
444
+ const payments = program2.command("payments").description("Stripe payouts setup");
445
+ payments.command("connect").description("Get the link where the store owner connects Stripe (the one step a human must do)").option("--country <code>", "Business country, e.g. US (asked during onboarding if omitted)").option("--email <email>", "Owner's email").option("--no-open", "Print the link without opening a browser").action(async (opts) => {
446
+ const client = storeClient();
447
+ const status = await client.payments.status().catch(() => null);
448
+ if (status?.ready) return out({ ready: true }, () => log(`${c.green("\u2714")} Payments are already set up.`));
449
+ const link = await client.payments.onboard({ country: opts.country, email: opts.email });
450
+ const opened = opts.open !== false && openBrowser(link.start_url);
451
+ out({ ready: false, onboarding_url: link.start_url }, () => {
452
+ log(`
453
+ The store owner needs to finish Stripe onboarding (about 5 minutes):
454
+ `);
455
+ log(` ${c.cyan(link.start_url)}
456
+ `);
457
+ log(c.dim(` ${opened ? "Opened in your browser. " : ""}The link stays valid for 7 days. Check progress with \`kaching payments status\`.`));
458
+ });
459
+ });
460
+ payments.command("status").action(async () => {
461
+ const status = await storeClient().payments.status();
462
+ out(
463
+ status,
464
+ (s) => log(s.ready ? `${c.green("\u2714")} Ready to accept payments` : `${c.yellow("\u2026")} Not ready (${s.connected ? s.card_payments_status : "not connected"})`)
465
+ );
466
+ });
467
+ const orders = program2.command("orders").description("View and fulfil orders");
468
+ orders.command("list").option("--unfulfilled", "Only orders waiting to ship").option("-l, --limit <n>", "How many", "20").action(async (opts) => {
469
+ const { data } = await storeClient().orders.list({
470
+ limit: Number(opts.limit),
471
+ ...opts.unfulfilled && { fulfillment_status: "unfulfilled" }
472
+ });
473
+ out(data, (list) => {
474
+ if (!list.length) return log("No orders yet.");
475
+ table(
476
+ list.map((o) => ({
477
+ number: `#${o.number}`,
478
+ date: o.created_at.slice(0, 10),
479
+ customer: o.email ?? "-",
480
+ items: o.items.reduce((n, i) => n + i.quantity, 0),
481
+ total: formatMoney(o.total, o.currency),
482
+ fulfillment: o.fulfillment_status,
483
+ id: o.id
484
+ }))
485
+ );
486
+ });
487
+ });
488
+ orders.command("fulfill").description("Mark an order as shipped").argument("<id>", "Order id").action(async (id) => {
489
+ const order = await storeClient().orders.fulfill(id);
490
+ out(order, (o) => log(`${c.green("\u2714")} Order #${o.number} marked fulfilled`));
491
+ });
492
+ }
493
+
494
+ // src/index.ts
495
+ var program = new Command().name("kaching").description("Commerce for AI agents: products, cart, Stripe checkout and orders for your storefront.").version("0.1.0").option("--json", "Machine-readable output (for agents and scripts)").showHelpAfterError().hook("preAction", (cmd) => setJsonMode(Boolean(cmd.opts().json)));
496
+ registerAuth(program);
497
+ registerCreate(program);
498
+ registerStoreCommands(program);
499
+ program.command("mcp").description("Run the kaching MCP server over stdio (for Claude Code and other agents)").action(async () => {
500
+ const { startMcpServer } = await import("./mcp-LTF62ASB.js");
501
+ await startMcpServer();
502
+ });
503
+ program.parseAsync().catch(handleError);