@commercelayer/agent-cli 1.0.0-dev.2 → 1.0.0-dev.22
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 +87 -13
- package/dist/argv.js +23 -5
- package/dist/builtins/auth.js +26 -5
- package/dist/builtins/extensions.js +336 -7
- package/dist/builtins/index.js +4 -2
- package/dist/builtins/loops.js +68 -31
- package/dist/builtins/sandbox.js +59 -23
- package/dist/builtins/shop.js +504 -0
- package/dist/builtins/skills.js +355 -0
- package/dist/config.js +39 -12
- package/dist/confirm.js +17 -6
- package/dist/engine.js +194 -28
- package/dist/errors.js +6 -0
- package/dist/help.js +36 -6
- package/dist/http.js +10 -0
- package/dist/inputs.js +21 -1
- package/dist/project.js +185 -0
- package/dist/schema.js +16 -4
- package/dist/schemas.js +82 -8
- package/dist/secrets.js +25 -7
- package/dist/storefront.js +280 -0
- package/dist/ucp.js +207 -0
- package/package.json +1 -1
package/dist/builtins/sandbox.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
// c11r sandbox create [--name] [--seed], get, reset, claim, destroy (contract
|
|
1
|
+
// c11r sandbox create [--name] [--seed [type]] [--catalogue file], get, reset, claim, destroy (contract sections 4 and 48), and
|
|
2
2
|
// c11r status (section 5). They are built in for what happens on this machine: create stores the
|
|
3
3
|
// sandbox token (mode 600, ~/.config/c11r/credentials.json) and links the folder (.c11r.json);
|
|
4
4
|
// reset moves both to the new organization; destroy forgets them. The token is printed never,
|
|
5
5
|
// not even by create: only its hint.
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
|
-
import { credentialsFile, defaultOrg,
|
|
7
|
+
import { credentialsFile, defaultOrg, forgetOutWritten, storedLogin, forgetOrg, LINK_FILE, readLink, removeLink, renameOrg, parentLinkKept, storedOrgs, storeOrg, writeLink, hintOf } from '../config.js';
|
|
8
8
|
import { CliError, fail } from '../errors.js';
|
|
9
9
|
import { apiUrl, envelope, request } from '../http.js';
|
|
10
10
|
import { parseSchema } from '../schema.js';
|
|
11
11
|
import { OUT_NOT_TRACKED, removeOrgSecrets } from '../secrets.js';
|
|
12
12
|
import { VERSION } from '../version.js';
|
|
13
|
+
import { readCatalogue, SEED_TYPES, withShop } from '../storefront.js';
|
|
13
14
|
/** The longest sandbox name the API takes (contract: 422 invalid_argument past it). */
|
|
14
15
|
const NAME_MAX = 25;
|
|
16
|
+
const SEED = {
|
|
17
|
+
name: 'seed',
|
|
18
|
+
type: 'enum',
|
|
19
|
+
values: [...SEED_TYPES],
|
|
20
|
+
bare: true,
|
|
21
|
+
description: 'Loads a demo shop of this type: coffee, tshirts or games, six products each, or custom, the person\'s own from --catalogue. Each has two markets, shipping, a manual payment setting (Wire transfer) and a storefront on, enough to place a test order at once. --seed alone is coffee.',
|
|
22
|
+
};
|
|
23
|
+
const CATALOGUE = {
|
|
24
|
+
name: 'catalogue',
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'With --seed custom: a YAML file with the shop\'s name, its products (name, description, prices in EUR and USD, code) and optionally its look. Checked here, then sent in the same call.',
|
|
27
|
+
};
|
|
15
28
|
export const sandbox = {
|
|
16
29
|
schema: parseSchema({
|
|
17
30
|
noun: 'sandbox',
|
|
@@ -28,9 +41,10 @@ export const sandbox = {
|
|
|
28
41
|
live: false,
|
|
29
42
|
body: [
|
|
30
43
|
{ name: 'name', type: 'string', description: 'The organization name; without it, an invented one. A name starting zztest marks a test organization.' },
|
|
31
|
-
|
|
44
|
+
SEED,
|
|
45
|
+
CATALOGUE,
|
|
32
46
|
],
|
|
33
|
-
effect: 'Creates a sandbox, stores its token on this machine (never printed) and links this folder to it with .c11r.json. With --seed it answers once the seed
|
|
47
|
+
effect: 'Creates a sandbox, stores its token on this machine (never printed) and links this folder to it with .c11r.json, in this folder: a link in a folder above is another project\'s and stays as it is. With --seed it answers once the seed and its storefront are in place, in about 20 seconds, with the shop link.',
|
|
34
48
|
},
|
|
35
49
|
{ verb: 'get', method: 'GET', path: '/v1/sandbox', live: false, effect: 'Returns the sandbox: organization, expiry, whether it is seeded, the token hint and the claim status.' },
|
|
36
50
|
{
|
|
@@ -39,8 +53,8 @@ export const sandbox = {
|
|
|
39
53
|
path: '/v1/sandbox/reset',
|
|
40
54
|
live: false,
|
|
41
55
|
destructive: true,
|
|
42
|
-
body: [{
|
|
43
|
-
effect: 'Replaces the sandbox with a fresh organization behind the same token, seeded as before, and retires the old one. The slug changes; this machine and .c11r.json follow it.',
|
|
56
|
+
body: [{ ...SEED, description: 'Seeds the fresh organization with this type, or not (--seed false); without it, as before. custom needs --catalogue again.' }, CATALOGUE],
|
|
57
|
+
effect: 'Replaces the sandbox with a fresh organization behind the same token, seeded as before (its storefront on again), and retires the old one. The slug changes; this machine and .c11r.json follow it.',
|
|
44
58
|
},
|
|
45
59
|
{ verb: 'claim', method: 'POST', path: '/v1/sandbox/claim', live: false, effect: 'Returns a one-time claim URL, valid 30 minutes, for the human to open: they sign in with Commerce Layer and pick a plan. The agent never opens it for them.' },
|
|
46
60
|
{
|
|
@@ -76,16 +90,18 @@ async function create(run, values) {
|
|
|
76
90
|
if (name !== undefined && (name.trim() === '' || name.length > NAME_MAX)) {
|
|
77
91
|
fail('invalid_value', `--name ${JSON.stringify(name)} is ${name.trim() === '' ? 'empty' : `${name.length} characters`}: a sandbox name has 1 to ${NAME_MAX}.`, { path: 'name', next_step: { action: 'fix_request', command: null } });
|
|
78
92
|
}
|
|
79
|
-
const seed =
|
|
93
|
+
const { seed, catalogue } = seedBody(values);
|
|
80
94
|
const test = process.env.C11R_TEST === '1' || Boolean(name?.startsWith('zztest'));
|
|
81
|
-
// The token goes only when this folder, --org or
|
|
82
|
-
// finishes a failed seed there, or answers 409 conflict naming it, rather
|
|
83
|
-
// one for the same folder. A
|
|
84
|
-
|
|
95
|
+
// The token goes only when this folder's own link, --org, C11R_ORG or C11R_TOKEN names a
|
|
96
|
+
// sandbox: the API then finishes a failed seed there, or answers 409 conflict naming it, rather
|
|
97
|
+
// than make a second one for the same folder. A link found above is another project's, and a
|
|
98
|
+
// token picked as the default or as the only one stored stays home: the create makes a new one.
|
|
99
|
+
const above = readLink();
|
|
100
|
+
const named = (run.org.source === 'link' && above?.here === true) || run.org.source === 'flag' || run.org.source === 'env' || run.org.source === 'token_env';
|
|
85
101
|
// A fresh key per create: after a 5xx or a lost connection the same create is repeated with it,
|
|
86
102
|
// and the API answers the sandbox (and token) it made, rather than leave one with a lost token.
|
|
87
103
|
const idempotencyKey = `c11r-${crypto.randomBytes(16).toString('base64url')}`;
|
|
88
|
-
const r = await request({ method: 'POST', path: '/sandboxes', body: { ...(name ? { name } : {}), seed, test }, dryRun: run.globals.dryRun, timeoutMs: seed ?
|
|
104
|
+
const r = await request({ method: 'POST', path: '/sandboxes', body: { ...(name ? { name } : {}), seed, ...(catalogue ? { catalogue } : {}), test }, dryRun: run.globals.dryRun, timeoutMs: seed ? 180_000 : 60_000, idempotencyKey }, { live: false, token: named ? run.org.token : null });
|
|
89
105
|
if (run.globals.dryRun)
|
|
90
106
|
return r;
|
|
91
107
|
const { token, ...data } = r.data ?? {};
|
|
@@ -93,15 +109,35 @@ async function create(run, values) {
|
|
|
93
109
|
fail('invalid_response', 'The API created a sandbox without returning its org and token.', { next_step: { action: 'none' } });
|
|
94
110
|
const hint = typeof data.token_hint === 'string' ? data.token_hint : hintOf(token);
|
|
95
111
|
storeOrg(data.org, { token, token_hint: hint, expires_at: data.expires_at ?? null, created_at: new Date().toISOString(), api_url: apiUrl() });
|
|
96
|
-
const link = writeLink(data.org);
|
|
112
|
+
const link = writeLink(data.org, { here: true });
|
|
97
113
|
// The API says to store the token: the CLI has, and printed none of it.
|
|
98
114
|
const warnings = r.warnings.filter((w) => w.code !== 'token_shown_once');
|
|
115
|
+
if (above && !above.here)
|
|
116
|
+
warnings.push(parentLinkKept(above));
|
|
99
117
|
if (link.previous)
|
|
100
118
|
warnings.push({ code: 'relinked', message: `${link.file} named ${link.previous}; it now names ${data.org}. The token of ${link.previous} stays stored.` });
|
|
101
|
-
return { ...r, data: { ...data, token_hint: hint, credentials: credentialsFile(), linked: link.file }, warnings };
|
|
119
|
+
return withShop({ ...r, data: { ...data, token_hint: hint, credentials: credentialsFile(), linked: link.file }, warnings }, data.store);
|
|
120
|
+
}
|
|
121
|
+
/** --seed and --catalogue as the API takes them: custom only with a catalogue, a catalogue only with custom. */
|
|
122
|
+
function seedBody(values) {
|
|
123
|
+
const seed = values.seed ?? false;
|
|
124
|
+
const file = values.catalogue;
|
|
125
|
+
if (seed === 'custom' && file === undefined) {
|
|
126
|
+
fail('missing_value', '--seed custom needs --catalogue: a YAML file with the shop\'s name and its products. Write it from what the human described, show it to them, then run the command with --catalogue <file>.', { path: 'catalogue', next_step: { action: 'fix_request', command: null } });
|
|
127
|
+
}
|
|
128
|
+
if (file !== undefined && seed !== 'custom')
|
|
129
|
+
fail('invalid_value', '--catalogue goes only with --seed custom: the other types bring their own products.', { path: 'catalogue', next_step: { action: 'fix_request', command: null } });
|
|
130
|
+
return { seed, catalogue: file === undefined ? undefined : readCatalogue(file) };
|
|
102
131
|
}
|
|
103
132
|
async function reset(run, values) {
|
|
104
|
-
const
|
|
133
|
+
const body = {};
|
|
134
|
+
if (values.seed !== undefined || values.catalogue !== undefined) {
|
|
135
|
+
const { seed, catalogue } = seedBody(values);
|
|
136
|
+
body.seed = seed;
|
|
137
|
+
if (catalogue)
|
|
138
|
+
body.catalogue = catalogue;
|
|
139
|
+
}
|
|
140
|
+
const r = await run.send({ method: 'POST', path: '/sandbox/reset', body });
|
|
105
141
|
if (run.globals.dryRun)
|
|
106
142
|
return r;
|
|
107
143
|
const { org, previous_org: previous } = r.data ?? {};
|
|
@@ -112,7 +148,7 @@ async function reset(run, values) {
|
|
|
112
148
|
if (link?.org === before)
|
|
113
149
|
writeLink(org);
|
|
114
150
|
}
|
|
115
|
-
return r;
|
|
151
|
+
return withShop(r, r.data?.store);
|
|
116
152
|
}
|
|
117
153
|
async function destroy(run) {
|
|
118
154
|
let r;
|
|
@@ -124,8 +160,7 @@ async function destroy(run) {
|
|
|
124
160
|
// here. The sandbox is gone either way, so this machine forgets it too.
|
|
125
161
|
if (!(e instanceof CliError && e.code === 'unauthorized') || run.globals.dryRun || !run.org.org)
|
|
126
162
|
throw e;
|
|
127
|
-
const out =
|
|
128
|
-
markOutWritten(run.org.org, false);
|
|
163
|
+
const out = forgetOutWritten(run.org.org);
|
|
129
164
|
forgetOrg(run.org.org);
|
|
130
165
|
const link = removeLink(run.org.org);
|
|
131
166
|
const secrets = removeOrgSecrets(run.org.org);
|
|
@@ -136,8 +171,7 @@ async function destroy(run) {
|
|
|
136
171
|
const org = r.data?.org ?? run.org.org;
|
|
137
172
|
if (!org)
|
|
138
173
|
return r;
|
|
139
|
-
const out =
|
|
140
|
-
markOutWritten(org, false);
|
|
174
|
+
const out = forgetOutWritten(org);
|
|
141
175
|
forgetOrg(org);
|
|
142
176
|
const link = removeLink(org);
|
|
143
177
|
const secrets = removeOrgSecrets(org);
|
|
@@ -148,12 +182,14 @@ export const status = {
|
|
|
148
182
|
noun: 'status',
|
|
149
183
|
kind: 'task',
|
|
150
184
|
description: 'c11r status: where this machine and folder stand. The organization picked and from where, its lifecycle state, plan, mode, limits, usage and what is waiting.',
|
|
151
|
-
verbs: [{ verb: 'get', method: 'GET', path: '/v1/status', state: 'none', effect: 'Returns the status. c11r status is the same as c11r status get.' }],
|
|
185
|
+
verbs: [{ verb: 'get', method: 'GET', path: '/v1/status', state: 'none', effect: 'Returns the status. c11r status is the same as c11r status get. With --live --yes it sends confirm=true: on an organization connected to c11r, the first live call asks for it once, before the live credential is made.' }],
|
|
152
186
|
}),
|
|
153
187
|
defaultVerb: 'get',
|
|
154
188
|
async run(run) {
|
|
155
189
|
// Status works at every state: an unresolved organization is shown, not raised.
|
|
156
|
-
|
|
190
|
+
// In live, --yes is the human's yes to the live credential of a connected organization
|
|
191
|
+
// (contract 49.4): only this read, confirmed, makes it, never a write's --yes.
|
|
192
|
+
const r = await run.send({ method: 'GET', path: '/status', ...(run.globals.live && run.globals.yes && !run.globals.dryRun ? { confirm: true } : {}) });
|
|
157
193
|
const link = (() => {
|
|
158
194
|
try {
|
|
159
195
|
return readLink();
|
|
@@ -168,7 +204,7 @@ export const status = {
|
|
|
168
204
|
api_url: apiUrl(),
|
|
169
205
|
org: run.org.org ? { slug: run.org.org, from: run.org.source } : null,
|
|
170
206
|
...(run.org.problem ? { org_problem: { code: run.org.problem.code, message: run.org.problem.message } } : {}),
|
|
171
|
-
link: link ? { file: link.file, org: link.org } : { file: null, name: LINK_FILE },
|
|
207
|
+
link: link ? { file: link.file, org: link.org, here: link.here } : { file: null, name: LINK_FILE, here: false },
|
|
172
208
|
credentials: credentialsFile(),
|
|
173
209
|
stored,
|
|
174
210
|
login: (() => {
|