@commercelayer/agent-cli 0.8.0 → 1.0.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +95 -215
  2. package/dist/argv.js +214 -0
  3. package/dist/bin.js +15 -178
  4. package/dist/builtins/auth.js +169 -0
  5. package/dist/builtins/extensions.js +123 -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 +211 -0
  10. package/dist/confirm.js +56 -0
  11. package/dist/engine.js +173 -0
  12. package/dist/errors.js +146 -0
  13. package/dist/help.js +78 -0
  14. package/dist/http.js +117 -0
  15. package/dist/login.js +133 -0
  16. package/dist/resources.js +41 -0
  17. package/dist/schema.js +152 -0
  18. package/dist/schemas.js +72 -0
  19. package/dist/secrets.js +189 -0
  20. package/dist/version.js +4 -8
  21. package/package.json +4 -9
  22. package/dist/api/client.js +0 -190
  23. package/dist/api/server.js +0 -26
  24. package/dist/api/stub/db.js +0 -37
  25. package/dist/api/stub/http.js +0 -18
  26. package/dist/api/stub/index.js +0 -52
  27. package/dist/api/stub/locale.js +0 -52
  28. package/dist/api/stub/management.js +0 -409
  29. package/dist/api/stub/messages.js +0 -126
  30. package/dist/api/stub/orders.js +0 -625
  31. package/dist/api/stub/search.js +0 -78
  32. package/dist/api/stub/shopping.js +0 -673
  33. package/dist/api/stub/theme.js +0 -197
  34. package/dist/api/stub/ucp.js +0 -377
  35. package/dist/api/transport.js +0 -57
  36. package/dist/api/types.js +0 -4
  37. package/dist/assets.js +0 -100
  38. package/dist/catalog.js +0 -366
  39. package/dist/check.js +0 -187
  40. package/dist/commands.js +0 -1241
  41. package/dist/cursor.js +0 -31
  42. package/dist/files.js +0 -98
  43. package/dist/inbox/cursor.js +0 -11
  44. package/dist/inbox/fake.js +0 -95
  45. package/dist/inbox/imap.js +0 -139
  46. package/dist/inbox/mailbox.js +0 -25
  47. package/dist/inbox/message.js +0 -88
  48. package/dist/inspect.js +0 -230
  49. package/dist/languages.js +0 -49
  50. package/dist/links.js +0 -118
  51. package/dist/locales.js +0 -43
  52. package/dist/ops.js +0 -190
  53. package/dist/orderRequest.js +0 -141
  54. package/dist/output.js +0 -39
  55. package/dist/products.js +0 -151
  56. package/dist/promotions.js +0 -291
  57. package/dist/store.js +0 -379
  58. package/dist/storefront.js +0 -285
  59. package/dist/ucp/buy.js +0 -90
  60. package/dist/ucp/check.js +0 -360
  61. package/dist/ucp/client.js +0 -129
  62. package/dist/ucp/images.js +0 -62
  63. package/dist/ucp/platform.js +0 -90
  64. package/dist/ucp/shop.js +0 -357
  65. package/dist/ucp/signing.js +0 -67
  66. package/dist/web.js +0 -316
  67. package/dist/website.js +0 -390
