@commercelayer/agent-cli 0.8.1 → 1.0.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +98 -215
  2. package/dist/argv.js +227 -0
  3. package/dist/bin.js +15 -178
  4. package/dist/builtins/auth.js +169 -0
  5. package/dist/builtins/extensions.js +107 -0
  6. package/dist/builtins/index.js +8 -0
  7. package/dist/builtins/loops.js +215 -0
  8. package/dist/builtins/sandbox.js +186 -0
  9. package/dist/config.js +238 -0
  10. package/dist/confirm.js +56 -0
  11. package/dist/engine.js +205 -0
  12. package/dist/errors.js +146 -0
  13. package/dist/help.js +81 -0
  14. package/dist/http.js +117 -0
  15. package/dist/inputs.js +112 -0
  16. package/dist/login.js +136 -0
  17. package/dist/resources.js +42 -0
  18. package/dist/schema.js +182 -0
  19. package/dist/schemas.js +72 -0
  20. package/dist/secrets.js +204 -0
  21. package/dist/version.js +4 -8
  22. package/package.json +4 -9
  23. package/dist/api/client.js +0 -190
  24. package/dist/api/server.js +0 -26
  25. package/dist/api/stub/db.js +0 -37
  26. package/dist/api/stub/http.js +0 -18
  27. package/dist/api/stub/index.js +0 -52
  28. package/dist/api/stub/locale.js +0 -52
  29. package/dist/api/stub/management.js +0 -409
  30. package/dist/api/stub/messages.js +0 -126
  31. package/dist/api/stub/orders.js +0 -625
  32. package/dist/api/stub/search.js +0 -78
  33. package/dist/api/stub/shopping.js +0 -673
  34. package/dist/api/stub/theme.js +0 -197
  35. package/dist/api/stub/ucp.js +0 -377
  36. package/dist/api/transport.js +0 -57
  37. package/dist/api/types.js +0 -4
  38. package/dist/assets.js +0 -100
  39. package/dist/catalog.js +0 -372
  40. package/dist/check.js +0 -187
  41. package/dist/commands.js +0 -1241
  42. package/dist/cursor.js +0 -31
  43. package/dist/files.js +0 -98
  44. package/dist/inbox/cursor.js +0 -11
  45. package/dist/inbox/fake.js +0 -95
  46. package/dist/inbox/imap.js +0 -139
  47. package/dist/inbox/mailbox.js +0 -25
  48. package/dist/inbox/message.js +0 -88
  49. package/dist/inspect.js +0 -237
  50. package/dist/languages.js +0 -49
  51. package/dist/links.js +0 -118
  52. package/dist/locales.js +0 -43
  53. package/dist/ops.js +0 -190
  54. package/dist/orderRequest.js +0 -141
  55. package/dist/output.js +0 -39
  56. package/dist/products.js +0 -151
  57. package/dist/promotions.js +0 -291
  58. package/dist/store.js +0 -379
  59. package/dist/storefront.js +0 -285
  60. package/dist/ucp/buy.js +0 -90
  61. package/dist/ucp/check.js +0 -362
  62. package/dist/ucp/client.js +0 -129
  63. package/dist/ucp/images.js +0 -62
  64. package/dist/ucp/platform.js +0 -90
  65. package/dist/ucp/shop.js +0 -357
  66. package/dist/ucp/signing.js +0 -67
  67. package/dist/web.js +0 -316
  68. package/dist/website.js +0 -390
