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

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/dist/secrets.js CHANGED
@@ -14,6 +14,8 @@ import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import { markOutWritten } from './config.js';
16
16
  import { fail } from './errors.js';
17
+ /** Where the record's id goes in a file named after it, once the answer gives the id. */
18
+ const ID_SLOT = '<id>';
17
19
  export function secretsDir() {
18
20
  const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
19
21
  return path.join(base, 'c11r', 'secrets');
@@ -23,10 +25,14 @@ const safe = (s) => s.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|
23
25
  export function planSecret(opts) {
24
26
  const out = opts.values.out;
25
27
  const format = opts.values.format ?? null;
26
- const base = opts.verb === 'token' ? `${safe(opts.id ?? 'credential')}-token` : safe(String(opts.values.name ?? opts.id ?? 'credential'));
28
+ // By the name given, else the id given; a record with neither (a gift card) by the id it is
29
+ // given, so each has a file of its own: gift-card-<id>.env.
30
+ const named = opts.values.name ?? opts.id;
31
+ const byRecord = !out && opts.verb !== 'token' && (named === undefined || named === null || named === '');
32
+ const base = opts.verb === 'token' ? `${safe(opts.id ?? 'credential')}-token` : byRecord ? `${safe(opts.noun.replace(/s$/, ''))}-${ID_SLOT}` : safe(String(named));
27
33
  const file = out ? path.resolve(out) : path.join(secretsDir(), safe(opts.org ?? 'default'), base);
28
- const plan = { file: out || !format ? file : `${file}.${format}`, format, fixed: Boolean(out), overwrite: opts.yes };
29
- const candidates = plan.fixed || format ? [plan.file] : [`${file}.env`, `${file}.json`];
34
+ const plan = { file: out || !format ? file : `${file}.${format}`, format, fixed: Boolean(out), overwrite: opts.yes, byRecord };
35
+ const candidates = byRecord ? [] : plan.fixed || format ? [plan.file] : [`${file}.env`, `${file}.json`];
30
36
  for (const c of candidates) {
31
37
  let st = null;
32
38
  try {
@@ -43,7 +49,7 @@ export function planSecret(opts) {
43
49
  fail('conflict', `${c} exists. c11r replaces it only with --yes, or writes the secret elsewhere with --out.`, { path: 'out', next_step: { action: 'fix_request', command: null }, details: { file: c } }, 'rejected');
44
50
  }
45
51
  }
46
- const dir = path.dirname(candidates[0]);
52
+ const dir = path.dirname(plan.file);
47
53
  const existing = nearestExisting(dir);
48
54
  try {
49
55
  fs.accessSync(existing, fs.constants.W_OK);
@@ -74,7 +80,7 @@ export function writeSecret(r, plan, ctx) {
74
80
  const credentialId = str(rec.id) ?? ctx.id;
75
81
  const record = { noun: ctx.noun, id: credentialId ?? null, name: str(rec.name), code: str(rec.code) };
76
82
  const format = plan.format ?? (secret.format === 'json' ? 'json' : 'env');
77
- const file = plan.fixed || plan.format ? plan.file : `${plan.file}.${format}`;
83
+ const file = byRecordFile(plan.fixed || plan.format ? plan.file : `${plan.file}.${format}`, plan, credentialId);
78
84
  const env = secret.env && Object.keys(secret.env).length ? { ...secret.env } : { [`CL_${(secret.kind ?? 'secret').toUpperCase()}`]: String(secret.value ?? '') };
79
85
  // The endpoint goes with every secret, so the file is enough to call Commerce Layer.
80
86
  if (!env.CL_BASE_ENDPOINT && typeof data.base_endpoint === 'string')
@@ -97,9 +103,9 @@ export function writeSecret(r, plan, ctx) {
97
103
  const created = ctx.verb === 'create';
98
104
  fail('secret_not_written', `The API answered the ${secret.kind ?? 'secret'} of credential ${id} once, and c11r could not write it to ${file}: ${e.message}. It was not printed, and the API cannot show it again. ${created ? `c11r credentials token ${id} gives a working token now; for a new secret, create another credential and delete ${id}.` : `c11r credentials token ${id} gives another token.`}`, { path: 'out', next_step: { action: 'fix_request', command: `c11r credentials token ${id}` }, details: { credential_id: credentialId ?? null, file } });
99
105
  }
100
- // An --out file is not removed with its credential: remember that one exists, for the warning.
106
+ // An --out file is not removed with its record: remember which record has one, for the warning.
101
107
  if (plan.fixed && ctx.org)
102
- markOutWritten(ctx.org, true);
108
+ markOutWritten(ctx.org, { noun: ctx.noun, keys: [record.id, record.name, record.code, ctx.id].filter((k) => typeof k === 'string' && k !== '') });
103
109
  // The API says the secret is shown once: c11r kept it in the file, and printed none of it.
104
110
  const warnings = r.warnings.filter((w) => w.code !== 'secret_shown_once' && w.code !== 'token_shown_once');
105
111
  if (!ignoredByGit(file))
@@ -111,6 +117,18 @@ export function writeSecret(r, plan, ctx) {
111
117
  warnings,
112
118
  };
113
119
  }
120
+ /** A file named after its record: the id in its slot, and a file there already left alone. */
121
+ function byRecordFile(file, plan, id) {
122
+ if (!plan.byRecord)
123
+ return file;
124
+ // The id as the API gives it, case kept: ids are case-sensitive (lqgwUkXlBw).
125
+ const slot = (id ?? crypto.randomBytes(4).toString('hex')).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '') || crypto.randomBytes(4).toString('hex');
126
+ const named = file.replace(ID_SLOT, slot);
127
+ if (plan.overwrite || !fs.existsSync(named))
128
+ return named;
129
+ const ext = path.extname(named);
130
+ return `${named.slice(0, -ext.length || undefined)}-${crypto.randomBytes(3).toString('hex')}${ext}`;
131
+ }
114
132
  /** What a dry run says about the file: where it would go and whether one is there. */
115
133
  export function describePlan(plan) {
116
134
  const files = plan.fixed || plan.format ? [plan.file] : [`${plan.file}.env`, `${plan.file}.json`];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercelayer/agent-cli",
3
- "version": "1.0.0-dev.1",
3
+ "version": "1.0.0-dev.11",
4
4
  "description": "c11r: the Commerce Layer CLI for coding agents. One grammar, c11r <noun> <verb>, from a sandbox to a running organization.",
5
5
  "homepage": "https://commercelayer.ai",
6
6
  "keywords": [