commerceforai 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # commerceforai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Products, inventory, checkout, orders, shipping, returns and print-on-demand on top of your own Stripe account. One API key, built for you and your AI coding agent.
4
+
5
+ ```bash
6
+ npm install commerceforai
7
+ ```
8
+
9
+ ```ts
10
+ import { Commerce } from "commerceforai";
11
+
12
+ const commerce = new Commerce(process.env.COMMERCEFORAI_API_KEY);
13
+
14
+ const { data: product, error } = await commerce.products.create({ name: "Trail Tee", price: 3600, inventory: 25 });
15
+ const { data: checkout } = await commerce.checkouts.create({
16
+ items: [{ variant_id: product!.variants[0]!.id, quantity: 1 }],
17
+ success_url: "https://yoursite.com/thanks",
18
+ });
19
+ // Send the customer to checkout.url
20
+ ```
21
+
22
+ No account yet? `npx commerceforai init` creates a test store and an API key without signing up.
23
+
24
+ Every method returns `{ data, error }`. All money is an integer in cents.
25
+
26
+ ## What's included
27
+
28
+ - `commerceforai`: products, variants, inventory, checkouts, orders, returns, shipping labels, collections, discounts, customers, reports, webhooks, image hosting, Printify tools, test/live sync
29
+ - `commerceforai/react`: `CartProvider`, `useCart`, `useVariantPicker`, `useClearCartOnSuccess` (draw nothing, fit any design)
30
+ - `commerceforai/cart`: the same cart logic with no framework
31
+ - `createCheckoutHandler()`: a one-line server route for your cart
32
+
33
+ ## Links
34
+
35
+ - [Docs](https://commerceforai.com/docs)
36
+ - [Quickstart](https://commerceforai.com/docs/quickstart)
37
+ - [For AI agents: llms.txt](https://commerceforai.com/llms.txt)
38
+ - [MCP server](https://www.npmjs.com/package/@commerceforai/mcp)
39
+
40
+ MIT licensed.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Cart and variant-picking logic with no framework attached. The React hooks in `commerceforai/react`
3
+ * are thin wrappers around these; use them directly from Vue, Svelte or plain JS.
4
+ */
5
+ /** What a cart stores: ids and quantities only. Names and prices are always read fresh from the catalog. */
6
+ export interface CartLine {
7
+ variant_id: string;
8
+ quantity: number;
9
+ /** Custom data for this line (e.g. a name to print). Lines with properties never merge. */
10
+ properties?: Record<string, string>;
11
+ }
12
+ export declare const MAX_QUANTITY = 20;
13
+ export declare const MAX_LINES = 30;
14
+ /** Add, merging into an existing line for the same variant (unless either line has custom properties). */
15
+ export declare function addLine(lines: CartLine[], variantId: string, quantity?: number, properties?: Record<string, string>): CartLine[];
16
+ /** Set a line's quantity by index (0 removes it). */
17
+ export declare function setLineQuantity(lines: CartLine[], index: number, quantity: number): CartLine[];
18
+ export declare const removeLine: (lines: CartLine[], index: number) => CartLine[];
19
+ /** Parse untrusted stored data (localStorage, a cookie) into valid lines. */
20
+ export declare function parseLines(raw: unknown): CartLine[];
21
+ /** The parts of a product the cart and picker need. Works with SDK products and public store pages. */
22
+ export interface CatalogVariant {
23
+ id: string;
24
+ name: string;
25
+ price: number;
26
+ currency: string;
27
+ options?: Record<string, string> | null;
28
+ active?: boolean;
29
+ /** SDK shape. */
30
+ inventory?: {
31
+ tracked?: boolean;
32
+ available: number | null;
33
+ };
34
+ /** Public store page shape. */
35
+ in_stock?: boolean;
36
+ }
37
+ export interface CatalogProduct {
38
+ id: string;
39
+ name: string;
40
+ images?: string[];
41
+ variants: CatalogVariant[];
42
+ }
43
+ /** How many can be bought (null = unlimited). */
44
+ export declare function availableOf(v: CatalogVariant): number | null;
45
+ export interface ResolvedLine extends CartLine {
46
+ index: number;
47
+ product: CatalogProduct;
48
+ variant: CatalogVariant;
49
+ unit_price: number;
50
+ line_total: number;
51
+ /** Stock left, or null for unlimited. */
52
+ available: number | null;
53
+ /** More in the cart than can be bought. */
54
+ over_stock: boolean;
55
+ }
56
+ /**
57
+ * Join cart lines to the catalog. Lines whose variant is gone or inactive go into `missing` (show a
58
+ * notice and drop them); everything else gets a current price and a stock check.
59
+ */
60
+ export declare function resolveCart(lines: CartLine[], products: CatalogProduct[]): {
61
+ lines: ResolvedLine[];
62
+ missing: CartLine[];
63
+ currency: string;
64
+ count: number;
65
+ subtotal: number;
66
+ /** Ready for checkout: something in it, nothing missing or over stock. */
67
+ ready: boolean;
68
+ };
69
+ /** Option names in the order they first appear (e.g. ["Size", "Color"]). */
70
+ export declare function optionNames(p: CatalogProduct): string[];
71
+ export interface OptionChoice {
72
+ value: string; /** Some in-stock variant has this value with the other current selections. */
73
+ available: boolean;
74
+ selected: boolean;
75
+ }
76
+ /**
77
+ * Given the current selections, which values are offered for each option, which are buyable, and which
78
+ * variant (if any) the selection resolves to.
79
+ */
80
+ export declare function pickVariant(p: CatalogProduct, selected: Record<string, string>): {
81
+ options: {
82
+ name: string;
83
+ values: OptionChoice[];
84
+ }[];
85
+ variant: CatalogVariant | null;
86
+ inStock: boolean;
87
+ available: number | null;
88
+ };
89
+ /** A sensible starting selection: the first in-stock variant's options (or the first variant's). */
90
+ export declare function initialSelection(p: CatalogProduct): Record<string, string>;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Cart and variant-picking logic with no framework attached. The React hooks in `commerceforai/react`
3
+ * are thin wrappers around these; use them directly from Vue, Svelte or plain JS.
4
+ */
5
+ export const MAX_QUANTITY = 20;
6
+ export const MAX_LINES = 30;
7
+ const clampQty = (n) => Math.max(0, Math.min(MAX_QUANTITY, Math.floor(Number.isFinite(n) ? n : 0)));
8
+ const sameProps = (a, b) => JSON.stringify(Object.entries(a ?? {}).sort()) === JSON.stringify(Object.entries(b ?? {}).sort());
9
+ /** Add, merging into an existing line for the same variant (unless either line has custom properties). */
10
+ export function addLine(lines, variantId, quantity = 1, properties) {
11
+ const hasProps = properties && Object.keys(properties).length > 0;
12
+ const i = hasProps ? -1 : lines.findIndex((l) => l.variant_id === variantId && !Object.keys(l.properties ?? {}).length);
13
+ if (i >= 0)
14
+ return lines.map((l, j) => (j === i ? { ...l, quantity: clampQty(l.quantity + quantity) } : l));
15
+ if (lines.length >= MAX_LINES)
16
+ return lines;
17
+ const q = clampQty(quantity);
18
+ return q ? [...lines, { variant_id: variantId, quantity: q, ...(hasProps ? { properties } : {}) }] : lines;
19
+ }
20
+ /** Set a line's quantity by index (0 removes it). */
21
+ export function setLineQuantity(lines, index, quantity) {
22
+ const q = clampQty(quantity);
23
+ return q ? lines.map((l, i) => (i === index ? { ...l, quantity: q } : l)) : lines.filter((_, i) => i !== index);
24
+ }
25
+ export const removeLine = (lines, index) => lines.filter((_, i) => i !== index);
26
+ /** Parse untrusted stored data (localStorage, a cookie) into valid lines. */
27
+ export function parseLines(raw) {
28
+ if (!Array.isArray(raw))
29
+ return [];
30
+ const out = [];
31
+ for (const l of raw.slice(0, MAX_LINES)) {
32
+ if (!l || typeof l.variant_id !== "string" || !l.variant_id)
33
+ continue;
34
+ const quantity = clampQty(Number(l.quantity));
35
+ if (!quantity)
36
+ continue;
37
+ const props = l.properties && typeof l.properties === "object" && !Array.isArray(l.properties)
38
+ ? Object.fromEntries(Object.entries(l.properties).filter(([, v]) => typeof v === "string").slice(0, 20))
39
+ : undefined;
40
+ out.push({ variant_id: l.variant_id, quantity, ...(props && Object.keys(props).length ? { properties: props } : {}) });
41
+ }
42
+ return out;
43
+ }
44
+ /** How many can be bought (null = unlimited). */
45
+ export function availableOf(v) {
46
+ if (v.active === false)
47
+ return 0;
48
+ if (v.inventory)
49
+ return v.inventory.available ?? null;
50
+ if (v.in_stock === false)
51
+ return 0;
52
+ return null;
53
+ }
54
+ /**
55
+ * Join cart lines to the catalog. Lines whose variant is gone or inactive go into `missing` (show a
56
+ * notice and drop them); everything else gets a current price and a stock check.
57
+ */
58
+ export function resolveCart(lines, products) {
59
+ const byVariant = new Map(products.flatMap((p) => p.variants.map((v) => [v.id, { p, v }])));
60
+ const resolved = [];
61
+ const missing = [];
62
+ lines.forEach((l, index) => {
63
+ const hit = byVariant.get(l.variant_id);
64
+ if (!hit || hit.v.active === false) {
65
+ missing.push(l);
66
+ return;
67
+ }
68
+ const available = availableOf(hit.v);
69
+ resolved.push({ ...l, index, product: hit.p, variant: hit.v, unit_price: hit.v.price, line_total: hit.v.price * l.quantity,
70
+ available, over_stock: available !== null && l.quantity > available });
71
+ });
72
+ const currency = resolved[0]?.variant.currency ?? "usd";
73
+ return {
74
+ lines: resolved, missing, currency,
75
+ count: lines.reduce((n, l) => n + l.quantity, 0),
76
+ subtotal: resolved.reduce((n, l) => n + l.line_total, 0),
77
+ /** Ready for checkout: something in it, nothing missing or over stock. */
78
+ ready: resolved.length > 0 && !missing.length && resolved.every((l) => !l.over_stock),
79
+ };
80
+ }
81
+ // ---------- variant picking ----------
82
+ /** Option names in the order they first appear (e.g. ["Size", "Color"]). */
83
+ export function optionNames(p) {
84
+ const names = [];
85
+ for (const v of p.variants)
86
+ for (const k of Object.keys(v.options ?? {}))
87
+ if (!names.includes(k))
88
+ names.push(k);
89
+ return names;
90
+ }
91
+ /**
92
+ * Given the current selections, which values are offered for each option, which are buyable, and which
93
+ * variant (if any) the selection resolves to.
94
+ */
95
+ export function pickVariant(p, selected) {
96
+ const live = p.variants.filter((v) => v.active !== false);
97
+ const names = optionNames(p);
98
+ const inStock = (v) => { const a = availableOf(v); return a === null || a > 0; };
99
+ const matches = (v, sel) => Object.entries(sel).every(([k, val]) => (v.options ?? {})[k] === val);
100
+ const options = names.map((name) => {
101
+ const values = [...new Set(live.map((v) => (v.options ?? {})[name]).filter((x) => !!x))];
102
+ const others = Object.fromEntries(Object.entries(selected).filter(([k]) => k !== name));
103
+ return {
104
+ name,
105
+ values: values.map((value) => ({
106
+ value, selected: selected[name] === value,
107
+ available: live.some((v) => (v.options ?? {})[name] === value && matches(v, others) && inStock(v)),
108
+ })),
109
+ };
110
+ });
111
+ // No options (a single "Default" variant, or variants named without options): match by the only one.
112
+ const variant = names.length
113
+ ? (names.every((n) => selected[n]) ? live.find((v) => matches(v, selected)) ?? null : null)
114
+ : live.length === 1 ? live[0] : null;
115
+ return { options, variant, inStock: variant ? inStock(variant) : false, available: variant ? availableOf(variant) : null };
116
+ }
117
+ /** A sensible starting selection: the first in-stock variant's options (or the first variant's). */
118
+ export function initialSelection(p) {
119
+ const live = p.variants.filter((v) => v.active !== false);
120
+ const first = live.find((v) => { const a = availableOf(v); return a === null || a > 0; }) ?? live[0];
121
+ return { ...(first?.options ?? {}) };
122
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A ready-made server endpoint for carts built with `commerceforai/react` (or any client):
3
+ * it takes `{ items, discount_code? }`, creates the checkout with your secret key, and returns `{ url }`.
4
+ *
5
+ * ```ts
6
+ * // app/api/checkout/route.ts (Next.js) — also works in Hono, Remix, Astro, SvelteKit, Bun, Deno
7
+ * import { createCheckoutHandler } from "commerceforai";
8
+ * export const POST = createCheckoutHandler();
9
+ * ```
10
+ *
11
+ * The browser only sends variant ids and quantities. Prices, stock and discounts are checked by the API.
12
+ */
13
+ import { Commerce } from "./index.js";
14
+ export interface CheckoutHandlerOptions {
15
+ /** A client to use (default: `new Commerce()` from COMMERCEFORAI_API_KEY). */
16
+ commerce?: Commerce;
17
+ /** Where Stripe sends shoppers after paying (default `<origin>/checkout/success`). `cfa_checkout` is appended. */
18
+ successUrl?: string | ((origin: string) => string);
19
+ /** Where "back" on Stripe's page goes (default the page that started checkout, else `<origin>/`). */
20
+ cancelUrl?: string | ((origin: string) => string);
21
+ /** Sites allowed to start checkouts (default: same origin as the request only). */
22
+ allowedOrigins?: string[];
23
+ /** Add metadata to every checkout (e.g. `{ channel: "web" }`). */
24
+ metadata?: Record<string, string>;
25
+ }
26
+ export declare function createCheckoutHandler(opts?: CheckoutHandlerOptions): (request: Request) => Promise<Response>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A ready-made server endpoint for carts built with `commerceforai/react` (or any client):
3
+ * it takes `{ items, discount_code? }`, creates the checkout with your secret key, and returns `{ url }`.
4
+ *
5
+ * ```ts
6
+ * // app/api/checkout/route.ts (Next.js) — also works in Hono, Remix, Astro, SvelteKit, Bun, Deno
7
+ * import { createCheckoutHandler } from "commerceforai";
8
+ * export const POST = createCheckoutHandler();
9
+ * ```
10
+ *
11
+ * The browser only sends variant ids and quantities. Prices, stock and discounts are checked by the API.
12
+ */
13
+ import { Commerce } from "./index.js";
14
+ import { parseLines } from "./cart-core.js";
15
+ const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
16
+ const fail = (status, type, message, extra = {}) => json({ error: { type, message, ...extra } }, status);
17
+ export function createCheckoutHandler(opts = {}) {
18
+ let client = opts.commerce;
19
+ return async function handler(request) {
20
+ if (request.method !== "POST")
21
+ return fail(405, "invalid_request", "Use POST");
22
+ const url = new URL(request.url);
23
+ const origin = request.headers.get("origin");
24
+ const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? url.host;
25
+ const proto = request.headers.get("x-forwarded-proto") ?? url.protocol.replace(":", "");
26
+ const self = `${proto}://${host}`;
27
+ // Only your own pages may start checkouts (stops other sites holding your stock).
28
+ const allowed = new Set([self, ...(opts.allowedOrigins ?? [])].map((o) => o.replace(/\/$/, "")));
29
+ if (origin && !allowed.has(origin.replace(/\/$/, "")))
30
+ return fail(403, "permission_error", "Checkout can only be started from this site");
31
+ let body;
32
+ try {
33
+ body = await request.json();
34
+ }
35
+ catch {
36
+ return fail(400, "invalid_request", "Body must be JSON: { items: [{ variant_id, quantity }] }");
37
+ }
38
+ const items = parseLines(body?.items);
39
+ if (!items.length)
40
+ return fail(400, "invalid_request", "Your cart is empty.", { param: "items" });
41
+ const base = origin && allowed.has(origin.replace(/\/$/, "")) ? origin.replace(/\/$/, "") : self;
42
+ const resolveUrl = (u, fallback) => (typeof u === "function" ? u(base) : u ?? fallback);
43
+ const success = new URL(resolveUrl(opts.successUrl, `${base}/checkout/success`));
44
+ // Stripe fills in {CHECKOUT_SESSION_ID}; useClearCartOnSuccess() looks for this parameter.
45
+ const successUrl = `${success.toString()}${success.search ? "&" : "?"}cfa_checkout={CHECKOUT_SESSION_ID}`;
46
+ const referer = request.headers.get("referer");
47
+ const cancelUrl = resolveUrl(opts.cancelUrl, referer && referer.startsWith(base) ? referer : `${base}/`);
48
+ client ??= new Commerce();
49
+ const { data, error } = await client.checkouts.create({
50
+ items, success_url: successUrl, cancel_url: cancelUrl,
51
+ ...(typeof body.discount_code === "string" && body.discount_code.trim() ? { discount_code: body.discount_code.trim().slice(0, 40) } : {}),
52
+ ...(opts.metadata ? { metadata: opts.metadata } : {}),
53
+ });
54
+ if (error || !data?.url) {
55
+ const e = (error ?? { type: "api_error", message: "Checkout isn't available right now." });
56
+ const status = e.type === "out_of_stock" ? 409 : e.type === "invalid_request" ? 400 : e.type === "rate_limited" ? 429 : 502;
57
+ // Pass through what a shopper can act on; keep internals (keys, Stripe details) server-side.
58
+ const safe = e.type === "out_of_stock" || e.type === "invalid_request" || e.type === "rate_limited";
59
+ return fail(status, e.type, safe ? e.message : "Checkout isn't available right now. Please try again.", safe ? Object.fromEntries(Object.entries(e).filter(([k]) => ["param", "variant_id", "available"].includes(k))) : {});
60
+ }
61
+ return json({ id: data.id, url: data.url }, 201);
62
+ };
63
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npx commerceforai init
4
+ *
5
+ * Creates a free test-mode store and API key with no sign-up, saves the key to .env.local,
6
+ * and prints a link a person uses later to claim the store and connect Stripe.
7
+ * Designed for coding agents: `--json` prints machine-readable output.
8
+ */
9
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { dirname, join, relative, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ const HELP = `Usage: npx commerceforai init [options]
13
+
14
+ Creates a free test-mode CommerceForAI store and API key (no sign-up needed).
15
+
16
+ Options:
17
+ --template nextjs Also add a working storefront to this Next.js app (App Router):
18
+ /shop product grid, product pages, cart, Stripe checkout,
19
+ thank-you page and order lookup. Never overwrites files.
20
+ Reuses COMMERCEFORAI_API_KEY if it's already set.
21
+ --name <name> Store name (default: this folder's package.json name)
22
+ --env-file <path> Where to save COMMERCEFORAI_API_KEY (default: .env.local)
23
+ --no-write Don't write any file; just print the key
24
+ --force Replace an existing COMMERCEFORAI_API_KEY in the env file
25
+ --json Print JSON (for agents and scripts)
26
+ --base-url <url> API base URL (default: https://api.commerceforai.com)
27
+ -h, --help Show this help
28
+ `;
29
+ function parse(argv) {
30
+ const o = {};
31
+ const pos = [];
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ if (a === "-h" || a === "--help")
35
+ o.help = true;
36
+ else if (a === "--no-write" || a === "--force" || a === "--json")
37
+ o[a.slice(2)] = true;
38
+ else if (a.startsWith("--")) {
39
+ const [k, inline] = a.slice(2).split("=", 2);
40
+ o[k] = inline ?? argv[++i] ?? "";
41
+ }
42
+ else
43
+ pos.push(a);
44
+ }
45
+ return { o, pos };
46
+ }
47
+ function defaultName() {
48
+ try {
49
+ const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
50
+ if (typeof pkg.name === "string" && pkg.name)
51
+ return pkg.name.replace(/^@[^/]+\//, "");
52
+ }
53
+ catch { }
54
+ return undefined;
55
+ }
56
+ async function init(o) {
57
+ const base = String(o["base-url"] ?? process.env.COMMERCEFORAI_BASE_URL ?? "https://api.commerceforai.com").replace(/\/$/, "");
58
+ const envFile = resolve(String(o["env-file"] ?? ".env.local"));
59
+ const write = !o["no-write"];
60
+ const template = typeof o.template === "string" ? o.template : null;
61
+ if (template && template !== "nextjs") {
62
+ const msg = `Unknown template "${template}". Available: nextjs`;
63
+ if (o.json)
64
+ console.log(JSON.stringify({ ok: false, error: { type: "invalid_request", message: msg } }));
65
+ else
66
+ console.error(msg);
67
+ process.exit(1);
68
+ }
69
+ const hasKey = existsSync(envFile) && /^COMMERCEFORAI_API_KEY=/m.test(readFileSync(envFile, "utf8"));
70
+ // With a template and an existing key: just add the storefront.
71
+ if (template && hasKey && !o.force) {
72
+ const sc = scaffoldNext();
73
+ if (o.json)
74
+ console.log(JSON.stringify({ ok: sc.ok, store_created: false, env_file: envFile, template: sc }));
75
+ else
76
+ printScaffold(sc, "Using the COMMERCEFORAI_API_KEY already in " + envFile.split("/").pop());
77
+ process.exit(sc.ok ? 0 : 1);
78
+ }
79
+ if (write && hasKey && !o.force) {
80
+ const msg = `${envFile} already has COMMERCEFORAI_API_KEY. Re-run with --force to replace it, or --no-write to just print a new key.`;
81
+ if (o.json)
82
+ console.log(JSON.stringify({ ok: false, error: { type: "already_configured", message: msg } }));
83
+ else
84
+ console.error(msg);
85
+ process.exit(1);
86
+ }
87
+ const res = await fetch(`${base}/sandbox/stores`, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json", "user-agent": "commerceforai-cli/0.1.0" },
90
+ body: JSON.stringify({ name: o.name || defaultName() }),
91
+ }).catch((e) => {
92
+ console.error(`Couldn't reach ${base}: ${e instanceof Error ? e.message : e}`);
93
+ process.exit(1);
94
+ });
95
+ const json = await res.json().catch(() => null);
96
+ if (!res.ok) {
97
+ if (o.json)
98
+ console.log(JSON.stringify({ ok: false, error: json?.error ?? { message: `HTTP ${res.status}` } }));
99
+ else
100
+ console.error(`CommerceForAI: ${json?.error?.message ?? `HTTP ${res.status}`}`);
101
+ process.exit(1);
102
+ }
103
+ if (write) {
104
+ const line = `COMMERCEFORAI_API_KEY=${json.api_key}\n`;
105
+ if (existsSync(envFile)) {
106
+ const cur = readFileSync(envFile, "utf8");
107
+ if (/^COMMERCEFORAI_API_KEY=.*$/m.test(cur))
108
+ writeFileSync(envFile, cur.replace(/^COMMERCEFORAI_API_KEY=.*$/m, line.trim()));
109
+ else
110
+ appendFileSync(envFile, (cur.endsWith("\n") || cur === "" ? "" : "\n") + line);
111
+ }
112
+ else
113
+ writeFileSync(envFile, line);
114
+ }
115
+ const gitignored = (() => {
116
+ try {
117
+ return /(^|\n)\s*\.env(\*|\.local|\.\*)?\s*(\n|$)/.test(readFileSync(resolve(".gitignore"), "utf8"));
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ })();
123
+ const sc = template ? scaffoldNext() : null;
124
+ if (o.json) {
125
+ console.log(JSON.stringify({
126
+ ok: true, store_id: json.store_id, mode: json.mode, api_key: json.api_key, claim_url: json.claim_url,
127
+ env_file: write ? envFile : null, docs: "https://commerceforai.com/llms.txt", ...(sc ? { template: sc } : {}),
128
+ }));
129
+ return;
130
+ }
131
+ if (sc) {
132
+ printScaffold(sc, `Created test store "${json.name}" (${json.store_id})${write ? ` and saved COMMERCEFORAI_API_KEY to ${envFile.split("/").pop()}` : `\n API key: ${json.api_key}`}`);
133
+ console.log(`To manage this test store in the dashboard, claim it:
134
+ ${json.claim_url}
135
+
136
+ When it's ready for real orders, create a go-live link for the store owner
137
+ (commerce.store.goLiveLink(), or the create_go_live_link MCP tool).
138
+ `);
139
+ return;
140
+ }
141
+ console.log(`
142
+ ✓ Created test store "${json.name}" (${json.store_id})
143
+ ${write ? `✓ Saved COMMERCEFORAI_API_KEY to ${envFile}` : ` API key: ${json.api_key}`}${write && !gitignored ? `\n! Add ${envFile.split("/").pop()} to .gitignore so the key isn't committed` : ""}
144
+
145
+ Test mode is free forever. Pay with card 4242 4242 4242 4242.
146
+
147
+ Next:
148
+ npm install commerceforai
149
+ Docs for you and your agent: https://commerceforai.com/llms.txt
150
+
151
+ To manage this test store in the dashboard, claim it:
152
+ ${json.claim_url}
153
+
154
+ When it's ready for real orders, create a go-live link and give it to the store owner
155
+ (commerce.store.goLiveLink(), or the create_go_live_link MCP tool). One link signs them in,
156
+ copies the store to live mode, and takes them to Connect Stripe.
157
+ `);
158
+ }
159
+ function scaffoldNext() {
160
+ const fail = (error) => ({ ok: false, template: "nextjs", base: "", created: [], skipped: [], install_command: null, open: "", error });
161
+ let pkg;
162
+ try {
163
+ pkg = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
164
+ }
165
+ catch {
166
+ return fail("No package.json here. Run this in a Next.js app (e.g. after `npx create-next-app@latest`).");
167
+ }
168
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
169
+ if (!deps.next)
170
+ return fail("This isn't a Next.js app (no `next` dependency). Create one with `npx create-next-app@latest`, then re-run.");
171
+ // App Router lives in src/app or app.
172
+ const base = existsSync(resolve("src/app")) ? "src" : existsSync(resolve("app")) ? "" : existsSync(resolve("src")) ? "src" : "";
173
+ const from = fileURLToPath(new URL("../templates/nextjs/", import.meta.url));
174
+ const created = [], skipped = [];
175
+ const walk = (dir) => readdirSync(dir).flatMap((f) => statSync(join(dir, f)).isDirectory() ? walk(join(dir, f)) : [join(dir, f)]);
176
+ for (const file of walk(from)) {
177
+ const rel = relative(from, file);
178
+ const to = resolve(base, rel);
179
+ const shown = relative(process.cwd(), to);
180
+ if (existsSync(to)) {
181
+ skipped.push(shown);
182
+ continue;
183
+ }
184
+ mkdirSync(dirname(to), { recursive: true });
185
+ writeFileSync(to, readFileSync(file));
186
+ created.push(shown);
187
+ }
188
+ const pm = existsSync(resolve("pnpm-lock.yaml")) ? "pnpm add" : existsSync(resolve("yarn.lock")) ? "yarn add" : existsSync(resolve("bun.lockb")) || existsSync(resolve("bun.lock")) ? "bun add" : "npm install";
189
+ return { ok: true, template: "nextjs", base: base || ".", created, skipped, install_command: deps.commerceforai ? null : `${pm} commerceforai`, open: "http://localhost:3000/shop" };
190
+ }
191
+ function printScaffold(sc, first) {
192
+ if (!sc.ok) {
193
+ console.error(sc.error);
194
+ return;
195
+ }
196
+ console.log(`
197
+ ✓ ${first}
198
+ ✓ Added the storefront: ${sc.created.length} file(s)${sc.skipped.length ? `, skipped ${sc.skipped.length} that already exist (${sc.skipped.join(", ")})` : ""}
199
+
200
+ Next:
201
+ ${sc.install_command ? ` ${sc.install_command}
202
+ ` : ""} Add a product (SDK, MCP, or connect Printify/Printful), then run your dev server and open ${sc.open}
203
+ Pay with test card 4242 4242 4242 4242. Agents: verify without a browser with commerce.store.simulateOrder().
204
+ `);
205
+ }
206
+ const { o, pos } = parse(process.argv.slice(2));
207
+ if (o.help || !pos[0]) {
208
+ console.log(HELP);
209
+ process.exit(pos[0] ? 0 : o.help ? 0 : 1);
210
+ }
211
+ if (pos[0] === "init")
212
+ await init(o);
213
+ else {
214
+ console.error(`Unknown command "${pos[0]}".\n\n${HELP}`);
215
+ process.exit(1);
216
+ }