@commercelayer/agent-cli 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +8 -69
- package/dist/commands.js +3 -1
- package/package.json +11 -2
- package/dist/api/stub.js +0 -112
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Commerce Layer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
# @commercelayer/agent-cli
|
|
2
|
-
|
|
3
2
|
`c11r`, the Commerce Layer CLI for coding agents. An agent reads [commercelayer.ai/start.md](https://commercelayer.ai/start.md), runs this CLI, and the human ends up with a working store in a sandbox: products, prices, a market, a storefront at `<org>.sandbox.commercelayer.store`, and a paid test order. No account needed until they decide to keep it.
|
|
4
|
-
|
|
5
3
|
Every store speaks the [Universal Commerce Protocol](https://ucp.dev) on its own hostname, so shopping agents can discover it, browse it and buy from it. The CLI itself buys through UCP (`orders test`) and checks the store against the spec (`ucp check`), so what it verifies is what any agent would get.
|
|
6
|
-
|
|
7
4
|
It is a separate tool from [`@commercelayer/cli`](https://www.npmjs.com/package/@commercelayer/cli), the CLI for humans. The package is `@commercelayer/agent-cli`; the command is `c11r` (c, eleven letters, r), short and unmistakable in an agent's transcript.
|
|
8
|
-
|
|
9
5
|
```
|
|
10
6
|
npx -y @commercelayer/agent-cli@latest init # runs c11r init
|
|
11
7
|
```
|
|
12
|
-
|
|
13
|
-
## The four repos of commercelayer.ai
|
|
14
|
-
|
|
15
|
-
commercelayer.ai lets a coding agent build and run a store on Commerce Layer. A human pastes one prompt, the agent reads `start.md`, runs the CLI, and the human ends up with a store in a sandbox that both people and shopping agents can buy from. It is four repos, siblings in the same workspace:
|
|
16
|
-
|
|
17
|
-
| Repo | What it is | Where it runs |
|
|
18
|
-
| --- | --- | --- |
|
|
19
|
-
| `commercelayer-ai-site` | the website and the agent-facing docs: `start.md`, `skills.md`, `skills/*.md`, plus the CLI's UCP platform profile and the sandbox payment-handler schema | commercelayer.ai, Cloudflare Pages |
|
|
20
|
-
| `commercelayer-ai-cli` | `@commercelayer/agent-cli`, the CLI the agent runs as `c11r`: `init`, `plan`, `apply`, `storefront`, `orders test`, `ucp check`, `claim`. Prints JSON only. | npm, in the agent's repo |
|
|
21
|
-
| `commercelayer-ai-api` | the management API the CLI talks to: sandboxes and their life cycle, `apply`, storefront uploads, orders, claims | api.commercelayer.ai, Cloudflare Worker |
|
|
22
|
-
| `commercelayer-ai-store` | the store host: every store's website and its [UCP](https://ucp.dev) merchant endpoints (release 2026-08-25) | `<org>.sandbox.commercelayer.store` and, once live, `<org>.commercelayer.store`, Cloudflare Worker |
|
|
23
|
-
|
|
24
|
-
**This repo is the CLI.** It owns everything the agent runs: the commands and their JSON, the store files in the agent's repo (`.commercelayer/store.yml` and `.commercelayer/products/`), the drop-in.js storefront scaffold, the UCP buyer and conformance check, and the stub of the whole platform in `src/api/stub/`, which is the reference the API and the store host are ported from. It depends on the API for the management contract in `src/api/types.ts`, on the store host for the UCP surface, and on the site for the platform profile and handler schema it names in `src/ucp/platform.ts`; a change to the stub is mirrored in the API and the store host, and a change to the management contract is mirrored in the API. The site depends on it: every command, flag, file and JSON field the skills describe must exist here, so a change to the commands or the store files is mirrored on the site.
|
|
25
|
-
|
|
26
8
|
## How it works
|
|
27
|
-
|
|
28
9
|
- **The repo is the store.** `.commercelayer/store.yml` holds the desired state (name, markets, storefront, policies, and the products of a small store) and is committed; a large catalog goes in `.commercelayer/products/` as `.yml` or `.csv` files. `plan` shows what `apply` would change; `apply` makes the store match the files.
|
|
29
10
|
- **JSON only.** Every command prints one JSON object. A failure exits 1 with `{ "error": { "code", "message", "hint" } }`; the hint is the fix.
|
|
30
11
|
- **Safe to re-run.** Every command is idempotent; a failed step can be fixed and run again.
|
|
31
12
|
- **No secrets in git.** `init` writes the sandbox token to `.commercelayer/credentials` (`C11R_ORG`, `C11R_TOKEN`) and gitignores it.
|
|
32
|
-
|
|
33
13
|
## Commands
|
|
34
|
-
|
|
35
14
|
| Command | Does |
|
|
36
15
|
| --- | --- |
|
|
37
16
|
| `init` | Create a sandbox store, with no login. Writes `store.yml` (a commented template) and `credentials`. Reuses the store if they exist. |
|
|
@@ -47,14 +26,10 @@ commercelayer.ai lets a coding agent build and run a store on Commerce Layer. A
|
|
|
47
26
|
| `status` | The sandbox, the sales channel client id, the published storefront, what is applied, pending changes, orders, the UCP profile URL. |
|
|
48
27
|
| `claim` | Start the device flow that keeps the sandbox: a URL and a code for the human. |
|
|
49
28
|
| `help [<command>]` | Describe the commands. `--version` prints the version. |
|
|
50
|
-
|
|
51
29
|
## store.yml
|
|
52
|
-
|
|
53
30
|
`init` writes a commented template. A filled-in store looks like this:
|
|
54
|
-
|
|
55
31
|
```yaml
|
|
56
32
|
name: Beans
|
|
57
|
-
|
|
58
33
|
markets:
|
|
59
34
|
# One market per region you sell in. countries are ISO 3166 codes.
|
|
60
35
|
# shipping is optional: without it orders ship free as "Standard".
|
|
@@ -67,7 +42,6 @@ markets:
|
|
|
67
42
|
name: United States
|
|
68
43
|
countries: [US]
|
|
69
44
|
currency: USD
|
|
70
|
-
|
|
71
45
|
products:
|
|
72
46
|
# sku is unique and stays the same when you rename a product.
|
|
73
47
|
- sku: COFFEE-ETH-250
|
|
@@ -75,11 +49,9 @@ products:
|
|
|
75
49
|
description: Washed, floral, bright.
|
|
76
50
|
image_url: https://example.com/eth.jpg
|
|
77
51
|
prices: { EUR: 14.50, USD: 16.00 }
|
|
78
|
-
|
|
79
52
|
storefront:
|
|
80
53
|
# The web app in this repo that sells the products, relative to the repo root.
|
|
81
54
|
path: storefront
|
|
82
|
-
|
|
83
55
|
policies:
|
|
84
56
|
# Pages shopping agents show buyers before they pay. Without them the store
|
|
85
57
|
# serves default sandbox pages at <preview_url>/policies/<name>.
|
|
@@ -87,30 +59,20 @@ policies:
|
|
|
87
59
|
terms_of_service: https://example.com/terms
|
|
88
60
|
returns: https://example.com/returns
|
|
89
61
|
```
|
|
90
|
-
|
|
91
62
|
### The products folder
|
|
92
|
-
|
|
93
63
|
Beyond a handful of products, put the catalog in `.commercelayer/products/` and leave `products` out of `store.yml`. Every file in the folder is merged into the catalog by sku, in file-name order:
|
|
94
|
-
|
|
95
64
|
- `<name>.yml`: a list of products in the shape above (one file per collection, or one per product, as you like).
|
|
96
65
|
- `<name>.csv`: one product per row. The first line names the columns: `sku`, `name`, `description`, `image_url`, and one column per currency, named by its ISO 4217 code, holding the price. Quoted cells and CRLF are fine; empty cells are left out.
|
|
97
|
-
|
|
98
66
|
```csv
|
|
99
67
|
sku,name,description,EUR,USD
|
|
100
68
|
COFFEE-ETH-250,"Ethiopia Yirgacheffe, 250 g","Washed, floral, bright.",14.50,16.00
|
|
101
69
|
COFFEE-COL-250,"Colombia Huila, 250 g",,12.00,13.50
|
|
102
70
|
```
|
|
103
|
-
|
|
104
71
|
Errors name the file (and line, for CSV) and the sku, so one product file can be fixed without reading the rest. Moving a product between `store.yml` and a file is not a change to the store: `plan` keys everything by sku. The catalog has no stock: inventory is tracked live in Commerce Layer, and the sandbox treats every product as in stock. Prices in these files are what the store charges; a published storefront reads prices and availability live from Commerce Layer.
|
|
105
|
-
|
|
106
72
|
Rules `plan` and `apply` enforce: a name, at least one market and one product; every market has `code`, `name`, `countries` and an ISO 4217 `currency`; codes and skus are unique across all files; a product has only `sku`, `name`, `description`, `image_url` and `prices`; every product has a price in the currency of every market; `shipping`, if present, has `method` and a non-negative `price`; policy entries are URLs. Currency, languages and tax follow from the countries.
|
|
107
|
-
|
|
108
73
|
Error codes: `no_store`, `invalid_yaml`, `invalid_store`, `invalid_market`, `invalid_product`, `missing_price`, `deletes_need_approval`, `storefront_exists`, `no_storefront`, `not_applied`, `not_found`, `missing_argument`, `invalid_argument`, `unknown_command`, `unauthorized`, `expired`, `api_unreachable`, `api_error`, `no_product`, `checkout_not_ready`, `ucp_error`, `ucp_profile_unreachable`, `ucp_profile_malformed`, `ucp_version_unsupported`, `ucp_no_rest_service`, `internal`.
|
|
109
|
-
|
|
110
74
|
## The store speaks UCP
|
|
111
|
-
|
|
112
75
|
Each store is a UCP business at `https://<org>.sandbox.commercelayer.store` (live stores: `<slug>.commercelayer.store`), implementing release **2026-08-25** over REST:
|
|
113
|
-
|
|
114
76
|
| What | Where |
|
|
115
77
|
| --- | --- |
|
|
116
78
|
| Business profile | `GET /.well-known/ucp` (`Cache-Control: public, max-age=300`, ES256 public key in `keys[]`) |
|
|
@@ -121,40 +83,17 @@ Each store is a UCP business at `https://<org>.sandbox.commercelayer.store` (liv
|
|
|
121
83
|
| Permalink `dev.ucp.shopping.permalink` | `GET /buy/{sku}:{qty}[,...]?context/postal_code=…&discounts/codes/0=…` → `303` to the cart |
|
|
122
84
|
| Payment handler `ai.commercelayer.sandbox` | id `cl_sandbox`, `card` instruments, credential `{ "type": "sandbox_token", "token": "sbx_tok_success" \| "sbx_tok_declined", "expiry": … }`. No money moves. |
|
|
123
85
|
| Pages | `/policies/{privacy,terms,returns}` (defaults when `store.yml` has no `policies`), `/carts/{id}`, `/checkout-sessions/{id}` (`continue_url`), `/orders/{id}` (`permalink_url`), and the published storefront at `/`. |
|
|
124
|
-
|
|
125
86
|
Transport rules the store enforces: `UCP-Agent` (an HTTPS platform profile URL) and `Request-Id` on every request; `Idempotency-Key` on every state change, replayed when the body matches and `409` when it does not; `Content-Digest` verified when present. Every service response carries `Content-Digest`, `Signature-Input` and `Signature` (ES256 over `@status`, `content-digest`, `content-type`, key `kid` from the profile). Orders are private: readable by the platform that placed them or with a buyer token carrying an order scope; anything else gets `401` with `WWW-Authenticate: Bearer`. Catalog ids are the skus (`product.id` is `prod_<sku>`, `variant.id` is the sku); amounts are integers in minor units.
|
|
126
|
-
|
|
127
87
|
The CLI is a UCP **platform**: its profile is served at `https://commercelayer.ai/ucp/agent-cli/profile.json` (`ucp profile --agent` prints it) and goes in `UCP-Agent` on every call. The store negotiates the capability intersection with it. `orders test` is the buyer; `ucp check` runs 30-odd checks across profile, transport rules, catalog, cart, discount, checkout, fulfillment, consent, payment, signatures, idempotency, order, identity linking, ask and permalink.
|
|
88
|
+
## Requirements
|
|
128
89
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
The CLI keeps no state of its own: the store lives behind `api.commercelayer.ai` (the `commercelayer-ai-api` Worker) and the store hosts (the `commercelayer-ai-store` Worker). This repo ships a stub of the whole platform with the same routes, which both Workers are ported from and tested against. `src/api/types.ts` is the management contract; `src/api/stub/management.ts` is the reference for the API, and `src/api/stub/ucp.ts` and `shopping.ts` are the reference for the store host. A change to the stub is mirrored in the Worker it belongs to.
|
|
132
|
-
|
|
133
|
-
| Route | Auth | Does |
|
|
134
|
-
| --- | --- | --- |
|
|
135
|
-
| `POST /sandboxes` `{ name }` | none | Create a sandbox with one sales channel and one signing key. Returns `org`, `token`, `claim_url`, `expires_at`, `preview_url`, `sales_channel_client_id`. |
|
|
136
|
-
| `GET /stores/:org` | bearer | The store: `applied`, `applied_hash`, `orders` (UCP entities, newest first), `storefront`, `sales_channel_client_id`, `claimed`, `expires_at`, `preview_url`, `claim_url`. |
|
|
137
|
-
| `PUT /stores/:org/resources` `{ applied, hash, name, policies }` | bearer | Make the store match the resources (markets by code, products by sku). `hash` digests every store file on disk; `status` compares it to report pending changes. Market scopes are `market:code:<code>`. |
|
|
138
|
-
| `PUT /stores/:org/storefront` `{ path, files: [{ path, content_base64 }] }` | bearer | Replace the static files served at the store host. `422 no_storefront` without an `index.html`. Returns `{ path, files, published_at }`. (The real API should take a `tar.gz` body with a size cap; base64 JSON is fine for the sandbox.) |
|
|
139
|
-
| `GET /stores/:org/orders` | bearer | The orders, newest first. |
|
|
140
|
-
| `GET /stores/:org/orders/:id` | bearer | One order, by id or label number, or `404 not_found`. |
|
|
141
|
-
| `POST /stores/:org/claim` | bearer | Start the device flow: `verification_url`, `user_code`. |
|
|
142
|
-
|
|
143
|
-
Errors come back as `{ "error": { "code", "message", "hint" } }` with 4xx/5xx and are printed as they are.
|
|
90
|
+
Node 20 or later. One runtime dependency (`yaml`).
|
|
144
91
|
|
|
145
|
-
|
|
92
|
+
## Links and license
|
|
146
93
|
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
- `
|
|
150
|
-
|
|
151
|
-
## Development
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
npm install
|
|
155
|
-
npm run dev -- help # run from source
|
|
156
|
-
npm test # build, then end-to-end tests against the in-process stub
|
|
157
|
-
npm run stub-api # serve the stub platform on :4747
|
|
158
|
-
```
|
|
94
|
+
- [commercelayer.ai/start.md](https://commercelayer.ai/start.md): the entry point an agent reads.
|
|
95
|
+
- [Universal Commerce Protocol](https://ucp.dev): the protocol every store speaks.
|
|
96
|
+
- [`@commercelayer/cli`](https://www.npmjs.com/package/@commercelayer/cli): the Commerce Layer CLI for humans.
|
|
97
|
+
- [Commerce Layer](https://commercelayer.io): the commerce API behind the stores.
|
|
159
98
|
|
|
160
|
-
|
|
99
|
+
MIT. Copyright Commerce Layer.
|
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// One function per command. Each returns the object the CLI prints, or throws a
|
|
2
2
|
// CliError. Every command is safe to re-run.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { createApi } from './api/client.js';
|
|
6
7
|
import { ignoreCredentials, paths, readCredentials, requireCredentials, writeCredentials } from './files.js';
|
|
@@ -11,7 +12,8 @@ import { buy } from './ucp/buy.js';
|
|
|
11
12
|
import { check } from './ucp/check.js';
|
|
12
13
|
import { discover } from './ucp/client.js';
|
|
13
14
|
import { agentProfile, AGENT_PROFILE_URL } from './ucp/platform.js';
|
|
14
|
-
|
|
15
|
+
/** The package version, read from package.json so a release bumps it in one place. */
|
|
16
|
+
export const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
15
17
|
export const HELP = {
|
|
16
18
|
init: 'Create a sandbox store for this repo, with no login. Writes .commercelayer/store.yml and .commercelayer/credentials. Reuses the store if store.yml already exists.',
|
|
17
19
|
plan: 'Show what apply would create, update or delete, from .commercelayer/store.yml and the product files under .commercelayer/products/ (*.yml lists, *.csv rows, merged by sku).',
|
package/package.json
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "c11r: the Commerce Layer CLI for coding agents. Builds a sandbox store that people and shopping agents can buy from.",
|
|
5
|
+
"homepage": "https://commercelayer.ai",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"commercelayer",
|
|
8
|
+
"commerce",
|
|
9
|
+
"cli",
|
|
10
|
+
"agent",
|
|
11
|
+
"ucp",
|
|
12
|
+
"sandbox"
|
|
13
|
+
],
|
|
5
14
|
"license": "MIT",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"engines": {
|
|
8
17
|
"node": ">=20"
|
|
9
18
|
},
|
|
10
19
|
"bin": {
|
|
11
|
-
"c11r": "
|
|
20
|
+
"c11r": "dist/bin.js"
|
|
12
21
|
},
|
|
13
22
|
"files": [
|
|
14
23
|
"dist",
|
package/dist/api/stub.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// A stand-in for api.commercelayer.ai: the same routes, answered from a JSON file.
|
|
2
|
-
// server.ts serves it over HTTP; client.ts can also call handle() in-process when
|
|
3
|
-
// COMMERCELAYER_AI_API_URL=stub, so agent tests need no server running.
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
const dbFile = process.env.COMMERCELAYER_AI_STUB_DB
|
|
9
|
-
?? path.join(process.env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state'), 'commercelayer-ai', 'stub-api.json');
|
|
10
|
-
function readDb() {
|
|
11
|
-
return fs.existsSync(dbFile) ? JSON.parse(fs.readFileSync(dbFile, 'utf8')) : {};
|
|
12
|
-
}
|
|
13
|
-
function writeDb(db) {
|
|
14
|
-
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
15
|
-
fs.writeFileSync(dbFile, JSON.stringify(db, null, 2) + '\n');
|
|
16
|
-
}
|
|
17
|
-
function error(status, code, message, hint) {
|
|
18
|
-
return { status, body: { error: { code, message, hint } } };
|
|
19
|
-
}
|
|
20
|
-
function publicState(r) {
|
|
21
|
-
const { token: _token, ...state } = r;
|
|
22
|
-
return state;
|
|
23
|
-
}
|
|
24
|
-
export function handle(req) {
|
|
25
|
-
const db = readDb();
|
|
26
|
-
if (req.method === 'POST' && req.path === '/sandboxes') {
|
|
27
|
-
const name = String(req.body?.name ?? 'store');
|
|
28
|
-
const slug = `${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'store'}-${crypto.randomBytes(2).toString('hex')}`;
|
|
29
|
-
const expires = new Date(Date.now() + 7 * 24 * 3600 * 1000);
|
|
30
|
-
expires.setUTCHours(0, 0, 0, 0);
|
|
31
|
-
const record = {
|
|
32
|
-
org: slug,
|
|
33
|
-
token: `sbx_${crypto.randomBytes(16).toString('hex')}`,
|
|
34
|
-
sandbox: true,
|
|
35
|
-
claimed: false,
|
|
36
|
-
claim_url: `https://commercelayer.ai/claim/${crypto.randomBytes(6).toString('hex')}`,
|
|
37
|
-
expires_at: expires.toISOString(),
|
|
38
|
-
preview_url: `https://${slug}.sandbox.commercelayer.store`,
|
|
39
|
-
sales_channel_client_id: `sc_${crypto.randomBytes(12).toString('hex')}`,
|
|
40
|
-
storefront: null,
|
|
41
|
-
applied: null,
|
|
42
|
-
applied_hash: null,
|
|
43
|
-
orders: [],
|
|
44
|
-
};
|
|
45
|
-
db[slug] = record;
|
|
46
|
-
writeDb(db);
|
|
47
|
-
const { org, token, claim_url, expires_at, preview_url, sales_channel_client_id } = record;
|
|
48
|
-
return { status: 201, body: { org, token, claim_url, expires_at, preview_url, sales_channel_client_id } };
|
|
49
|
-
}
|
|
50
|
-
const m = req.path.match(/^\/stores\/([^/]+)(\/.*)?$/);
|
|
51
|
-
if (!m)
|
|
52
|
-
return error(404, 'not_found', `No route ${req.method} ${req.path}.`, 'Check the command; run `commercelayer-ai help`.');
|
|
53
|
-
const [, org, rest = ''] = m;
|
|
54
|
-
const record = db[org];
|
|
55
|
-
if (!record || record.token !== req.token) {
|
|
56
|
-
return error(401, 'unauthorized', `No sandbox ${org} for this token.`, 'The sandbox may have expired or the credentials file may be stale. Remove .commercelayer/credentials and run `commercelayer-ai init` again.');
|
|
57
|
-
}
|
|
58
|
-
if (new Date(record.expires_at) < new Date() && !record.claimed) {
|
|
59
|
-
return error(410, 'expired', `Sandbox ${org} expired on ${record.expires_at}.`, 'Remove .commercelayer/credentials and run `commercelayer-ai init` to create a new sandbox; store.yml is kept.');
|
|
60
|
-
}
|
|
61
|
-
if (req.method === 'GET' && rest === '')
|
|
62
|
-
return { status: 200, body: publicState(record) };
|
|
63
|
-
if (req.method === 'PUT' && rest === '/resources') {
|
|
64
|
-
const { applied, hash } = req.body;
|
|
65
|
-
record.applied = applied;
|
|
66
|
-
record.applied_hash = hash;
|
|
67
|
-
writeDb(db);
|
|
68
|
-
return { status: 200, body: publicState(record) };
|
|
69
|
-
}
|
|
70
|
-
if (req.method === 'PUT' && rest === '/storefront') {
|
|
71
|
-
const { path: dir, files } = req.body;
|
|
72
|
-
if (!files?.some((f) => f.path === 'index.html')) {
|
|
73
|
-
return error(422, 'no_storefront', 'The upload has no index.html.', 'Run `commercelayer-ai storefront init` to create one, or set storefront.path in store.yml to the folder that has index.html.');
|
|
74
|
-
}
|
|
75
|
-
// The real API serves these files at preview_url; the stub only remembers them.
|
|
76
|
-
record.storefront = { path: dir, files: files.length, published_at: new Date().toISOString() };
|
|
77
|
-
writeDb(db);
|
|
78
|
-
return { status: 200, body: record.storefront };
|
|
79
|
-
}
|
|
80
|
-
if (req.method === 'GET' && rest === '/orders')
|
|
81
|
-
return { status: 200, body: [...record.orders].reverse() };
|
|
82
|
-
const order = rest.match(/^\/orders\/(\d+)$/);
|
|
83
|
-
if (req.method === 'GET' && order) {
|
|
84
|
-
const found = record.orders.find((o) => String(o.number) === order[1]);
|
|
85
|
-
if (!found)
|
|
86
|
-
return error(404, 'not_found', `There is no order ${order[1]}.`, 'Run `commercelayer-ai orders list` to see the order numbers.');
|
|
87
|
-
return { status: 200, body: found };
|
|
88
|
-
}
|
|
89
|
-
if (req.method === 'POST' && rest === '/orders/test') {
|
|
90
|
-
if (!record.applied)
|
|
91
|
-
return error(409, 'not_applied', 'The store has nothing applied yet.', 'Run `commercelayer-ai apply` first.');
|
|
92
|
-
const market = Object.values(record.applied.markets)[0];
|
|
93
|
-
const product = Object.values(record.applied.products)[0];
|
|
94
|
-
const order = {
|
|
95
|
-
number: 1001 + record.orders.length,
|
|
96
|
-
status: 'paid',
|
|
97
|
-
market: market.code,
|
|
98
|
-
currency: market.currency,
|
|
99
|
-
total: product.prices[market.currency],
|
|
100
|
-
items: [{ sku: product.sku, quantity: 1 }],
|
|
101
|
-
payment_gateway: 'sandbox',
|
|
102
|
-
payment: 'captured',
|
|
103
|
-
};
|
|
104
|
-
record.orders.push(order);
|
|
105
|
-
writeDb(db);
|
|
106
|
-
return { status: 201, body: order };
|
|
107
|
-
}
|
|
108
|
-
if (req.method === 'POST' && rest === '/claim') {
|
|
109
|
-
return { status: 200, body: { verification_url: 'https://commercelayer.ai/device', user_code: 'BXQT-4KMF' } };
|
|
110
|
-
}
|
|
111
|
-
return error(404, 'not_found', `No route ${req.method} ${req.path}.`, 'Check the command; run `commercelayer-ai help`.');
|
|
112
|
-
}
|