package/dist/check.js DELETED
@@ -1,187 +0,0 @@
1
- // c11r storefront check: read the published storefront at preview_url, as a browser
2
- // would fetch it, and report what would break it or what is still the scaffold's.
3
- // This is the static phase: HTML and CSS as served, images by request. Every check
4
- // has a kind; a rendering phase (contrast, overflow, minicart fit, screenshots)
5
- // adds checks of kind "render" and fills `render`, without changing the shape.
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { send } from './api/transport.js';
9
- import { fail } from './output.js';
10
- import { storefrontPath } from './store.js';
11
- const decode = (t) => t.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (m, e) => {
12
- const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
13
- if (e[0] !== '#')
14
- return named[e.toLowerCase()] ?? m;
15
- return String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1)));
16
- });
17
- /** Every start tag with its attributes, comments and script bodies left out. */
18
- function tags(html) {
19
- const text = html.replace(/<!--[\s\S]*?-->/g, '').replace(/(<script\b[^>]*>)[\s\S]*?(<\/script>)/gi, '$1$2');
20
- return [...text.matchAll(/<([a-zA-Z][\w-]*)((?:\s+[^\s=/>]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?)*)\s*\/?>/g)].map(([, name, raw]) => {
21
- const attrs = {};
22
- for (const [, k, d, s, u] of raw.matchAll(/([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g))
23
- attrs[k.toLowerCase()] = decode(d ?? s ?? u ?? '');
24
- return { name: name.toLowerCase(), attrs };
25
- });
26
- }
27
- const scripts = (html) => [...html.replace(/<!--[\s\S]*?-->/g, '').matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)].map(([, attrs, body]) => ({ attrs, body }));
28
- /** GET a URL: through the CLI's transport on the store host (so the stub works), plain fetch elsewhere. */
29
- async function get(url, origin, method = 'GET') {
30
- if (new URL(url).origin === origin) {
31
- const r = await send('GET', url);
32
- return { status: r.status, type: r.headers['content-type'] ?? '', body: r.body };
33
- }
34
- try {
35
- let r = await fetch(url, { method, redirect: 'follow' });
36
- if (method === 'HEAD' && (r.status === 405 || r.status === 403))
37
- r = await fetch(url, { redirect: 'follow' });
38
- return { status: r.status, type: r.headers.get('content-type') ?? '', body: method === 'HEAD' ? '' : await r.text() };
39
- }
40
- catch (e) {
41
- return { status: 0, type: '', body: String(e.cause?.message ?? e.message) };
42
- }
43
- }
44
- /** In batches, so a large catalog does not open hundreds of connections at once. */
45
- async function mapLimit(items, limit, fn) {
46
- const out = [];
47
- for (let i = 0; i < items.length; i += limit)
48
- out.push(...(await Promise.all(items.slice(i, i + limit).map(fn))));
49
- return out;
50
- }
51
- const check = (id, status, message, extra = {}) => ({ id, kind: 'static', status, message, ...extra });
52
- export async function storefrontCheck(api) {
53
- const state = await api.getStore();
54
- if (!state.storefront)
55
- fail('no_storefront', 'Nothing is published at the store host yet.', 'Run `c11r storefront init` if there is no storefront folder, then `c11r storefront publish`.');
56
- const origin = new URL(state.preview_url).origin;
57
- const pageUrl = `${origin}/`;
58
- const page = await get(pageUrl, origin);
59
- if (page.status !== 200 || !page.type.includes('text/html'))
60
- fail('storefront_unreachable', `${pageUrl} answered ${page.status} ${page.type}.`, 'Run `c11r storefront publish` again; if it keeps happening, tell the human, with this output.');
61
- const html = page.body;
62
- const all = tags(html);
63
- const find = (name) => all.filter((t) => t.name === name);
64
- const checks = [];
65
- // drop-in.js: the script, its stylesheets, and the config block wired to this store's sales channel.
66
- const dropInJs = find('script').some((t) => /@commercelayer\/drop-in\.js@[^/]+\/dist\/drop-in\/drop-in\.esm\.js/.test(t.attrs.src ?? '') && t.attrs.type === 'module');
67
- checks.push(dropInJs ? check('drop_in_script', 'pass', 'drop-in.js is loaded as a module.') : check('drop_in_script', 'fail', 'drop-in.js is not loaded: prices, cart and checkout will not work.', { fix: 'Keep <script type="module" src="https://cdn.jsdelivr.net/npm/@commercelayer/drop-in.js@2/dist/drop-in/drop-in.esm.js"> in the <head>, as storefront init writes it.' }));
68
- const config = scripts(html).find((s) => s.body.includes('commercelayerConfig'))?.body ?? '';
69
- const clientId = config.match(/clientId\s*:\s*['"]([^'"]+)['"]/)?.[1];
70
- if (!config)
71
- checks.push(check('config', 'fail', 'There is no window.commercelayerConfig block.', { fix: 'Restore the config <script> from storefront init (clientId, scope, defaultAttributes) before the drop-in.js script.' }));
72
- else if (clientId !== state.sales_channel_client_id)
73
- checks.push(check('config', 'fail', `The config block's clientId is ${clientId ? `"${clientId}"` : 'missing'}, not this store's sales channel.`, { fix: `Set clientId: '${state.sales_channel_client_id}' in the config block.` }));
74
- else if (!/market:code:/.test(config))
75
- checks.push(check('config', 'fail', 'The config block has no market scope.', { fix: "Keep scope: 'market:code:' + market in the config block." }));
76
- else
77
- checks.push(check('config', 'pass', "The config block uses this store's sales channel and a market scope."));
78
- // The elements a shopper needs, and an add to cart for every product applied.
79
- const missing = ['cl-price', 'cl-add-to-cart', 'cl-cart-link', 'cl-checkout-link'].filter((n) => find(n).length === 0);
80
- checks.push(missing.length ? check('drop_in_elements', 'fail', `The page has no ${missing.join(', ')}.`, { fix: 'Keep the drop-in.js elements storefront init writes: cl-price and cl-add-to-cart per product, cl-cart-link with cl-cart-count and the mini cl-cart, cl-checkout-link.', items: missing }) : check('drop_in_elements', 'pass', 'Prices, add to cart, the cart link and the checkout link are on the page.'));
81
- let data = {};
82
- const json = scripts(html).find((s) => /id\s*=\s*["']?products["']?/.test(s.attrs) && /application\/json/.test(s.attrs));
83
- if (json) {
84
- try {
85
- data = JSON.parse(json.body);
86
- }
87
- catch {
88
- checks.push(check('product_data', 'fail', 'The #products data is not valid JSON: product pages will not open.', { fix: 'Fix the JSON in <script type="application/json" id="products">, or regenerate with `c11r storefront init --force`.' }));
89
- }
90
- }
91
- const templated = json && all.some((t) => t.name === 'cl-add-to-cart' && 'data-code' in t.attrs);
92
- const codes = new Set(all.filter((t) => t.name.startsWith('cl-') && t.attrs.code).map((t) => t.attrs.code));
93
- const buyable = new Set([...find('cl-add-to-cart').map((t) => t.attrs.code).filter(Boolean), ...(templated ? Object.keys(data) : [])]);
94
- const skus = Object.keys(state.applied?.products ?? {});
95
- const unsold = skus.filter((s) => !buyable.has(s));
96
- const unknown = [...new Set([...codes, ...(templated ? Object.keys(data) : [])])].filter((c) => !skus.includes(c));
97
- if (unknown.length)
98
- checks.push(check('products', 'fail', `The page offers skus the store does not have: ${unknown.join(', ')}.`, { fix: 'Use the skus in the catalog (code="<sku>"), or run `c11r apply` if the catalog has them and the store does not yet.', items: unknown }));
99
- else if (unsold.length)
100
- checks.push(check('products', 'warn', `${unsold.length} of ${skus.length} products have no add to cart on the page.`, { fix: 'Add a card with <cl-add-to-cart code="<sku>"> for each, or regenerate with `c11r storefront init --force`.', items: unsold }));
101
- else
102
- checks.push(check('products', 'pass', `All ${skus.length} products can be added to the cart.`));
103
- // Images: every <img> and product image, requested; and alt text on the page's images.
104
- const imageUrls = new Set();
105
- for (const t of find('img'))
106
- if (t.attrs.src && !('data-field' in t.attrs))
107
- imageUrls.add(t.attrs.src);
108
- for (const p of Object.values(data))
109
- if (p?.image_url)
110
- imageUrls.add(p.image_url);
111
- const resolved = [...imageUrls].slice(0, 200).map((src) => {
112
- try {
113
- return { src, url: new URL(src, pageUrl).href };
114
- }
115
- catch {
116
- return { src, url: '' };
117
- }
118
- });
119
- const answers = await mapLimit(resolved, 8, async (i) => ({ ...i, res: i.url ? await get(i.url, origin, 'HEAD') : { status: 0, type: '', body: 'not a URL' } }));
120
- const broken = answers.filter((a) => a.res.status < 200 || a.res.status >= 300 || (a.res.type && !a.res.type.startsWith('image/')));
121
- checks.push(broken.length
122
- ? check('images', 'fail', `${broken.length} of ${answers.length} images do not load.`, { fix: 'Put the files in the storefront folder (img/<file>) and publish, or fix the URLs.', items: broken.map((b) => `${b.src}: ${b.res.status || b.res.body}${b.res.type && !b.res.type.startsWith('image/') ? ` ${b.res.type}` : ''}`) })
123
- : check('images', 'pass', answers.length ? `All ${answers.length} images load.` : 'The page has no images.'));
124
- const noAlt = find('img').filter((t) => !('alt' in t.attrs) && !('data-field' in t.attrs)).map((t) => t.attrs.src ?? '(no src)');
125
- checks.push(noAlt.length ? check('image_alt', 'warn', `${noAlt.length} images have no alt text.`, { fix: 'Give every <img> an alt: the product name, or alt="" for decoration.', items: noAlt }) : check('image_alt', 'pass', 'Every image has alt text.'));
126
- // Styles: the page's own CSS after drop-in.css, the theme tokens, overrides that can apply.
127
- const sheets = find('link').filter((t) => /\bstylesheet\b/i.test(t.attrs.rel ?? '') && t.attrs.href);
128
- const dropInCss = sheets.findIndex((t) => /drop-in\.js@[^/]+\/dist\/drop-in\/drop-in\.css/.test(t.attrs.href));
129
- const own = sheets.filter((t) => new URL(t.attrs.href, pageUrl).origin === origin);
130
- const inline = [...html.replace(/<!--[\s\S]*?-->/g, '').matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]);
131
- const ownCss = await mapLimit(own, 4, async (t) => (await get(new URL(t.attrs.href, pageUrl).href, origin)).body);
132
- const css = [...ownCss, ...inline].join('\n');
133
- if (dropInCss < 0)
134
- checks.push(check('stylesheets', 'warn', 'drop-in.css is not linked: drop-in.js elements fall back to unstyled.', { fix: 'Keep <link href="https://cdn.jsdelivr.net/npm/@commercelayer/drop-in.js@2/dist/drop-in/drop-in.css" rel="stylesheet">, before the page\'s own stylesheet.' }));
135
- else if (own.some((t) => sheets.indexOf(t) < dropInCss))
136
- checks.push(check('stylesheets', 'warn', "A stylesheet of the page loads before drop-in.css, so drop-in.css's rules win over it.", { fix: 'Link the page\'s stylesheets after drop-in.css and minicart.css.', items: own.filter((t) => sheets.indexOf(t) < dropInCss).map((t) => t.attrs.href) }));
137
- else
138
- checks.push(check('stylesheets', 'pass', "The page's stylesheets load after drop-in.css."));
139
- const tokens = ['--paper', '--ink', '--accent'].filter((t) => !new RegExp(`${t}\\s*:`).test(css));
140
- checks.push(tokens.length ? check('theme_tokens', 'warn', `The theme tokens ${tokens.join(', ')} are not defined.`, { fix: 'Keep the tokens block at the top of styles.css and restyle through it.', items: tokens }) : check('theme_tokens', 'pass', 'The theme tokens are defined.'));
141
- const dead = [...css.matchAll(/(cl-add-to-cart|cl-checkout-link|cl-cart-link)::part\([^)]*\)/g)].map((m) => m[0]);
142
- const overrides = /cl-price-amount\s*\{[^}]*color\s*:/.test(css) && /cl-add-to-cart(?::not\(\[aria-disabled=["']?true["']?\]\))?\s*\{[^}]*background(-color)?\s*:/.test(css);
143
- if (dead.length)
144
- checks.push(check('drop_in_overrides', 'fail', 'Some rules style ::part()s that these drop-in.js elements do not have: they never apply.', { fix: 'Style the elements themselves (cl-add-to-cart, cl-checkout-link a), as styles.css does.', items: [...new Set(dead)] }));
145
- else if (!overrides)
146
- checks.push(check('drop_in_overrides', 'warn', "Nothing overrides drop-in.css's colours for prices and the add to cart button, so they ignore the theme.", { fix: 'Keep the "drop-in.js overrides" section of styles.css.' }));
147
- else
148
- checks.push(check('drop_in_overrides', 'pass', 'Prices and the add to cart button follow the theme.'));
149
- // The page itself.
150
- const title = decode(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? '').trim();
151
- if (!title)
152
- checks.push(check('title', 'fail', 'The page has no <title>.', { fix: 'Add a <title> with the store name and what it sells.' }));
153
- else if (title === state.name)
154
- checks.push(check('title', 'warn', `The <title> is still the scaffold's: "${title}".`, { fix: 'Say what the store sells too, e.g. "<name> · Specialty coffee".' }));
155
- else
156
- checks.push(check('title', 'pass', `The <title> is "${title}".`));
157
- const lang = find('html')[0]?.attrs.lang;
158
- checks.push(lang ? check('lang', 'pass', `The page language is ${lang}.`) : check('lang', 'warn', 'The <html> element has no lang.', { fix: 'Add lang="en" (or the store\'s language) to <html>.' }));
159
- const viewport = find('meta').some((t) => t.attrs.name === 'viewport');
160
- checks.push(viewport ? check('viewport', 'pass', 'The page has a viewport meta tag.') : check('viewport', 'fail', 'The page has no viewport meta tag: phones show it zoomed out.', { fix: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.' }));
161
- const description = find('meta').find((t) => t.attrs.name === 'description')?.attrs.content;
162
- checks.push(description ? check('description', 'pass', 'The page has a meta description.') : check('description', 'warn', 'The page has no meta description, what search results and link previews show.', { fix: 'Add <meta name="description" content="<one sentence on the store>">.' }));
163
- // The local folder against what is published.
164
- const dir = storefrontPath();
165
- if (fs.existsSync(dir)) {
166
- const since = Date.parse(state.storefront.published_at);
167
- const newer = fs
168
- .readdirSync(dir, { recursive: true, withFileTypes: true })
169
- .filter((e) => e.isFile() && !path.relative(dir, path.join(e.parentPath, e.name)).split(path.sep).some((p) => p.startsWith('.')))
170
- .filter((e) => fs.statSync(path.join(e.parentPath, e.name)).mtimeMs > since)
171
- .map((e) => path.relative(dir, path.join(e.parentPath, e.name)).split(path.sep).join('/'));
172
- checks.push(newer.length ? check('published', 'warn', `${newer.length} files in ${dir}/ changed after the last publish: this check read the published version.`, { fix: 'Run `c11r storefront publish`, then check again.', items: newer }) : check('published', 'pass', `The published storefront matches ${dir}/.`));
173
- }
174
- const summary = { pass: 0, warn: 0, fail: 0 };
175
- for (const c of checks)
176
- summary[c.status]++;
177
- return {
178
- status: summary.fail ? 'fail' : 'pass',
179
- url: pageUrl,
180
- published_at: state.storefront.published_at,
181
- summary,
182
- checks,
183
- // Filled by the rendering phase (1440 and 390 px: contrast, overflow, minicart fit, screenshots); null until then.
184
- render: null,
185
- next: summary.fail ? 'Fix the failed checks (see fix), publish, and check again.' : summary.warn ? 'The storefront works. The warnings are worth fixing before the human sees it.' : undefined,
186
- };
187
- }