@@ -1,190 +0,0 @@
1
- // The client for the management API at api.commercelayer.ai.
2
- import { CliError, fail, notice } from '../output.js';
3
- import { NPX } from '../version.js';
4
- import { apiUrl, DEFAULT_API_URL, send } from './transport.js';
5
- export function createApi(auth) {
6
- const base = apiUrl() === 'stub' ? DEFAULT_API_URL : apiUrl().replace(/\/$/, '');
7
- const org = () => {
8
- if (!auth)
9
- fail('no_store', 'There is no store in this repo.', 'Run `c11r init` in the repo root first.');
10
- return `/stores/${auth.org}`;
11
- };
12
- const request = async (method, path, body) => {
13
- const headers = { accept: 'application/json' };
14
- if (auth)
15
- headers.authorization = `Bearer ${auth.token}`;
16
- const res = await send(method, base + path, headers, body === undefined ? undefined : JSON.stringify(body));
17
- let data;
18
- try {
19
- data = res.body ? JSON.parse(res.body) : null;
20
- }
21
- catch {
22
- fail('api_error', `${base} answered ${res.status} with something that is not JSON.`, 'Try again. If it keeps happening, tell the human, with the output of `c11r status`.');
23
- }
24
- // The API flags an outdated CLI (from the User-Agent) in warnings, on any answer, errors included.
25
- const warnings = data && typeof data === 'object' && !Array.isArray(data) ? data.warnings : undefined;
26
- if (Array.isArray(warnings)) {
27
- const isOutdated = (w) => !!w && typeof w === 'object' && w.code === 'cli_outdated';
28
- for (const n of warnings.filter(isOutdated))
29
- notice({ ...n, hint: n.hint ?? `Run the commands with ${NPX}. If c11r is installed globally, tell the human it is on ${n.current} and ${n.latest} exists.` });
30
- // Printed once, at the top of the output, not inside the order or store it came with.
31
- const rest = warnings.filter((w) => !isOutdated(w));
32
- if (rest.length)
33
- data.warnings = rest;
34
- else
35
- delete data.warnings;
36
- }
37
- if (res.status < 400)
38
- return data;
39
- const err = data?.error;
40
- if (err)
41
- fail(err.code, err.message, err.hint, err.path);
42
- fail('api_error', `The API answered ${res.status}.`, 'Try again. If it keeps happening, tell the human, with the output of `c11r status`.');
43
- };
44
- // Promotions and links: on an API older than them, the collection route is an unknown route (404 not_found).
45
- const collection = (kind) => `${org()}/${kind}`;
46
- const one = (kind, id) => `${collection(kind)}/${encodeURIComponent(id)}`;
47
- const unsupported = async (kind, call) => {
48
- try {
49
- return await call;
50
- }
51
- catch (e) {
52
- if (e instanceof CliError && (e.code === 'not_found' || (e.code === 'api_error' && /\b404\b/.test(e.message))))
53
- fail(`${kind}_unsupported`, `This API does not manage ${kind} yet.`, 'Try again later; tell the human if it persists.');
54
- throw e;
55
- }
56
- };
57
- const found = async (kind, id, call) => {
58
- try {
59
- return await call;
60
- }
61
- catch (e) {
62
- if (e instanceof CliError && e.code === 'not_found')
63
- fail('not_found', `There is no ${kind.slice(0, -1)} ${id}.`, `Run \`c11r ${kind} list\` for the ids, or \`c11r ${kind} create\` to make it.`);
64
- throw e;
65
- }
66
- };
67
- // Orders and shipments for the merchant: an API older than them answers the UCP array on GET
68
- // /orders?view=merchant and not_found on the routes it lacks. A not_found on one order or
69
- // shipment is checked against the list, so a missing order is still not_found.
70
- const qs = (query) => {
71
- const p = new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined && v !== ''));
72
- return p.toString() ? `&${p}` : '';
73
- };
74
- const ordersUnsupported = () => fail('orders_unsupported', 'This API does not manage orders for the merchant yet.', 'Try again later; tell the human if it persists. c11r orders test still places test orders.');
75
- const shipmentsUnsupported = () => fail('fulfillment_unsupported', 'This API does not manage shipments yet.', 'Try again later; tell the human if it persists.');
76
- const listMerchantOrders = async (query) => {
77
- const r = (await request('GET', `${org()}/orders?view=merchant${qs(query)}`));
78
- if (Array.isArray(r) || !Array.isArray(r.orders))
79
- ordersUnsupported();
80
- return r;
81
- };
82
- const listShipments = async (query) => {
83
- try {
84
- const p = new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined && v !== ''));
85
- return (await request('GET', `${org()}/shipments${p.toString() ? `?${p}` : ''}`));
86
- }
87
- catch (e) {
88
- if (e instanceof CliError && e.code === 'not_found')
89
- shipmentsUnsupported();
90
- throw e;
91
- }
92
- };
93
- const orderCall = async (call) => {
94
- try {
95
- return await call();
96
- }
97
- catch (e) {
98
- if (e instanceof CliError && e.code === 'not_found')
99
- await listMerchantOrders({ limit: '1' });
100
- throw e;
101
- }
102
- };
103
- const shipmentCall = async (call) => {
104
- try {
105
- return await call();
106
- }
107
- catch (e) {
108
- if (e instanceof CliError && e.code === 'not_found')
109
- await listShipments({ limit: '1' });
110
- throw e;
111
- }
112
- };
113
- const orderPath = (id) => `${org()}/orders/${encodeURIComponent(id)}`;
114
- const shipmentPath = (id) => `${org()}/shipments/${encodeURIComponent(id)}`;
115
- return {
116
- listMerchantOrders,
117
- getMerchantOrder: (id) => orderCall(async () => {
118
- const o = (await request('GET', `${orderPath(id)}?view=merchant`));
119
- if (o && o.payment_status === undefined)
120
- ordersUnsupported();
121
- return o;
122
- }),
123
- createOrder: async (body) => {
124
- try {
125
- return (await request('POST', `${org()}/orders`, body));
126
- }
127
- catch (e) {
128
- // An API before orders from messages has no POST /orders.
129
- if (e instanceof CliError && e.code === 'not_found')
130
- fail('orders_unsupported', 'This API cannot create orders from messages yet.', 'Try again later; tell the human if it persists.');
131
- throw e;
132
- }
133
- },
134
- orderAction: (id, action, body) => orderCall(() => request('POST', `${orderPath(id)}/${action}`, body ?? {})),
135
- listShipments,
136
- // An API before the snapshot answers not_found: ops_unsupported, and the CLI reads the lists instead.
137
- getOps: async (query) => {
138
- try {
139
- const r = (await request('GET', `${org()}/ops${qs(query).replace(/^&/, '?')}`));
140
- if (!r || typeof r !== 'object' || !r.orders || !r.shipments)
141
- fail('ops_unsupported', 'This API has no operations snapshot yet.', 'The CLI reads the order and shipment lists instead.');
142
- return r;
143
- }
144
- catch (e) {
145
- if (e instanceof CliError && e.code === 'not_found')
146
- fail('ops_unsupported', 'This API has no operations snapshot yet.', 'The CLI reads the order and shipment lists instead.');
147
- throw e;
148
- }
149
- },
150
- insights: async (query) => {
151
- try {
152
- const r = (await request('GET', `${org()}/insights${qs(query).replace(/^&/, '?')}`));
153
- if (!r || typeof r !== 'object' || !('totals' in r))
154
- fail('insights_unsupported', 'This API does not report insights yet.', 'Try again later; tell the human if it persists.');
155
- return r;
156
- }
157
- catch (e) {
158
- if (e instanceof CliError && e.code === 'not_found')
159
- fail('insights_unsupported', 'This API does not report insights yet.', 'Try again later; tell the human if it persists.');
160
- throw e;
161
- }
162
- },
163
- getShipment: (id) => shipmentCall(() => request('GET', shipmentPath(id))),
164
- shipmentAction: (id, action, body) => shipmentCall(() => request('POST', `${shipmentPath(id)}/${action}`, body ?? {})),
165
- listPromotions: () => unsupported('promotions', request('GET', collection('promotions'))),
166
- createPromotion: (spec) => unsupported('promotions', request('POST', collection('promotions'), spec)),
167
- getPromotion: (id) => found('promotions', id, request('GET', one('promotions', id))),
168
- updatePromotion: (id, spec) => found('promotions', id, request('PUT', one('promotions', id), spec)),
169
- setPromotionEnabled: (id, enabled) => found('promotions', id, request('POST', `${one('promotions', id)}/${enabled ? 'enable' : 'disable'}`)),
170
- deletePromotion: async (id) => {
171
- await found('promotions', id, request('DELETE', one('promotions', id)));
172
- },
173
- listLinks: () => unsupported('links', request('GET', collection('links'))),
174
- createLink: (spec) => unsupported('links', request('POST', collection('links'), spec)),
175
- getLink: (id) => found('links', id, request('GET', one('links', id))),
176
- setLinkEnabled: (id, enabled) => found('links', id, request('POST', `${one('links', id)}/${enabled ? 'enable' : 'disable'}`)),
177
- deleteLink: async (id) => {
178
- await found('links', id, request('DELETE', one('links', id)));
179
- },
180
- // C11R_TEST=1 (our own test runs) asks the API to tag the organization as a test.
181
- createSandbox: (name) => request('POST', '/sandboxes', process.env.C11R_TEST === '1' ? { name, test: true } : { name }),
182
- getStore: () => request('GET', org()),
183
- applyStore: (body) => request('PUT', `${org()}/resources`, body),
184
- uploadAssets: (files) => request('PUT', `${org()}/assets`, { files }),
185
- publishStorefront: (path, files) => request('PUT', `${org()}/storefront`, { path, files }),
186
- listOrders: () => request('GET', `${org()}/orders`),
187
- getOrder: (id) => request('GET', `${org()}/orders/${encodeURIComponent(id)}`),
188
- startClaim: () => request('POST', `${org()}/claim`),
189
- };
190
- }
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env node
2
- // Serve the stub platform over HTTP, so the CLI's real HTTP client can be exercised:
3
- // npm run stub-api (port 4747)
4
- // C11R_API_URL=http://localhost:4747 c11r init
5
- // Store hosts are reached as http://localhost:4747/-/<org>.sandbox.commercelayer.store/...
6
- // or with a real Host header.
7
- import http from 'node:http';
8
- import { handle } from './stub/index.js';
9
- const port = Number(process.env.PORT ?? 4747);
10
- http
11
- .createServer((req, res) => {
12
- let raw = '';
13
- req.on('data', (chunk) => (raw += chunk));
14
- req.on('end', () => {
15
- const headers = {};
16
- for (const [k, v] of Object.entries(req.headers))
17
- if (typeof v === 'string')
18
- headers[k.toLowerCase()] = v;
19
- const host = headers.host ?? `localhost:${port}`;
20
- const url = new URL(req.url ?? '/', `http://${host}`);
21
- const { status, headers: resHeaders, body } = handle({ method: req.method ?? 'GET', url, headers, body: raw || undefined });
22
- res.writeHead(status, resHeaders);
23
- res.end(body);
24
- });
25
- })
26
- .listen(port, () => console.log(`stub platform on http://localhost:${port}`));
@@ -1,37 +0,0 @@
1
- // The stub's state: one JSON file, one record per sandbox. The real platform keeps
2
- // the same data behind api.commercelayer.ai and the store hosts.
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- const dbFile = process.env.C11R_STUB_DB
7
- ?? path.join(process.env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state'), 'c11r', 'stub-api.json');
8
- export function readDb() {
9
- return fs.existsSync(dbFile) ? JSON.parse(fs.readFileSync(dbFile, 'utf8')) : {};
10
- }
11
- export function writeDb(db) {
12
- fs.mkdirSync(path.dirname(dbFile), { recursive: true });
13
- fs.writeFileSync(dbFile, JSON.stringify(db, null, 2) + '\n');
14
- }
15
- export const SANDBOX_DOMAIN = 'sandbox.commercelayer.store';
16
- /** The org a store hostname belongs to, or null if it is not a store host. */
17
- export function orgOfHost(hostname) {
18
- const m = hostname.match(/^([a-z0-9-]+)\.sandbox\.commercelayer\.store$/);
19
- return m ? m[1] : null;
20
- }
21
- /** Orders of a store, newest first, without private members. */
22
- export function publicOrders(r) {
23
- return Object.values(r.ucp.orders).map((o) => strip(o)).reverse();
24
- }
25
- /** A deep copy without the members whose name starts with _. */
26
- export function strip(value) {
27
- if (Array.isArray(value))
28
- return value.map(strip);
29
- if (value && typeof value === 'object') {
30
- const out = {};
31
- for (const [k, v] of Object.entries(value))
32
- if (!k.startsWith('_'))
33
- out[k] = strip(v);
34
- return out;
35
- }
36
- return value;
37
- }
@@ -1,18 +0,0 @@
1
- // The HTTP shapes the stub works with: what a Cloudflare Worker would see. Keeping
2
- // the stub HTTP-shaped lets UCP headers (UCP-Agent, Idempotency-Key, signatures)
3
- // be modelled the way the real API will have to handle them.
4
- export function json(status, body, headers = {}) {
5
- return { status, headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body) };
6
- }
7
- /** The management API's error shape, the same the CLI prints. */
8
- export function error(status, code, message, hint, path) {
9
- return json(status, { error: { code, message, hint, ...(path ? { path } : {}) } });
10
- }
11
- export function readJson(req) {
12
- if (!req.body)
13
- return undefined;
14
- return JSON.parse(req.body);
15
- }
16
- export function bearer(req) {
17
- return req.headers.authorization?.replace(/^Bearer /i, '');
18
- }
@@ -1,52 +0,0 @@
1
- // A stand-in for the whole platform: api.commercelayer.ai (management) and
2
- // <org>.sandbox.commercelayer.store (the store: UCP, OAuth, the storefront).
3
- // server.ts serves it over HTTP; transport.ts calls handle() in-process when
4
- // C11R_API_URL=stub, so agent tests need no server running.
5
- //
6
- // Routing is by hostname. Over local HTTP, where subdomains do not resolve, the
7
- // path prefix /-/<host>/... stands in for the hostname.
8
- import { orgOfHost, readDb, writeDb } from './db.js';
9
- import { json } from './http.js';
10
- import { handleManagement } from './management.js';
11
- import { handleUcp } from './ucp.js';
12
- export function handle(req) {
13
- let url = req.url;
14
- const prefixed = url.pathname.match(/^\/-\/([^/]+)(\/.*)?$/);
15
- if (prefixed)
16
- url = new URL(`https://${prefixed[1]}${prefixed[2] ?? '/'}${url.search}`);
17
- const org = orgOfHost(url.hostname);
18
- if (org === null)
19
- return outdated(req, handleManagement({ ...req, url }));
20
- const db = readDb();
21
- const store = db[org];
22
- if (!store)
23
- return json(404, { error: { code: 'not_found', message: `There is no store at ${url.hostname}.`, hint: 'The sandbox may have expired.' } });
24
- const res = handleUcp({ ...req, url }, store);
25
- writeDb(db);
26
- return res;
27
- }
28
- /** "0.5.1" → [0, 5, 1], for comparing versions. */
29
- const parts = (v) => v.split('.').map((n) => Number.parseInt(n, 10) || 0);
30
- const older = (a, b) => {
31
- const [x, y] = [parts(a), parts(b)];
32
- for (let i = 0; i < 3; i++)
33
- if ((x[i] ?? 0) !== (y[i] ?? 0))
34
- return (x[i] ?? 0) < (y[i] ?? 0);
35
- return false;
36
- };
37
- /**
38
- * Like the API: a CLI whose User-Agent (c11r/<version>) is older than the latest gets a
39
- * cli_outdated warning on every JSON object it is answered. The stub's latest is
40
- * C11R_STUB_LATEST_CLI; unset, every CLI is current.
41
- */
42
- function outdated(req, res) {
43
- const latest = process.env.C11R_STUB_LATEST_CLI;
44
- const current = req.headers['user-agent']?.match(/^c11r\/(\d+\.\d+\.\d+)/)?.[1];
45
- if (!latest || !current || !older(current, latest) || !res.body)
46
- return res;
47
- const body = JSON.parse(res.body.toString());
48
- if (!body || typeof body !== 'object' || Array.isArray(body))
49
- return res;
50
- const warning = { code: 'cli_outdated', current, latest, hint: `Run the commands with npx -y @commercelayer/agent-cli@latest. If c11r is installed globally, tell the human it is on ${current} and ${latest} exists.` };
51
- return { ...res, body: JSON.stringify({ ...body, warnings: [...(Array.isArray(body.warnings) ? body.warnings : []), warning] }) };
52
- }
@@ -1,52 +0,0 @@
1
- // Locales on the store host, as the store Worker does them: one per country, from the
2
- // `locales` the API derives on apply (the stub keeps them on the record). The texts come
3
- // from applied.products[sku].translations[ll], falling back to the base language.
4
- export const COOKIE = 'c11r_locale';
5
- export const baseLanguage = (r) => r.applied?.language ?? 'en';
6
- /** The languages the store has: the base one, then every language a product is translated into. */
7
- export function languagesOf(r) {
8
- const out = new Set([baseLanguage(r)]);
9
- for (const p of Object.values(r.applied?.products ?? {}))
10
- for (const ll of Object.keys(p.translations ?? {}))
11
- out.add(ll);
12
- return out;
13
- }
14
- /** A product's name, description and category in a language, falling back to the base one. */
15
- export function texts(p, ll) {
16
- const t = p.translations?.[ll] ?? {};
17
- return { name: t.name ?? p.name, description: t.description ?? p.description, category: t.category ?? p.category };
18
- }
19
- /** The store's locales; none before an apply that derived them (the site stays as it was). */
20
- export const localesOf = (r) => r.locales ?? [];
21
- /** The locale at /<path>/, by its country: a known country with the wrong language still matches. */
22
- export const localeOfCountry = (r, country) => localesOf(r).find((l) => l.country === country.toUpperCase());
23
- /** The locale a cookie names, if it is one of the store's. */
24
- export function cookieLocale(r, cookie) {
25
- const m = cookie?.match(new RegExp(`(?:^|;\\s*)${COOKIE}=([a-z]{2}-[a-z]{2})`));
26
- return m ? localesOf(r).find((l) => l.path === m[1]) : undefined;
27
- }
28
- /** The locale of a product URL for a market: the buyer's country when it is in the market, else the market's first. */
29
- export function marketLocale(r, market, country) {
30
- const ls = localesOf(r).filter((l) => l.market === market);
31
- return ls.find((l) => country && l.country === country.toUpperCase()) ?? ls[0];
32
- }
33
- /**
34
- * The language of a UCP catalog answer: context.language (primary subtag), else the first
35
- * Accept-Language the store has, else the language of the buyer's country's locale, else the base.
36
- */
37
- export function ucpLanguage(r, context, acceptLanguage) {
38
- const has = languagesOf(r);
39
- const primary = (tag) => tag.trim().split(/[-_;]/)[0].toLowerCase();
40
- const asked = typeof context?.language === 'string' ? primary(context.language) : '';
41
- if (has.has(asked))
42
- return asked;
43
- for (const part of (acceptLanguage ?? '').split(',')) {
44
- const ll = primary(part);
45
- if (ll && has.has(ll))
46
- return ll;
47
- }
48
- const country = typeof context?.address_country === 'string' ? localeOfCountry(r, context.address_country) : undefined;
49
- if (country && has.has(country.language))
50
- return country.language;
51
- return baseLanguage(r);
52
- }