@voltade/volty 0.1.18 → 0.1.20
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/volty.js +172 -75
- package/package.json +1 -1
- package/skill/volty-cli-ops/SKILL.md +1 -0
package/dist/volty.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/volty.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
import { join as join4 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { tmpdir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
@@ -41,51 +42,36 @@ async function apiFetch(ctx, path, init) {
|
|
|
41
42
|
} catch {}
|
|
42
43
|
return { ok: res.ok, status: res.status, body };
|
|
43
44
|
}
|
|
44
|
-
var
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
var keyTag = (ctx) => createHash("sha256").update(ctx.apiKey ?? "").digest("hex").slice(0, 12);
|
|
46
|
+
var cachePath = (env, ctx) => join(tmpdir(), `volty-catalog-${env}-${targetOrg ?? "default"}-${keyTag(ctx)}.json`);
|
|
47
|
+
function refusalSuffix(res, env) {
|
|
48
|
+
if (res.body.error?.message)
|
|
49
|
+
return `: ${res.body.error.message}`;
|
|
50
|
+
return res.status === 401 ? `: this key is not valid any more, run \`volty auth login --env ${env}\`` : "";
|
|
47
51
|
}
|
|
48
52
|
async function fetchCatalog(ctx, env, useCache = true) {
|
|
49
|
-
const path = cachePath(env);
|
|
53
|
+
const path = cachePath(env, ctx);
|
|
50
54
|
if (useCache && existsSync(path)) {
|
|
51
55
|
try {
|
|
52
56
|
const cached = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
-
if (Date.now() - cached.at < CATALOG_TTL_MS && cached.renamed && cached.inApp)
|
|
57
|
+
if (Date.now() - cached.at < CATALOG_TTL_MS && cached.renamed && cached.inApp && cached.outOfScope)
|
|
54
58
|
return cached;
|
|
55
59
|
} catch {}
|
|
56
60
|
}
|
|
57
61
|
const res = await apiFetch(ctx, "/api/v/_catalog");
|
|
58
62
|
if (!res.ok || !res.body.catalog) {
|
|
59
|
-
throw new Error(`could not fetch the operation catalog (HTTP ${res.status})${res
|
|
63
|
+
throw new Error(`could not fetch the operation catalog (HTTP ${res.status})${refusalSuffix(res, env)}`);
|
|
60
64
|
}
|
|
61
65
|
const ops = res.body.catalog;
|
|
62
66
|
const retired = res.body.retired ?? {};
|
|
63
67
|
const renamed = res.body.renamed ?? {};
|
|
64
68
|
const inApp = res.body.inApp ?? {};
|
|
69
|
+
const outOfScope = res.body.outOfScope ?? {};
|
|
65
70
|
try {
|
|
66
71
|
mkdirSync(tmpdir(), { recursive: true });
|
|
67
|
-
writeFileSync(path, JSON.stringify({ at: Date.now(), ops, retired, renamed, inApp }));
|
|
72
|
+
writeFileSync(path, JSON.stringify({ at: Date.now(), ops, retired, renamed, inApp, outOfScope }));
|
|
68
73
|
} catch {}
|
|
69
|
-
return { ops, retired, renamed, inApp };
|
|
70
|
-
}
|
|
71
|
-
async function fetchOrgs(ctx, env, useCache = true) {
|
|
72
|
-
const path = join(tmpdir(), `volty-orgs-${env}.json`);
|
|
73
|
-
if (useCache && existsSync(path)) {
|
|
74
|
-
try {
|
|
75
|
-
const cached = JSON.parse(readFileSync(path, "utf8"));
|
|
76
|
-
if (Date.now() - cached.at < CATALOG_TTL_MS)
|
|
77
|
-
return cached.orgs;
|
|
78
|
-
} catch {}
|
|
79
|
-
}
|
|
80
|
-
const res = await apiFetch(ctx, "/api/cli/orgs");
|
|
81
|
-
if (!res.ok || !res.body.orgs) {
|
|
82
|
-
throw new Error(`could not fetch your org memberships (HTTP ${res.status})${res.status === 401 ? staleKeyHint(env) : ""}`);
|
|
83
|
-
}
|
|
84
|
-
const orgs = res.body.orgs;
|
|
85
|
-
try {
|
|
86
|
-
writeFileSync(path, JSON.stringify({ at: Date.now(), orgs }));
|
|
87
|
-
} catch {}
|
|
88
|
-
return orgs;
|
|
74
|
+
return { ops, retired, renamed, inApp, outOfScope };
|
|
89
75
|
}
|
|
90
76
|
function pathForOp(name) {
|
|
91
77
|
return `/api/v/${name.trim().split(/\s+/).join("/")}`;
|
|
@@ -166,16 +152,59 @@ function ensureContext(config, env) {
|
|
|
166
152
|
return ctx;
|
|
167
153
|
}
|
|
168
154
|
|
|
155
|
+
// src/org-keys.ts
|
|
156
|
+
function storedKeyFor(ctx, org, now = Date.now()) {
|
|
157
|
+
if (!ctx.apiKey)
|
|
158
|
+
return null;
|
|
159
|
+
if (!org || org === ctx.organizationId || org === ctx.organizationSlug) {
|
|
160
|
+
return { apiKey: ctx.apiKey, organizationId: ctx.organizationId };
|
|
161
|
+
}
|
|
162
|
+
for (const [organizationId, k] of Object.entries(ctx.orgKeys ?? {})) {
|
|
163
|
+
if (organizationId !== org && k.slug !== org)
|
|
164
|
+
continue;
|
|
165
|
+
if (k.expiresAt && Date.parse(k.expiresAt) <= now)
|
|
166
|
+
return null;
|
|
167
|
+
return { apiKey: k.apiKey, organizationId };
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
async function ensureOrgKey(ctx, org, onMint) {
|
|
172
|
+
const stored = storedKeyFor(ctx, org);
|
|
173
|
+
if (stored)
|
|
174
|
+
return { ...stored, minted: false };
|
|
175
|
+
setOrgOverride(undefined);
|
|
176
|
+
const res = await apiFetch(ctx, "/api/cli/keys", { method: "POST", body: { org } });
|
|
177
|
+
if (!res.ok || !res.body.apiKey || !res.body.organizationId) {
|
|
178
|
+
throw new Error(res.body.error?.message ?? `could not get a key for '${org}' (HTTP ${res.status})`);
|
|
179
|
+
}
|
|
180
|
+
const entry = { apiKey: res.body.apiKey, slug: res.body.slug ?? org, expiresAt: res.body.expiresAt };
|
|
181
|
+
ctx.orgKeys = { ...ctx.orgKeys, [res.body.organizationId]: entry };
|
|
182
|
+
onMint(`Minted a key for ${entry.slug} (expires ${entry.expiresAt?.slice(0, 10) ?? "never"})`);
|
|
183
|
+
return { apiKey: entry.apiKey, organizationId: res.body.organizationId, minted: true };
|
|
184
|
+
}
|
|
185
|
+
async function dropOrgKeys(ctx) {
|
|
186
|
+
await Promise.all(Object.values(ctx.orgKeys ?? {}).map(async (k) => {
|
|
187
|
+
const res = await apiFetch({ ...ctx, apiKey: k.apiKey }, "/api/cli/logout", { method: "POST", body: {} }).catch(() => null);
|
|
188
|
+
if (!res || !res.ok && res.status !== 401) {
|
|
189
|
+
process.stderr.write(`Could not revoke the key for ${k.slug}; \`volty keys revoke cli --org ${k.slug}\` removes it.
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
}));
|
|
193
|
+
ctx.orgKeys = undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
// src/auth.ts
|
|
170
197
|
async function whoami(ctx) {
|
|
171
198
|
if (!ctx.apiKey)
|
|
172
|
-
return
|
|
199
|
+
return { error: "not logged in" };
|
|
173
200
|
const res = await apiFetch(ctx, "/api/cli/whoami");
|
|
174
|
-
if (!res.ok || !res.body.organizationId)
|
|
175
|
-
return
|
|
201
|
+
if (!res.ok || !res.body.organizationId) {
|
|
202
|
+
return { error: res.body?.error?.message ?? "the stored key did not validate, run `volty auth login --rotate`" };
|
|
203
|
+
}
|
|
176
204
|
ctx.organizationId = res.body.organizationId;
|
|
205
|
+
ctx.organizationSlug = res.body.organizationSlug ?? undefined;
|
|
177
206
|
ctx.principal = res.body.email ?? res.body.name ?? res.body.userId;
|
|
178
|
-
return res.body;
|
|
207
|
+
return { who: res.body };
|
|
179
208
|
}
|
|
180
209
|
function openBrowser(url) {
|
|
181
210
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
|
|
@@ -233,13 +262,13 @@ async function login(config, env, opts) {
|
|
|
233
262
|
ctx.baseUrl = opts.baseUrl;
|
|
234
263
|
if (!ctx.baseUrl)
|
|
235
264
|
throw new Error(`no base URL for '${env}' — pass --base-url <url>`);
|
|
236
|
-
if (!opts.rotate && await whoami(ctx)) {
|
|
265
|
+
if (!opts.rotate && "who" in await whoami(ctx)) {
|
|
237
266
|
return `Already authenticated to '${env}' as ${ctx.principal ?? "(unknown)"} — reusing the stored key.`;
|
|
238
267
|
}
|
|
268
|
+
await dropOrgKeys(ctx);
|
|
239
269
|
if (opts.token) {
|
|
240
270
|
ctx.apiKey = opts.token;
|
|
241
|
-
|
|
242
|
-
if (!who)
|
|
271
|
+
if ("error" in await whoami(ctx))
|
|
243
272
|
throw new Error("the provided --token did not validate");
|
|
244
273
|
return `Authenticated to '${env}' as ${ctx.principal ?? "(unknown)"} (org ${ctx.organizationId}).`;
|
|
245
274
|
}
|
|
@@ -247,28 +276,29 @@ async function login(config, env, opts) {
|
|
|
247
276
|
const key = await devGrant(ctx, opts.devPersona, opts.org);
|
|
248
277
|
if (key) {
|
|
249
278
|
ctx.apiKey = key;
|
|
250
|
-
|
|
251
|
-
return `Authenticated to '${env}' as ${
|
|
279
|
+
await whoami(ctx);
|
|
280
|
+
return `Authenticated to '${env}' as ${ctx.principal ?? ctx.organizationId ?? "(unknown)"} (dev persona ${opts.devPersona}).`;
|
|
252
281
|
}
|
|
253
282
|
process.stdout.write(`The '${env}' server is not in development mode, so --as has no dev grant to use. Falling back to the browser grant.
|
|
254
283
|
`);
|
|
255
284
|
}
|
|
256
285
|
ctx.apiKey = await deviceGrant(ctx);
|
|
257
|
-
|
|
258
|
-
return `Authenticated to '${env}' as ${
|
|
286
|
+
await whoami(ctx);
|
|
287
|
+
return `Authenticated to '${env}' as ${ctx.principal ?? ctx.organizationId ?? "(unknown)"}.`;
|
|
259
288
|
}
|
|
260
289
|
async function logout(config, env) {
|
|
261
290
|
const ctx = config.contexts[env];
|
|
262
291
|
if (!ctx?.apiKey)
|
|
263
292
|
return `No stored key for '${env}'.`;
|
|
264
293
|
await apiFetch(ctx, "/api/cli/logout", { method: "POST", body: {} });
|
|
294
|
+
await dropOrgKeys(ctx);
|
|
265
295
|
ctx.apiKey = undefined;
|
|
266
296
|
ctx.principal = undefined;
|
|
267
297
|
return `Logged out of '${env}' (key revoked).`;
|
|
268
298
|
}
|
|
269
299
|
|
|
270
300
|
// src/dispatch.ts
|
|
271
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
301
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
272
302
|
|
|
273
303
|
// ../shared/ops/flag-parse.ts
|
|
274
304
|
function acceptsNull(prop) {
|
|
@@ -386,6 +416,9 @@ function missMessage(catalog, argv) {
|
|
|
386
416
|
const inApp = retiredPrefix(catalog.inApp, argv);
|
|
387
417
|
if (inApp)
|
|
388
418
|
return `\`${inApp.name}\` is not run from here. ${inApp.replacement}`;
|
|
419
|
+
const outOfScope = retiredPrefix(catalog.outOfScope, argv);
|
|
420
|
+
if (outOfScope)
|
|
421
|
+
return outOfScope.replacement;
|
|
389
422
|
const retired = retiredPrefix(catalog.retired, argv);
|
|
390
423
|
if (retired)
|
|
391
424
|
return `\`${retired.name}\` is retired: ${retired.replacement}`;
|
|
@@ -406,12 +439,29 @@ function parseFlags(op, rest) {
|
|
|
406
439
|
const parsed = parseFlagTokens(props, rest, CliUsageError);
|
|
407
440
|
for (const [key, value] of Object.entries(parsed.input)) {
|
|
408
441
|
const prop = props[key];
|
|
409
|
-
if (typeof value
|
|
442
|
+
if (typeof value !== "string" || !value.startsWith("@") || !prop)
|
|
443
|
+
continue;
|
|
444
|
+
if (isBase64(prop))
|
|
410
445
|
parsed.input[key] = readBase64File(key, value.slice(1));
|
|
411
|
-
|
|
446
|
+
else if (isText(prop))
|
|
447
|
+
parsed.input[key] = readTextFlag(key, value.slice(1)) ?? value;
|
|
412
448
|
}
|
|
413
449
|
return parsed;
|
|
414
450
|
}
|
|
451
|
+
function isText(prop) {
|
|
452
|
+
return [prop, ...prop.anyOf ?? []].some((branch) => branch.type === "string" && !branch.enum);
|
|
453
|
+
}
|
|
454
|
+
function readTextFlag(flag, path) {
|
|
455
|
+
if (path === "-")
|
|
456
|
+
return readFileSync3(0, "utf8");
|
|
457
|
+
if (!existsSync3(path))
|
|
458
|
+
return null;
|
|
459
|
+
try {
|
|
460
|
+
return readFileSync3(path, "utf8");
|
|
461
|
+
} catch (err) {
|
|
462
|
+
throw new CliUsageError(`--${flag}: cannot read ${path} (${err.code ?? "error"})`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
415
465
|
function isBase64(prop) {
|
|
416
466
|
return [prop, ...prop.anyOf ?? []].some((branch) => branch.contentEncoding === "base64");
|
|
417
467
|
}
|
|
@@ -478,14 +528,19 @@ function opUsage(op) {
|
|
|
478
528
|
}
|
|
479
529
|
return parts.join(" ");
|
|
480
530
|
}
|
|
481
|
-
function renderCatalog(catalog) {
|
|
482
|
-
const
|
|
531
|
+
function renderCatalog(catalog, group) {
|
|
532
|
+
const shown = group ? catalog.filter((op) => op.name === group || op.name.startsWith(`${group} `)) : catalog;
|
|
533
|
+
if (group && shown.length === 0) {
|
|
534
|
+
const groups = [...new Set(catalog.map((op) => op.name.split(" ")[0]))].toSorted();
|
|
535
|
+
return `No operations in '${group}'. Groups: ${groups.join(", ")}`;
|
|
536
|
+
}
|
|
537
|
+
const lines = shown.toSorted((a, b) => a.name.localeCompare(b.name)).map((op) => ` ${op.name} — ${op.description}`);
|
|
483
538
|
return [
|
|
484
539
|
"Run a Volty platform operation: volty <operation> [--flags] [--json]",
|
|
485
540
|
"",
|
|
486
541
|
...lines,
|
|
487
542
|
"",
|
|
488
|
-
"`volty <operation> --help` shows usage; `--json` returns the machine-readable result."
|
|
543
|
+
"`volty help <group>` lists one group (`volty help chat-flow`); `volty <operation> --help` shows usage; `--json` returns the machine-readable result. A text flag takes a file as `--source=@flow.ts`, or stdin as `--source=@-`."
|
|
489
544
|
].join(`
|
|
490
545
|
`);
|
|
491
546
|
}
|
|
@@ -517,6 +572,35 @@ function renderResult(body, json) {
|
|
|
517
572
|
return text || "(no output)";
|
|
518
573
|
}
|
|
519
574
|
|
|
575
|
+
// src/keys.ts
|
|
576
|
+
var VALUE_FLAGS = new Set(["name", "for", "for-user-id", "expires", "expires-in-days", "families"]);
|
|
577
|
+
function keysArgv(argv) {
|
|
578
|
+
const out = argv.slice(0, 2);
|
|
579
|
+
const rest = argv.slice(2);
|
|
580
|
+
for (let i = 0;i < rest.length; i++) {
|
|
581
|
+
const token = rest[i];
|
|
582
|
+
if (!token.startsWith("--")) {
|
|
583
|
+
out.push("--name", token);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const eq = token.indexOf("=");
|
|
587
|
+
const flag = eq > 0 ? token.slice(2, eq) : token.slice(2);
|
|
588
|
+
const value = eq > 0 ? token.slice(eq + 1) : VALUE_FLAGS.has(flag) ? rest[++i] : undefined;
|
|
589
|
+
if (value === undefined)
|
|
590
|
+
out.push(token);
|
|
591
|
+
else if (flag === "for")
|
|
592
|
+
out.push("--for-user-id", value);
|
|
593
|
+
else if (flag === "expires")
|
|
594
|
+
out.push("--expires-in-days", value.replace(/d$/, ""));
|
|
595
|
+
else if (flag === "families")
|
|
596
|
+
for (const f of value.split(","))
|
|
597
|
+
out.push("--families", f.trim());
|
|
598
|
+
else
|
|
599
|
+
out.push(`--${flag}`, value);
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
|
|
520
604
|
// src/org-guard.ts
|
|
521
605
|
var ORG_NATIVE = new Set(["use", "current", "list"]);
|
|
522
606
|
function isOrgFreeCommand(command) {
|
|
@@ -553,12 +637,6 @@ function resolveTargetOrg({ env, baseUrl, override, sticky, command }) {
|
|
|
553
637
|
return { org: undefined };
|
|
554
638
|
return { refusal: PROD_ORG_REFUSAL };
|
|
555
639
|
}
|
|
556
|
-
function describeOrgTarget(orgs, org, env) {
|
|
557
|
-
const match = orgs.find((o) => o.organizationId === org);
|
|
558
|
-
if (!match)
|
|
559
|
-
return { refusal: `org ${org} is not one of your memberships on ${env}` };
|
|
560
|
-
return { line: `→ ${env} · ${match.name} (${match.organizationId}) · ${match.role}` };
|
|
561
|
-
}
|
|
562
640
|
|
|
563
641
|
// src/proposals.ts
|
|
564
642
|
function decideProposal(ctx, proposalId, decision, reason) {
|
|
@@ -690,11 +768,11 @@ async function handleAuth(rest, config, env, orgOverride) {
|
|
|
690
768
|
const ctx = config.contexts[env];
|
|
691
769
|
if (!ctx?.apiKey)
|
|
692
770
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
693
|
-
const
|
|
694
|
-
if (
|
|
695
|
-
fail(
|
|
771
|
+
const res = await whoami(ctx);
|
|
772
|
+
if ("error" in res)
|
|
773
|
+
fail(res.error);
|
|
696
774
|
saveConfig(config);
|
|
697
|
-
process.stdout.write(`${who.email ?? who.userId} · org ${who.organizationId} · role ${who.role}
|
|
775
|
+
process.stdout.write(`${res.who.email ?? res.who.userId} · org ${res.who.organizationId} · role ${res.who.role}
|
|
698
776
|
`);
|
|
699
777
|
return;
|
|
700
778
|
}
|
|
@@ -778,23 +856,27 @@ async function handleOrg(rest, config, env, target) {
|
|
|
778
856
|
const json = takeBool(rest.slice(1), "--json");
|
|
779
857
|
if (!ctx.apiKey)
|
|
780
858
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
859
|
+
const orgs = [
|
|
860
|
+
{ organizationId: ctx.organizationId ?? "", slug: ctx.organizationSlug ?? "", expiresAt: null },
|
|
861
|
+
...Object.entries(ctx.orgKeys ?? {}).map(([organizationId, k]) => ({
|
|
862
|
+
organizationId,
|
|
863
|
+
slug: k.slug,
|
|
864
|
+
expiresAt: k.expiresAt ?? null
|
|
865
|
+
}))
|
|
866
|
+
];
|
|
784
867
|
if (json.present) {
|
|
785
|
-
process.stdout.write(`${JSON.stringify(
|
|
868
|
+
process.stdout.write(`${JSON.stringify({ orgs }, null, 2)}
|
|
786
869
|
`);
|
|
787
870
|
return;
|
|
788
871
|
}
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
for (const o of res.body.orgs) {
|
|
794
|
-
const marker = o.organizationId === active ? "*" : " ";
|
|
795
|
-
process.stdout.write(`${marker} ${o.organizationId} ${o.role} ${o.name}
|
|
872
|
+
for (const [i, o] of orgs.entries()) {
|
|
873
|
+
const active = target.org ? target.org === o.organizationId || target.org === o.slug : i === 0;
|
|
874
|
+
const note = i === 0 ? "signed in" : `expires ${o.expiresAt?.slice(0, 10) ?? "never"}`;
|
|
875
|
+
process.stdout.write(`${active ? "*" : " "} ${o.organizationId} ${o.slug} ${note}
|
|
796
876
|
`);
|
|
797
877
|
}
|
|
878
|
+
process.stdout.write(`Another workspace you belong to: pass --org <slug> and the CLI gets a key for it.
|
|
879
|
+
`);
|
|
798
880
|
return;
|
|
799
881
|
}
|
|
800
882
|
fail("usage: volty org <use|current|list>");
|
|
@@ -837,7 +919,7 @@ function handleSkill(rest) {
|
|
|
837
919
|
join4(import.meta.dirname, "..", "skill", "volty-cli-ops", "SKILL.md"),
|
|
838
920
|
join4(import.meta.dirname, "..", "..", ".claude", "skills", "volty-cli-ops", "SKILL.md")
|
|
839
921
|
];
|
|
840
|
-
const source = candidates.find((p) =>
|
|
922
|
+
const source = candidates.find((p) => existsSync4(p));
|
|
841
923
|
if (!source)
|
|
842
924
|
fail("bundled SKILL.md not found — reinstall the @voltade/volty package");
|
|
843
925
|
const destDir = join4(homedir3(), ".claude", "skills", "volty-cli-ops");
|
|
@@ -867,7 +949,6 @@ async function main() {
|
|
|
867
949
|
});
|
|
868
950
|
if ("refusal" in target)
|
|
869
951
|
fail(target.refusal);
|
|
870
|
-
setOrgOverride(target.org);
|
|
871
952
|
const [cmd, ...rest] = argv;
|
|
872
953
|
if (cmd === "auth")
|
|
873
954
|
return handleAuth(rest, config, env, orgExtract.value ?? (process.env.VOLTY_ORG || undefined));
|
|
@@ -879,21 +960,37 @@ async function main() {
|
|
|
879
960
|
return handleDocs(config, env);
|
|
880
961
|
if (cmd === "skill")
|
|
881
962
|
return handleSkill(rest);
|
|
882
|
-
const
|
|
883
|
-
if (!
|
|
963
|
+
const login = config.contexts[env];
|
|
964
|
+
if (!login?.apiKey)
|
|
884
965
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
966
|
+
let ctx = login;
|
|
967
|
+
if (target.org) {
|
|
968
|
+
let selected;
|
|
969
|
+
try {
|
|
970
|
+
selected = await ensureOrgKey(login, target.org, (line) => process.stderr.write(`${line}
|
|
971
|
+
`));
|
|
972
|
+
} catch (err) {
|
|
973
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
974
|
+
}
|
|
975
|
+
if (selected.minted)
|
|
976
|
+
saveConfig(config);
|
|
977
|
+
ctx = { ...login, apiKey: selected.apiKey, organizationId: selected.organizationId };
|
|
978
|
+
setOrgOverride(selected.organizationId);
|
|
979
|
+
}
|
|
885
980
|
if (prod && !isOrgFreeCommand(argv) && target.org) {
|
|
886
|
-
const
|
|
887
|
-
if ("
|
|
888
|
-
fail(
|
|
889
|
-
process.stderr.write(
|
|
981
|
+
const res = await whoami(ctx);
|
|
982
|
+
if ("error" in res)
|
|
983
|
+
fail(res.error);
|
|
984
|
+
process.stderr.write(`→ ${env} · ${res.who.organizationName} (${res.who.organizationId}) · ${res.who.role}
|
|
890
985
|
`);
|
|
891
986
|
}
|
|
892
987
|
if (cmd === "changes" && rest[0] === "decide")
|
|
893
988
|
return handleChanges(rest, ctx);
|
|
989
|
+
if (cmd === "keys")
|
|
990
|
+
argv = keysArgv(argv);
|
|
894
991
|
const catalog = await fetchCatalog(ctx, env, !cacheFlag.present);
|
|
895
992
|
if (!cmd || cmd === "help" || cmd === "--help") {
|
|
896
|
-
process.stdout.write(`${renderCatalog(catalog.ops)}
|
|
993
|
+
process.stdout.write(`${renderCatalog(catalog.ops, rest.length ? rest.join(" ") : undefined)}
|
|
897
994
|
`);
|
|
898
995
|
return;
|
|
899
996
|
}
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ Use the lane the behaviour was reported on. On the contact lane Volty declines t
|
|
|
25
25
|
|
|
26
26
|
## On prod
|
|
27
27
|
|
|
28
|
+
- A key reaches one org and calls operations only; `--org` gets each org you belong to its own key the first time. A call a key cannot make is refused with a sentence naming why; report that sentence instead of working around it. `volty keys create <name>` mints a narrower named key for a script (`volty docs` has the flags).
|
|
28
29
|
- Name the org on every command (`--env prod --org <id>`, or `VOLTY_ENV` and `VOLTY_ORG` in the process), and read the stderr line the CLI prints before each command to check it names the org you meant. Act only in the org the user named.
|
|
29
30
|
- Test through the Sandbox, where a reply to the synthetic contact is recorded as sent and never leaves. A real conversation is never a test target.
|
|
30
31
|
- Run `volty debug sandbox-cleanup --all --env prod --org <id>` when you finish.
|