@commercelayer/agent-cli 0.1.8 → 0.1.10
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 +3 -3
- package/dist/api/stub/management.js +72 -7
- package/dist/commands.js +65 -7
- package/dist/output.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11
|
|
|
15
15
|
| --- | --- |
|
|
16
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
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
|
-
| `apply [--allow-delete]` | Make the store match the files. Refuses deletes without the
|
|
18
|
+
| `apply [--allow-delete] [--full]` | Make the store match the files. Refuses deletes without `--allow-delete`. Product changes go through a Commerce Layer import: `apply` waits for it, with progress as JSON lines on stderr, and prints one result. `apply_failed` lists the products Commerce Layer refused; While Commerce Layer is busy or unavailable, `apply` keeps reading (backing off on busy) and writes a `retrying` line. `apply_pending` means the import keeps going after 10 minutes, and `c11r status` follows it. An apply still running is finished before a new one starts. `--full` re-imports every product and price, after edits in the Commerce Layer dashboard. |
|
|
19
19
|
| `catalog pull` | Read the products from the catalog source in `store.yml` (a Supabase table, a Google Sheet or a Google Merchant Center feed) into `.commercelayer/products/catalog.csv`. Then `plan` and `apply` as usual. |
|
|
20
20
|
| `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`, grouped by top-level category with a navigation, with a minicart that slides in on add to cart, and the hosted cart and checkout, wired to the sandbox's sales channel. Refuses to overwrite without the flag. |
|
|
21
21
|
| `storefront publish` | Upload the folder at `storefront.path` (default `storefront`) to the store host. |
|
|
@@ -24,7 +24,7 @@ npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11
|
|
|
24
24
|
| `orders show <id or number>` | One order. |
|
|
25
25
|
| `ucp profile [--agent]` | The store's UCP business profile (`/.well-known/ucp`); with `--agent`, this CLI's own platform profile. |
|
|
26
26
|
| `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. |
|
|
27
|
-
| `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
|
+
| `status` | The sandbox, the sales channel client id, the published storefront, what is applied, the last import job (`apply`), the pending changes (exactly what `plan` lists; a store that does not load is pending too, with the error), orders, the UCP profile URL. |
|
|
28
28
|
| `claim` | Start the device flow that keeps the sandbox: a URL and a code for the human. |
|
|
29
29
|
| `help [<command>]` | Describe the commands. `--version` prints the version. |
|
|
30
30
|
## store.yml
|
|
@@ -84,7 +84,7 @@ catalog:
|
|
|
84
84
|
- `merchant`: a Google Merchant Center product feed URL, RSS 2.0 (XML, `g:` namespace) or TSV. `id` is the sku, `title` the name, `description`, `image_link` the image, `price` (`14.50 EUR`) the price in its currency, `product_type` the category. An id listed once per currency is one product with several prices. Other attributes, `sale_price` included, are ignored.
|
|
85
85
|
Error codes: `no_catalog_source`, `invalid_catalog`, `missing_credentials`, `catalog_unreachable`, `catalog_unauthorized`, `catalog_not_found`, `catalog_not_csv`, `catalog_error`, `empty_catalog`, plus `invalid_product` and `missing_price`.
|
|
86
86
|
|
|
87
|
-
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`.
|
|
87
|
+
Error codes: `no_store`, `invalid_yaml`, `invalid_store`, `invalid_market`, `invalid_product`, `missing_price`, `deletes_need_approval`, `apply_failed`, `apply_pending`, `apply_in_progress`, `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`.
|
|
88
88
|
## The store speaks UCP
|
|
89
89
|
Each store is a UCP business at `https://<org>.sandbox.commercelayer.store` (live stores: `<slug>.commercelayer.store`), implementing release **2026-08-25** over REST:
|
|
90
90
|
| What | Where |
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
// The management API at api.commercelayer.ai: sandboxes, resources, storefront
|
|
2
2
|
// uploads, orders, claims. The CLI's only private API; the store hosts speak UCP.
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import { canonical } from '../../store.js';
|
|
4
5
|
import { generateKey } from '../../ucp/signing.js';
|
|
5
6
|
import { publicOrders, readDb, SANDBOX_DOMAIN, writeDb } from './db.js';
|
|
6
7
|
import { bearer, error, json, readJson } from './http.js';
|
|
7
8
|
function publicState(r) {
|
|
8
|
-
const { token: _token, ucp: _ucp, ...state } = r;
|
|
9
|
-
return { ...state, name: r.name ?? null, policies: r.policies ?? null, orders: publicOrders(r) };
|
|
9
|
+
const { token: _token, ucp: _ucp, pending: _pending, ...state } = r;
|
|
10
|
+
return { ...state, name: r.name ?? null, policies: r.policies ?? null, apply: r.apply ?? null, orders: publicOrders(r) };
|
|
11
|
+
}
|
|
12
|
+
/** Like Commerce Layer refusing a sku in an import. Only the stub does this, so tests can exercise apply_failed. */
|
|
13
|
+
const REFUSED = /^REFUSED-/;
|
|
14
|
+
/** C11R_STUB_BUSY_READS=n: the first n reads of a running import answer 503 commercelayer_busy, as when Commerce Layer rate-limits. Per process. */
|
|
15
|
+
let busyReads = Number(process.env.C11R_STUB_BUSY_READS) || 0;
|
|
16
|
+
/** Items an import gets through per read: small, so the CLI polls a few times. */
|
|
17
|
+
const ITEMS_PER_READ = 2;
|
|
18
|
+
/**
|
|
19
|
+
* Move a running apply job forward, as the API does on every read: nothing runs in
|
|
20
|
+
* the background. When it ends, the pending resources are installed, or refused.
|
|
21
|
+
*/
|
|
22
|
+
function advance(r) {
|
|
23
|
+
const job = r.apply;
|
|
24
|
+
if (!job || job.status !== 'in_progress' || !r.pending)
|
|
25
|
+
return;
|
|
26
|
+
job.processed = Math.min(job.total, job.processed + ITEMS_PER_READ);
|
|
27
|
+
if (job.processed < job.total)
|
|
28
|
+
return;
|
|
29
|
+
job.step = 'done';
|
|
30
|
+
job.finished_at = new Date().toISOString();
|
|
31
|
+
if (r.pending.refused.length) {
|
|
32
|
+
job.status = 'failed';
|
|
33
|
+
job.errors = r.pending.refused.slice(0, 50).map((sku) => ({ sku, message: 'code is invalid' }));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
job.status = 'completed';
|
|
37
|
+
r.applied = r.pending.applied;
|
|
38
|
+
r.applied_hash = job.hash;
|
|
39
|
+
}
|
|
40
|
+
delete r.pending;
|
|
10
41
|
}
|
|
11
42
|
export function handleManagement(req) {
|
|
12
43
|
const db = readDb();
|
|
@@ -53,16 +84,50 @@ export function handleManagement(req) {
|
|
|
53
84
|
if (new Date(record.expires_at) < new Date() && !record.claimed) {
|
|
54
85
|
return error(410, 'expired', `Sandbox ${org} expired on ${record.expires_at}.`, 'Remove .commercelayer/credentials and run `c11r init` to create a new sandbox; store.yml is kept.');
|
|
55
86
|
}
|
|
56
|
-
if (req.method === 'GET' && rest === '')
|
|
87
|
+
if (req.method === 'GET' && rest === '') {
|
|
88
|
+
if (record.apply?.status === 'in_progress' && busyReads > 0) {
|
|
89
|
+
busyReads--;
|
|
90
|
+
return error(503, 'commercelayer_busy', 'Commerce Layer is rate-limiting this organization.', 'Wait a minute and try again; never edit store.yml for this.');
|
|
91
|
+
}
|
|
92
|
+
advance(record);
|
|
93
|
+
writeDb(db);
|
|
57
94
|
return json(200, publicState(record));
|
|
95
|
+
}
|
|
58
96
|
if (req.method === 'PUT' && rest === '/resources') {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
record.
|
|
97
|
+
advance(record);
|
|
98
|
+
writeDb(db);
|
|
99
|
+
if (record.apply?.status === 'in_progress')
|
|
100
|
+
return error(409, 'apply_in_progress', 'Another apply is still running.', 'Run `c11r status` to follow it, then apply again.');
|
|
101
|
+
const { applied, hash, name, policies, allow_delete, full } = readJson(req);
|
|
102
|
+
const before = record.applied?.products ?? {};
|
|
103
|
+
const removed = Object.keys(before).filter((sku) => !(sku in applied.products));
|
|
104
|
+
if (removed.length && !allow_delete)
|
|
105
|
+
return error(409, 'deletes_need_approval', `Apply would delete product ${removed.join(', ')}.`, 'Ask the human whether to delete these. If they agree, run `c11r apply --allow-delete`.');
|
|
106
|
+
// The name, policies and markets apply in the request; products go through an
|
|
107
|
+
// import job (SKUs, prices, then cleanups for removals) when any changed.
|
|
62
108
|
record.name = name;
|
|
63
109
|
record.policies = policies ?? {};
|
|
110
|
+
const changed = Object.keys(applied.products).filter((sku) => full || !(sku in before) || canonical(before[sku]) !== canonical(applied.products[sku]));
|
|
111
|
+
if (changed.length === 0 && removed.length === 0) {
|
|
112
|
+
record.applied = applied;
|
|
113
|
+
record.applied_hash = hash;
|
|
114
|
+
writeDb(db);
|
|
115
|
+
return json(200, publicState(record));
|
|
116
|
+
}
|
|
117
|
+
record.apply = {
|
|
118
|
+
id: `apply_${crypto.randomBytes(6).toString('hex')}`,
|
|
119
|
+
status: 'in_progress',
|
|
120
|
+
step: 'skus',
|
|
121
|
+
hash,
|
|
122
|
+
processed: 0,
|
|
123
|
+
total: changed.length + removed.length,
|
|
124
|
+
started_at: new Date().toISOString(),
|
|
125
|
+
finished_at: null,
|
|
126
|
+
errors: [],
|
|
127
|
+
};
|
|
128
|
+
record.pending = { applied, refused: changed.filter((sku) => REFUSED.test(sku)) };
|
|
64
129
|
writeDb(db);
|
|
65
|
-
return json(
|
|
130
|
+
return json(202, publicState(record));
|
|
66
131
|
}
|
|
67
132
|
if (req.method === 'PUT' && rest === '/storefront') {
|
|
68
133
|
const { path: dir, files } = readJson(req);
|
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 { CliError, fail } from './output.js';
|
|
8
|
+
import { CliError, fail, progress } 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 { pull } from './catalog.js';
|
|
@@ -18,7 +18,7 @@ export const VERSION = createRequire(import.meta.url)('../package.json').version
|
|
|
18
18
|
export const HELP = {
|
|
19
19
|
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.',
|
|
20
20
|
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).',
|
|
21
|
-
apply: 'Make the store match .commercelayer/store.yml and .commercelayer/products/. Refuses deletes unless --allow-delete is given.',
|
|
21
|
+
apply: 'Make the store match .commercelayer/store.yml and .commercelayer/products/. Refuses deletes unless --allow-delete is given. Product changes go through a Commerce Layer import: apply waits for it (progress as JSON lines on stderr) and prints one result; if it stops with apply_pending the import keeps going, so run `c11r status` to follow it. While Commerce Layer is busy or unavailable it keeps reading, with a retrying line. An apply still running is finished first. --full re-imports every product and price, after edits in the Commerce Layer dashboard.',
|
|
22
22
|
'catalog pull': 'Read the products from the catalog source in store.yml (catalog: { source: supabase | sheets | merchant, url, table }) and write them to .commercelayer/products/catalog.csv; then plan and apply as usual. Columns: sku, name, description, image_url, category and one price column per market currency, in lowercase (eur, usd). Supabase reads C11R_SUPABASE_URL and C11R_SUPABASE_KEY (the anon key) from .commercelayer/credentials or the environment; a Google Sheet needs its CSV link or a link anyone can open; a Merchant Center feed is RSS/XML or TSV.',
|
|
23
23
|
'storefront init': 'Write storefront/index.html: a static page on drop-in.js listing the products in store.yml, with a minicart that opens on add to cart, and the hosted cart and checkout. Refuses to overwrite unless --force is given.',
|
|
24
24
|
'storefront publish': 'Upload the storefront folder (store.yml storefront.path, default storefront) to the sandbox preview URL.',
|
|
@@ -29,9 +29,15 @@ export const HELP = {
|
|
|
29
29
|
'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.',
|
|
30
30
|
status: 'Show the sandbox, what is applied, and whether the store files have changes not yet applied (the same changes plan lists).',
|
|
31
31
|
claim: 'Start keeping this sandbox store: returns a URL and a code for the human to approve in the browser.',
|
|
32
|
-
help: 'Describe the commands. `c11r help <command>` for one command.',
|
|
32
|
+
help: 'Describe the commands. `c11r help <command>` for one command, `c11r help <group>` (catalog, orders, storefront, ucp) for a group.',
|
|
33
33
|
};
|
|
34
34
|
export function help(name) {
|
|
35
|
+
if (name && !HELP[name]) {
|
|
36
|
+
// A group (catalog, orders, storefront, ucp): its commands.
|
|
37
|
+
const group = Object.entries(HELP).filter(([k]) => k.startsWith(`${name} `));
|
|
38
|
+
if (group.length)
|
|
39
|
+
return { group: name, commands: Object.fromEntries(group) };
|
|
40
|
+
}
|
|
35
41
|
if (name) {
|
|
36
42
|
if (!HELP[name])
|
|
37
43
|
fail('unknown_command', `There is no command "${name}".`, `Commands: ${Object.keys(HELP).join(', ')}.`);
|
|
@@ -77,23 +83,74 @@ export async function plan() {
|
|
|
77
83
|
const changes = diff(state, desired);
|
|
78
84
|
return { status: changes.length ? 'changes' : 'up_to_date', changes, deletes: changes.filter((c) => c.action === 'delete').length };
|
|
79
85
|
}
|
|
86
|
+
/** How often apply reads a running import, and when it stops waiting. Env overrides are for tests. */
|
|
87
|
+
const POLL_MS = Number(process.env.C11R_APPLY_POLL_MS) || 2000;
|
|
88
|
+
const WAIT_MS = Number(process.env.C11R_APPLY_WAIT_MS) || 10 * 60 * 1000;
|
|
89
|
+
/** Errors that say nothing about the import itself: apply keeps polling through them. */
|
|
90
|
+
const TRANSIENT = new Set(['commercelayer_busy', 'commercelayer_unavailable', 'api_error', 'api_unreachable']);
|
|
91
|
+
/** The longest wait between reads while Commerce Layer answers busy (429). */
|
|
92
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
93
|
+
const jobSummary = (j) => ({ id: j.id, status: j.status, step: j.step, processed: j.processed, total: j.total });
|
|
94
|
+
/** Read the store until its apply job is no longer in progress, with a progress line per change. */
|
|
95
|
+
async function follow(api, state) {
|
|
96
|
+
const until = Date.now() + WAIT_MS;
|
|
97
|
+
let s = state;
|
|
98
|
+
let last = '';
|
|
99
|
+
let wait = POLL_MS;
|
|
100
|
+
while (s.apply?.status === 'in_progress') {
|
|
101
|
+
const line = JSON.stringify(jobSummary(s.apply));
|
|
102
|
+
if (line !== last)
|
|
103
|
+
progress({ apply: jobSummary(s.apply) });
|
|
104
|
+
last = line;
|
|
105
|
+
if (Date.now() >= until) {
|
|
106
|
+
const j = s.apply;
|
|
107
|
+
fail('apply_pending', `The apply is still running: ${j.processed} of ${j.total} items done (step ${j.step}).`, 'The apply keeps going; run `c11r status` to follow it.');
|
|
108
|
+
}
|
|
109
|
+
await new Promise((r) => setTimeout(r, Math.min(wait, Math.max(0, until - Date.now()))));
|
|
110
|
+
try {
|
|
111
|
+
s = await api.getStore();
|
|
112
|
+
wait = POLL_MS;
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// The import runs on regardless: report the error and read again, backing off while Commerce Layer is busy.
|
|
116
|
+
if (!(e instanceof CliError) || !TRANSIENT.has(e.code))
|
|
117
|
+
throw e;
|
|
118
|
+
wait = e.code === 'commercelayer_busy' ? Math.min(MAX_BACKOFF_MS, wait * 2) : POLL_MS;
|
|
119
|
+
progress({ retrying: { code: e.code, message: e.message, next_read_ms: wait } });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return s;
|
|
123
|
+
}
|
|
80
124
|
export async function apply(flags) {
|
|
81
125
|
const api = createApi(requireCredentials());
|
|
82
126
|
const desired = loadStore();
|
|
83
|
-
|
|
127
|
+
// An apply still running is finished first, so the changes are counted from what it installs.
|
|
128
|
+
let state = await api.getStore();
|
|
129
|
+
if (state.apply?.status === 'in_progress')
|
|
130
|
+
state = await follow(api, state);
|
|
84
131
|
const changes = diff(state, desired);
|
|
85
132
|
const deletes = changes.filter((c) => c.action === 'delete');
|
|
86
133
|
if (deletes.length && !flags.has('--allow-delete')) {
|
|
87
134
|
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`.');
|
|
88
135
|
}
|
|
89
|
-
const
|
|
136
|
+
const body = { applied: toApplied(desired), hash: hashStore(), name: desired.name, policies: desired.policies, ...(flags.has('--allow-delete') && { allow_delete: true }), ...(flags.has('--full') && { full: true }) };
|
|
137
|
+
const started = await api.applyStore(body);
|
|
138
|
+
const job = started.apply?.status === 'in_progress' ? started.apply : null;
|
|
139
|
+
const done = job ? await follow(api, started) : started;
|
|
140
|
+
if (job && done.apply?.id === job.id && done.apply.status === 'failed') {
|
|
141
|
+
const errors = done.apply.errors;
|
|
142
|
+
const list = errors.map((e) => (e.sku ? `${e.sku}: ${e.message}` : e.message)).join('; ');
|
|
143
|
+
const what = errors.length === 1 ? '1 product was refused' : `${errors.length} products were refused`;
|
|
144
|
+
fail('apply_failed', errors.length ? `${what}: ${list}` : 'The apply failed in Commerce Layer.', 'Fix those products in store.yml or the catalog source and run `c11r apply` again; the rest of the store is unchanged.');
|
|
145
|
+
}
|
|
90
146
|
return {
|
|
91
147
|
status: 'applied',
|
|
92
148
|
changes,
|
|
93
|
-
preview_url:
|
|
149
|
+
preview_url: done.preview_url,
|
|
94
150
|
markets: desired.markets.length,
|
|
95
151
|
products: desired.products.length,
|
|
96
152
|
payment_gateway: 'sandbox (test payments only)',
|
|
153
|
+
apply: job && done.apply ? jobSummary(done.apply) : undefined,
|
|
97
154
|
};
|
|
98
155
|
}
|
|
99
156
|
export async function catalogPull() {
|
|
@@ -208,6 +265,7 @@ export async function status() {
|
|
|
208
265
|
sales_channel_client_id: s.sales_channel_client_id,
|
|
209
266
|
storefront: s.storefront,
|
|
210
267
|
applied: Boolean(s.applied),
|
|
268
|
+
apply: s.apply ?? undefined,
|
|
211
269
|
pending_changes: pending.length > 0 || storeError !== undefined,
|
|
212
270
|
pending: pending.length ? pending : undefined,
|
|
213
271
|
store_error: storeError,
|
|
@@ -215,7 +273,7 @@ export async function status() {
|
|
|
215
273
|
products: s.applied ? Object.keys(s.applied.products).length : 0,
|
|
216
274
|
orders: s.orders.map((o) => ({ id: o.id, label: o.label })),
|
|
217
275
|
ucp_profile: `${s.preview_url}/.well-known/ucp`,
|
|
218
|
-
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,
|
|
276
|
+
next: s.apply?.status === 'in_progress' ? 'An apply is running: run `c11r status` again to follow it, or `c11r apply` to wait for it.' : 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,
|
|
219
277
|
};
|
|
220
278
|
}
|
|
221
279
|
export async function claim() {
|
package/dist/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Everything the CLI says is JSON on stdout: one object per run, pretty-printed.
|
|
2
2
|
// A failure is { error: { code, message, hint } } with exit code 1. The hint is
|
|
3
3
|
// the fix: an agent reads it and acts on it before trying anything else.
|
|
4
|
+
// A command that waits (apply) writes progress to stderr, one JSON object per line.
|
|
4
5
|
export class CliError extends Error {
|
|
5
6
|
code;
|
|
6
7
|
hint;
|
|
@@ -13,6 +14,9 @@ export class CliError extends Error {
|
|
|
13
14
|
export function fail(code, message, hint) {
|
|
14
15
|
throw new CliError(code, message, hint);
|
|
15
16
|
}
|
|
17
|
+
export function progress(data) {
|
|
18
|
+
process.stderr.write(JSON.stringify(data) + '\n');
|
|
19
|
+
}
|
|
16
20
|
export function print(data) {
|
|
17
21
|
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
18
22
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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": [
|