@junoflow/cli 0.14.0 → 0.17.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/src/help.js CHANGED
@@ -50,12 +50,18 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
50
50
  const authoring = Object.hasOwn(AUTHORING_HEADINGS, group) ? group : null;
51
51
  let tools = null;
52
52
  let skills = null;
53
+ let readFirst = [];
53
54
  if (onInstance.some((row) => row.tool) || authoring) {
54
55
  try {
55
56
  const { client } = await connect(args, io);
56
57
  if (onInstance.some((row) => row.tool)) {
57
58
  tools = new Map((await client.listTools()).map((t) => [t.name, t]));
58
59
  }
60
+ readFirst = await readFirstFor(
61
+ client,
62
+ [group, ...members.map((r) => r.tool)],
63
+ io
64
+ );
59
65
  if (authoring) {
60
66
  skills = (await client.listSkills({ authoring }))?.skills ?? [];
61
67
  }
@@ -65,19 +71,17 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
65
71
  }
66
72
 
67
73
  const lines = members.map((row) => {
68
- const synopsis = row.tool
69
- ? `juno ${row.command}${positionalName(row) ? ` ${positionalName(row)}` : ''}`
70
- : handlerFor(row).usage;
71
- if (!row.tool) return [synopsis, row.effect, row.summary];
74
+ const line = synopsis(row, handlerFor);
75
+ if (!row.tool) return [line, row.effect, row.summary];
72
76
  const tool = tools?.get(row.tool);
77
+ // A row that fixes part of its tool's input (one verb of command_task)
78
+ // says what it does itself; the tool's first sentence covers every verb.
79
+ if (row.summary)
80
+ return [line, row.effect ?? tool?.effect ?? '', row.summary];
73
81
  // Offline, no tool row shows an effect: printing only the rows that carry
74
82
  // their own would read as if the others had none.
75
- if (!tool) return [synopsis, '', ''];
76
- return [
77
- synopsis,
78
- row.effect ?? tool.effect,
79
- firstSentence(tool.description),
80
- ];
83
+ if (!tool) return [line, '', ''];
84
+ return [line, row.effect ?? tool.effect, firstSentence(tool.description)];
81
85
  });
82
86
  // One column width across both headings, so the two lists line up.
83
87
  const render = tabulate(lines);
@@ -89,6 +93,7 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
89
93
  : `usage: juno ${group} <verb>`,
90
94
  '',
91
95
  ];
