@retasc/cli 1.11.0 → 1.13.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.
- package/dist/commands/bind.js +26 -0
- package/dist/commands/identity.js +11 -7
- package/dist/commands/join.js +45 -21
- package/dist/index.js +23 -10
- package/dist/lib/format.js +157 -0
- package/package.json +1 -1
package/dist/commands/bind.js
CHANGED
|
@@ -277,7 +277,33 @@ export async function completeWorkspaceSetup(args) {
|
|
|
277
277
|
catch {
|
|
278
278
|
/* binding written; whoami confirmation is best-effort */
|
|
279
279
|
}
|
|
280
|
+
// RTSC-519 — say that setup FINISHED, and what to do with it.
|
|
281
|
+
//
|
|
282
|
+
// Here rather than in either command, because it is true of both and this is the tail
|
|
283
|
+
// they share. `join` said it and `bind` did not, so the person who set up their own org
|
|
284
|
+
// was left reading a status line and guessing whether anything else was required — while
|
|
285
|
+
// the invited teammate, who arrived through the other command, was told. A line in one
|
|
286
|
+
// caller is a line the other has to remember; a line here is one neither can lose.
|
|
287
|
+
//
|
|
288
|
+
// AFTER the confirmation, and after everything that can throw, so it is only ever
|
|
289
|
+
// printed by a run that actually finished. Nothing below it can fail.
|
|
290
|
+
if (isInteractive())
|
|
291
|
+
console.log(`\n${NEXT_STEP}`);
|
|
280
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* What to do with a workspace that is now set up.
|
|
295
|
+
*
|
|
296
|
+
* "or restart it if it's already open" is the load-bearing half. `.mcp.json` is read when
|
|
297
|
+
* an MCP client starts, so an agent already running on this folder will not see Retasc
|
|
298
|
+
* until it restarts — the same trap RTSC-496 records for the Dash's version of this
|
|
299
|
+
* handover, where the screen otherwise waits forever on an agent that can never call in.
|
|
300
|
+
* It bites harder here: `bind` is routinely run from inside a session already open on the
|
|
301
|
+
* folder being bound.
|
|
302
|
+
*
|
|
303
|
+
* Deliberately NOT "run `retasc next`". The agent pulls work over MCP; the CLI's claim
|
|
304
|
+
* commands are a human convenience, and naming one here would teach the wrong first move.
|
|
305
|
+
*/
|
|
306
|
+
export const NEXT_STEP = "Start your agent in this folder, or restart it if it's already open, and it'll pull from the queue.";
|
|
281
307
|
/**
|
|
282
308
|
* Sign in first when there is no session at all (RTSC-481, extended to `join` by RTSC-492).
|
|
283
309
|
*
|
|
@@ -50,15 +50,19 @@ function report(outcome, orgLabel) {
|
|
|
50
50
|
console.log("Run this again after a migration — each import brings its own people across.");
|
|
51
51
|
return;
|
|
52
52
|
case "answered":
|
|
53
|
-
//
|
|
54
|
-
// from "none"
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// They have answered here before, and nothing is left from a tool they haven't
|
|
54
|
+
// answered about. It differs from "none" only in being able to say what already
|
|
55
|
+
// happened; the advice is identical, because it is now TRUE for both.
|
|
56
|
+
//
|
|
57
|
+
// RTSC-507 rewrote this. It used to read "that answer covers the whole org,
|
|
58
|
+
// including later migrations" and send them to an owner — because one decline really
|
|
59
|
+
// did shut them out of every future import. That sentence existed only to describe a
|
|
60
|
+
// bug, and it goes in the same release the bug does.
|
|
61
|
+
console.log(`You've answered about every tool imported into ${orgLabel} so far.`);
|
|
62
|
+
console.log("Run this again after the next migration — each one is asked about separately.");
|
|
59
63
|
return;
|
|
60
64
|
case "declined":
|
|
61
|
-
console.log("Noted
|
|
65
|
+
console.log("Noted. That covers those tools only, so a later import will ask again.");
|
|
62
66
|
return;
|
|
63
67
|
case "left":
|
|
64
68
|
console.log("Nothing linked. Run `retasc identity` again whenever you want to look.");
|
package/dist/commands/join.js
CHANGED
|
@@ -16,6 +16,24 @@ const UNKNOWN_SOURCE = "another tool";
|
|
|
16
16
|
* number of migrated tools.
|
|
17
17
|
*/
|
|
18
18
|
const MAX_IDENTITY_ROUNDS = 10;
|
|
19
|
+
/**
|
|
20
|
+
* The tools on screen, named the way they'd be read aloud (RTSC-507).
|
|
21
|
+
*
|
|
22
|
+
* The twin of `sourcePhrase` in `dash/src/lib/ghostPrompt.ts`, duplicated rather than
|
|
23
|
+
* shared because the two live in separate packages with no common build — the same reason
|
|
24
|
+
* `UNKNOWN_SOURCE` above is a second copy of the Dash's constant. Keep the two in step:
|
|
25
|
+
* they answer the same question in front of the same irreversible decision.
|
|
26
|
+
*/
|
|
27
|
+
export function sourcePhrase(ghosts) {
|
|
28
|
+
const names = [...new Set(ghosts.map((g) => clean(g.sourceLabel ?? UNKNOWN_SOURCE)))].sort((a, b) =>
|
|
29
|
+
// Keep the vague fallback last, so a known tool always leads.
|
|
30
|
+
a === UNKNOWN_SOURCE ? 1 : b === UNKNOWN_SOURCE ? -1 : a.localeCompare(b));
|
|
31
|
+
if (names.length <= 1)
|
|
32
|
+
return names[0] ?? UNKNOWN_SOURCE;
|
|
33
|
+
if (names.length === 2)
|
|
34
|
+
return `${names[0]} and ${names[1]}`;
|
|
35
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
36
|
+
}
|
|
19
37
|
/** How one row reads: the person, and the tool they were carried in from. */
|
|
20
38
|
function ghostRow(g) {
|
|
21
39
|
// BOTH fields are third-party text: the name was typed by someone in ClickUp/Jira/Asana
|
|
@@ -62,13 +80,16 @@ deps = {}) {
|
|
|
62
80
|
let ghosts;
|
|
63
81
|
try {
|
|
64
82
|
const res = await d.claimableGhosts({ orgId });
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!res.ghosts.length)
|
|
71
|
-
|
|
83
|
+
// An empty list is the only ending now (RTSC-507). `asked` no longer gates anything
|
|
84
|
+
// — it used to mean "declined, permanently", and branching on it FIRST is exactly
|
|
85
|
+
// what hid a later migration's placeholders for good. It now only picks the wording:
|
|
86
|
+
// "you've answered everything so far" reads differently from "nothing was ever
|
|
87
|
+
// imported here", and both are empty. `join` treats them alike; `identity` does not.
|
|
88
|
+
if (!res.ghosts.length) {
|
|
89
|
+
if (claimedAny)
|
|
90
|
+
return "claimed";
|
|
91
|
+
return res.asked ? "answered" : "none";
|
|
92
|
+
}
|
|
72
93
|
ghosts = res.ghosts;
|
|
73
94
|
}
|
|
74
95
|
catch (e) {
|
|
@@ -80,14 +101,18 @@ deps = {}) {
|
|
|
80
101
|
? `\nWhen ${orgLabel ?? "this org"} migrated, some people were carried across.\n` +
|
|
81
102
|
"One of them may be you:"
|
|
82
103
|
: "\nAnything else here you recognise?");
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
104
|
+
// RTSC-507 — the row says what the answer COVERS, because that is what changed.
|
|
105
|
+
// Declining used to write one org-wide flag that shut the person out of every later
|
|
106
|
+
// migration, so the row had to carry the warning "don't ask again". It now records
|
|
107
|
+
// only the tools on screen, so a bare "none of these are me" is finally the literal
|
|
108
|
+
// truth, and naming the tools is what makes the answer safe to give.
|
|
87
109
|
// Compared by REFERENCE below, not by a magic id value: a sentinel that is merely a
|
|
88
|
-
// row with an empty id would let any future ghost with a falsy id read as a decline
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
// row with an empty id would let any future ghost with a falsy id read as a decline.
|
|
111
|
+
const NONE = {
|
|
112
|
+
id: "",
|
|
113
|
+
name: `None of these are me (covers ${sourcePhrase(ghosts)} only)`,
|
|
114
|
+
sourceLabel: null,
|
|
115
|
+
};
|
|
91
116
|
// No label: the heading above is the prompt, and `choose` would print a second one.
|
|
92
117
|
const chosen = await pickExisting("", [...ghosts, NONE], (g) => (g === NONE ? g.name : ghostRow(g)), d.askFn);
|
|
93
118
|
if (chosen === NONE) {
|
|
@@ -96,10 +121,10 @@ deps = {}) {
|
|
|
96
121
|
}
|
|
97
122
|
catch (e) {
|
|
98
123
|
console.error(` ! ${formatError(e).message}`);
|
|
99
|
-
// NOT "declined". A caller answers that with "
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
124
|
+
// NOT "declined". A caller answers that with "noted", and the write is the only
|
|
125
|
+
// thing that makes it true — nothing landed, so these tools WILL be offered again.
|
|
126
|
+
// The error is already on stderr; `unavailable` adds no second sentence on top of
|
|
127
|
+
// it rather than a reassuring false one.
|
|
103
128
|
return "unavailable";
|
|
104
129
|
}
|
|
105
130
|
return "declined";
|
|
@@ -212,7 +237,6 @@ export async function joinAction(link, opts) {
|
|
|
212
237
|
console.error(` Resume with: ${resume}`);
|
|
213
238
|
throw e;
|
|
214
239
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
240
|
+
// RTSC-519 — the closing line moved into `completeWorkspaceSetup`, the tail both commands
|
|
241
|
+
// already share, so `bind` gets it too. Saying it here as well would say it twice.
|
|
218
242
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { identityAction } from "./commands/identity.js";
|
|
|
11
11
|
import { doctorAction } from "./commands/doctor.js";
|
|
12
12
|
import { billingAction } from "./commands/billing.js";
|
|
13
13
|
import { isNetworkError, readLocalBinding, resolveBinding } from "./lib/binding.js";
|
|
14
|
+
import { whoamiView, orgCreatedView, projectCreatedView, keyListView, inviteListView, } from "./lib/format.js";
|
|
14
15
|
import { tidyAction, doneAction } from "./commands/tidy.js";
|
|
15
16
|
import { runProxy } from "./proxy.js";
|
|
16
17
|
import { deviceLogin } from "./auth.js";
|
|
@@ -77,7 +78,8 @@ program
|
|
|
77
78
|
program
|
|
78
79
|
.command("whoami")
|
|
79
80
|
.description("Show THIS folder's org/project binding, plus the signed-in user and their orgs.")
|
|
80
|
-
.
|
|
81
|
+
.option("--json", "Emit the raw payload instead of the summary.")
|
|
82
|
+
.action(async (opts) => {
|
|
81
83
|
// RTSC-91 (§13): lead with the binding for the folder you're in — the same
|
|
82
84
|
// "you are in org X / project Y" heads-up the agent gets — so a human can
|
|
83
85
|
// confirm scope before any work. Resolved from the local key, server-enforced.
|
|
@@ -111,7 +113,9 @@ program
|
|
|
111
113
|
}
|
|
112
114
|
try {
|
|
113
115
|
const me = await api.me();
|
|
114
|
-
|
|
116
|
+
// RTSC-521: the binding block above is untouched — it was already the good half of
|
|
117
|
+
// this command. Only the payload dump becomes a summary.
|
|
118
|
+
console.log(opts.json ? JSON.stringify(me, null, 2) : whoamiView(me));
|
|
115
119
|
}
|
|
116
120
|
catch (e) {
|
|
117
121
|
fail(e);
|
|
@@ -200,11 +204,12 @@ org
|
|
|
200
204
|
.command("create")
|
|
201
205
|
.requiredOption("--name <name>")
|
|
202
206
|
.option("--slug <slug>")
|
|
207
|
+
.option("--json", "Emit the raw payload instead of the summary.")
|
|
203
208
|
.action(async (opts) => {
|
|
204
209
|
requireLogin();
|
|
205
210
|
try {
|
|
206
|
-
const res = await api.createOrg({ name: opts.name, slug: opts.slug });
|
|
207
|
-
console.log(JSON.stringify(res, null, 2));
|
|
211
|
+
const res = (await api.createOrg({ name: opts.name, slug: opts.slug }));
|
|
212
|
+
console.log(opts.json ? JSON.stringify(res, null, 2) : orgCreatedView(res, opts.name));
|
|
208
213
|
}
|
|
209
214
|
catch (e) {
|
|
210
215
|
fail(e);
|
|
@@ -216,11 +221,16 @@ project
|
|
|
216
221
|
.requiredOption("--org-id <id>")
|
|
217
222
|
.requiredOption("--name <name>")
|
|
218
223
|
.requiredOption("--prefix <PREFIX>")
|
|
224
|
+
.option("--json", "Emit the raw payload instead of the summary.")
|
|
219
225
|
.action(async (opts) => {
|
|
220
226
|
requireLogin();
|
|
221
227
|
try {
|
|
222
|
-
const res = await api.createProject({
|
|
223
|
-
|
|
228
|
+
const res = (await api.createProject({
|
|
229
|
+
orgId: opts.orgId,
|
|
230
|
+
name: opts.name,
|
|
231
|
+
prefix: opts.prefix,
|
|
232
|
+
}));
|
|
233
|
+
console.log(opts.json ? JSON.stringify(res, null, 2) : projectCreatedView(res, opts.name));
|
|
224
234
|
}
|
|
225
235
|
catch (e) {
|
|
226
236
|
fail(e);
|
|
@@ -279,12 +289,14 @@ key
|
|
|
279
289
|
});
|
|
280
290
|
key
|
|
281
291
|
.command("list")
|
|
292
|
+
.description("List an org's agent keys, newest first.")
|
|
282
293
|
.requiredOption("--org-id <id>")
|
|
294
|
+
.option("--json", "Emit the raw payload instead of the table.")
|
|
283
295
|
.action(async (opts) => {
|
|
284
296
|
requireLogin();
|
|
285
297
|
try {
|
|
286
|
-
const res = await api.listKeys({ orgId: opts.orgId });
|
|
287
|
-
console.log(JSON.stringify(res, null, 2));
|
|
298
|
+
const res = (await api.listKeys({ orgId: opts.orgId }));
|
|
299
|
+
console.log(opts.json ? JSON.stringify(res, null, 2) : keyListView(res));
|
|
288
300
|
}
|
|
289
301
|
catch (e) {
|
|
290
302
|
fail(e);
|
|
@@ -347,11 +359,12 @@ members
|
|
|
347
359
|
.command("list")
|
|
348
360
|
.description("List an org's invites and their status (owner only).")
|
|
349
361
|
.requiredOption("--org-id <id>")
|
|
362
|
+
.option("--json", "Emit the raw payload instead of the table.")
|
|
350
363
|
.action(async (opts) => {
|
|
351
364
|
requireLogin();
|
|
352
365
|
try {
|
|
353
|
-
const res = await api.listInvites({ orgId: opts.orgId });
|
|
354
|
-
console.log(JSON.stringify(res, null, 2));
|
|
366
|
+
const res = (await api.listInvites({ orgId: opts.orgId }));
|
|
367
|
+
console.log(opts.json ? JSON.stringify(res, null, 2) : inviteListView(res));
|
|
355
368
|
}
|
|
356
369
|
catch (e) {
|
|
357
370
|
fail(e);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { clean } from "./text.js";
|
|
2
|
+
// RTSC-521 — the CLI's human-readable views.
|
|
3
|
+
//
|
|
4
|
+
// Five commands used to answer a person with `JSON.stringify(res, null, 2)`, including
|
|
5
|
+
// `whoami`, which is the command someone runs to find out who they are. The one fact they
|
|
6
|
+
// wanted was a bracket to parse, next to a Convex document id that means nothing to them.
|
|
7
|
+
//
|
|
8
|
+
// The "it's good for the agent" defence does not apply: agents never see CLI stdout. They
|
|
9
|
+
// talk to MCP directly, and `whoami` exists there as its own tool. Nothing was reading
|
|
10
|
+
// these payloads but a human.
|
|
11
|
+
//
|
|
12
|
+
// PURE, and returning strings rather than printing them, for two reasons. Tests can pin the
|
|
13
|
+
// wording without scraping stdout, and every one of these values came off the wire — so
|
|
14
|
+
// `clean()` belongs at the point of formatting, once, rather than at each interpolation
|
|
15
|
+
// where a later line can forget it.
|
|
16
|
+
//
|
|
17
|
+
// The shape follows `commands/billing.ts`, which already got this right: an aligned summary
|
|
18
|
+
// by default, `--json` for the raw payload. It is one pattern, not two.
|
|
19
|
+
/** Width of the label column. Matches billing.ts's `row()` so the CLI reads as one tool. */
|
|
20
|
+
const LABEL = 13;
|
|
21
|
+
/** One `label value` line. Multi-line values stay hung under the value column. */
|
|
22
|
+
export function labelled(rows) {
|
|
23
|
+
return rows
|
|
24
|
+
.map(([label, value]) => {
|
|
25
|
+
const [head, ...rest] = String(value).split("\n");
|
|
26
|
+
const pad = " ".repeat(LABEL);
|
|
27
|
+
return [`${label.padEnd(LABEL)}${head}`, ...rest.map((r) => `${pad}${r}`)].join("\n");
|
|
28
|
+
})
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* An aligned table, or a plain sentence when there is nothing in it.
|
|
33
|
+
*
|
|
34
|
+
* `empty` is required rather than optional: a list command that prints a bare header row
|
|
35
|
+
* and nothing else reads like a failure, and every caller has something truer to say.
|
|
36
|
+
*/
|
|
37
|
+
export function table(headers, rows, empty) {
|
|
38
|
+
if (rows.length === 0)
|
|
39
|
+
return empty;
|
|
40
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
41
|
+
// The last column is never padded — trailing spaces are invisible until someone copies
|
|
42
|
+
// a line out of their terminal and finds them.
|
|
43
|
+
const line = (cells) => cells.map((c, i) => (i === cells.length - 1 ? c : c.padEnd(widths[i]))).join(" ").trimEnd();
|
|
44
|
+
return [line(headers), ...rows.map(line)].join("\n");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* How long ago, in the coarsest unit that is still true.
|
|
48
|
+
*
|
|
49
|
+
* A raw epoch is the specific thing this issue exists to remove: a revoked key reading as
|
|
50
|
+
* `1785660358922` is a number someone skims past, and "revoked" is the fact.
|
|
51
|
+
*/
|
|
52
|
+
export function ago(ms, now = Date.now()) {
|
|
53
|
+
if (ms == null)
|
|
54
|
+
return "never";
|
|
55
|
+
const s = Math.max(0, Math.round((now - ms) / 1000));
|
|
56
|
+
if (s < 60)
|
|
57
|
+
return "just now";
|
|
58
|
+
const m = Math.round(s / 60);
|
|
59
|
+
if (m < 60)
|
|
60
|
+
return `${m}m ago`;
|
|
61
|
+
const h = Math.round(m / 60);
|
|
62
|
+
if (h < 24)
|
|
63
|
+
return `${h}h ago`;
|
|
64
|
+
return `${Math.round(h / 24)}d ago`;
|
|
65
|
+
}
|
|
66
|
+
/** A date a human can read, for a deadline rather than an elapsed time. */
|
|
67
|
+
export function day(ms) {
|
|
68
|
+
return ms == null ? "—" : new Date(ms).toISOString().slice(0, 10);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Who you are, and where you can act.
|
|
72
|
+
*
|
|
73
|
+
* The user id is dropped: nothing a person types accepts it. The org id is KEPT, because
|
|
74
|
+
* the very next command genuinely wants it (`retasc bind --org-id …`, `members invite
|
|
75
|
+
* --org-id …`) — the same reason the Dash surfaces it on the Settings page.
|
|
76
|
+
*/
|
|
77
|
+
export function whoamiView(me) {
|
|
78
|
+
const u = me.user ?? {};
|
|
79
|
+
const who = [u.name, u.email && `<${u.email}>`].filter(Boolean).map(clean).join(" ");
|
|
80
|
+
const orgs = me.orgs ?? [];
|
|
81
|
+
return labelled([
|
|
82
|
+
["Signed in", who || "unknown"],
|
|
83
|
+
[
|
|
84
|
+
"Orgs",
|
|
85
|
+
// Said in words. An empty array printed as `[]` is the exact moment this command
|
|
86
|
+
// stops answering the question it was asked.
|
|
87
|
+
orgs.length === 0
|
|
88
|
+
? "none"
|
|
89
|
+
: orgs
|
|
90
|
+
.map((o) => {
|
|
91
|
+
const bits = [clean(o.name)];
|
|
92
|
+
if (o.slug)
|
|
93
|
+
bits.push(`(${clean(o.slug)})`);
|
|
94
|
+
const tail = [o.role && clean(o.role), o.deleting && "DELETING"].filter(Boolean);
|
|
95
|
+
// `o.id` is Convex-generated and so not attacker-influenced today. Cleaned
|
|
96
|
+
// anyway: this function's job is that nothing reaches a terminal unsanitised,
|
|
97
|
+
// and one raw interpolation is how the next field added here gets missed.
|
|
98
|
+
return `${bits.join(" ")}${tail.length ? ` · ${tail.join(" · ")}` : ""}\n ${clean(o.id)}`;
|
|
99
|
+
})
|
|
100
|
+
.join("\n"),
|
|
101
|
+
],
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
export function orgCreatedView(res, name) {
|
|
105
|
+
// `ownerMemberId` is deliberately not shown. It comes back in the payload and there is no
|
|
106
|
+
// command that takes it, so printing it is noise a reader has to decide to ignore.
|
|
107
|
+
return [
|
|
108
|
+
`✓ Created org "${clean(name)}"${res.slug ? ` (${clean(res.slug)})` : ""}.`,
|
|
109
|
+
"",
|
|
110
|
+
labelled([["Org id", clean(res.orgId ?? "—")]]),
|
|
111
|
+
"",
|
|
112
|
+
`Next: retasc bind --org-id ${clean(res.orgId ?? "<id>")}`,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
export function projectCreatedView(res, name) {
|
|
116
|
+
return [
|
|
117
|
+
`✓ Created project "${clean(name)}"${res.prefix ? ` (${clean(res.prefix)})` : ""}.`,
|
|
118
|
+
"",
|
|
119
|
+
labelled([["Project id", clean(res.projectId ?? "—")]]),
|
|
120
|
+
"",
|
|
121
|
+
`Issues here will be numbered ${clean(res.prefix ?? "PREFIX")}-1, ${clean(res.prefix ?? "PREFIX")}-2, and so on.`,
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The org's agent keys.
|
|
126
|
+
*
|
|
127
|
+
* Session keys (RTSC-50) are folded into a count rather than listed. They are auto-minted
|
|
128
|
+
* per session and can outnumber the real workspace keys many to one, which is why the Dash
|
|
129
|
+
* groups them too — a list where the manageable rows are lost among machine-minted children
|
|
130
|
+
* is a list nobody reads.
|
|
131
|
+
*/
|
|
132
|
+
export function keyListView(rows, now = Date.now()) {
|
|
133
|
+
const workspace = rows.filter((k) => !k.parentKeyId);
|
|
134
|
+
const sessions = rows.length - workspace.length;
|
|
135
|
+
const body = table(["KEY", "PROJECT", "AGENT", "RUNTIME", "LAST USED"], workspace.map((k) => [
|
|
136
|
+
clean(k.displayPrefix ?? k.name ?? "—"),
|
|
137
|
+
clean(k.project ?? "—"),
|
|
138
|
+
clean(k.agent ?? "—"),
|
|
139
|
+
clean(k.runtime ?? "—"),
|
|
140
|
+
// State first: a revoked key that reads as a date is one someone counts as live.
|
|
141
|
+
k.revokedAt ? "revoked" : ago(k.lastUsedAt, now),
|
|
142
|
+
]), "No keys yet. Mint one with `retasc key mint`.");
|
|
143
|
+
return sessions > 0
|
|
144
|
+
? `${body}\n\n(${sessions} session key${sessions === 1 ? "" : "s"} not shown — auto-minted per agent session.)`
|
|
145
|
+
: body;
|
|
146
|
+
}
|
|
147
|
+
export function inviteListView(rows) {
|
|
148
|
+
return table(["CODE", "ROLE", "STATUS", "INVITED BY", "EXPIRES"], rows.map((i) => [
|
|
149
|
+
clean(i.displayPrefix ?? "—"),
|
|
150
|
+
clean(i.role ?? "—"),
|
|
151
|
+
clean(i.status ?? "—"),
|
|
152
|
+
clean(i.invitedBy ?? "—"),
|
|
153
|
+
// Only a LIVE invite has a deadline worth reading. On a spent one the date is
|
|
154
|
+
// still in the payload and still means nothing.
|
|
155
|
+
i.status === "pending" ? day(i.expiresAt) : "—",
|
|
156
|
+
]), "No invites yet. Create one with `retasc members invite --org-id <id>`.");
|
|
157
|
+
}
|
package/package.json
CHANGED