@commercelayer/agent-cli 0.1.2 → 0.1.4
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 +5 -5
- package/dist/api/stub/management.js +8 -3
- package/dist/bin.js +2 -2
- package/dist/commands.js +35 -11
- package/dist/store.js +10 -1
- package/dist/ucp/check.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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.
|
|
4
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.
|
|
5
5
|
```
|
|
6
|
-
npx -y @commercelayer/agent-cli@latest init # runs c11r init
|
|
6
|
+
npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11r init
|
|
7
7
|
```
|
|
8
8
|
## How it works
|
|
9
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.
|
|
@@ -13,8 +13,8 @@ npx -y @commercelayer/agent-cli@latest init # runs c11r init
|
|
|
13
13
|
## Commands
|
|
14
14
|
| Command | Does |
|
|
15
15
|
| --- | --- |
|
|
16
|
-
| `init` | Create a sandbox store, with no login. Writes `store.yml` (a commented template) and `credentials`. Reuses the store if they exist. |
|
|
17
|
-
| `plan` | Validate `store.yml` and the product files, and list what `apply` would create, update or delete. |
|
|
16
|
+
| `init [--name <business name>]` | Create a sandbox store, with no login. The name (the folder name when absent) names the organization; the org slug and store hostname derive from it, numbered when taken. Writes `store.yml` (a commented template) and `credentials`. Reuses the store if they exist. |
|
|
17
|
+
| `plan` | Validate `store.yml` and the product files, and list what `apply` would create, update or delete: markets, products, and the store's name and policies. |
|
|
18
18
|
| `apply [--allow-delete]` | Make the store match the files. Refuses deletes without the flag. |
|
|
19
19
|
| `storefront init [--force]` | Write `storefront/index.html`: a static page on [drop-in.js](https://commercelayer.github.io/drop-in.js/) listing the products in `store.yml`, with the hosted cart and checkout, wired to the sandbox's sales channel. Refuses to overwrite without the flag. |
|
|
20
20
|
| `storefront publish` | Upload the folder at `storefront.path` (default `storefront`) to the store host. |
|
|
@@ -22,8 +22,8 @@ npx -y @commercelayer/agent-cli@latest init # runs c11r init
|
|
|
22
22
|
| `orders list` | The orders placed in the store, newest first, as UCP order entities. |
|
|
23
23
|
| `orders show <id or number>` | One order. |
|
|
24
24
|
| `ucp profile [--agent]` | The store's UCP business profile (`/.well-known/ucp`); with `--agent`, this CLI's own platform profile. |
|
|
25
|
-
| `ucp check` | Walk every UCP capability the store advertises and report what passes. Exits 1 on a failed check. |
|
|
26
|
-
| `status` | The sandbox, the sales channel client id, the published storefront, what is applied, pending changes, orders, the UCP profile URL. |
|
|
25
|
+
| `ucp check` | Walk every UCP capability the store advertises and report what passes. Places and pays one real test order on the way (the output says which). Exits 1 on a failed check. |
|
|
26
|
+
| `status` | The sandbox, the sales channel client id, the published storefront, what is applied, the pending changes (exactly what `plan` lists; a store that does not load is pending too, with the error), orders, the UCP profile URL. |
|
|
27
27
|
| `claim` | Start the device flow that keeps the sandbox: a URL and a code for the human. |
|
|
28
28
|
| `help [<command>]` | Describe the commands. `--version` prints the version. |
|
|
29
29
|
## store.yml
|
|
@@ -5,15 +5,20 @@ import { generateKey } from '../../ucp/signing.js';
|
|
|
5
5
|
import { publicOrders, readDb, SANDBOX_DOMAIN, writeDb } from './db.js';
|
|
6
6
|
import { bearer, error, json, readJson } from './http.js';
|
|
7
7
|
function publicState(r) {
|
|
8
|
-
const { token: _token, ucp: _ucp,
|
|
9
|
-
return { ...state, orders: publicOrders(r) };
|
|
8
|
+
const { token: _token, ucp: _ucp, ...state } = r;
|
|
9
|
+
return { ...state, name: r.name ?? null, policies: r.policies ?? null, orders: publicOrders(r) };
|
|
10
10
|
}
|
|
11
11
|
export function handleManagement(req) {
|
|
12
12
|
const db = readDb();
|
|
13
13
|
const path = req.url.pathname;
|
|
14
14
|
if (req.method === 'POST' && path === '/sandboxes') {
|
|
15
|
+
// Like Commerce Layer: the organization is named `name`, and its slug is derived
|
|
16
|
+
// from it, suffixed -2, -3, ... when taken. The slug is the org and the store host.
|
|
15
17
|
const name = String(readJson(req)?.name ?? 'store');
|
|
16
|
-
const
|
|
18
|
+
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'store';
|
|
19
|
+
let slug = base;
|
|
20
|
+
for (let n = 2; db[slug]; n++)
|
|
21
|
+
slug = `${base}-${n}`;
|
|
17
22
|
const expires = new Date(Date.now() + 7 * 24 * 3600 * 1000);
|
|
18
23
|
expires.setUTCHours(0, 0, 0, 0);
|
|
19
24
|
const kid = `${slug}-${new Date().toISOString().slice(0, 7)}`;
|
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Output is always JSON; exit code 1 on an error or a failed check.
|
|
5
5
|
import * as cmd from './commands.js';
|
|
6
6
|
import { CliError, print } from './output.js';
|
|
7
|
-
const VALUE_OPTIONS = new Set(['--sku', '--quantity', '--country', '--code']);
|
|
7
|
+
const VALUE_OPTIONS = new Set(['--name', '--sku', '--quantity', '--country', '--code']);
|
|
8
8
|
const argv = process.argv.slice(2);
|
|
9
9
|
const flags = new Set();
|
|
10
10
|
const values = {};
|
|
@@ -32,7 +32,7 @@ async function run() {
|
|
|
32
32
|
case 'help':
|
|
33
33
|
return cmd.help(args.slice(1).join(' ') || undefined);
|
|
34
34
|
case 'init':
|
|
35
|
-
return cmd.init();
|
|
35
|
+
return cmd.init(values);
|
|
36
36
|
case 'plan':
|
|
37
37
|
return cmd.plan();
|
|
38
38
|
case 'apply':
|
package/dist/commands.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { createApi } from './api/client.js';
|
|
7
7
|
import { ignoreCredentials, paths, readCredentials, requireCredentials, writeCredentials } from './files.js';
|
|
8
|
-
import { fail } from './output.js';
|
|
8
|
+
import { CliError, fail } from './output.js';
|
|
9
9
|
import { diff, hashStore, loadStore, storefrontPath, template, toApplied } from './store.js';
|
|
10
10
|
import { storefrontHtml } from './storefront.js';
|
|
11
11
|
import { buy } from './ucp/buy.js';
|
|
@@ -15,8 +15,8 @@ import { agentProfile, AGENT_PROFILE_URL } from './ucp/platform.js';
|
|
|
15
15
|
/** The package version, read from package.json so a release bumps it in one place. */
|
|
16
16
|
export const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
17
17
|
export const HELP = {
|
|
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.',
|
|
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).',
|
|
18
|
+
init: 'Create a sandbox store for this repo, with no login: c11r init --name "<business name>" (the folder name when --name is absent). The name becomes the organization\'s name, and the org slug and store hostname derive from it. Writes .commercelayer/store.yml and .commercelayer/credentials. Reuses the store if store.yml already exists.',
|
|
19
|
+
plan: 'Show what apply would create, update or delete: markets, products and the store\'s name and policies, from .commercelayer/store.yml and the product files under .commercelayer/products/ (*.yml lists, *.csv rows, merged by sku).',
|
|
20
20
|
apply: 'Make the store match .commercelayer/store.yml and .commercelayer/products/. Refuses deletes unless --allow-delete is given.',
|
|
21
21
|
'storefront init': 'Write storefront/index.html: a static page on drop-in.js listing the products in store.yml, with the hosted cart and checkout. Refuses to overwrite unless --force is given.',
|
|
22
22
|
'storefront publish': 'Upload the storefront folder (store.yml storefront.path, default storefront) to the sandbox preview URL.',
|
|
@@ -24,8 +24,8 @@ export const HELP = {
|
|
|
24
24
|
'orders list': 'List the orders placed in the store, newest first, as UCP order entities.',
|
|
25
25
|
'orders show': 'Show one order: c11r orders show <id or number>.',
|
|
26
26
|
'ucp profile': 'Show the store\'s UCP business profile (/.well-known/ucp). With --agent, this CLI\'s own platform profile instead.',
|
|
27
|
-
'ucp check': 'Walk every UCP capability the store advertises and report what passes. Exits 1 when a check fails.',
|
|
28
|
-
status: 'Show the sandbox, what is applied, and whether store
|
|
27
|
+
'ucp check': 'Walk every UCP capability the store advertises and report what passes. Places and pays one real test order on the way. Exits 1 when a check fails.',
|
|
28
|
+
status: 'Show the sandbox, what is applied, and whether the store files have changes not yet applied (the same changes plan lists).',
|
|
29
29
|
claim: 'Start keeping this sandbox store: returns a URL and a code for the human to approve in the browser.',
|
|
30
30
|
help: 'Describe the commands. `c11r help <command>` for one command.',
|
|
31
31
|
};
|
|
@@ -38,13 +38,17 @@ export function help(name) {
|
|
|
38
38
|
}
|
|
39
39
|
return { version: VERSION, commands: HELP, note: 'Every command prints JSON. The store format is documented in the comments of .commercelayer/store.yml; large catalogs go in .commercelayer/products/ as .yml or .csv files.' };
|
|
40
40
|
}
|
|
41
|
-
export async function init() {
|
|
41
|
+
export async function init(values) {
|
|
42
42
|
const existing = readCredentials();
|
|
43
43
|
if (existing && fs.existsSync(paths.store)) {
|
|
44
44
|
const s = await createApi(existing).getStore();
|
|
45
45
|
return { status: 'reused', org: s.org, claim_url: s.claim_url, expires_at: s.expires_at, preview_url: s.preview_url };
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// The business name becomes the organization's name; the platform derives the
|
|
48
|
+
// org slug (and so the store hostname) from it. The folder name is the fallback.
|
|
49
|
+
if (values.name !== undefined && !values.name.trim())
|
|
50
|
+
fail('invalid_argument', '--name needs the business name.', 'Example: c11r init --name "The Blue Brand". Leave it out to use the folder name.');
|
|
51
|
+
const name = values.name?.trim() || path.basename(process.cwd());
|
|
48
52
|
const sandbox = await createApi(null).createSandbox(name);
|
|
49
53
|
fs.mkdirSync(paths.dir, { recursive: true });
|
|
50
54
|
if (!fs.existsSync(paths.store))
|
|
@@ -56,6 +60,7 @@ export async function init() {
|
|
|
56
60
|
return {
|
|
57
61
|
status: 'created',
|
|
58
62
|
org: sandbox.org,
|
|
63
|
+
name,
|
|
59
64
|
claim_url: sandbox.claim_url,
|
|
60
65
|
expires_at: sandbox.expires_at,
|
|
61
66
|
preview_url: sandbox.preview_url,
|
|
@@ -67,14 +72,14 @@ export async function plan() {
|
|
|
67
72
|
const api = createApi(requireCredentials());
|
|
68
73
|
const desired = loadStore();
|
|
69
74
|
const state = await api.getStore();
|
|
70
|
-
const changes = diff(state
|
|
75
|
+
const changes = diff(state, desired);
|
|
71
76
|
return { status: changes.length ? 'changes' : 'up_to_date', changes, deletes: changes.filter((c) => c.action === 'delete').length };
|
|
72
77
|
}
|
|
73
78
|
export async function apply(flags) {
|
|
74
79
|
const api = createApi(requireCredentials());
|
|
75
80
|
const desired = loadStore();
|
|
76
81
|
const state = await api.getStore();
|
|
77
|
-
const changes = diff(state
|
|
82
|
+
const changes = diff(state, desired);
|
|
78
83
|
const deletes = changes.filter((c) => c.action === 'delete');
|
|
79
84
|
if (deletes.length && !flags.has('--allow-delete')) {
|
|
80
85
|
fail('deletes_need_approval', `Apply would delete ${deletes.map((d) => `${d.type} ${d.id}`).join(', ')}.`, 'Ask the human whether to delete these. If they agree, run `c11r apply --allow-delete`.');
|
|
@@ -168,11 +173,27 @@ export async function ucpProfile(flags) {
|
|
|
168
173
|
export async function ucpCheck() {
|
|
169
174
|
const origin = await storeOrigin();
|
|
170
175
|
const report = await check(origin);
|
|
171
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
...report,
|
|
178
|
+
note: report.order ? `The checkout checks placed and paid a real test order (${report.order.label ?? report.order.id}) with the sandbox handler; it appears in \`c11r orders list\` like one from \`orders test\`.` : undefined,
|
|
179
|
+
next: report.status === 'pass' ? 'Any shopping agent can buy from this store.' : 'Fix the failed checks and run `c11r ucp check` again.',
|
|
180
|
+
};
|
|
172
181
|
}
|
|
173
182
|
export async function status() {
|
|
174
183
|
const api = createApi(requireCredentials());
|
|
175
184
|
const s = await api.getStore();
|
|
185
|
+
// Pending means exactly what plan would list, so the two never disagree. If
|
|
186
|
+
// the store files do not even load, that is pending too, with the error.
|
|
187
|
+
let pending = [];
|
|
188
|
+
let storeError;
|
|
189
|
+
try {
|
|
190
|
+
pending = diff(s, loadStore());
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
if (!(e instanceof CliError))
|
|
194
|
+
throw e;
|
|
195
|
+
storeError = { code: e.code, message: e.message, hint: e.hint };
|
|
196
|
+
}
|
|
176
197
|
return {
|
|
177
198
|
org: s.org,
|
|
178
199
|
sandbox: s.sandbox,
|
|
@@ -182,11 +203,14 @@ export async function status() {
|
|
|
182
203
|
sales_channel_client_id: s.sales_channel_client_id,
|
|
183
204
|
storefront: s.storefront,
|
|
184
205
|
applied: Boolean(s.applied),
|
|
185
|
-
pending_changes:
|
|
206
|
+
pending_changes: pending.length > 0 || storeError !== undefined,
|
|
207
|
+
pending: pending.length ? pending : undefined,
|
|
208
|
+
store_error: storeError,
|
|
186
209
|
markets: s.applied ? Object.keys(s.applied.markets) : [],
|
|
187
210
|
products: s.applied ? Object.keys(s.applied.products).length : 0,
|
|
188
211
|
orders: s.orders.map((o) => ({ id: o.id, label: o.label })),
|
|
189
212
|
ucp_profile: `${s.preview_url}/.well-known/ucp`,
|
|
213
|
+
next: storeError ? 'Fix the store files (see store_error), then run `c11r plan`.' : pending.length ? 'Run `c11r plan` to see the changes, then `c11r apply`.' : undefined,
|
|
190
214
|
};
|
|
191
215
|
}
|
|
192
216
|
export async function claim() {
|
package/dist/store.js
CHANGED
|
@@ -152,8 +152,17 @@ export function toApplied(store) {
|
|
|
152
152
|
products: Object.fromEntries(store.products.map((p) => [p.sku, p])),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
const canonical = (v) => JSON.stringify(v ?? {}, (_k, x) => (x === undefined ? undefined : x));
|
|
156
|
+
/** The one definition of "pending changes": what apply would do. plan and status both use it. */
|
|
157
|
+
export function diff(state, desired) {
|
|
156
158
|
const changes = [];
|
|
159
|
+
const { applied } = state;
|
|
160
|
+
if (applied) {
|
|
161
|
+
if (state.name !== undefined && state.name !== null && state.name !== desired.name)
|
|
162
|
+
changes.push({ action: 'update', type: 'store', id: 'name' });
|
|
163
|
+
if (state.policies !== undefined && canonical(state.policies) !== canonical(desired.policies))
|
|
164
|
+
changes.push({ action: 'update', type: 'store', id: 'policies' });
|
|
165
|
+
}
|
|
157
166
|
const after = toApplied(desired);
|
|
158
167
|
for (const kind of ['markets', 'products']) {
|
|
159
168
|
const type = kind.slice(0, -1);
|
package/dist/ucp/check.js
CHANGED
|
@@ -235,6 +235,7 @@ export async function check(origin) {
|
|
|
235
235
|
return 'payment_failed';
|
|
236
236
|
});
|
|
237
237
|
let completeCall = null;
|
|
238
|
+
let placed;
|
|
238
239
|
let orderId = null;
|
|
239
240
|
const key = crypto.randomUUID();
|
|
240
241
|
const completeBody = { payment: { instruments: [sandboxInstrument()] }, signals: { 'dev.ucp.buyer_ip': '203.0.113.1', 'dev.ucp.user_agent': 'c11r' } };
|
|
@@ -246,6 +247,7 @@ export async function check(origin) {
|
|
|
246
247
|
must(done.continue_url === undefined && done.expires_at === undefined, 'completed checkouts should omit continue_url and expires_at');
|
|
247
248
|
must(JSON.stringify(done.payment ?? {}).includes('"token"') === false, 'the credential token must not be echoed');
|
|
248
249
|
orderId = done.order.id;
|
|
250
|
+
placed = { id: done.order.id, label: done.order.label };
|
|
249
251
|
return `order ${done.order.id} ${done.order.label ?? ''}`;
|
|
250
252
|
});
|
|
251
253
|
await run('signatures', 'the completion response is signed and verifies', async () => {
|
|
@@ -347,9 +349,12 @@ export async function check(origin) {
|
|
|
347
349
|
return `303 → ${res.headers.location}`;
|
|
348
350
|
});
|
|
349
351
|
}
|
|
350
|
-
return finish(origin, checks);
|
|
352
|
+
return finish(origin, checks, placed);
|
|
351
353
|
}
|
|
352
|
-
function finish(origin, checks) {
|
|
354
|
+
function finish(origin, checks, order) {
|
|
353
355
|
const failed = checks.filter((c) => !c.pass).length;
|
|
354
|
-
|
|
356
|
+
const report = { status: failed ? 'fail' : 'pass', store: origin, version: UCP_VERSION, passed: checks.length - failed, failed, checks };
|
|
357
|
+
if (order)
|
|
358
|
+
report.order = order;
|
|
359
|
+
return report;
|
|
355
360
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "c11r: the Commerce Layer CLI for coding agents. Builds a sandbox store that people and shopping agents can buy from.",
|
|
5
5
|
"homepage": "https://commercelayer.ai",
|
|
6
6
|
"keywords": [
|