96
+ if (readFirst.length) out.push(readFirstLine(readFirst), '');
92
97
  if (inWorkingCopy.length === 0) {
93
98
  out.push(...body(members));
94
99
  } else {
@@ -109,21 +114,99 @@ export async function groupHelp(group, rows, handlerFor, args, io) {
109
114
  return out.join('\n');
110
115
  }
111
116
 
112
- export const WORKING_COPY_HEADING =
113
- 'In a working copy (run from the project directory)';
114
-
115
117
  /**
116
- * workingCopyListing is every working-copy command with its effect and
117
- * summary, for `juno --help`; offline, since all of it is in verbs.json.
118
+ * rootHelp is `juno --help` and bare `juno`: the place an agent with nothing
119
+ * installed starts. Usage first, then the primer from GET /api/agent-primer
120
+ * (what Juno is made of, how to work with it, what only the user does), then
121
+ * every group and top-level command by one line from verbs.json, the
122
+ * working-copy commands, and the skill index from GET /api/skills. The
123
+ * instance owns the primer and the skills; the CLI owns the listing.
124
+ *
125
+ * Offline — no instance configured, unreachable, refused — the primer and the
126
+ * skills are dropped, the reason goes to stderr as for a group's help, and one
127
+ * line says how to connect. A help must not fail offline.
118
128
  */
119
- export function workingCopyListing(rows, handlerFor) {
120
- return tabulate(
121
- rows
122
- .filter((row) => row.working_copy)
123
- .map((row) => [handlerFor(row).usage, row.effect, row.summary])
124
- ).join('\n');
129
+ export async function rootHelp(rows, handlerFor, groupSummaries, args, io) {
130
+ let primer = null;
131
+ let skills = null;
132
+ try {
133
+ const { client } = await connect(args, io);
134
+ const [p, index] = await Promise.all([
135
+ client.getAgentPrimer(),
136
+ client.listSkills(),
137
+ ]);
138
+ primer = p?.body ?? null;
139
+ skills = index?.skills ?? [];
140
+ } catch (err) {
141
+ io.warn(`(guide unavailable: ${err.message})`);
142
+ }
143
+
144
+ const out = [
145
+ 'juno — operate a Juno instance, and develop flows, integrations and apps for it',
146
+ '',
147
+ 'Usage: juno <command> [options]',
148
+ ` juno <group> <verb> [id] ${INPUT_USAGE}`,
149
+ '',
150
+ ];
151
+ if (primer) {
152
+ out.push(primer.trim(), '');
153
+ } else {
154
+ out.push(
155
+ 'The guide to working with Juno and the skill index come from the instance,',
156
+ 'which did not answer (the reason is on stderr). `juno config set url <url>`',
157
+ 'and `juno config set token <key>` connect this CLI to one.',
158
+ ''
159
+ );
160
+ }
161
+
162
+ const groups = [];
163
+ for (const row of rows) {
164
+ const group = groupOf(row);
165
+ if (group && !groups.includes(group)) groups.push(group);
166
+ }
167
+ out.push("Groups (run `juno <group> --help` for a group's commands):");
168
+ out.push(...tabulate(groups.map((g) => [g, '', groupSummaries[g] ?? ''])));
169
+ out.push('', 'Commands:');
170
+ out.push(
171
+ ...tabulate(
172
+ rows
173
+ .filter((row) => !groupOf(row))
174
+ .map((row) => [synopsis(row, handlerFor), '', row.summary ?? ''])
175
+ )
176
+ );
177
+
178
+ if (skills?.length) {
179
+ out.push(
180
+ '',
181
+ 'Skills (`juno skill <name>...` prints them; `juno skill` gives each its full description):'
182
+ );
183
+ const width = Math.max(...skills.map((s) => s.name.length));
184
+ for (const s of skills) {
185
+ out.push(` ${s.name.padEnd(width)} ${firstSentence(s.description)}`);
186
+ }
187
+ }
188
+
189
+ out.push(
190
+ '',
191
+ '--url <url> picks the instance; else $JUNO_URL, package.json juno.target,',
192
+ 'then the user config (juno config set url <url>).',
193
+ '--token <key> is the API key to send; else $JUNO_TOKEN, then the user config',
194
+ '(juno config set token <key>). Create one under Settings → API keys.',
195
+ ''
196
+ );
197
+ return out.join('\n');
198
+ }
199
+
200
+ /** One row's synopsis as a listing shows it: `juno state get <id>`. */
201
+ function synopsis(row, handlerFor) {
202
+ if (!row.tool) return handlerFor(row).usage;
203
+ const positional = positionalName(row);
204
+ return `juno ${row.command}${positional ? ` ${positional}` : ''}`;
125
205
  }
126
206
 
207
+ export const WORKING_COPY_HEADING =
208
+ 'In a working copy (run from the project directory)';
209
+
127
210
  /** Rows of [synopsis, effect, text] as aligned, indented lines. */
128
211
  function tabulate(lines) {
129
212
  const width = Math.max(...lines.map(([s]) => s.length));
@@ -149,7 +232,15 @@ export async function verbHelp(row, args, io) {
149
232
  );
150
233
  }
151
234
 
152
- const out = [`usage: ${rowUsage(row)}`, '', tool.description.trim(), ''];
235
+ const out = [`usage: ${rowUsage(row)}`, ''];
236
+ // Which skills to read first is the server's mapping (each skill's
237
+ // applies_to); a failure to fetch it drops the line, never the help.
238
+ const readFirst = await readFirstFor(client, [groupOf(row), row.tool], io);
239
+ if (readFirst.length) out.push(readFirstLine(readFirst), '');
240
+ // A top-level command's summary is its line in `juno --help`; the tool's
241
+ // description says the same at length.
242
+ if (row.summary && row.fixed_input) out.push(row.summary, '');
243
+ out.push(tool.description.trim(), '');
153
244
  out.push(`effect: ${row.effect ?? tool.effect}`);
154
245
  out.push(`tool: ${row.tool}`);
155
246
  if (row.id_field) {
@@ -177,6 +268,36 @@ export async function verbHelp(row, args, io) {
177
268
  return out.join('\n');
178
269
  }
179
270
 
271
+ /**
272
+ * skillsFor is the names of the skills whose applies_to names any of `names`
273
+ * (a group, a tool), in the index's order. The mapping is the server's: the CLI
274
+ * only matches the strings.
275
+ */
276
+ export function skillsFor(skills, names) {
277
+ const wanted = new Set(names.filter(Boolean));
278
+ return skills
279
+ .filter((s) => (s.applies_to ?? []).some((n) => wanted.has(n)))
280
+ .map((s) => s.name);
281
+ }
282
+
283
+ /**
284
+ * readFirstFor fetches the skill index and answers skillsFor over it. Its
285
+ * own failure costs only the line: the reason goes to stderr and the help
286
+ * goes on.
287
+ */
288
+ async function readFirstFor(client, names, io) {
289
+ try {
290
+ return skillsFor((await client.listSkills())?.skills ?? [], names);
291
+ } catch (err) {
292
+ io.warn(`(skills unavailable: ${err.message})`);
293
+ return [];
294
+ }
295
+ }
296
+
297
+ function readFirstLine(names) {
298
+ return `Read first: juno skill ${names.join(' ')}`;
299
+ }
300
+
180
301
  /** The help of a non-tool command, offline: its summary and its usage. */
181
302
  export function nonToolHelp(row, handler) {
182
303
  return `${row.summary}\n\nusage: ${handler.usage}`;
package/src/index.js CHANGED
@@ -1,15 +1,12 @@
1
1
  import { parseArgs } from 'node:util';
2
2
 
3
- import {
4
- AGENT_SKILL_INSTALL_USAGE,
5
- agentSkillInstall,
6
- } from './commands/agent-skill-install.js';
7
3
  import { APP_CHECK_USAGE, appCheck } from './commands/app-check.js';
8
4
  import { APP_DEPLOY_USAGE, appDeploy } from './commands/app-deploy.js';
9
5
  import { APP_DEV_USAGE, appDev } from './commands/app-dev.js';
10
6
  import { APP_GEN_TYPES_USAGE, appGenTypes } from './commands/app-gen-types.js';
11
7
  import { APP_INIT_USAGE, appInit } from './commands/app-init.js';
12
8
  import { APP_PULL_USAGE, appPull } from './commands/app-pull.js';
9
+ import { APPROVAL_WAIT_USAGE, approvalWait } from './commands/approval-wait.js';
13
10
  import { ASK_USAGE, ask } from './commands/ask.js';
14
11
  import { CHECK_USAGE, check } from './commands/check.js';
15
12
  import { CONFIG_SET_USAGE, configSet } from './commands/config.js';
@@ -30,19 +27,39 @@ import { FLOW_PULL_USAGE, flowPull } from './commands/flow-pull.js';
30
27
  import { GEN_TYPES_USAGE, genTypes } from './commands/gen-types.js';
31
28
  import { INIT_USAGE, init } from './commands/init.js';
32
29
  import {
33
- JOB_PING_USAGE,
34
- JOB_RUN_USAGE,
35
- jobPing,
36
- jobRun,
37
- } from './commands/job-run.js';
30
+ TASK_PING_USAGE,
31
+ TASK_RUN_USAGE,
32
+ taskPing,
33
+ taskRun,
34
+ } from './commands/task-run.js';
38
35
  import { PULL_USAGE, pull } from './commands/pull.js';
36
+ import { PERMISSIONS_USAGE, permissions } from './commands/permissions.js';
39
37
  import { SKILL_USAGE, skill } from './commands/skill.js';
40
38
  import {
41
39
  SESSION_APPROVE_USAGE,
42
40
  sessionApprove,
43
41
  } from './commands/session-approve.js';
42
+ import { runFollow } from './commands/run-follow.js';
43
+ import {
44
+ SESSION_CANCEL_USAGE,
45
+ SESSION_EXPORT_USAGE,
46
+ SESSION_GET_USAGE,
47
+ SESSION_LIST_USAGE,
48
+ SESSION_RATE_USAGE,
49
+ sessionCancel,
50
+ sessionExport,
51
+ sessionGet,
52
+ sessionList,
53
+ sessionRate,
54
+ } from './commands/session.js';
44
55
  import { runTool } from './commands/tool.js';
45
56
  import { VERIFY_TYPES_USAGE, verifyTypes } from './commands/verify-types.js';
57
+ import {
58
+ VERSION_USAGE,
59
+ WHOAMI_USAGE,
60
+ version,
61
+ whoami,
62
+ } from './commands/whoami.js';
46
63
  import {
47
64
  ConflictError,
48
65
  DiagnosticsError,
@@ -58,15 +75,15 @@ import {
58
75
  EXIT_UNREACHABLE,
59
76
  EXIT_USAGE,
60
77
  } from './exit.js';
61
- import {
62
- groupHelp,
63
- nonToolHelp,
64
- verbHelp,
65
- WORKING_COPY_HEADING,
66
- workingCopyListing,
67
- } from './help.js';
78
+ import { groupHelp, nonToolHelp, rootHelp, verbHelp } from './help.js';
68
79
  import { closest, closestByVerb, levenshtein } from './suggest.js';
69
- import { groupOf, normalizeVerb, rowFor, rows } from './verbs.js';
80
+ import {
81
+ groupOf,
82
+ groupSummaries,
83
+ normalizeVerb,
84
+ rowFor,
85
+ rows,
86
+ } from './verbs.js';
70
87
 
71
88
  /**
72
89
  * The two flags every command that reaches an instance takes, and the two a
@@ -94,11 +111,58 @@ const NON_TOOL = {
94
111
  },
95
112
  usage: ASK_USAGE,
96
113
  },
114
+ whoami: { run: whoami, options: URL_OPTION, usage: WHOAMI_USAGE },
115
+ version: { run: version, options: URL_OPTION, usage: VERSION_USAGE },
116
+ permissions: {
117
+ run: permissions,
118
+ options: {
119
+ ...URL_OPTION,
120
+ 'allow-change': { type: 'boolean', default: false },
121
+ 'working-copy': { type: 'boolean', default: false },
122
+ json: { type: 'boolean', default: false },
123
+ },
124
+ usage: PERMISSIONS_USAGE,
125
+ },
126
+ 'session list': {
127
+ run: sessionList,
128
+ options: {
129
+ ...URL_OPTION,
130
+ rating: { type: 'string' },
131
+ profile: { type: 'string' },
132
+ limit: { type: 'string' },
133
+ },
134
+ usage: SESSION_LIST_USAGE,
135
+ },
136
+ 'session get': {
137
+ run: sessionGet,
138
+ options: { ...URL_OPTION, transcript: { type: 'boolean', default: false } },
139
+ usage: SESSION_GET_USAGE,
140
+ },
141
+ 'session export': {
142
+ run: sessionExport,
143
+ options: URL_OPTION,
144
+ usage: SESSION_EXPORT_USAGE,
145
+ },
146
+ 'session rate': {
147
+ run: sessionRate,
148
+ options: { ...URL_OPTION, rating: { type: 'string' } },
149
+ usage: SESSION_RATE_USAGE,
150
+ },
151
+ 'session cancel': {
152
+ run: sessionCancel,
153
+ options: URL_OPTION,
154
+ usage: SESSION_CANCEL_USAGE,
155
+ },
97
156
  'session approve': {
98
157
  run: sessionApprove,
99
158
  options: { ...URL_OPTION, wait: { type: 'string' } },
100
159
  usage: SESSION_APPROVE_USAGE,
101
160
  },
161
+ 'approval wait': {
162
+ run: approvalWait,
163
+ options: { ...URL_OPTION, wait: { type: 'string' } },
164
+ usage: APPROVAL_WAIT_USAGE,
165
+ },
102
166
  'credential set': {
103
167
  run: credentialSet,
104
168
  options: {
@@ -111,19 +175,19 @@ const NON_TOOL = {
111
175
  // raw: everything after `--` is the wrapped command's, verbatim, so the
112
176
  // argv goes to the handler unparsed (see runRow). No --url or --token: the
113
177
  // wrapper reads no Juno config, only the ping URL.
114
- 'job run': {
115
- run: (argv, io) => jobRun(argv, io, { env: io.env }),
178
+ 'task run': {
179
+ run: (argv, io) => taskRun(argv, io, { env: io.env }),
116
180
  options: {
117
181
  'url-file': { type: 'string' },
118
182
  verbose: { type: 'boolean', default: false },
119
183
  },
120
- usage: JOB_RUN_USAGE,
184
+ usage: TASK_RUN_USAGE,
121
185
  raw: true,
122
186
  },
123
- // No --url or --token either: one report to the ping URL, found as job run
187
+ // No --url or --token either: one report to the ping URL, found as task run
124
188
  // finds it.
125
- 'job ping': {
126
- run: (args, io) => jobPing(args, io, { env: io.env }),
189
+ 'task ping': {
190
+ run: (args, io) => taskPing(args, io, { env: io.env }),
127
191
  options: {
128
192
  'url-file': { type: 'string' },
129
193
  start: { type: 'boolean' },
@@ -132,21 +196,9 @@ const NON_TOOL = {
132
196
  log: { type: 'boolean' },
133
197
  rid: { type: 'string' },
134
198
  },
135
- usage: JOB_PING_USAGE,
199
+ usage: TASK_PING_USAGE,
136
200
  },
137
201
  'config set': { run: configSet, options: {}, usage: CONFIG_SET_USAGE },
138
- 'agent-skill install': {
139
- run: agentSkillInstall,
140
- options: {
141
- ...URL_OPTION,
142
- dir: { type: 'string' },
143
- 'allow-change': { type: 'boolean', default: false },
144
- 'working-copy': { type: 'boolean', default: false },
145
- 'agents-md': { type: 'boolean', default: false },
146
- json: { type: 'boolean', default: false },
147
- },
148
- usage: AGENT_SKILL_INSTALL_USAGE,
149
- },
150
202
  };
151
203
 
152
204
  /**
@@ -301,6 +353,21 @@ const WORKING_COPY = {
301
353
  },
302
354
  };
303
355
 
356
+ /**
357
+ * Flags a tool row takes beyond its verbs.json ones, which name no field of
358
+ * the tool's input and so cannot be in the table: what the CLI does around
359
+ * the call. `run get --follow` streams the run's steps to stderr until it is
360
+ * done, then prints get_run as without it.
361
+ */
362
+ const TOOL_EXTRAS = {
363
+ 'run get': {
364
+ options: { follow: { type: 'boolean', default: false } },
365
+ help: "flag: --follow → no input field: stream the run's steps to stderr until it ends, then print the run",
366
+ run: (row, args, io) =>
367
+ args.follow ? runFollow(row, args, io) : runTool(row, args, io),
368
+ },
369
+ };
370
+
304
371
  /** The handler of a non-tool row, working-copy or not, or undefined. */
305
372
  function handlerFor(row) {
306
373
  if (row.tool) return undefined;
@@ -320,26 +387,6 @@ function dispatchable(command) {
320
387
  return row && DISPATCHABLE.includes(row) ? row : undefined;
321
388
  }
322
389
 
323
- const USAGE = `juno — operate a Juno instance, and develop flows, integrations and apps for it
324
-
325
- Usage: juno <command> [options]
326
- juno <group> <verb> [id] [--input <json>|-|@file] [--jq <expr>]
327
-
328
- Groups: ${[...GROUPS].join(', ')}
329
- Commands: ${DISPATCHABLE.filter((r) => !groupOf(r))
330
- .map((r) => r.command)
331
- .join(', ')}
332
- Run \`juno <group> --help\` for a group's commands.
333
-
334
- ${WORKING_COPY_HEADING}:
335
- ${workingCopyListing(DISPATCHABLE, handlerFor)}
336
-
337
- --url <url> picks the instance; else $JUNO_URL, package.json juno.target,
338
- then the user config (juno config set url <url>).
339
- --token <key> is the API key to send; else $JUNO_TOKEN, then the user config
340
- (juno config set token <key>). Create one under Settings → API keys.
341
- `;
342
-
343
390
  /**
344
391
  * run is the whole CLI, injectable so the tests drive it without spawning a
345
392
  * process: io carries cwd, env, the TTY bit, stdin and the three output
@@ -387,9 +434,11 @@ async function dispatch(argv, io) {
387
434
  const { leading, rest } = leadingFlags(argv);
388
435
  const [first, ...tail] = rest;
389
436
 
437
+ // Bare `juno` is `juno --help`, exit 0: it is where an agent with nothing
438
+ // installed starts, and the primer is the answer it came for.
390
439
  if (!first || first === '--help' || first === '-h' || first === 'help') {
391
- io.log(USAGE);
392
- return first ? EXIT_OK : EXIT_USAGE;
440
+ const show = (args) => showRootHelp(args, io);
441
+ return runCommand({ options: URL_OPTION }, tail, leading, io, show, show);
393
442
  }
394
443
 
395
444
  const top = dispatchable(first);
@@ -428,6 +477,11 @@ async function dispatch(argv, io) {
428
477
  throw unknownTopLevel(first, first, [first]);
429
478
  }
430
479
 
480
+ async function showRootHelp(args, io) {
481
+ io.log(await rootHelp(DISPATCHABLE, handlerFor, groupSummaries, args, io));
482
+ return EXIT_OK;
483
+ }
484
+
431
485
  async function showGroupHelp(group, args, io, code) {
432
486
  io.log(await groupHelp(group, DISPATCHABLE, handlerFor, args, io));
433
487
  return code;
@@ -468,21 +522,28 @@ function runRow(row, argv, leading, io) {
468
522
  );
469
523
  }
470
524
 
525
+ // --wait is how long a destructive tool's approval is waited for; every
526
+ // tool row takes it, since which tools are destructive is the server's.
471
527
  const options = {
472
528
  ...URL_OPTION,
473
529
  input: { type: 'string' },
474
530
  jq: { type: 'string' },
531
+ wait: { type: 'string' },
475
532
  };
476
533
  for (const [flag, spec] of Object.entries(row.flags ?? {})) {
477
534
  options[flag] = { type: 'string', multiple: spec.multiple === true };
478
535
  }
536
+ const extras = TOOL_EXTRAS[row.command];
537
+ Object.assign(options, extras?.options);
538
+ const run = extras?.run ?? runTool;
479
539
  return runCommand(
480
- { run: (args) => runTool(row, args, io), options },
540
+ { run: (args) => run(row, args, io), options },
481
541
  argv,
482
542
  leading,
483
543
  io,
484
544
  async (args) => {
485
- io.log(await verbHelp(row, args, io));
545
+ const help = await verbHelp(row, args, io);
546
+ io.log(extras?.help ? `${help}\n${extras.help}` : help);
486
547
  return EXIT_OK;
487
548
  }
488
549
  );
@@ -611,6 +672,12 @@ export function defaultIO() {
611
672
  write: (text) => process.stdout.write(text),
612
673
  warn: (msg) => process.stderr.write(`${msg}\n`),
613
674
  error: (msg) => process.stderr.write(`${msg}\n`),
675
+ // Ctrl-C while waiting for an approval: the handler says how to resume,
676
+ // and the wait returns rather than the process dying mid-line.
677
+ onInterrupt: (fn) => {
678
+ process.once('SIGINT', fn);
679
+ return () => process.off('SIGINT', fn);
680
+ },
614
681
  };
615
682
  }
616
683
 
package/src/prompt.js CHANGED
@@ -182,6 +182,15 @@ function nextStep(event, id) {
182
182
  return `still running; keep waiting with: juno ask --session ${id}`;
183
183
  }
184
184
  if (event.type !== 'awaiting_input') return null;
185
+ const approvalID = event.pending_input?.approval_id;
186
+ if (approvalID) {
187
+ // A destructive call's approval record: only a person in Juno answers it.
188
+ const url = event.pending_input.approval_url ?? `/approvals/${approvalID}`;
189
+ return (
190
+ `waiting for a person to approve it in Juno: ${url}\n` +
191
+ `wait for the outcome with: juno ask --session ${id}`
192
+ );
193
+ }
185
194
  if (event.pending_input?.kind === 'approval') {
186
195
  return (
187
196
  `awaiting approval; approve with: juno session approve ${id}\n` +
package/src/verbs.js CHANGED
@@ -2,7 +2,9 @@ import { createRequire } from 'node:module';
2
2
 
3
3
  // createRequire rather than an import attribute: `with { type: 'json' }` needs
4
4
  // Node 20.10+, and engines says >=20.
5
- const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
5
+ const { commands: ROWS, groups: GROUPS } = createRequire(import.meta.url)(
6
+ '../verbs.json'
7
+ );
6
8
 
7
9
  /**
8
10
  * verbs.json is the command table: every `juno <group> <verb>` the CLI
@@ -12,6 +14,9 @@ const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
12
14
  */
13
15
  export const rows = ROWS;
14
16
 
17
+ /** The one line `juno --help` lists each group by, keyed by group. */
18
+ export const groupSummaries = GROUPS;
19
+
15
20
  const byCommand = new Map(ROWS.map((row) => [row.command, row]));
16
21
 
17
22
  /** The row for a command string ("state get"), or undefined. */